Byrefs
O F# tem duas grandes áreas de recursos que lidam com o espaço da programação de baixo nível:
- Os
byref
//inref
outref
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
byref
struct -like, que é um struct que tem semântica semelhante e as mesmas restrições de 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
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 dentroSomeStruct
é dado tipoinref<_>
.
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 dex
ser uminref
.
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:
- Um parâmetro .NET ou tipo de retorno que tem um
IsReadOnly
atributo. - O
this
ponteiro em um tipo struct que não tem campos mutáveis. - 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.DateTime
inref<System.DateTime>
.
Estruturas semelhantes a Byref
Além do byref
//inref
outref
trio, você pode definir suas próprias estruturas que podem aderir à byref
semâ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 byref
struct -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 byref
mé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 let
valor 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.