具有空字段的EqualityComparer的奇怪行为

问题描述 投票:2回答:2

假设有此类:

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而引起的。

有人可以澄清那里发生的事情吗?我应该怎样改变才能达到预期的行为?

c# .net nullable iequalitycomparer
2个回答
4
投票

您的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);

您具有x.Equals(y)y.Equals(z),这意味着您也应该具有x.Equals(z),但是您的实现未执行此操作。由于不符合规范,因此不能期望任何依赖于Equals方法的算法都能正常运行。


您问您可以做什么。这完全取决于您需要做什么。问题的一部分是,如果确实出现了极端情况,目前尚不清楚。如果一个Id在一个或两个列表中以相同的NullableId出现多次,该怎么办?举一个简单的例子,如果new Foo(1, 1)在第一个列表中存在3次,在第二个列表中存在3次,那么输出中应该是什么?九个项目,每个配对一个?

这是天真的尝试解决您的问题。这仅在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建立每个可能的对。如果每个列表具有Id == 1的10,000个项目,则有100,000,000对可供选择。


0
投票

如您从这里看到的https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.join?view=netframework-4.8

Join方法

基于匹配键将两个序列的元素相关。

这意味着您需要指定一个密钥。如果您重写代码以仅将Id用作键,则将看到两个Join调用返回相同的结果,并且您不需要覆盖任何内容:

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

但是如果您在键中包含属性NullableId,则在两种情况下您都将始终看到空结果,因为键将不匹配且结果中将不包含任何元素。您可以使用以下初始化代码在上面的代码中看到它:

var first = new List<Foo> { new Foo(5, null) };
var second = new List<Foo> { new Foo(1, 1), new Foo(1, 2), new Foo(1, 3) };
© www.soinside.com 2019 - 2024. All rights reserved.