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
的写入访问权限。 - 这并不意味着由于
x
为inref
,SomeStruct
为不可变。
但是,对于为不可变的 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 ref
和 out 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>
类型:
- 具有
IsReadOnly
属性的 .NET 参数或返回类型。 - 没有可变字段的结构类型上的
this
指针。 - 从另一个
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!
()
这可以防止根据是否使用优化进行编译而获得不同的结果。