F# 联合类型与函数大小写的相等行为

Kas*_*man 5 f# equality discriminated-union

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

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)
Run Code Online (Sandbox Code Playgroud)

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

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

Fyo*_*kin 4

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

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

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

如果你真的希望它起作用(并且你真的知道如何比较函数),你总是可以Equals自己实现:

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

请注意,您必须做很多工作才能完成此任务:您必须添加 aCustomEqualityNoComparison(或CustomComparison)属性,并且还要实现GetHashCode. 如果你不这样做,编译器会抱怨你的相等性实现不一致。

然而,“正确”的解决方案是始终尽可能多地使用 F# 设施。在这种特定情况下,这意味着使用=运算符进行比较:

Assert.IsTrue( Case1 = Case1 )
Run Code Online (Sandbox Code Playgroud)

这样,如果您遗漏了某些内容,编译器将始终告诉您:

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

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