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

In F#, is it possible to pass a reference to a mutable, defaulted value as a parameter?

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

error FS0421: The address of the variable 'element' cannot be used at this point

在不更改 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

我会尽力澄清我在评论中所说的内容。你是对的,你对 assign 的定义是完全正确的,它 看起来 具有签名 byref<'a> -> 'a -> unit。但是,如果您查看生成的程序集,您会发现它在 .NET 表示级别的编译方式是:

Void assign[a](a ByRef, a)

(也就是说,它是一个接受两个参数但不 return 任何东西的方法,不是一个接受一个参数的函数值,return 是一个接受下一个参数的函数和 returns 类型的值 unit - 编译器使用一些额外的元数据来确定方法的实际声明方式。

不涉及byref的函数定义也是如此。例如,假设您有以下定义:

let someFunc (x:int) (y:string) = ()

然后编译器实际上创建了一个带有签名的方法

Void someFunc(Int32, System.String)

当您尝试将 someFunc 之类的函数用作第一个 class 值时,编译器足够聪明,可以做正确的事情 - 如果您在不适合的上下文中使用它应用于任何参数,编译器将生成 int -> string -> unit 的子类型(在 .NET 表示级别为 FSharpFunc<int, FSharpFunc<string, unit>>),并且一切都无缝运行。

但是,如果您尝试用 assign 做同样的事情,它不会工作(或者不应该工作,但有几个编译器错误可能使它看起来像某些变体时工作实际上他们没有 - 你可能不会得到编译器错误,但你可能会得到一个格式错误的输出程序集) - .NET 类型实例化使用 byref 类型作为泛型类型参数是不合法的,所以 FSharpFunc<int byref, FSharpFunc<int, unit>> 不是有效的 .NET 类型。当有 byref 个参数时,F# 表示函数值的基本方法就不起作用了。

因此解决方法是使用采用 byref 参数的方法创建您自己的类型,然后创建具有您想要的行为的 subtypes/instances,有点像手动执行编译器自动执行的操作在非 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)

请注意,在委托或标称类型上调用 Invoke 之类的方法时,不会创建实际的元组,因此您不必担心那里有任何额外开销(这是一个需要两个参数并被编译器如此对待)。存在虚方法调用或委托调用的成本,但在大多数情况下,以第一种 class 方式使用函数值时也存在类似的成本。一般来说,如果您担心性能,那么您应该设定一个目标并针对它进行衡量,而不是尝试过早地进行优化。