Partager via


Byrefs

F# a deux domaines de fonctionnalités majeurs qui traitent de l’espace de programmation de bas niveau :

  • Typesbyref//inrefoutref, qui sont des pointeurs managés. Ils ont des restrictions sur l’utilisation afin que vous ne puissiez pas compiler un programme non valide au moment de l’exécution.
  • Un byrefstruct similaire, qui est un struct qui a une sémantique similaire et les mêmes restrictions au moment de la compilation que byref<'T>. Voici un 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>, pointeur managé pour lire la valeur sous-jacente.
  • outref<'T>, pointeur managé pour l’écriture dans la valeur sous-jacente.
  • byref<'T>, pointeur managé pour la lecture et l’écriture de la valeur sous-jacente.

Un byref<'T> peut être passé à l’endroit où une inref<'T> valeur est attendue. De même, un byref<'T> peut être transmis là où un 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 d’un outref<'T> ou byref<'T>, vous devez également rendre la valeur vers laquelle vous récupérez un pointeur 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, envisagez d’utiliser outref<'T> au lieu de byref<'T>.

Sémantique d’inref

Considérez le code suivant :

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

Sémantiquement, cela signifie ce qui suit :

  • Le titulaire du x pointeur peut uniquement l’utiliser pour lire la valeur.
  • Tout pointeur acquis vers struct des champs imbriqués dans SomeStruct le type inref<_>est donné.

Voici également la valeur true :

  • Il n’y a aucune implication que d’autres threads ou alias n’ont pas d’accès en écriture à x.
  • Il n’y a aucune implication immuable SomeStruct en vertu d’être x un inref.

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

Toutes ces règles signifient ensemble que le titulaire d’un inref pointeur peut ne pas modifier le contenu immédiat de la mémoire vers laquelle il pointe.

Sémantique Outref

L’objectif est d’indiquer outref<'T> que le pointeur doit uniquement être écrit. De façon inattendue, outref<'T> permet de lire la valeur sous-jacente malgré son nom. Cela est destiné à des fins de compatibilité.

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

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 et out ref les in ref mots clés, en plus des ref retours. Le tableau suivant montre comment F# interprète ce qu’émet C# :

Construction C# Inférences F#
ref valeur de retour outref<'T>
ref readonly valeur de retour inref<'T>
Paramètre in ref inref<'T>
Paramètre out ref outref<'T>

Le tableau suivant montre ce que L#F# émet :

Construction F# Construction émise
Argument inref<'T> [In] attribut sur l’argument
inref<'T> rendre modreq attribut sur la valeur
inref<'T> dans l’emplacement ou l’implémentation abstraits modreq sur l’argument ou le retour
Argument outref<'T> [Out] attribut sur l’argument

Règles d’inférence et de surcharge de type

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

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

Lorsqu’une adresse implicite d’un élément inref est prise, 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 prises System.DateTime sont résolues plutôt que les surcharges prises inref<System.DateTime>.

Structs de type Byref

En plus du byref//inrefoutref trio, vous pouvez définir vos propres structs qui peuvent adhérer à byrefla sémantique .like. Pour ce faire, 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 Structpas . Les deux doivent être présents sur le type.

Un struct «byref like » en F# est un type de valeur lié à la pile. Il n’est jamais alloué sur le tas managé. Un byrefstruct semblable est utile pour la programmation hautes performances, 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 comme paramètres de fonction, paramètres de méthode, variables locales, retours de méthode.
  • Ils ne peuvent pas être des membres statiques ou d’instance d’une classe ou d’un struct normal.
  • Ils ne peuvent pas être capturés par une construction de fermeture (async méthodes ou expressions lambda).
  • Ils ne peuvent pas être utilisés comme paramètre générique.
    • À compter de F# 9, cette restriction est assouplie si le paramètre générique est défini en C# à l’aide de l’anti-contrainte de structure ref. F# peut instancier ces génériques dans les types et méthodes avec des types de type parref. Dans quelques exemples, cela affecte les types de délégués BCL (Action<>, Func<>), les interfaces (IEnumerable<>, IComparable<>) et les arguments génériques avec une fonction d’accumulation fournie par l’utilisateur (String.string Create<TState>(int length, TState state, SpanAction<char, TState> action)).
    • Il est impossible de créer du code générique prenant en charge des types de type typeref dans F#.

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

Bien que ces règles restreignent fortement l’utilisation, elles le font pour répondre à la promesse d’un calcul hautes performances de manière sécurisée.

Byref retourne

Les retours Parref des fonctions ou membres F# peuvent être produits et consommés. Lors de l’utilisation d’une byrefméthode -returning, la valeur est implicitement déréférente. Par exemple:

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

Pour retourner une valeur parref, la variable qui contient la valeur doit vivre plus longtemps que l’étendue actuelle. En outre, pour retourner byref, utilisez &value (où la valeur 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 la déréférencement implicite, telle que la transmission d’une référence via plusieurs appels chaînés, utilisez &x (où x se trouve la valeur).

Vous pouvez également affecter directement à un retour byref. Considérez le programme suivant (fortement impératif) :

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

Voici le résultat :

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 pour les byrefs

Une letvaleur liée ne peut pas avoir sa référence dépasse la portée dans laquelle elle a été définie. Par exemple, les éléments suivants ne sont pas autorisés :

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 compilez avec des optimisations ou non.