c# - 具有可为空字段的 EqualityComparer 的奇怪行为

标签 c# .net 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;
    }
}

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

  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;
}

再往下我有两个 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) };

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

var result = second.Join(first, s => s, f => f, (f, s) => new {f, s}).ToList();

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

var result = first.Join(second, f => f, s => s, (f, s) => new {f, s}).ToList();

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

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

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

最佳答案

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

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

If (x.Equals(y) && y.Equals(z)) returns true, then x.Equals(z) returns true.

来自 https://learn.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);

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


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

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

using System;
using System.Linq;
using System.Collections.Generic;

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

    public Foo(int id, int? nullableId)
    {
        Id = id;
        NullableId = nullableId;
    }

    public override string ToString() => $"Foo({Id}, {NullableId?.ToString()??"null"})";
}

class MainClass {
  public static IEnumerable<Foo> JoinFoos(IEnumerable<Foo> first, IEnumerable<Foo> second) {
    return first
        .Join(second, f=>f.Id, s=>s.Id, (f,s) => new {f,s})
        .Where(fs =>
            fs.f.NullableId == null ||
            fs.s.NullableId == null ||
            fs.f.NullableId == fs.s.NullableId)
        .Select(fs => new Foo(fs.f.Id, fs.f.NullableId ?? fs.s.NullableId));    
  }
  public static void Main (string[] args) {
    var first = new List<Foo> { new Foo(1, null), new Foo(1, null), new Foo(1, 3) };
    var second = new List<Foo> { new Foo(1, 1), new Foo(1, 2), new Foo(1, 3), new Foo(1, null) };
    foreach (var f in JoinFoos(first, second)) {
      Console.WriteLine(f);
    }
  }
}

输出:

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)

如果您有数万个具有相同 Id 的项目,它对您来说也可能太慢了,因为它会在过滤之前构建所有可能的具有匹配 Id 的对他们出来。如果每个列表有 10,000 个带有 Id == 1 的项目,那么就有 100,000,000 对要挑选。

关于c# - 具有可为空字段的 EqualityComparer 的奇怪行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60696070/

相关文章:

c# - 您可以使用不使用剪贴板的 Excel Interop 一次粘贴一个单元格 block 吗?

c# - 用用户名和密码模拟?

c# - 使用 Enterprise Library 将字段添加到数据库日志

c# - 可空类型存储在内存中的什么位置?

c# - 如何知道方法参数是否为可为空类型

c# - 如果 User.IsInRole 带有字符串数组?

c# - 如何将变量从 ActionFilter 传递到 C# MVC 中的 Controller 操作?

.net - IValueConverter - 在 Convert 方法中获取源对象

c# - 为什么foreach循环里面的switch只执行一次

c# - 下面的空条件运算符有什么问题?