Condividi tramite


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 di byref<'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 di SomeStruct viene assegnato il tipo inref<_>.

È 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 che x sia un inref.

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:

  1. Parametro o tipo di ritorno .NET che ha un attributo IsReadOnly.
  2. Puntatore this su un tipo di struct che non ha campi modificabili.
  3. 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#.

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 byrefrestituito. 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 letnon 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.