Byrefs

F# har två viktiga funktionsområden som handlar om lågnivåprogrammering:

  • Typernabyref//inrefoutref, som är hanterade pekare. De har begränsningar för användning så att du inte kan kompilera ett program som är ogiltigt vid körning.
  • En byref-like struct, som är en struct som har liknande semantik och samma kompileringstidsbegränsningar som byref<'T>. Ett exempel är Span<T>.

Syntax

// 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 och outref

Det finns tre former av byref:

  • inref<'T>, en hanterad pekare för att läsa det underliggande värdet.
  • outref<'T>, en hanterad pekare för att skriva till det underliggande värdet.
  • byref<'T>, en hanterad pekare för att läsa och skriva det underliggande värdet.

En byref<'T> kan skickas där en inref<'T> förväntas. På samma sätt kan en byref<'T> skickas där en outref<'T> förväntas.

Använda byrefs

Om du vill använda måste inref<'T>du hämta ett pekarvärde med &:

open System

let f (dt: inref<DateTime>) =
    printfn $"Now: %O{dt}"

let usage =
    let dt = DateTime.Now
    f &dt // Pass a pointer to 'dt'

Om du vill skriva till pekaren med hjälp av en outref<'T> eller byref<'T>måste du också göra det värde som du hämtar en pekare till 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

Om du bara skriver pekaren i stället för att läsa den bör du överväga att använda outref<'T> i stället byref<'T>för .

Inref-semantik

Ta följande kod som exempel:

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

Semantiskt innebär detta följande:

  • Pekarens x innehavare får bara använda den för att läsa värdet.
  • Alla pekare som hämtas till struct fält som är kapslade inom SomeStruct är av typen inref<_>.

Följande är också sant:

  • Det finns ingen antydning om att andra trådar eller alias inte har skrivåtkomst till x.
  • Det finns ingen implikation som SomeStruct är oföränderlig på grund av x att vara en inref.

För F#-värdetyper som är oföränderliga härleds pekaren this dock till en inref .

Alla dessa regler innebär tillsammans att pekarens inref innehavare inte får ändra det omedelbara innehållet i det minne som pekas på.

Outref-semantik

Syftet med outref<'T> är att ange att pekaren endast ska skrivas till. Oväntat tillåter outref<'T> läsning av det underliggande värdet trots dess namn. Detta är i kompatibilitetssyfte.

Semantiskt outref<'T> sett skiljer sig inte från byref<'T>, förutom en skillnad: metoder med outref<'T> parametrar konstrueras implicit till en tuppelns returtyp, precis som när du anropar en metod med en [<Out>] parameter.

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 med C#

C# stöder nyckelorden in ref och out ref förutom ref returer. Följande tabell visar hur F# tolkar vad C# genererar:

C#-konstruktion F#-slutsatsdragningar
ref returvärde outref<'T>
ref readonly returvärde inref<'T>
in ref Parametern inref<'T>
out ref Parametern outref<'T>

Följande tabell visar vad F# genererar:

F#-konstruktion Genererad konstruktion
inref<'T>-argument [In] attributet på argumentet
inref<'T> Återvända modreq attribut för värde
inref<'T> i abstrakt fack eller implementering modreq på argument eller retur
outref<'T>-argument [Out] attributet på argumentet

Skriv slutsatsdragning och överlagringsregler

En inref<'T> typ härleds av F#-kompilatorn i följande fall:

  1. En .NET-parameter eller returtyp som har ett IsReadOnly attribut.
  2. Pekaren this på en structtyp som inte har några föränderliga fält.
  3. Adressen till en minnesplats som härleds från en annan inref<_> pekare.

När en implicit adress för en inref tas, föredras en överlagring med ett argument av typen SomeType till en överlagring med ett argument av typen inref<SomeType>. Till exempel:

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)

I båda fallen löses överlagringarna System.DateTime i stället för de överlagringar som tar inref<System.DateTime>.

Byref-liknande structs

Förutom byref//inrefoutref trion kan du definiera dina egna structs som kan följa byref- som semantik. Detta görs med attributet 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 innebär Structinte . Båda måste finnas på typen.

En "byref-liknande" struct i F# är en stackbunden värdetyp. Det allokeras aldrig på den hanterade heapen. En byref-liknande struct är användbar för programmering med höga prestanda, eftersom den tillämpas med en uppsättning starka kontroller om livslängd och icke-avbildning. Reglerna är:

  • De kan användas som funktionsparametrar, metodparametrar, lokala variabler, metodreturer.
  • De kan inte vara statiska eller instansmedlemmar i en klass eller normal struct.
  • De kan inte fångas upp av någon stängningskonstruktion (async metoder eller lambda-uttryck).
  • De kan inte användas som en allmän parameter.

Den här sista punkten är avgörande för programmering i F#-pipelinestil, liksom |> en allmän funktion som parameteriserar indatatyperna. Denna begränsning kan vara avslappnad för |> i framtiden, eftersom den är infogad och inte gör några anrop till icke-inlined generiska funktioner i kroppen.

Även om dessa regler starkt begränsar användningen, gör de det för att uppfylla löftet om databehandling med höga prestanda på ett säkert sätt.

Byref returnerar

Byref-returer från F#-funktioner eller medlemmar kan skapas och användas. När du använder en byref-returning-metod avrefereras värdet implicit. Till exempel:

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

Om du vill returnera ett värde byref måste variabeln som innehåller värdet vara längre än det aktuella omfånget. För att returnera byref använder du &value (där värdet är en variabel som lever längre än det aktuella omfånget).

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.

För att undvika implicit dereference, till exempel att skicka en referens via flera länkade anrop, använder du &x (där x är värdet).

Du kan också tilldela direkt till en retur byref. Överväg följande (mycket imperativa) program:

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

Det här är utdata:

Original sequence: 1 3 7 15 31 63 127 255 511 1023
New sequence:      1 3 7 30 31 63 127 255 511 1023

Omfång för byrefs

Ett let-bundet värde får inte ha en referens som överskrider det omfång som det definierades i. Följande är till exempel otillåtet:

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!
    ()

Detta hindrar dig från att få olika resultat beroende på om du kompilerar med optimeringar eller inte.