带有可为空字段的 EqualityComparer 的奇怪行为

Yur*_*rii 3 .net c# nullable iequalitycomparer

假设有这个类:

public class Foo
{
    public int Id { get; set; }
    public int? NullableId { get; set; }

    public Foo(int id, int? nullableId)
    {
        Id = id;
        NullableId = nullableId;
    }
}
Run Code Online (Sandbox Code Playgroud)

我需要按照以下规则比较这些对象:

  1. 如果两个对象都有 NullableId 的值,那么我们比较 Id 和 NullableId
  2. 如果某些对象/它们都没有 NullableId,则忽略它并仅比较 Id。

为了实现它,我像这样覆盖了 Equals 和 GetHashCode:

public override bool Equals(object obj)
{
    var otherFoo = (Foo)obj;

    var equalityCondition = Id == otherFoo.Id;

    if (NullableId.HasValue && otherFoo.NullableId.HasValue)
        equalityCondition &= (NullableId== otherFoo.NullableId);

    return equalityCondition;
}

public override int GetHashCode()
{
    var hashCode = 806340729;
    hashCode = hashCode * -1521134295 + Id.GetHashCode();
    return hashCode;
}
Run Code Online (Sandbox Code Playgroud)

再往下,我有两个 Foo 列表:

var first = new List<Foo> { new Foo(1, null) };
var second = new List<Foo> { new Foo(1, 1), new Foo(1, 2), new Foo(1, 3) };
Run Code Online (Sandbox Code Playgroud)

接下来,我想加入这些列表。如果我这样做:

var result = second.Join(first, s => s, f => f, (f, s) => new {f, s}).ToList();
Run Code Online (Sandbox Code Playgroud)

那么结果将如我所料,我将得到 3 个项目。但是,如果我更改顺序并首先加入第二个:

var result = first.Join(second, f => f, s => s, (f, s) => new {f, s}).ToList();
Run Code Online (Sandbox Code Playgroud)

那么结果将只有 1 个项目 - new Foo(1, null)new Foo(1 ,3)

我不明白我做错了什么。如果尝试在 Equals 方法中放置一个断点,那么我可以看到它尝试比较来自同一列表的项目(例如比较new Foo(1, 1)new Foo(1 ,2))。对我来说,这看起来是因为在 Join 方法中创建了 Lookup。

有人可以澄清那里发生了什么吗?我应该改变什么来实现所需的行为?

Wee*_*ble 6

您的 Equals 方法是自反和对称的,但它不是可传递的。

您的实现不符合文档中指定的要求:

如果 (x.Equals(y) && y.Equals(z)) 返回 true,则 x.Equals(z) 返回 true。

来自https://docs.microsoft.com/en-us/dotnet/api/system.object.equals?view=netframework-4.8

例如,假设您有:

var x = new Foo(1, 100);
var y = new Foo(1, null);
var z = new Foo(1, 200);
Run Code Online (Sandbox Code Playgroud)

你有x.Equals(y)y.Equals(z)这意味着你也应该有x.Equals(z),但你的实现没有这样做。由于您不符合规范,因此您不能期望任何依赖于您的 Equals 方法的算法都能正确运行。


你问你能做什么。这完全取决于您需要做什么。部分问题在于,如果它们确实可以出现,我们还不清楚在极端情况下的意图是什么。如果在一个或两个列表中Id多次出现相同的内容NullableId,会发生什么情况?举个简单的例子,如果new Foo(1, 1)在第一个列表中存在 3 次,在第二个列表中存在 3 次,那么输出中应该是什么?九个项目,每个配对一个?

这是解决您问题的幼稚尝试。这仅加入Id,然后过滤掉任何不兼容的配对NullableId。但是当 aId在每个列表中多次出现时,您可能不会期望重复,如示例输出中所示。

var x = new Foo(1, 100);
var y = new Foo(1, null);
var z = new Foo(1, 200);
Run Code Online (Sandbox Code Playgroud)

输出:

Foo(1, 1)
Foo(1, 2)
Foo(1, 3)
Foo(1, null)
Foo(1, 1)
Foo(1, 2)
Foo(1, 3)
Foo(1, null)
Foo(1, 3)
Foo(1, 3)
Run Code Online (Sandbox Code Playgroud)

如果您有成千上万个具有相同 的项目Id,它对您来说也可能太慢了,因为它会Id在过滤掉它们之前建立每个可能的匹配对。如果每个列表有 10,000 个项目,Id == 1则有 100,000,000 对可供挑选。