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 beibyref<'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 inSomeStruct
geschachtelt sind, erhalten den Typinref<_>
.
Folgendes gilt auch:
- Es gibt keinen Hinweis darauf, dass andere Threads oder Aliase keinen Schreibzugriff auf
x
haben. - Es wird nicht impliziert, dass
SomeStruct
unveränderlich ist, nur weil es sich beix
uminref
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:
- Ein .NET-Parameter oder Rückgabetyp mit einem
IsReadOnly
-Attribut. this
-Zeiger für einen Strukturtyp ohne veränderliche Felder- 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.
- 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 (
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.