Byrefs
F# tiene dos áreas de características principales que se ocupan del espacio de 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. - Estructura similar a
byref
, que es una estructura que tiene una semántica similar y las mismas restricciones en tiempo de compilación quebyref<'T>
. 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ás escribiendo el puntero en lugar de leerlo, considera la posibilidad de usar outref<'T>
en lugar de byref<'T>
.
Semántica de inref
Observe 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<_>
.
También se cumple lo siguiente:
- No hay ninguna implicación que otros subprocesos o alias no tengan acceso de escritura a
x
. - No hay ninguna implicación que
SomeStruct
sea inmutable en virtud dex
serinref
.
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. Es para fines de compatibilidad.
Semánticamente, outref<'T>
no es diferente de byref<'T>
, excepto por una diferencia: los métodos con outref<'T>
parámetros se construyen implícitamente en un tipo de valor devuelto de tupla, al igual que al llamar a un método con un [<Out>]
parámetro.
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 interpreta F# lo que emite C#:
Construcción C# | Inferencias de F# |
---|---|
ref valor devuelto |
outref<'T> |
ref readonly valor devuelto |
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 |
---|---|
Argumento inref<'T> |
[In] atributo en el argumento |
inref<'T> retorno |
modreq atributo en el valor |
inref<'T> en ranura abstracta o implementación |
modreq en argumento o retorno |
Argumento outref<'T> |
[Out] atributo en el argumento |
Reglas de inferencia y sobrecarga de tipos
El compilador de F# deduce un inref<'T>
tipo en los casos siguientes:
- Parámetro de .NET o tipo de valor devuelto 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 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 byref
/inref
/outref
, puede definir sus propias estructuras que pueden adherirse a byref
la semántica similar. Esto se realiza a través del 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 abyref
» en F# es un tipo de valor enlazado 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 una 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.
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 una manera 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 residir 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 desreferenciación implícita, como pasar una referencia a través de varias llamadas encadenadas, usa &x
(donde x
es el valor).
También puedes asignar directamente a un valor devuelto byref
. Ten en cuenta el siguiente programa (altamente 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
Éste es el resultado:
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, la siguiente vista no se admite:
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 le impide obtener resultados diferentes dependiendo de si se compila con optimizaciones o no.