Byrefs
O F# tem duas áreas de recursos principais que lidam com o espaço da 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 quebyref<'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
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 noSomeStruct
são determinado tipoinref<_>
.
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ção de que
SomeStruct
seja imutável em virtude dex
ser uminref
.
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, apesar de seu nome, outref<'T>
permite ler o valor subjacente. Isso é 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"
Interoperabilidade 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 F# interpreta o que o C# emite:
Constructo do C# | Inferências do F# |
---|---|
ref valor de devolução |
outref<'T> |
ref readonly valor de devolução |
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 |
---|---|
inref<'T> argumento |
[In] atributo no argumento |
inref<'T> devolução |
modreq atributo no valor |
inref<'T> no slot abstrato ou na implementação |
modreq no argumento ou na devolução |
outref<'T> argumento |
[Out] atributo no argumento |
Regras de inferência e sobrecarga de tipo
Um tipo de inref<'T>
é inferido pelo compilador F# nos seguintes casos:
- Um parâmetro .NET ou tipo de retorno que tem um atributo
IsReadOnly
. - O ponteiro
this
em um tipo de struct que não tem campos mutáveis. - 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 tomado, uma sobrecarga com um argumento de tipo SomeType
é preferida 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, retornos de 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 qualquer construção de fechamento (métodos
async
ou expressões lambda). - Eles não podem ser usados como um parâmetro genérico.
- A partir do F# 9, essa restrição será reduzida se o parâmetro genérico for definido em C# usando a anti-restrição de struct de ref. O F# pode instanciar esses genéricos em tipos e métodos com tipos do tipo byref. Como alguns exemplos, isso afeta tipos de delegado BCL (
Action<>
,Func<>
), interfaces (IEnumerable<>
,IComparable<>
) e argumentos genéricos com uma função de acumulador fornecida pelo usuário (String.string Create<TState>(int length, TState state, SpanAction<char, TState> action)
). - É impossível criar códigos genéricos que dão suporte a tipos semelhantes a byref em F#.
- A partir do F# 9, essa restrição será reduzida se o parâmetro genérico for definido em C# usando a anti-restrição de struct de ref. O F# pode instanciar esses genéricos em tipos e métodos com tipos do tipo byref. Como alguns exemplos, isso afeta tipos de delegado BCL (
Este último ponto é crucial para a programação em estilo de pipeline F#, pois |>
é uma função genérica que parametriza seus 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 se você compilar com otimizações ou não.