F# 记录:ref 与可变字段

F# record: ref vs mutable field

在重构我的 F# 代码时,我发现了一个字段类型为 bool ref:

的记录
type MyType =
  {
    Enabled : bool ref
    // other, irrelevant fields here
  }

我决定尝试将其改为 mutable 字段

// Refactored version
type MyType =
  {
    mutable Enabled : bool
    // other fields unchanged
  }

此外,我应用了编译代码所需的所有更改(即将 := 更改为 <-,删除 incrdecr 函数等)。

我注意到更改后一些单元测试开始失败。 由于代码非常大,我真的看不出到底发生了什么变化。

这两者的实现是否存在显着差异,可能会改变我的程序的行为?

是的,有区别。引用是 first-class 值,而可变变量是一种语言结构。

或者,从不同的角度来看,您可能会说 ref 单元格是通过引用传递的,而可变变量是通过值传递的。

考虑一下:

type T = { mutable x : int }
type U = { y : int ref }

let t = { x = 5 }
let u = { y = ref 5 }

let mutable xx = t.x
xx <- 10
printfn "%d" t.x  // Prints 5

let mutable yy = u.y
yy := 10
printfn "%d" !u.y // Prints 10

发生这种情况是因为 xx 是一个全新的可变变量,与 t.x 无关,因此变异 xxx 没有影响。

但是 yy 是对 u.y 完全相同的引用单元格 的引用,因此在引用它时将新值推入该单元格通过 yy 与通过 u.y.

引用它具有相同的效果

如果您“复制”一个 ref,副本最终指向同一个 ref,但如果您复制一个可变变量,则只会复制它的值。

你有区别不是因为一个是 first-value,被 reference/value 或其他东西传递。这是因为 ref 本身就是一个容器 (class)。

当你自己实现一个ref时,差异会更加明显。你可以这样做:

type Reference<'a> = {
    mutable Value: 'a
}

现在看看这两个定义。

type MyTypeA = {
    mutable Enabled: bool
}

type MyTypeB = {
    Enabled: Reference<bool>
}

MyTypeA 有一个 Enabled 字段可以直接改变或者用其他词是可变的。

在 other-side 上,您有 MyTypeB 理论上是不可变的,但有一个 Enabled 引用了可变的 class。

MyTypeB 中的 Enabled 只是引用了一个可变对象,就像 .NET 中的数百万其他 classes 一样。从上面的类型定义,你可以创建这样的对象。

let t = { MyTypeA.Enabled = true }
let u = { MyTypeB.Enabled = { Value = true }}

创建类型使其更加明显,第一个是可变字段,第二个包含具有可变字段的对象。

您在 FSharp.Core/prim-types.fs 中找到 ref 的实现,它看起来像这样:

    [<DebuggerDisplay("{contents}")>]
    [<StructuralEquality; StructuralComparison>]
    [<CompiledName("FSharpRef`1")>]
    type Ref<'T> = 
        { 
          [<DebuggerBrowsable(DebuggerBrowsableState.Never)>]
          mutable contents: 'T }
        member x.Value 
            with get() = x.contents
            and  set v = x.contents <- v

    and 'T ref = Ref<'T>

F# 中的 ref 关键字只是创建此类 pre-defined 可变引用对象的 built-in 方法,而不是为此创建您自己的类型。它有一些好处,只要您需要在 .NET 中传递 byrefinout 值,它就可以很好地工作。所以你应该使用ref。但是您也可以为此使用 mutable。例如,两个代码示例的作用相同。

有参考

let parsed =
    let result = ref 0
    match System.Int32.TryParse("1234", result) with
    | true  -> result.Value
    | false -> result.Value

有一个可变的

let parsed =
    let mutable result = 0
    match System.Int32.TryParse("1234", &result) with
    | true  -> result
    | false -> result

在这两个示例中,您都会将 1234 作为 int 进行解析。但是第一个示例将创建一个 FSharpRef 并将其传递给 Int32.TryParse,而第二个示例将创建一个字段或变量并将其与 out 一起传递给 Int32.TryParse