合并两个自定义类会返回重复项

3

我有两个自定义类,ChangeRequestChangeRequests,其中一个ChangeRequests可以包含多个ChangeRequest实例。

public class ChangeRequests : IXmlSerializable, ICloneable, IEnumerable<ChangeRequest>,
    IEquatable<ChangeRequests> { ... }

public class ChangeRequest : ICloneable, IXmlSerializable, IEquatable<ChangeRequest>
    { ... }

我正在尝试对两个ChangeRequests实例进行联合。然而,重复项似乎没有被删除。我的MSTest单元测试如下:

var cr1 = new ChangeRequest { CRID = "12" };
var crs1 = new ChangeRequests { cr1 };
var crs2 = new ChangeRequests
               {
                   cr1.Clone(),
                   new ChangeRequest { CRID = "34" }
               };
Assert.AreEqual(crs1[0], crs2[0], "First CR in both ChangeRequests should be equal");
var unionedCRs = new ChangeRequests(crs1.Union<ChangeRequest>(crs2));
ChangeRequests expected = crs2.Clone();
Assert.AreEqual(expected, unionedCRs, "Duplicates should be removed from a Union");

测试在最后一行失败,unionedCRs 包含两个 cr1 的副本。当我尝试调试并逐行查看每一行时,在 ChangeRequest.Equals(object) 的第一行和 ChangeRequest.Equals(ChangeRequest) 的第一行中设置了断点,但都没有触发。为什么联合包含重复的 ChangeRequest 实例? 编辑:如请求所示,这里是 ChangeRequests.Equals(ChangeRequests)
public bool Equals(ChangeRequests other)
{
    if (ReferenceEquals(this, other))
    {
        return true;
    }

    return null != other && this.SequenceEqual<ChangeRequest>(other);
}

这里是ChangeRequests.Equals(object)函数:

public override bool Equals(object obj)
{
    return Equals(obj as ChangeRequests);
}

编辑:我在ChangeRequestChangeRequests上都覆盖了GetHashCode,但在我的测试中,如果我执行IEnumerable<ChangeRequest> unionedCRsIEnum = crs1.Union<ChangeRequest>(crs2);unionedCRsIEnum最终会有两个具有CRID为12的ChangeRequest

编辑:我的EqualsGetHashCode实现肯定出了问题,因为Assert.AreEqual(expected, unionedCRs.Distinct(), "Distinct should remove duplicates");失败了,而expectedunionedCRs.Distinct()的字符串表示显示unionedCRs.Distinct()中肯定有两个CR 12的副本。


1
你能否发布一下 ChangeRequests.EqualsChangeRequests.GetHashCode 的实现?在这些方法中犯一个拼写错误就会破坏对象的标识,所以需要小心。 - Tim Robinson
@Tim:我添加了ChangeRequests.Equals的两个实现到我的问题中,但是我没有重写GetHashCode...也许我应该这样做! - Sarah Vessels
1
没错,你的 GetHashCode 方法需要与你的 Equals 方法保持一致。看起来 Union 方法确实同时使用了这两个方法。 - Tim Robinson
1
你应该绝对重写GetHashCode方法。我很惊讶编译器还没有警告你这个问题。 - Jon Skeet
@Tim:你应该把你“修复GetHashCode”的建议发表为一个答案——我会选择它作为选定的答案!结果证明这是一个GetHashCode问题。现在Union已经按预期工作。 :) - Sarah Vessels
3个回答

4
请确保您的GetHashCode实现与Equals一致——Enumerable.Union方法似乎使用了两者。

如果您只实现了其中一个方法而没有实现另一个方法,则编译器会发出警告;但仍需要您自行确保这两种方法相互一致。以下链接是一份方便的规则摘要:为什么在覆盖Equals方法时重写GetHashCode方法很重要?


3
我不相信Assert.AreEqual()会检查序列的内容——它比较的是序列对象本身,而这些对象显然不相等。
你需要的是一个SequenceEqual()方法,它会实际检查两个序列的内容。这篇答案可能会对你有所帮助。这是对一个类似问题的回答,描述了如何比较IEnumerable<>序列。
你可以轻松地采用答复者的答案,并创建一个扩展方法,使调用看起来更像断言。
public static class AssertionExt
{
  public static bool AreSequencesEqual<T>( IEnumerable<T> expected, 
                                           IEnumerable<T> sequence )
  {
    Assert.AreEqual(expected.Count(), sequence .Count()); 

    IEnumerator<Token> e1 = expected.GetEnumerator(); 
    IEnumerator<Token> e2 = sequence .GetEnumerator(); 

    while (e1.MoveNext() && e2.MoveNext()) 
    { 
        Assert.AreEqual(e1.Current, e2.Current); 
    }
  }
}

或者您可以使用SequenceEqual()来比较序列,但需要注意的是它不提供关于哪些元素不相等的任何信息。


尝试使用SequenceEqual仍然失败了。我已经在ChangeRequests上重写了Equals,因此在两个ChangeRequests上调用AreEqual应该可以工作。我的ChangeRequests.Equals覆盖比较序列。 - Sarah Vessels
@Sarah - 很奇怪SequenceEqual不起作用 - 它应该使用默认的相等比较器,这将测试T是否为IEquatable<T>,并使用适合该实现的相等比较器。 - LBushkin

0
正如LBushkin所说,“Assert.AreEqual”只会在序列上调用“Equals”。
但你可以使用SequenceEqual扩展方法:
Assert.IsTrue(expected.SequenceEqual(unionedCRs));

然而,如果失败了,这并不会提供太多信息。

您可能想使用我们为 MoreLINQ 编写的 测试代码,它是以序列为重点的 - 如果序列不相等,它将指定它们之间的差异方式。(我正在尝试获取有关源文件的链接,但我的网络连接很差。)


我已经在 ChangeRequests 上重写了 Equals 方法,所以使用 AreEqual 应该可以工作,我认为。用 Assert.IsTrue(expected.SequenceEqual<ChangeRequest>(unionedCRs), "从 Union 中移除重复项") 替换我的测试的最后一行仍然失败了。 - Sarah Vessels
@Sarah:使用Assert.Equal时,它不会调用ChangeRequests类型的Equals方法,而是调用序列的Equals方法,这不会覆盖Equals方法。但使用SequenceEqual应该可以解决问题。 - Jon Skeet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接