LINQ 上的 IEnumerable.Except() 不会在理想的源列表和目标列表之间运行

问题描述

我首先提到这个问题开始我的问题 - 我已经完成了其他 SO 问题,但最终遇到了一个我找不到答案的情况/问题。所以如果有的话,请指点我。

我的问题: 我有两个模型对象列表。 考虑一下,我有一个模型类 -

public class Contact 
{
        public string FirstName {get;set;}
        public string LastName {get;set;}
        public string MiddleName {get;set;}
        public long ContactId {get;set;}
        public long? DestKey {get;set;} 
}

我有两个数据源,可能有一些联系人数据。想象一下,从 Db 来源 1,我有 2 个联系人,从 Db 来源 2,我有 10 个联系人。

我正在尝试从 Db1 列表中查找不在 Db2 列表中的唯一联系人。我确实使用自定义 Equality 比较器通过检查 FirstName 和 Lastname 字段来比较数据。我也覆盖了 GetHashCode()。

所以,我的自定义平等比较器如下所示:

public class MyContactComparer : IEqualityComparer<Contact>
{
        public bool Equals(Contact src,Contact dest)
        {
            // compare LastName
            if (!src.LastName.Equals(dest.LastName,StringComparison.CurrentCultureIgnoreCase)) return false;

            // if LastName matches,compare FirstName
            if (!src.FirstName.Equals(dest.FirstName,StringComparison.CurrentCultureIgnoreCase))
                if (!(src.FirstName.Contains(dest.FirstName,StringComparison.CurrentCultureIgnoreCase) || 
                    dest.FirstName.Contains(src.FirstName,StringComparison.CurrentCultureIgnoreCase)))
                return false;
            // do other needful comparisons
            //Todo: check for other comparison

            return true;

        }

        public int GetHashCode(MmdContact obj)
        {
            return obj.FirstName.GetHashCode() ^ obj.LastName.GetHashCode();
        }
}

我称之为,

var nonMatchingContactsList = db2srcModelleddb1Data
                              .Except(db2ContactsData.ToArray(),new MyContactComparer())
                              .ToList()
                              .Select(person => person.ContactId);

现在,我将 Db1 上的数据设置为

  1. {FirstName = "Studo Mid",LastName = "Tar",MiddleName = null,ContactId = 1}
  2. {FirstName = "Foo",LastName = "Bar",MiddleName = "H",ContactId = 2}

Db2 上的数据设置为,

  1. {FirstName = "Studo",MiddleName = "Mid",DestKey = 10001}
  2. {FirstName = "Studo",DestKey = 10002}
  3. {FirstName = "Studo",DestKey = 10003}
  4. {FirstName = "Studo",DestKey = 10004}
  5. {FirstName = "Studo",DestKey = 10005}
  6. {FirstName = "Studo",DestKey = 10006} ... 等等, 通过名称重复记录但具有唯一的 DestKey。假设它们是由我在下面解释的逻辑创建的,结果是重复的。无论数据质量如何,我都希望将 Db1 集中的 2 个联系人与 Db2 集中的 10 个联系人进行比较。

但是当我调试它时,Equals() 方法只是在 Db2 集的 10 个联系人之间进行迭代和检查,因为我可以看到“src”和“Dest”之间的 DestKey 值。在我看来,它在 Db2 集中进行比较,然后将 Db1 上的 2 个联系人识别为不存在。所以我的逻辑是创建它们,在此基础上,“Studo Mid Tar”记录被一次又一次地创建。

当我再次重新运行时,它不会检测到该联系人是否匹配,并且不会执行 except() 部分。我想说,Db1 上的第二个联系人(Foo Bar)是我希望看到的作为要创建的输出的东西。 GetHashCode() 仅针对 db2 集发生。

那么,出了什么问题,为什么会出现这种行为? 需要什么来针对适当的列表运行它,即 2 对 10 条记录

更新: 我的主要问题是为什么 Equals() 与它自己的列表进行比较?看看这个小提琴 - https://dotnetfiddle.net/upCgbb

我看到了所需的输出,但我不明白的是,为什么 Equals() 方法比较相同模型类型(在本例中为 DataB)的数据进行几次迭代,而不是比较 A 与 B?它确实将 1001 与 1002 进行比较,然后将 1001 与 1003 进行比较,然后再与实际的 A ContactId 1 进行比较。这就是我为什么要比较自己的列表的问题?

解决方法

如果您需要比较两个列表并且您有某种一对多关系(一对无也是一对多)或左连接,您应该使用 .GroupJoin()

所以如果你用这个替换你的工作方法:

public void DoMyWork()
{
    // expecting 1 record from A which is ContactId = 2 {Foo Bar}
    var nonMatchingContactsList = dataFromA
        .GroupJoin(
            dataFromB,a => $"{a.FirstName}|{a.LastName}",b => $"{b.FirstName} {b.MiddleName}|{b.LastName}",(a,matchingBs) => matchingBs.Any() ? null : a)
        .Where(a => a != null)
        .ToList();

    Console.WriteLine($"Total contacts specific to DbA: {nonMatchingContactsList.Count()}\r\n{string.Join("\n",nonMatchingContactsList)}");
}

您无需使用任何自行编写的比较器即可获得所需的答案。 魔法发生在这些行中:

a => $"{a.FirstName}|{a.LastName}"

这将从您的类中创建所需的密钥。在我的心智模型中,这类似于 Dictionary<string,ModelA>

b => $"{b.FirstName} {b.MiddleName}|{b.LastName}"

这对您的第二个类执行相同的操作,并使用所需的属性生成类似的键以进行比较。根据您的数据,如果中间名为空或为空,您可能需要改进此方法以删除空格。

(a,matchingBs) => matchingBs.Any() ? null : a

这是组加入方法的结果选择器。第一个参数是一个 modelA 对象,第二个参数是所有匹配的 modelB 对象的列表。因为您想获取 ModelB 列表中不存在匹配对象的所有 ModelA 对象,所以我们检查 matchingBs.Any(),如果有匹配项,则返回 null,否则返回 a。之后,您只需扔掉所有空值,即可从 ModelA 列表中获得所需的项目。

如果中间名为空,可能是避免空格的更好方法:

b => string.isNullOrEmpty(b.MiddleName)
     ? $"{b.FirstName}|{b.LastName}"
     : $"{b.FirstName} {b.MiddleName}|{b.LastName}"

关于编写自己的比较器类的最后一个技巧。在实现方法 GetHashCode()Equals() 时,需要遵守很多规则。要将它们全部匹配,您应该使用此蓝图作为起点,并采用应比较的属性。通过使用此蓝图,您可以避免很多问题:

public bool Equals(MyObject x,MyObject y)
{
    if (ReferenceEquals(x,y))
        return true;

    if (ReferenceEquals(x,null))
        return false;

    if (ReferenceEquals(y,null))
        return false;

    if (x.MostDifferentialProperty != y.MostDifferentialProperty)
        return false;

    if (x.DifferentialProperty != y.DifferentialProperty)
        return false;

    // Other properties to compare...

    return true;
}

public int GetHashCode(MyObject obj)
{
    if (ReferenceEquals(obj,null))
        return -1;

    // Add all properties from Equals() method here.
    return HashCode.Combine(obj.MostDifferentialProperty,obj.DifferentialProperty);
}