Byrefs
F# tiene dos principales áreas de funcionalidades que abordan el ámbito de la programación de bajo nivel:
- Los tipos
byref
/inref
/outref
, que son punteros administrados. Tienen restricciones de uso para que no pueda compilar un programa que no sea válido en tiempo de ejecución. - Una estructura similar a
, que es una estructura de que tiene una semántica similar y las mismas restricciones en tiempo de compilación que . Un ejemplo es Span<T>.
Sintaxis
// 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 y outref
Hay tres formas de byref
:
-
inref<'T>
, un puntero administrado para leer el valor subyacente. -
outref<'T>
, un puntero administrado para escribir en el valor subyacente. -
byref<'T>
, un puntero administrado para leer y escribir el valor subyacente.
Se puede pasar un byref<'T>
donde se espera inref<'T>
. Del mismo modo, se puede pasar un objeto byref<'T>
donde se espera outref<'T>
.
Uso de byrefs
Para usar un inref<'T>
, debes obtener un valor de puntero con &
:
open System
let f (dt: inref<DateTime>) =
printfn $"Now: %O{dt}"
let usage =
let dt = DateTime.Now
f &dt // Pass a pointer to 'dt'
Para escribir en el puntero mediante outref<'T>
o byref<'T>
, también debe hacer que el valor que tome un puntero a 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
Si solo está escribiendo el puntero en lugar de leerlo, considere la posibilidad de usar outref<'T>
en lugar de byref<'T>
.
Semántica de inref
Tenga en cuenta el código siguiente:
let f (x: inref<SomeStruct>) = x.SomeField
Semánticamente, esto significa lo siguiente:
- El titular del puntero
x
solo puede usarlo para leer el valor. - Cualquier puntero adquirido en
struct
campos anidados dentroSomeStruct
de se asigna al tipoinref<_>
.
Lo siguiente también es cierto:
- No hay ninguna implicación que otros subprocesos o alias no tengan acceso de escritura a
x
. - No hay ninguna implicación de que
SomeStruct
sea inmutable en virtud dex
ser uninref
.
Sin embargo, para los tipos de valor de F# que son inmutables, el this
puntero se deduce que es inref
.
Todas estas reglas juntas significan que el titular de un inref
puntero no puede modificar el contenido inmediato de la memoria a la que se apunta.
Semántica de outref
El propósito de outref<'T>
es indicar que el puntero solo se debe escribir en. Inesperadamente, outref<'T>
permite leer el valor subyacente a pesar de su nombre. Esto es para fines de compatibilidad.
Semánticamente, outref<'T>
no es diferente de byref<'T>
, excepto por una diferencia: los métodos con parámetros outref<'T>
se construyen implícitamente en un tipo de retorno de tupla, al igual que al llamar a un método con un parámetro [<Out>]
.
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"
Interoperabilidad con C#
C# admite las palabras clave in ref
y out ref
, además de ref
las devoluciones. En la tabla siguiente se muestra cómo F# interpreta lo que emite C#:
Construcción C# | Inferencias de F# |
---|---|
ref valor de devolución |
outref<'T> |
ref readonly valor de devolución |
inref<'T> |
parámetro in ref |
inref<'T> |
parámetro out ref |
outref<'T> |
En la tabla siguiente se muestra lo que emite F#:
Construcción F# | Construcción emitida |
---|---|
inref<'T> argumento |
atributo [In] en el argumento |
inref<'T> devolución |
modreq atributo en el valor |
inref<'T> en ranura abstracta o implementación |
modreq en argumento o retorno |
outref<'T> argumento |
atributo [Out] en el argumento |
Reglas de inferencia y sobrecarga de tipos
El compilador de F# deduce un tipo inref<'T>
en los casos siguientes:
- Parámetro o tipo de valor devuelto de .NET que tiene un atributo
IsReadOnly
. - Puntero
this
en un tipo de estructura que no tiene campos mutables. - Dirección de una ubicación de memoria derivada de otro
inref<_>
puntero.
Cuando se toma una dirección implícita de un inref
, se prefiere una sobrecarga con un argumento de tipo SomeType
a una sobrecarga con un argumento de tipo inref<SomeType>
. Por ejemplo:
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)
En ambos casos, las sobrecargas que toman System.DateTime
se resuelven en lugar de las sobrecargas que toman inref<System.DateTime>
.
Estructuras similares a byref
Además del trío de byref
/inref
/outref
, puede definir sus propias estructuras que pueden adherirse a una semántica similar a la de byref
. Esto se hace con el atributo 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
no implica Struct
. Ambos deben estar presentes en el tipo.
Una estructura similar a "byref
" en F# es un tipo de valor vinculado a la pila. Nunca se asigna en el montón administrado. Una estructura similiar a byref
es útil para la programación de alto rendimiento, ya que se aplica con un conjunto de comprobaciones seguras sobre la duración y la no captura. Las reglas son:
- Se pueden usar como parámetros de función, parámetros de método, variables locales, devoluciones de método.
- No pueden ser miembros estáticos o de instancia de una clase o estructura normal.
- No se pueden capturar mediante ninguna construcción de cierre (métodos
async
o expresiones lambda). - No se pueden usar como parámetro genérico.
- A partir de F# 9, esta restricción se relaja si el parámetro genérico se define en C# utilizando la anti-restricción allows ref struct. F# puede crear instancias de estos genéricos en tipos y métodos con tipos similares a byref. Como algunos ejemplos, esto afecta a los tipos de delegado BCL (
Action<>
,Func<>
), interfaces (IEnumerable<>
,IComparable<>
) y argumentos genéricos con una función de acumulador proporcionada por el usuario (String.string Create<TState>(int length, TState state, SpanAction<char, TState> action)
). - Es imposible crear código genérico que admita tipos similares a byref en F#.
- A partir de F# 9, esta restricción se relaja si el parámetro genérico se define en C# utilizando la anti-restricción allows ref struct. F# puede crear instancias de estos genéricos en tipos y métodos con tipos similares a byref. Como algunos ejemplos, esto afecta a los tipos de delegado BCL (
Este último punto es fundamental para la programación de estilo de canalización de F#, ya que |>
es una función genérica que parametriza sus tipos de entrada. Esta restricción puede ser relajada en |>
el futuro, ya que está insertada y no realiza ninguna llamada a funciones genéricas no insertadas en su cuerpo.
Aunque estas reglas restringen fuertemente el uso, lo hacen para cumplir la promesa de informática de alto rendimiento de forma segura.
Byref devuelve
Byref devuelve funciones o miembros de F# que se pueden producir y consumir. Al consumir un método que devuelve byref
, el valor se desreferencia implícitamente. Por ejemplo:
let squareAndPrint (data : byref<int>) =
let squared = data*data // data is implicitly dereferenced
printfn $"%d{squared}"
Para devolver un valor byref, la variable que contiene el valor debe existir más tiempo que el ámbito actual.
Además, para devolver byref, usa &value
(donde value es una variable que reside más tiempo que el ámbito actual).
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.
Para evitar la desreferencia implícita, como pasar una referencia a través de varias llamadas encadenadas, use &x
(donde x
es el valor).
También puedes asignar directamente a un valor devuelto byref
. Tenga en cuenta el siguiente programa (muy imperativo):
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
Esta es la salida:
Original sequence: 1 3 7 15 31 63 127 255 511 1023
New sequence: 1 3 7 30 31 63 127 255 511 1023
Ámbito de byrefs
Un valor enlazado a let
no puede hacer que su referencia supere el ámbito en el que se definió. Por ejemplo, no se permite lo siguiente:
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!
()
Esto evita que obtenga resultados diferentes en función de si se compila con optimizaciones o no.