创建一个 IEqualityComparer<IEnumerable<T>>

8

我正在使用xUnit,但它没有一种方法来确定两个IEnumerable<T>是否相等,如果T是自定义类型。

我尝试使用LINQ SequenceEqual,但由于T实例不同,这会返回false;

这里是一个基本的测试,使用了一个不起作用的IEqualityComparer

    [Fact]
    public void FactMethodName()
    {
        var one = new[] { new KeywordSchedule() { Id = 1 } };
        var two = new[] { new KeywordSchedule() { Id = 1 } };

        Assert.Equal(one, two, new KeywordScheduleComparer());
    }

public class KeywordScheduleComparer : IEqualityComparer<IEnumerable<KeywordSchedule>>
{
    public bool Equals(IEnumerable<KeywordSchedule> x, IEnumerable<KeywordSchedule> y)
    {
        return Object.ReferenceEquals(x, y) || (x != null && y != null && x.SequenceEqual(y));
    }

    public int GetHashCode(IEnumerable<KeywordSchedule> obj)
    {
        if (obj == null)
            return 0;

        return unchecked(obj.Select(e => e.GetHashCode()).Aggregate(0, (a, b) => a + b));  // BAD
    }
}

我正在进行一项集成测试,因此我在开始时将IEnumerable中的数据插入到数据库中,然后调用我的SUT从数据库中检索数据并进行比较。

如果您能帮助我使集合比较工作起来,我将不胜感激!

3个回答

17

我刚验证了这个在xUnit.net 1.9.2上工作正常:

public class MyClass
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class MyClassComparer : IEqualityComparer<MyClass>
{
    public bool Equals(MyClass x, MyClass y)
    {
        return x.ID == y.ID;
    }

    public int GetHashCode(MyClass obj)
    {
        return obj.ID.GetHashCode();
    }
}

public class ExampleTest
{
    [Fact]
    public void TestForEquality()
    {
        var obj1 = new MyClass { ID = 42, Name = "Brad" };
        var obj2 = new MyClass { ID = 42, Name = "Joe" };

        Assert.Equal(new[] { obj1 }, new[] { obj2 }, new MyClassComparer());
    }
}

我不完全清楚为什么你需要额外的比较器。单一的比较器应该足够了。


啊,谢谢,我没有太离谱。我会给Equals方法添加更多属性,但是我需要为GetHashCode方法做些什么吗? - Jon
从技术上讲,你应该实现GetHashCode,但如果比较器仅用于xUnit.net,你可以跳过它,因为我们不使用GetHashCode。只需留下一条注释,以便在将来决定真正使用该功能时再实现它。 :) - Brad Wilson

1

好的,您的实现还未完成。您为 IEnumerable<KeywordSchedule> 实现了自定义比较器,但忘记为 KeywordSchedule 实现相同的比较器。

x.SequenceEqual 仍然使用 Comparer<T>.Default,因此进行引用比较,结果为false。

public class KScheduleComparer : IEqualityComparer<KeywordSchedule>
{
    public bool Equals(KeywordSchedule x, KeywordSchedule y)
    {
        return x.Id == y.Id;                
    }

    public int GetHashCode(KeywordSchedule obj)
    {
        return obj.GetHashCode();
    }
}

然后在KeywordScheduleComparer类中修改您的Equals方法,如下所示。
public class KeywordScheduleComparer : IEqualityComparer<IEnumerable<KeywordSchedule>>
{
    public bool Equals(IEnumerable<KeywordSchedule> x, IEnumerable<KeywordSchedule> y)
    {
        return Object.ReferenceEquals(x, y) || (x != null && y != null && x.SequenceEqual(y, new KScheduleComparer()));
    }

    public int GetHashCode(IEnumerable<KeywordSchedule> obj)
    {
        if (obj == null)
            return 0;

        return unchecked(obj.Select(e => e.GetHashCode()).Aggregate(0, (a, b) => a + b));  // BAD
    }
}

那么在 KeywordSchedule 类上放置公共布尔 Equals(IEnumerable<KeywordSchedule> x, IEnumerable<KeywordSchedule> y) 方法? - Jon
不应该在其他类中,应该放在KeywordScheduleComparer类中。如果不清楚,我会更新完整的代码。 - Sriram Sakthivel
谢谢。为什么有两个比较器? - Jon
@Jon 因为你需要能够比较两种不同类型的事物,即底层项目和项目序列。如果你只需要比较一种类型,那么你只需要一个比较器。 - Servy
@Jon,我认为Servy已经回答了你的问题。或者你可以直接使用bool res = one.SequenceEqual(two, new KScheduleComparer());,这样就可以摆脱第二个比较器了。 - Sriram Sakthivel
显示剩余2条评论

0

您可以使用FluentAssertions库更优雅地完成此操作。它具有丰富的集合断言方法。

public class MyClass
{
    public int ID { get; set; }
    public string Name { get; set; }

    protected bool Equals(MyClass other)
    {
        return ID == other.ID;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != GetType()) return false;
        return Equals((MyClass) obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return (ID*397) ^ (Name != null ? Name.GetHashCode() : 0);
        }
    }
}

public class ExampleTest
{
    [Fact]
    public void TestForEquality()
    {
        var obj1 = new MyClass { ID = 42, Name = "Rock" };
        var obj2 = new MyClass { ID = 42, Name = "Paper" };
        var obj3 = new MyClass { ID = 42, Name = "Scissors" };
        var obj4 = new MyClass { ID = 42, Name = "Lizard" };

        var list1 = new List<MyClass> {obj1, obj2};
        list1.Should().BeEquivalentTo(obj3, obj4);
    }
}

虽然我同意,FluentAssertions 是许多此类问题的好解决方案,但你的例子并没有直接回答这个问题。也就是说,你没有断言两个列表是否相等(你只有一个),而且如果我没记错的话,默认使用 BeEquivalentTo 会忽略比较集合中项目的顺序,这可能不是期望的结果(因为问题中的示例使用了 SequenceEqual)。此外,当使用 FluentAssetions 时,实际上你不需要在 MyClass 中重写 Equals - 你可以提供比较规则给 FluentAssertions 来按属性进行比较。 - Hilarion

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