Byrefs
F# ha due principali aree di funzionalità che si occupano dello spazio di programmazione di basso livello:
- I tipi
byref
/inref
/outref
, che sono puntatori gestiti. Hanno restrizioni sull'utilizzo in modo che non sia possibile compilare un programma non valido in fase di esecuzione. - Una struct simile a
byref
, che è una struct con semantica simile e le stesse restrizioni in fase di compilazione dibyref<'T>
. Un esempio è Span<T>.
Sintassi
// 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 e outref
Esistono tre forme di byref
:
-
inref<'T>
, un puntatore gestito per la lettura del valore sottostante. -
outref<'T>
, un puntatore gestito per la scrittura al valore sottostante. -
byref<'T>
, un puntatore gestito per la lettura e la scrittura del valore sottostante.
È possibile passare un byref<'T>
dove ci si aspetta un inref<'T>
. Analogamente, si può passare un byref<'T>
dove ci si aspetta un outref<'T>
.
Uso di byrefs
Per usare un inref<'T>
, è necessario ottenere un valore del puntatore con &
:
open System
let f (dt: inref<DateTime>) =
printfn $"Now: %O{dt}"
let usage =
let dt = DateTime.Now
f &dt // Pass a pointer to 'dt'
Per scrivere nel puntatore usando un outref<'T>
o byref<'T>
, è necessario impostare anche il valore che si afferra per 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
Se stai solo scrivendo il puntatore invece di leggerlo, considera l'uso di outref<'T>
al posto di byref<'T>
.
Semantica Inref
Si consideri il codice seguente:
let f (x: inref<SomeStruct>) = x.SomeField
Semanticamente, ciò significa quanto segue:
- Il titolare del puntatore
x
può usarlo solo per leggere il valore. - A qualsiasi puntatore acquisito per i campi
struct
annidati all'interno diSomeStruct
viene assegnato il tipoinref<_>
.
È vero anche quanto segue:
- Non c'è alcuna implicazione che altri thread o alias non abbiano accesso in scrittura a
x
. - Non c'è alcuna implicazione che
SomeStruct
sia immutabile dato chex
sia uninref
.
Tuttavia, per i tipi valore F# che sono non modificabili, il puntatore this
viene dedotto come un inref
.
Tutte queste regole insieme indicano che il titolare di un puntatore inref
non può modificare i contenuti immediati della memoria a cui punta.
Semantica outref
Lo scopo di outref<'T>
è indicare che il puntatore deve essere utilizzato solo per operazioni di scrittura. Inaspettatamente, outref<'T>
consente di leggere il valore sottostante nonostante il suo nome. Questo è a scopo di compatibilità.
Semanticamente, outref<'T>
non è diverso da byref<'T>
, ad eccezione di una differenza: i metodi con parametri outref<'T>
vengono costruiti in modo implicito in un tipo restituito di tupla, proprio come quando si chiama un metodo con un parametro [<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"
Interoperabilità con C#
C# supporta le parole chiave in ref
e out ref
, oltre alla restituzione di ref
. La tabella seguente illustra come F# interpreta ciò che C# emette.
Costrutto C# | F# deduce |
---|---|
ref valore restituito |
outref<'T> |
ref readonly valore restituito |
inref<'T> |
parametro in ref |
inref<'T> |
parametro out ref |
outref<'T> |
La tabella seguente mostra cosa emette F#:
Costrutto F# | Costrutto generato |
---|---|
argomento inref<'T> |
attributo sull'argomento [In] |
inref<'T> ritorno |
attributo modreq sul valore |
inref<'T> nello slot astratto o nell'implementazione |
modreq sull'argomento o sul valore restituito |
argomento outref<'T> |
attributo [Out] sull'argomento |
Regole di inferenza dei tipi e di sovraccarico
Un tipo inref<'T>
viene dedotto dal compilatore F# nei casi seguenti:
- Parametro o tipo di ritorno .NET che ha un attributo
IsReadOnly
. - Puntatore
this
su un tipo di struct che non ha campi modificabili. - Indirizzo di una posizione di memoria ricavato da un altro puntatore
inref<_>
.
Quando viene preso un indirizzo implicito di un inref
, un overload con un argomento di tipo SomeType
è preferibile rispetto a un overload con un argomento di tipo inref<SomeType>
. Per esempio:
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)
In entrambi i casi, gli overload che accettano System.DateTime
vengono risolti anziché gli overload che accettano inref<System.DateTime>
.
Strutture simili a byref
Oltre al trio di byref
/inref
/outref
, è possibile definire structs personalizzate che possono essere conformi a una semantica simile a byref
. Questa operazione viene eseguita con l'attributo 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
non implica Struct
. Entrambi devono essere presenti sul tipo.
Uno struct simile a "byref
" in F# è un tipo di valore legato allo stack. Non viene mai allocata nell'heap gestito. Una struttura simile a byref
è utile per la programmazione ad alte prestazioni, in quanto è soggetta a una serie di controlli rigorosi sulla durata e sull'assenza di acquisizione. Le regole sono:
- Possono essere usati come parametri di funzione, parametri del metodo, variabili locali, metodo restituito.
- Non possono essere membri statici o di istanza di una classe o di uno struct normale.
- Non possono essere acquisiti da alcun costrutto di chiusura (
async
metodi o espressioni lambda). - Non possono essere usati come parametro generico.
- A partire da F# 9, questa restrizione viene rilassata se il parametro generico è definito in C# usando consente l'anti-vincolo dello struct ref. F# può creare un'istanza di tali generics nei tipi e nei metodi con tipi simili a byref. Ad esempio, questo influisce sui tipi di delegato BCL (
Action<>
,Func<>
), sulle interfacce (IEnumerable<>
,IComparable<>
) e sugli argomenti generici con una funzione accumulatrice fornita dall'utente (String.string Create<TState>(int length, TState state, SpanAction<char, TState> action)
). - Non è possibile creare codice generico che supporti tipi simili a byref in F#.
- A partire da F# 9, questa restrizione viene rilassata se il parametro generico è definito in C# usando consente l'anti-vincolo dello struct ref. F# può creare un'istanza di tali generics nei tipi e nei metodi con tipi simili a byref. Ad esempio, questo influisce sui tipi di delegato BCL (
Questo ultimo punto è fondamentale per la programmazione in stile pipeline F#, poiché |>
è una funzione generica che parametrizza i relativi tipi di input. Questa restrizione potrebbe essere allentata per |>
in futuro, poiché è in linea e non effettua alcuna chiamata a funzioni generiche non in line nel proprio corpo.
Anche se queste regole limitano fortemente l'utilizzo, lo fanno per soddisfare la promessa di elaborazione ad alte prestazioni in modo sicuro.
Restituisce Byref
Le restituzioni Byref dalle funzioni o dai membri F# possono essere prodotte e consumate. Quando si utilizza un metodo che restituisce byref
, il valore viene dereferenziato implicitamente. Per esempio:
let squareAndPrint (data : byref<int>) =
let squared = data*data // data is implicitly dereferenced
printfn $"%d{squared}"
Per restituire un valore per riferimento, la variabile che contiene il valore deve esistere più a lungo dell'ambito corrente.
Inoltre, per restituire byref, usare &value
(dove value è una variabile che dura più a lungo dell'ambito corrente).
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.
Per evitare la dereferenziazione implicita, ad esempio, il passaggio di un riferimento tramite più chiamate concatenate, usare &x
(dove x
è il valore).
È anche possibile assegnare direttamente a un byref
restituito. Si consideri il programma seguente (estremamente imperativo):
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
Questo è l'output:
Original sequence: 1 3 7 15 31 63 127 255 511 1023
New sequence: 1 3 7 30 31 63 127 255 511 1023
Definizione dell'ambito per byrefs
Un valore vincolato a let
non può avere il suo riferimento che esce dall'ambito in cui è stato definito. Ad esempio, non è consentito quanto segue:
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!
()
Ciò impedisce di ottenere risultati diversi a seconda della compilazione con ottimizzazioni o meno.