Dela via


Byrefs

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

  • De byref/inref/outref typerna är hanterade pekare. De har användningsbegränsningar så att du inte kan kompilera ett program som är ogiltigt vid exekveringstid.
  • 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 både 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 en inref<'T>måste 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'

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

Inref-semantik

Överväg följande kod:

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

Semantiskt innebär detta följande:

  • Innehavaren av x pekare får bara använda den för att läsa värdet.
  • Alla pekare som erhålls för struct fält som är inbäddade i SomeStruct ges typ 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 antydning om att SomeStruct är oföränderlig på grund av att x är en inref.

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

Alla dessa regler innebär tillsammans att innehavaren av en inref pekare inte får ändra det omedelbara innehållet i minnet som pekas på.

Outref-semantik

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

Semantiskt sett är outref<'T> inte annorlunda ä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"

Interoperabilitet med C#

C# stöder nyckelorden in ref och out ref, samt ref return. Följande tabell visar hur F# tolkar vad C# genererar:

C#-konstruktion F# förutsäger
ref returvärde outref<'T>
ref readonly returvärde inref<'T>
in ref parameter inref<'T>
out ref parameter outref<'T>

Följande tabell visar vad F# genererar:

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

Typinferens 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. Den this pekare för en structtyp som inte har några muterbara 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 framför 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 de överlagringar som tar System.DateTime i stället för de överlagringar som tar inref<System.DateTime>.

Byref-liknande strukturer

Förutom byref/inref/outref-trion kan du definiera dina egna structs som kan följa byref-liknande 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 inte Struct. 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 högpresterande programmering, eftersom den tillämpas med en uppsättning starka kontroller för livslängd och icke-fångst. 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.
    • Från och med F# 9 är den här begränsningen mindre strikt om den generiska parametern definieras i C# med hjälp av "ref struct anti-constraint". F# kan instansiera sådana generiska objekt i typer och metoder med byref-liknande typer. Som några exempel påverkar detta BCL-delegattyper (Action<>, Func<>), gränssnitt (IEnumerable<>, IComparable<>) och generiska argument med en användar-definierad ackumulatorfunktion (String.string Create<TState>(int length, TState state, SpanAction<char, TState> action)).
    • Det går inte att skapa generisk kod som stöder byref-liknande typer i F#.

Den här sista punkten är avgörande för programmering i F#-pipelinestil eftersom |> är en allmän funktion som parameteriserar indatatyperna. Denna begränsning kan lättas för |> i framtiden, eftersom den är inlinje och inte anropar några icke inlagrade generiska funktioner i dess kod.

Ä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 existera längre än den aktuella scope. Om du vill 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 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

Omfattning för byrefs

Ett let-bound-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.