Byrefs

F# verfügt über zwei Hauptfunktionsbereiche für die Programmierung auf niedriger Ebene:

  • Die Typen byref/inref/outref. Bei ihnen handelt es sich um verwaltete Zeiger. Ihre Verwendung unterliegt gewissen Einschränkungen, 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 zugrunde liegenden Werts
  • outref<'T>: Ein verwalteter Zeiger zum Schreiben in den zugrunde liegenden Wert
  • byref<'T>: Ein verwalteter Zeiger zum Lesen und Schreiben des zugrunde liegenden Werts

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 den Zeiger nur schreiben, anstatt ihn zu lesen, können Sie anstelle von outref<'T> auch byref<'T> verwenden.

Inref-Semantik

Betrachten Sie folgenden Code:

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

Semantisch bedeutet das Folgendes:

  • Der Inhaber bzw. die Inhaberin 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<_>.

Außerdem gilt Folgendes:

  • Es wird nicht impliziert, dass andere Threads oder Aliase keinen Schreibzugriff auf x haben.
  • 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.

Aus der Kombination dieser Regeln ergibt sich, dass der Inhaber bzw. die Inhaberin eines inref-Zeigers den unmittelbaren Inhalt des Arbeitsspeichers, 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 ist aus Kompatibilitätsgründen der Fall.

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# die Ausgaben von C# interpretiert:

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 die Ausgabe von F#:

F#-Konstrukt Ausgegebenes Konstrukt
inref<'T>-Argument [In]-Attribut für Argument
inref<'T>-Rückgabe modreq-Attribut für Wert
inref<'T> in abstraktem Slot oder in abstrakter Implementierung modreq für Argument oder Rückgabe
outref<'T>-Argument [Out]-Attribut 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. Verwenden Sie dazu das IsByRefLikeAttribute-Attribut:

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 lauten:

  • Sie können als Funktionsparameter, Methodenparameter, lokale Variablen und Methodenrückgaben verwendet werden.
  • Sie können nicht statisch oder Instanzmember einer Klasse oder 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.

Der letzte Punkt ist für die F#-Pipelineprogrammierung entscheidend, 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.

Diese Regeln schränken die Nutzung zwar stark ein, dies geschieht jedoch, um sicheres Hochleistungscomputing zu ermöglichen.

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. Folgendes ist beispielsweise nicht zulä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 abhängig von der Kompilierung mit oder ohne Optimierungen unterschiedliche Ergebnisse erhalten.