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 sombyref<'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 iSomeStruct
ges typinref<_>
.
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 attx
är eninref
.
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:
- En .NET-parameter eller returtyp som har ett
IsReadOnly
-attribut. - Den
this
pekare för en structtyp som inte har några muterbara fält. - 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#.
- 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 (
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.