在F#中,是否可以将对可变,默认值的引用作为参数传递?

时间:2016-03-07 16:48:15

标签: f# mutable

对于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

1 个答案:

答案 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方法,由编译器处理)。存在虚拟方法调用或委托调用的成本,但在大多数情况下,当以第一类方式使用函数值时也存在类似的成本。一般来说,如果你担心性能,那么你应该设定一个目标并对其进行衡量,而不是过早地进行优化。