Compartilhar via


Byrefs

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

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

Há 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.

Um byref<'T> pode ser passado para onde um inref<'T> é esperado. Do mesmo modo, um byref<'T> pode ser passado para onde um outref<'T> é esperado.

Uso de 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 gravando o ponteiro, e não lendo, use outref<'T> em vez de byref<'T>.

Semântica de inref

Considere o seguinte código:

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

Semanticamente, isso significa o seguinte:

  • O titular do ponteiro x só pode usá-lo para ler o valor.
  • Os ponteiros adquiridos nos campos struct aninhados no SomeStruct são determinado tipo inref<_>.

O seguinte também é verdadeiro:

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

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

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

Semântica de outref

A finalidade de outref<'T> é indicar que o ponteiro só deve ser gravado. Inesperadamente, outref<'T> permite ler o valor subjacente, apesar do nome. É usado para fins de compatibilidade.

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

Interoperação com C#

O C# é compatível com as palavras-chave in ref e out ref, além do retorno de ref. A tabela a seguir mostra como o F# interpreta o que o C# emite:

Constructo do C# Inferências do F#
Valor de retorno de ref outref<'T>
Valor de retorno de ref readonly inref<'T>
Parâmetro in ref inref<'T>
Parâmetro out ref outref<'T>

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

Constructo do F# Constructo emitido
Argumento inref<'T> Atributo de [In] no argumento
Retorno de inref<'T> Atributo de modreq no valor
inref<'T> no slot abstrato ou na implementação modreq no argumento ou no retorno
Argumento outref<'T> Atributo de [Out] no argumento

Regras de inferência e sobrecarga de tipo

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

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

Quando um endereço implícito de um inref está sendo usado, uma sobrecarga com um argumento do tipo SomeType é preferencial a uma sobrecarga com um argumento do 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 usam System.DateTime são resolvidas, em vez das sobrecargas que usam inref<System.DateTime>.

Structs semelhantes a byref

Além do trio byref/inref/outref, você pode definir seus próprios structs que podem aderir à semântica de byref-like. Isso é feito com o 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 não implica Struct. Ambos devem estar presentes no tipo.

Um struct "byref-like" em F# é um tipo de valor associado à pilha. Ele nunca é alocado no heap gerenciado. Um struct byref-like é útil para programação de alto desempenho, pois é aplicado com um conjunto de verificações robustas sobre vida útil e 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, retorna método.
  • Eles não podem ser membros estáticos ou de instância de uma classe ou struct normal.
  • Eles não podem ser capturados por nenhum constructo de fechamento (métodos async 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 de pipeline do F#, assim como |> é uma função genérica que parametriza os tipos de entrada. Essa restrição pode ser flexibilizada para |> futuramente, pois está embutida e não faz chamadas a funções genéricas não embutidas no corpo.

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

Retornos de byref

Os retornos de byref das funções ou dos membros do F# podem ser produzidos e consumidos. Ao consumir um método byref-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 byref de valor, 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 o valor é 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 por meio de várias chamadas encadeadas, use &x (onde x é o valor).

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

A referência de um valor let-bound não pode exceder o escopo em que foi definida. 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ê tenha resultados diferentes, dependendo de se você compilar com otimizações.