对于Froto项目(F#中的Google Protobuf),我正在尝试将反序列化代码更新为使用'a ref
个对象传递值byref<'a>
,以提高性能。
但是,以下代码在hydrator &element field
行上失败:
type Field = TypeA | TypeB | Etc
let hydrateRepeated
(hydrator:byref<'a> -> Field -> unit)
(result:byref<'a list>)
(field:Field) =
let mutable element = Unchecked.defaultof<'a>
hydrator &element field
result <- element :: result
错误FS0421:此时不能使用变量'element'的地址
有什么办法可以让代码在不改变hydrator
参数签名的情况下工作吗?
我非常清楚我可以使用hydrator:'a ref -> Field -> unit
并让事情发挥作用。但是,目标是支持反序列化为record
类型,而无需在每次反序列化记录时在堆上创建一堆ref
个对象。
请注意,以下代码完全合法且与上面的hydrator
函数声明具有相同的签名,因此我不清楚问题所在。
let assign (result:byref<'a>) (x:'a) =
result <- x
let thisWorks() =
let mutable v = Unchecked.defaultof<int>
assign &v 5
printfn "%A" v
答案 0 :(得分:6)
我会在评论中澄清我在说什么。你对assign
的定义完全正确,并且出现以获得签名byref<'a> -> 'a -> unit
,你是对的。但是,如果查看生成的程序集,您会发现它在.NET表示级别编译的方式是:
Void assign[a](a ByRef, a)
(也就是说,它是一个接受两个参数且不返回任何内容的方法,不是一个接受一个参数的函数值,而是返回一个接受下一个参数并返回类型unit
的值的函数 - 编译器使用一些额外的元数据来确定实际声明方法的方式。
不涉及byref
的函数定义也是如此。例如,假设您有以下定义:
let someFunc (x:int) (y:string) = ()
然后编译器实际上创建了一个带有签名
的方法Void someFunc(Int32, System.String)
当你尝试使用像someFunc
这样的函数作为第一类值时,编译器足够聪明,可以做正确的事情 - 如果你在不应用于任何参数的上下文中使用它,编译器将生成int -> string -> unit
的子类型(在.NET表示级别为FSharpFunc<int, FSharpFunc<string, unit>>
),并且一切都可以无缝地工作。
但是,如果你尝试用assign
执行相同的操作,它将无法工作(或者不应该工作,但是有几个编译器错误可能会使某些变体看起来真的很有效不要 - 你可能没有得到编译器错误,但你可能得到一个格式错误的输出程序集) - 使用byref
类型作为泛型类型参数的.NET类型实例化是不合法的,所以{{1} }不是有效的.NET类型。当存在FSharpFunc<int byref, FSharpFunc<int, unit>>
个参数时,F#表示函数值的基本方法不起作用。
因此,解决方法是使用带有byref
参数的方法创建自己的类型,然后创建具有所需行为的子类型/实例,有点像手动执行编译器在非byref
案例。您可以使用命名类型
byref
或委托类型
type MyByrefFunc2<'a,'b> =
abstract Invoke : 'a byref * 'b -> unit
let assign = {
new MyByrefFunc2<_,_> with
member this.Invoke(result, x) =
result <- x }
请注意,在委托或名义类型上调用类似type MyByrefDelegate2<'a,'b> = delegate of 'a byref * 'b -> unit
let assign = MyByrefDelegate2(fun result x -> result <- x)
的方法时,不会创建实际的元组,因此您不应该担心任何额外的开销(它是一个带有两个参数的.NET方法,由编译器处理)。存在虚拟方法调用或委托调用的成本,但在大多数情况下,当以第一类方式使用函数值时也存在类似的成本。一般来说,如果你担心性能,那么你应该设定一个目标并对其进行衡量,而不是过早地进行优化。