Byrefs

F# 有两个主要功能领域,用于处理低级别编程的空间:

  • 这些byref//inrefoutref类型是托管指针。 它们对用法有限制,因此无法在运行时编译无效的程序。
  • 类似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>位置的 Abyref<'T>。 同样,可以传递预期outref<'T>位置的 Abyref<'T>

Using byrefs

若要使用 a 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 持有者只能使用它来读取值。
  • 获取到 struct 嵌套在内 SomeStruct 字段的任何指针都属于给定类型 inref<_>

以下内容也属实:

  • 没有任何意义,其他线程或别名没有写入访问权限 x
  • 没有意义, SomeStruct 由于 x 是一个 inref不可变的。

但是, 对于不可变 的 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 的互作#

除返回外,refC# 还支持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 on 参数或返回
outref<'T> 参数 [Out] 参数上的属性

类型推理和重载规则

inref<'T>以下情况由 F# 编译器推断类型:

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

当采用隐式地址时,具有类型SomeType参数的inref重载优先于具有类型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//inrefoutref三者,还可以定义可以遵循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# 9 开始,如果使用允许构造反约束在 C# 中定义泛型参数,则会放宽此限制。 F# 可以使用类似 byref 的类型和方法实例化此类泛型。 作为一些示例,这会影响 BCL 委托类型(、)、接口(Action<>IEnumerable<>IComparable<>)以及具有用户提供的累加器函数的泛型参数(String.string Create<TState>(int length, TState state, SpanAction<char, TState> action))。 Func<>
    • 不可能在 F# 中创作支持类似 byref 的类型的泛型代码。

最后一点对于 F# 管道样式编程至关重要,一 |> 个参数化其输入类型的泛型函数也至关重要。 将来可能会放宽 |> 此限制,因为它是内联的,并且不会对其正文中的非内联泛型函数进行任何调用。

尽管这些规则强烈限制使用,但它们这样做是以安全的方式履行高性能计算的承诺。

Byref 返回

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

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

若要返回值 byref,包含该值的变量必须比当前范围长。 此外,若要返回 byref,请使用 &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

byrefs 的范围

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

这可以防止你获得不同的结果,具体取决于是否使用优化进行编译。