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 quebyref<'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 dansSomeStruct
reçoit le typeinref<_>
.
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 quex
est uninref
.
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 :
- Paramètre ou type de retour .NET qui a un attribut
IsReadOnly
. - Pointeur
this
sur un type de struct qui n’a aucun champ mutable. - 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.