F# 联合类型与函数案例的相等行为

F# equality behavior on union type with function case

我正在尝试理解这种平等行为。记录相等性测试失败,但记录的唯一 属性 的相等性测试通过。这是一个错误吗?或者有人可以解释这种行为吗?

type TestUnion =
    | Case1
    | Case2 of (int -> string)

type TestType =
    {
        Foo : TestUnion
    }

open Microsoft.VisualStudio.TestTools.UnitTesting

[<TestClass>]
public Testing() =

    let a = { Foo = Case1 }
    let b = { Foo = Case1 }

    [<TestMethod>]
    member __.ThisFails () =
        Assert.AreEqual(a, b)

    [<TestMethod>]
    member __.ThisPasses () =
        Assert.AreEqual(a.Foo, b.Foo)

我知道它失败的原因是因为其中一个案例是一个函数。如果我将其更改为简单值,则两个测试都会通过。但令我感到奇怪的是 a) 相等性完全失败,因为使用了没有值的简单情况,b) 记录相等性失败,而 属性 相等性通过。

注意:当其他简单属性也存在时,记录相等性将失败。 IOW,联合类型毒化了整个记录的相等性,即使联合类型 属性 测试为相等。

如果您尝试直接比较 a = b:

,您应该会看到此错误

error FS0001: The type 'TestType' does not support the 'equality' constraint because it is a record, union or struct with one or more structural element types which do not support the 'equality' constraint. Either avoid the use of equality with this type, or add the 'StructuralEquality' attribute to the type to determine which field type does not support equality

函数值不支持 F# 中的 equality,因此您不能在这样的类型上具有结构相等性。我怀疑 Assert.AreEqual 调用是 而不是 依赖于 F# 的结构相等性比较。

let areEqual x y = obj.Equals(x, y)
> areEqual a.Foo b.Foo;;
val it : bool = true

Assert.AreEqual方法想耍小聪明当然失败了。给定两个对象,此方法要做的第一件事是测试引用相等性:obj.ReferenceEquals( Case1, Case1 )。这立即起作用,因为所有 Case1 值都是同一个对象。

现在,如果 Assert.AreEqual 的参数不是同一个对象,它将继续调用 obj.Equals。请注意,Equals 的实现将始终 return false,因为 F# 编译器没有为它实现相等性。为什么?因为有些字段(即TestUnion)的类型不相等。为什么 TestUnion 没有平等?因为它至少有一个类型不相等的情况 - 即 int -> string.

如果您将 Case1 更改为 Case1 of int 之类的内容,然后尝试 Assert.AreEqual( Case1 42, Case1 42 ),测试将失败。这将会发生,因为 Case1 42 的两个实例化将不再是同一个对象(除非您使用优化进行编译),并且 TestUnionEquals 实现将始终 return假的。

如果你真的想让它工作(并且你真的知道如何比较函数),你总是可以自己实现 Equals

[<CustomEquality; NoComparison>]
type TestType = { Foo: TestUnion }
    with 
        override this.Equals other = (* whatever *)
        override this.GetHashCode() = (* whatever *)

请注意,您必须花很多功夫才能完成此操作:您必须添加 CustomEqualityNoComparison(或 CustomComparison)属性,并实现 GetHashCode.如果您不这样做,编译器会抱怨您的相等性实现不一致。

但是,"correct" 解决方案是始终尽可能多地使用 F# 工具。在这种特定情况下,这意味着使用 = 运算符进行比较:

Assert.IsTrue( Case1 = Case1 )

这样,如果您遗漏了什么,编译器将始终告诉您:

Assert.IsTrue( a = b )
// The type 'TestType' does not support the 'equality' constraint because blah-blah-blah

F# 编译器通常比底层 .NET CLR 更正确、更一致。