Compartir vía


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 dentro SomeStruct de se asigna al tipo inref<_>.

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 de x ser un inref.

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:

  1. Parámetro o tipo de valor devuelto de .NET que tiene un atributo IsReadOnly.
  2. Puntero this en un tipo de estructura que no tiene campos mutables.
  3. 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#.

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.