Byref

F# 有两个与低级别编程领域相关的主要功能领域:

  • byref/inref/outref 类型,它们是托管指针。 它们对使用情况有限制,因此你无法编译在运行时无效的程序。
  • byref 类似的结构,它是一种与 byref<'T> 具有类似语义和相同编译时限制的结构。 一个示例是 Span<T>

语法

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

byref 有三种形式:

  • inref<'T>,用于读取基础值的托管指针。
  • outref<'T>,用于写入基础值的托管指针。
  • byref<'T>,用于读取和写入基础值的托管指针。

可以在需要 inref<'T> 时传递 byref<'T>。 同样,可以在需要 outref<'T> 时传递 byref<'T>

使用 byref

若要使用 inref<'T>,需要获取具有 & 的指针值:

open System

let f (dt: inref<DateTime>) =
    printfn $"Now: %O{dt}"

let usage =
    let dt = DateTime.Now
    f &dt // Pass a pointer to 'dt'

若要使用 outref<'T>byref<'T> 写入指针,还必须使获取的值为指向 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

如果仅写入指针而不是读取指针,请考虑使用 outref<'T> 而不是 byref<'T>

Inref 语义

请考虑以下代码:

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

从语义上来说,这意味着:

  • x 指针的持有者只能使用它来读取值。
  • 任何获取的指向嵌套在 SomeStruct 中的 struct 字段的指针都具有类型 inref<_>

以下内容也成立:

  • 这并不意味着其他线程或别名没有对 x 的写入访问权限。
  • 这并不意味着由于 xinrefSomeStruct 为不可变。

但是,对于为不可变的 F# 值类型,this 指针被推断为 inref

所有这些规则共同意味着 inref 指针的持有者不能修改所指向内存的直接内容。

Outref 语义

outref<'T> 的目的是指示只应写入指针。 意外的是,outref<'T> 允许读取基础值,而不管其名称如何。 这是出于兼容性考虑。

从语义上讲,outref<'T>byref<'T> 没有什么不同,只不过有一个区别:带有 outref<'T> 参数的方法隐式构造为元组返回类型,就像调用带有 [<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"

与 C# 交互

除了 ref 返回值之外,C# 还支持 in refout ref 关键字。 下表显示了 F# 如何解释 C# 发出的内容:

C# 构造 F# 推断
ref 返回值 outref<'T>
ref readonly 返回值 inref<'T>
in ref 参数 inref<'T>
out ref 参数 outref<'T>

下表显示了 F# 发出的内容:

F# 构造 发出的构造
inref<'T> 参数 参数的 [In] 属性
inref<'T> 返回值 值的 modreq 属性
抽象槽或实现中的 inref<'T> 参数或返回值的 modreq
outref<'T> 参数 参数的 [Out] 属性

类型推理和重载规则

在以下情况下,F# 编译器会推断出 inref<'T> 类型:

  1. 具有 IsReadOnly 属性的 .NET 参数或返回类型。
  2. 没有可变字段的结构类型上的 this 指针。
  3. 从另一个 inref<_> 指针派生的内存位置的地址。

当使用 inref 的隐式地址时,具有 SomeType 类型的参数的重载优于具有 inref<SomeType> 类型的参数的重载。 例如:

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)

在这两种情况下,将解析使用 System.DateTime 的重载,而不是使用 inref<System.DateTime> 的重载。

与 byref 类似的结构

除了 byref/inref/outref 三种结构之外,还可以定义自己的结构,这些结构可遵循类似 byref 的语义。 这通过 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 并不表示 Struct。 这两者必须都存在于类型中。

F# 中类似 byref 的结构是堆栈绑定值类型。 永远不会在托管堆上分配它。 类似 byref 的结构可用于高性能编程,因为它是通过一组有关生存期和非捕获的强检查强制执行的。 规则包括:

  • 它们可用作函数参数、方法参数、局部变量、方法返回。
  • 它们不能是类或普通结构的静态成员或实例成员。
  • 它们不能由任何闭包构造(async 方法或 Lambda 表达式)捕获。
  • 它们不能用作泛型参数。

最后一点对于 F# 管道式编程至关重要,因为 |> 是一种参数化其输入类型的泛型函数。 将来可能会放宽 |> 的此限制,因为它是内联的,不会在其主体中调用非内联的泛型函数。

虽然这些规则施加了非常严格的使用限制,但这样做是为了履行安全地进行高性能计算的承诺。

Byref 返回值

可以生成和使用 F# 函数或成员的 byref 返回值。 使用 byref 返回方法时,将隐式取消引用该值。 例如:

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

若要返回值 byref,包含该值的变量的生存期必须长于当前范围。 此外,若要返回 byref,请使用 &value(其中 value 是一个生存期长于当前范围的变量)。

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.

若要避免隐式取消引用(例如通过多个链接调用传递引用),请使用 &x(其中 x 是值)。

还可以直接分配给返回值 byref。 请考虑以下(非常必要的)程序:

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

以下是输出:

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

Byref 的范围

let 绑定值的引用不能超出定义它的范围。 例如,不允许出现以下情况:

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

这可以防止根据是否使用优化进行编译而获得不同的结果。