Freigeben über


Byrefs

F# hat zwei Hauptmerkmale, die sich mit der Programmierung auf niedriger Ebene befassen.

  • Die Typen byref/inref/outref. Bei ihnen handelt es sich um verwaltete Zeiger. Sie haben Einschränkungen bei der Verwendung, sodass Sie kein Programm kompilieren können, das zur Laufzeit ungültig ist.
  • Eine byref-ähnliche Struktur. Hierbei handelt es sich um eine Struktur mit einer ähnlichen Semantik und den gleichen Kompilierzeiteinschränkungen wie bei byref<'T>. Ein Beispiel ist 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 und outref

Es gibt drei Formen von byref:

  • inref<'T>, ein verwalteter Zeiger zum Lesen des zugrundeliegenden Wertes.
  • outref<'T>, ein verwalteter Zeiger zum Schreiben auf den zugrundeliegenden Wert.
  • byref<'T>, ein verwalteter Zeiger zum Lesen und Schreiben des zugrundeliegenden Wertes.

byref<'T> kann dort übergeben werden, wo inref<'T> erwartet wird. Analog dazu kann byref<'T> dort übergeben werden, wo outref<'T> erwartet wird.

Verwenden von byref

Wenn Sie inref<'T> verwenden möchten, müssen Sie einen Zeigerwert mit & abrufen:

open System

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

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

Wenn Sie mithilfe von outref<'T> oder byref<'T> in den Zeiger schreiben möchten, müssen Sie außerdem den Wert, auf den Sie einen Zeiger abrufen, als mutable konfigurieren.

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

Wenn Sie nur den Zeiger schreiben, anstatt ihn zu lesen, sollten Sie outref<'T> anstelle von byref<'T>verwenden.

Inref-Semantik

Beachten Sie den folgenden Code:

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

Semantisch bedeutet dies Folgendes:

  • Der Inhaber des x Zeigers darf ihn nur zum Lesen des Werts verwenden.
  • Alle abgerufenen Zeiger auf struct-Felder, die in SomeStruct geschachtelt sind, erhalten den Typ inref<_>.

Folgendes gilt auch:

  • Es gibt keinen Hinweis darauf, dass andere Threads oder Aliase keinen Schreibzugriff auf xhaben.
  • Es wird nicht impliziert, dass SomeStruct unveränderlich ist, nur weil es sich bei x um inref handelt.

Für F#-Werttypen, die tatsächlich unveränderlich sind, wird der this-Zeiger allerdings als inref abgeleitet.

Alle diese Regeln bedeuten zusammen, dass der Inhaber eines inref Zeigers den unmittelbaren Inhalt des Speichers, auf den verwiesen wird, nicht ändern darf.

Outref-Semantik

Der Zweck von outref<'T> besteht darin, anzugeben, dass nur in den Zeiger geschrieben werden soll. Überraschenderweise lässt outref<'T> trotz seines Namens das Lesen des zugrunde liegenden Werts zu. Dies dient zu Kompatibilitätszwecken.

Semantisch unterscheidet sich outref<'T> nicht von byref<'T> – mit einer Ausnahme: Methoden mit outref<'T>-Parametern werden implizit als Tupelrückgabetyp erstellt, genau wie beim Aufrufen einer Methode mit einem [<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"

Interoperabilität mit C#

C# unterstützt neben ref-Rückgaben auch die Schlüsselwörter in ref und out ref. Die folgende Tabelle zeigt, wie F# interpretiert, was C# ausgibt:

C#-Konstrukt F#-Interpretation
ref Rückgabewert outref<'T>
ref readonly Rückgabewert inref<'T>
in ref Parameter inref<'T>
out ref Parameter outref<'T>

Die folgende Tabelle zeigt, was F# ausgibt:

F#-Konstrukt Ausgegebenes Konstrukt
inref<'T>-Argument [In]-Attribute für Argument
inref<'T> Rückgabe modreq-Attribute für Wert
inref<'T> im abstrakten Slot oder in der Implementierung modreq für Argument oder Rückgabe
outref<'T>-Argument [Out]-Attribute für Argument

Regeln für Typrückschluss und Überladung

Ein inref<'T>-Typ wird in den folgenden Fällen vom F#-Compiler abgeleitet:

  1. Ein .NET-Parameter oder Rückgabetyp mit einem IsReadOnly-Attribut.
  2. this-Zeiger für einen Strukturtyp ohne veränderliche Felder
  3. Von einem anderen inref<_>-Zeiger abgeleitete Speicheradresse

Bei Verwendung einer impliziten Adresse eines inref-Elements wird eine Überladung mit einem Argument vom Typ SomeType einer Überladung mit einem Argument vom Typ inref<SomeType> vorgezogen. Zum Beispiel:

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 beiden Fällen werden die Überladungen, die System.DateTime verwenden, aufgelöst (anstelle der Überladungen, die inref<System.DateTime> verwenden).

Byref-ähnliche Strukturen

Zusätzlich zu den drei Optionen byref/inref/outref können Sie auch Ihre eigenen Strukturen definieren, die einer byref-ähnlichen Semantik entsprechen. Dies geschieht mit dem attribut 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 impliziert nicht Struct. Beides muss im Typ vorhanden sein.

Eine "byref-ähnliche" Struktur in F# ist ein stapelgebundener Werttyp. Sie wird niemals für den verwalteten Heap zugeordnet. Eine byref-ähnliche Struktur ist bei der Hochleistungsprogrammierung hilfreich, da sie mit einer Reihe strenger Überprüfungen der Lebensdauer und der Nichterfassung erzwungen wird. Die Regeln sind:

  • Sie können als Funktionsparameter, Methodenparameter, lokale Variablen, Methodenrückkehrer verwendet werden.
  • Sie können weder statische noch Instanzmitglieder einer Klasse oder einer normalen Struktur sein.
  • Sie können nicht von einem Abschlusskonstrukt (async-Methoden oder Lambdaausdrücke) erfasst werden.
  • Sie können nicht als generischer Parameter verwendet werden.
    • Ab F# 9 wird diese Einschränkung gelockert, wenn der generische Parameter in C# mit der Anti-Beschränkung allows ref struct definiert wird. F# kann solche Generika in Typen und Methoden mit byref-ähnlichen Typen instanziieren. Als einige Beispiele betrifft dies BCL-Delegatentypen (Action<>, Func<>), Schnittstellen (IEnumerable<>, IComparable<>) und generische Argumente mit einer vom Benutzer bereitgestellten Akkumulatorfunktion (String.string Create<TState>(int length, TState state, SpanAction<char, TState> action)).
    • Es ist unmöglich, generischen Code zu erstellen, der byref-ähnliche Typen in F# unterstützt.

Dieser letzte Punkt ist für die Programmierung im F#-Pipelinestil von entscheidender Bedeutung, da |> eine generische Funktion ist, die ihre Eingabetypen parametrisiert. Diese Einschränkung wird für |> möglicherweise in Zukunft gelockert, da es sich hierbei um eine Inlinefunktion handelt, die in ihrem Textkörper keine Aufrufe an generische Funktionen sendet, die nicht inline sind.

Obwohl diese Regeln die Nutzung stark einschränken, tun sie dies, um das Versprechen von Hochleistungs-Computing auf sichere Weise zu erfüllen.

byref-Rückgaben

byref-Rückgaben von F#-Funktionen oder Membern können erstellt und genutzt werden. Bei der Nutzung einer Methode, die byref zurückgibt, wird der Wert implizit dereferenziert. Zum Beispiel:

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

Für die Rückgabe eines byref-Werts muss die Lebensdauer der Variablen, die den Wert enthält, über die Lebensdauer des aktuellen Bereichs hinausgehen. Verwenden Sie für die byref-Rückgabe außerdem &value (wobei „value“ eine Variable ist, deren Lebensdauer über die Lebensdauer des aktuellen Bereichs hinausgeht).

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.

Wenn Sie die implizite Dereferenzierung (beispielsweise die Übergabe eines Verweises über mehrere verkettete Aufrufe) vermeiden möchten, verwenden Sie &x (wobei x der Wert ist).

Auch eine direkte Zuweisung zu einer Rückgabe in Form von byref ist möglich. Sehen Sie sich das folgende (hochgradig imperative) Programm an:

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

Dies ist die Ausgabe:

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

Bereichsdefinition für byref

Der Verweis eines let-gebundenen Werts darf nicht über den Bereich hinausgehen, in dem er definiert wurde. Beispielsweise ist Folgendes unzulässig:

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

Dadurch wird verhindert, dass Sie unterschiedliche Ergebnisse erhalten, je nachdem, ob Sie mit Optimierungen kompilieren oder nicht.