Partilhar via


Byrefs

O F# tem duas grandes áreas de recursos que lidam com o espaço da programação de baixo nível:

  • Os byref//inrefoutref tipos, que são ponteiros gerenciados. Eles têm restrições de uso para que você não possa compilar um programa que é inválido em tempo de execução.
  • Um byrefstruct -like, que é um struct que tem semântica semelhante e as mesmas restrições de tempo de compilação que byref<'T>. Um exemplo é Span<T>.

Sintaxe

// 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 e outref

Existem três formas de byref:

  • inref<'T>, um ponteiro gerenciado para ler o valor subjacente.
  • outref<'T>, um ponteiro gerenciado para gravar no valor subjacente.
  • byref<'T>, um ponteiro gerenciado para ler e gravar o valor subjacente.

A byref<'T> pode ser passado onde um inref<'T> é esperado. Da mesma forma, um byref<'T> pode ser passado onde um outref<'T> é esperado.

Usando byrefs

Para usar um inref<'T>, você precisa obter um valor de ponteiro com &:

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 gravar no ponteiro usando um outref<'T> ou byref<'T>, você também deve fazer o valor que você pega um ponteiro para 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

Se você estiver apenas escrevendo o ponteiro em vez de lê-lo, considere usar outref<'T> em vez de byref<'T>.

Semântica Inref

Considere o seguinte código:

let f (x: inref<SomeStruct>) = x.SomeField

Semanticamente, isso significa o seguinte:

  • O detentor do x ponteiro só pode usá-lo para ler o valor.
  • Qualquer ponteiro adquirido para struct campos aninhados dentro SomeStruct é dado tipo inref<_>.

O seguinte também é verdadeiro:

  • Não há nenhuma implicação de que outros threads ou aliases não tenham acesso de gravação ao x.
  • Não há nenhuma implicação que SomeStruct seja imutável em virtude de x ser um inref.

No entanto, para tipos de valor F# que são imutáveis, o this ponteiro é inferido como um inref.

Todas essas regras juntas significam que o detentor de um inref ponteiro não pode modificar o conteúdo imediato da memória que está sendo apontada.

Semântica Outref

O objetivo é outref<'T> indicar que o ponteiro só deve ser escrito. Inesperadamente, permite a leitura do valor subjacente, outref<'T> apesar do seu nome. Isto é para fins de compatibilidade.

Semanticamente, outref<'T> não é diferente de byref<'T>, exceto por uma diferença: métodos com outref<'T> parâmetros são implicitamente construídos em um tipo de retorno de tupla, assim como ao chamar um método com um [<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"

Interoperabilidade com C#

C# suporta as in ref palavras-chave e out ref , além de ref retornos. A tabela a seguir mostra como o F# interpreta o que o C# emite:

Construção C# F# infere
ref Valor de retorno outref<'T>
ref readonly Valor de retorno inref<'T>
in ref parâmetro inref<'T>
out ref parâmetro outref<'T>

A tabela a seguir mostra o que o F# emite:

Construção F# Construção emitida
Argumento inref<'T> [In] atributo no argumento
inref<'T> regresso modreq atributo no valor
inref<'T> em slot abstrato ou implementação modreq na argumentação ou no retorno
Argumento outref<'T> [Out] atributo no argumento

Regras de inferência de tipo e sobrecarga

Um inref<'T> tipo é inferido pelo compilador F# nos seguintes casos:

  1. Um parâmetro .NET ou tipo de retorno que tem um IsReadOnly atributo.
  2. O this ponteiro em um tipo struct que não tem campos mutáveis.
  3. O endereço de um local de memória derivado de outro inref<_> ponteiro.

Quando um endereço implícito de um inref está sendo tomado, uma sobrecarga com um argumento de tipo SomeType é preferível a uma sobrecarga com um argumento de tipo inref<SomeType>. Por exemplo:

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)

Em ambos os casos, as sobrecargas que tomam são resolvidas em vez das sobrecargas que tomam System.DateTimeinref<System.DateTime>.

Estruturas semelhantes a Byref

Além do byref//inrefoutref trio, você pode definir suas próprias estruturas que podem aderir à byrefsemântica -like. Isso é feito com o IsByRefLikeAttribute atributo:

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 não implica Struct. Ambos devem estar presentes no tipo.

Uma estrutura "byref-like" em F# é um tipo de valor vinculado a pilha. Ele nunca é alocado na pilha gerenciada. Um byrefstruct -like é útil para programação de alto desempenho, pois é aplicado com um conjunto de verificações fortes sobre o tempo de vida e a não captura. As regras são:

  • Eles podem ser usados como parâmetros de função, parâmetros de método, variáveis locais, retornos de método.
  • Eles não podem ser estáticos ou membros de instância de uma classe ou struct normal.
  • Eles não podem ser capturados por qualquer construção de fechamento (async métodos ou expressões lambda).
  • Eles não podem ser usados como um parâmetro genérico.

Este último ponto é crucial para a programação no estilo pipeline F#, assim como |> uma função genérica que parametriza seus tipos de entrada. Esta restrição pode ser relaxada no |> futuro, uma vez que está em linha e não faz quaisquer chamadas para funções genéricas não alinhadas no seu corpo.

Embora essas regras restrinjam fortemente o uso, elas o fazem para cumprir a promessa de computação de alto desempenho de maneira segura.

Devoluções Byref

Os retornos Byref de funções ou membros do F# podem ser produzidos e consumidos. Ao consumir um byrefmétodo -returning, o valor é implicitamente desreferenciado. Por exemplo:

let squareAndPrint (data : byref<int>) =
    let squared = data*data    // data is implicitly dereferenced
    printfn $"%d{squared}"

Para retornar um valor byref, a variável que contém o valor deve viver mais do que o escopo atual. Além disso, para retornar byref, use &value (onde value é uma variável que vive mais do que o escopo atual).

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 a desreferência implícita, como passar uma referência através de várias chamadas encadeadas, use &x (onde x está o valor).

Você também pode atribuir diretamente a um retorno byref. Considere o seguinte 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

Esta é a saída:

Original sequence: 1 3 7 15 31 63 127 255 511 1023
New sequence:      1 3 7 30 31 63 127 255 511 1023

Escopo para byrefs

Um letvalor vinculado não pode ter sua referência excedendo o escopo no qual foi definido. Por exemplo, o seguinte não é permitido:

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!
    ()

Isso impede que você obtenha resultados diferentes, dependendo se você compila com otimizações ou não.