Partager via


Byrefs

F# comporte deux domaines de fonctionnalités majeurs qui ciblent la programmation de bas niveau :

  • Les types byref/inref/outref, qui sont des pointeurs managés. Ils comportent des restrictions d’utilisation, ce qui vous empêche de compiler un programme non valide à l’exécution.
  • Un struct similaire à byref, c’est-à-dire un struct présentant une sémantique similaire et les mêmes restrictions au moment de la compilation que byref<'T>. Par exemple, Span<T>.

Syntaxe

// Byref types as parameters
let f (x: byref<'T>) = ()
let g (x: inref<'T>) = ()
let h (x: outref<'T>) = ()

// Calling a function with a byref parameter
let mutable x = 3
f &x

// Declaring a byref-like struct
open System.Runtime.CompilerServices

[<Struct; IsByRefLike>]
type S(count1: int, count2: int) =
    member x.Count1 = count1
    member x.Count2 = count2

Byref, inref et outref

Il existe trois formes de byref :

  • inref<'T>, un pointeur managé pour la lecture de la valeur sous-jacente.
  • outref<'T>, un pointeur managé pour l’écriture de la valeur sous-jacente.
  • byref<'T>, un pointeur managé pour la lecture et l’écriture de la valeur sous-jacente.

Vous pouvez passer byref<'T> quand inref<'T> est attendu. De même, vous pouvez passer byref<'T> quand outref<'T> est attendu.

Utilisation de byrefs

Pour utiliser un inref<'T>, vous devez obtenir une valeur de pointeur avec & :

open System

let f (dt: inref<DateTime>) =
    printfn $"Now: %O{dt}"

let usage =
    let dt = DateTime.Now
    f &dt // Pass a pointer to 'dt'

Pour écrire dans le pointeur à l’aide de outref<'T> ou de byref<'T>, vous devez également faire de la valeur que vous récupérez un pointeur vers mutable.

open System

let f (dt: byref<DateTime>) =
    printfn $"Now: %O{dt}"
    dt <- DateTime.Now

// Make 'dt' mutable
let mutable dt = DateTime.Now

// Now you can pass the pointer to 'dt'
f &dt

Si vous écrivez uniquement le pointeur au lieu de le lire, utilisez outref<'T> à la place de byref<'T>.

Sémantique de inref

Prenez le code suivant :

let f (x: inref<SomeStruct>) = x.SomeField

Sur le plan sémantique, cela signifie ce qui suit :

  • Le détenteur du pointeur x peut uniquement l’utiliser pour lire la valeur.
  • Tout pointeur acquis vers les champs struct imbriqués dans SomeStruct reçoit le type inref<_>.

Ce qui suit est également vrai :

  • Il n’existe aucune implication selon laquelle d’autres threads ou alias n’ont pas d’accès en écriture à x.
  • Il n’existe aucune implication selon laquelle SomeStruct est immuable, du fait que x est un inref.

Toutefois, pour les types valeur F# qui sont immuables, le pointeur this est déduit comme étant un inref.

L’ensemble de ces règles signifie que le détenteur d’un pointeur inref ne peut pas modifier le contenu immédiat de la mémoire référencée.

Sémantique de outref

La finalité de outref<'T> est d’indiquer que le pointeur doit uniquement faire l’objet d’opérations d’écriture. De manière inattendue, outref<'T> permet de lire la valeur sous-jacente malgré son nom. Cela répond à un besoin de compatibilité.

D’un point de vue sémantique, outref<'T> n’est pas différent de byref<'T>, à une différence près : les méthodes avec des paramètres outref<'T> sont implicitement construites dans un type de retour de tuple, comme pour l’appel d’une méthode avec un paramètre [<Out>].

type C =
    static member M1(x, y: _ outref) =
        y <- x
        true

match C.M1 1 with
| true, 1 -> printfn "Expected" // Fine with outref, error with byref
| _ -> printfn "Never matched"

Interopérabilité avec C#

C# prend en charge les mots clés in ref et out ref en plus des retours ref. Le tableau suivant montre la façon dont F# interprète ce que C# émet :

Construction C# F# déduit
Valeur de retour ref outref<'T>
Valeur de retour ref readonly inref<'T>
Paramètre in ref inref<'T>
Paramètre out ref outref<'T>

Le tableau suivant montre ce que F# émet :

Construction F# Construction émise
Argument inref<'T> Attribut [In] sur l’argument
Retour de inref<'T> Attribut modreq sur la valeur
inref<'T> dans un emplacement ou une implémentation de type abstrait modreq sur l’argument ou la valeur de retour
Argument outref<'T> Attribut [Out] sur l’argument

Inférence de type et règles de surcharge

Un type inref<'T> est déduit par le compilateur F# dans les cas suivants :

  1. Paramètre ou type de retour .NET qui a un attribut IsReadOnly.
  2. Pointeur this sur un type de struct qui n’a aucun champ mutable.
  3. Adresse d’un emplacement mémoire dérivé d’un autre pointeur inref<_>.

Quand une adresse implicite d’un inref est utilisée, une surcharge avec un argument de type SomeType est préférée à une surcharge avec un argument de type inref<SomeType>. Par exemple :

type C() =
    static member M(x: System.DateTime) = x.AddDays(1.0)
    static member M(x: inref<System.DateTime>) = x.AddDays(2.0)
    static member M2(x: System.DateTime, y: int) = x.AddDays(1.0)
    static member M2(x: inref<System.DateTime>, y: int) = x.AddDays(2.0)

let res = System.DateTime.Now
let v =  C.M(res)
let v2 =  C.M2(res, 4)

Dans les deux cas, les surcharges qui acceptent System.DateTime sont résolues à la place des surcharges qui acceptent inref<System.DateTime>.

Structs se comportant comme des types byref

En plus du trio byref/inref/outref, vous pouvez définir vos propres structs qui peuvent adhérer à une sémantique propre à byref. Pour cela, vous utilisez l’attribut IsByRefLikeAttribute :

open System
open System.Runtime.CompilerServices

[<IsByRefLike; Struct>]
type S(count1: Span<int>, count2: Span<int>) =
    member x.Count1 = count1
    member x.Count2 = count2

IsByRefLike n’implique pas Struct. Les deux doivent être présents dans le type.

Un struct "se comportant comme un type byref" en F# est un type valeur lié à la pile. Il n’est jamais alloué sur le tas managé. Un struct se comportant comme un type byref est utile pour la programmation haute performance, car il est appliqué avec un ensemble de vérifications fortes sur la durée de vie et la non-capture. Les règles sont les suivantes :

  • Ils peuvent être utilisés en tant que paramètres de fonction, paramètres de méthode, variables locales, retours de méthode.
  • Ils ne peuvent pas être des membres statiques ou des membres d’instance d’une classe ou d’un struct normal.
  • Ils ne peuvent pas être capturés par une construction de fermeture (méthodes async ou expressions lambda).
  • Ils ne peuvent pas être utilisés en tant que paramètres génériques.

Ce dernier point est crucial pour la programmation de style pipeline F#, car |> est une fonction générique qui paramétrise ses types d’entrée. Cette restriction sera peut-être assouplie pour |> à l’avenir, car il est inline et n’effectue aucun appel à des fonctions génériques non inlined dans son corps.

Bien que ces règles restreignent fortement l’utilisation, elles sont nécessaires pour garantir l’exécution de calculs haute performance de manière sécurisée.

Retours byref

Les retours byref des fonctions ou des membres F# peuvent être produits et consommés. Quand vous consommez une méthode avec retour byref, la valeur est implicitement déréférencée. Par exemple :

let squareAndPrint (data : byref<int>) =
    let squared = data*data    // data is implicitly dereferenced
    printfn $"%d{squared}"

Pour retourner une valeur byref, la variable qui contient la valeur doit vivre plus longtemps que l’étendue actuelle. De plus, pour effectuer un retour byref, utilisez &value (où value est une variable qui vit plus longtemps que l’étendue actuelle).

let mutable sum = 0
let safeSum (bytes: Span<byte>) =
    for i in 0 .. bytes.Length - 1 do
        sum <- sum + int bytes[i]
    &sum  // sum lives longer than the scope of this function.

Pour éviter le déréférencement implicite, par exemple le passage d’une référence via plusieurs appels chaînés, utilisez &x (où x représente la valeur).

Vous pouvez également affecter une valeur directement à un retour byref. Prenons le programme (hautement impératif) suivant :

type C() =
    let mutable nums = [| 1; 3; 7; 15; 31; 63; 127; 255; 511; 1023 |]

    override _.ToString() = String.Join(' ', nums)

    member _.FindLargestSmallerThan(target: int) =
        let mutable ctr = nums.Length - 1

        while ctr > 0 && nums[ctr] >= target do ctr <- ctr - 1

        if ctr > 0 then &nums[ctr] else &nums[0]

[<EntryPoint>]
let main argv =
    let c = C()
    printfn $"Original sequence: %O{c}"

    let v = &c.FindLargestSmallerThan 16

    v <- v*2 // Directly assign to the byref return

    printfn $"New sequence:      %O{c}"

    0 // return an integer exit code

Il s'agit de la sortie :

Original sequence: 1 3 7 15 31 63 127 255 511 1023
New sequence:      1 3 7 30 31 63 127 255 511 1023

Étendue des byrefs

La référence d’une valeur liée à let ne peut pas dépasser l’étendue dans laquelle elle a été définie. Par exemple, ce qui suit n’est pas autorisé :

let test2 () =
    let x = 12
    &x // Error: 'x' exceeds its defined scope!

let test () =
    let x =
        let y = 1
        &y // Error: `y` exceeds its defined scope!
    ()

Cela vous empêche d’obtenir des résultats différents selon que vous effectuez la compilation avec des optimisations ou non.