Byref

F# ha due principali aree di funzionalità che si occupano dello spazio di programmazione di basso livello:

  • Tipibyref//inrefoutref, che sono puntatori gestiti. Hanno restrizioni sull'utilizzo in modo che non sia possibile compilare un programma non valido in fase di esecuzione.
  • byrefStruct simile a , ovvero uno 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 nel valore sottostante.
  • byref<'T>, un puntatore gestito per la lettura e la scrittura del valore sottostante.

È possibile passare un byref<'T> oggetto in cui è previsto un oggetto inref<'T> . Analogamente, è possibile passare un oggetto byref<'T> in cui è previsto un oggetto outref<'T> .

Uso di byrefs

Per usare un inref<'T>oggetto , è 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> oggetto o byref<'T>, è necessario impostare anche il valore che si desidera impostare mutablesu .

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 si scrive solo il puntatore anziché leggerlo, è consigliabile usare outref<'T> anziché byref<'T>.

Semantica inref

Osservare il codice seguente:

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

Semanticamente, ciò significa quanto segue:

  • Il titolare del x puntatore può usarlo solo per leggere il valore.
  • A qualsiasi puntatore acquisito nei struct campi annidati all'interno 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 non è modificabile in virtù di x essere un inref.

Tuttavia, per i tipi valore F# non modificabili, il this puntatore viene dedotto come .inref

Tutte queste regole insieme indicano che il titolare di un inref puntatore potrebbe non modificare il contenuto immediato della memoria a cui punta.

Semantica outref

Lo scopo di outref<'T> è indicare che il puntatore deve essere scritto solo in . In modo imprevisto, outref<'T> consente di leggere il valore sottostante nonostante il nome. Questo è a scopo di compatibilità.

Semanticamente, outref<'T> non è diverso da , ad eccezione di byref<'T>una differenza: i metodi con outref<'T> parametri vengono costruiti in modo implicito in un tipo restituito di tupla, proprio come quando si chiama un metodo con un [<Out>] parametro .

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 in ref parole chiave e out ref , oltre a restituisce ref . La tabella seguente illustra come F# interpreta gli elementi generati da C#:

Costrutto C# Inferi F#
ref valore restituito outref<'T>
ref readonly valore restituito inref<'T>
parametro in ref inref<'T>
parametro out ref outref<'T>

La tabella seguente illustra gli elementi generati da F#:

Costrutto F# Costrutto generato
inref<'T> argomento [In] attributo sull'argomento
inref<'T> Ritorno modreq attributo sul valore
inref<'T> nello slot astratto o nell'implementazione modreq argomento o restituzione
outref<'T> argomento [Out] attributo sull'argomento

Regole di inferenza e overload dei tipi

Un inref<'T> tipo viene dedotto dal compilatore F# nei casi seguenti:

  1. Parametro .NET o tipo restituito con un IsReadOnly attributo .
  2. Puntatore this in un tipo di struct senza campi modificabili.
  3. Indirizzo di una posizione di memoria derivata da un altro inref<_> puntatore.

Quando viene acquisito un indirizzo implicito di un oggetto inref , un overload con un argomento di tipo SomeType è preferibile a un overload con un argomento di tipo inref<SomeType>. Ad 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>.

Struct simili a byref

Oltre al byref//inrefoutref trio, è possibile definire struct personalizzati che possono essere conformi alla semantica simile.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 nel tipo .

Uno struct "byrefsimile" in F# è un tipo di valore associato a stack. Non viene mai allocata nell'heap gestito. Uno byrefstruct -like è utile per la programmazione ad alte prestazioni, perché viene applicato con un set di controlli sicuri sulla durata e non 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.

Questo ultimo punto è fondamentale per la programmazione in stile pipeline F#, come |> una funzione generica che parametrizza i relativi tipi di input. Questa restrizione può essere rilassata per |> in futuro, in quanto è inline e non effettua alcuna chiamata a funzioni generiche non inlined nel suo 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

La funzione Byref restituisce da funzioni o membri F# può essere prodotta e utilizzata. Quando si utilizza un byrefmetodo -returning, il valore viene dereferenziato in modo implicito. Ad esempio:

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

Per restituire un byref di valore, la variabile che contiene il valore deve essere più lunga 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 concatenati, usare &x (dove x è il valore).

È anche possibile assegnare direttamente a un oggetto restituito byref. 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

L'output è il seguente.

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 letvalore con associazione a -non può avere il riferimento superiore all'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.