C#:如何在检查相等性时评估对象的多重比较 类?

C#: How to evaluate multipe Comparer classes for an object when checking equality?

动机

我希望以与下面的演示代码类似的方式实现 IComparer<>。其中 Foo 是我需要比较的对象类型。它没有实现 IComparable,但我为每个字段提供了 IComparer class,因此用户可以选择等同于基于一个字段值的实例。

enum Day {Sat, Sun, Mon, Tue, Wed, Thu, Fri};

class Foo {
    public int Bar;
    public string Name;
    public Day Day;
}

比较器class是:

// Compares all fields in Foo
public class FooComparer : IEqualityComparer<Foo>
{
    public bool Equals(Foo x, Foo y)
    {
        if (ReferenceEquals(x, y)) return true;
        return x.Bar == y.Bar && x.Name == y.Name && return x.Day == y.Day;
    }

    public int GetHashCode(Foo obj)
    {
        unchecked
        {
            var hashCode = obj.Bar;
            hashCode = (hashCode * 397) ^ (obj.Name != null ? obj.Name.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (int) obj.Day; 0);
            return hashCode;
        }
    }
}

// Compares only in Foo.Bar
public class FooBarComparer : IEqualityComparer<Foo>
{
    public bool Equals(Foo x, Foo y)
    {
        if (ReferenceEquals(x, y)) return true;
        return x.Bar == y.Bar;
    }

    public int GetHashCode(Foo obj)
    {
        unchecked
        {
            var hashCode = obj.Bar;
            hashCode = (hashCode * 397) ^ (obj.Name != null ? obj.Name.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (int) obj.Day; 0);
            return hashCode;
        }
    }
}

// Compares only in Foo.Name
public class FooNameComparer : IEqualityComparer<Foo>
{
    public bool Equals(Foo x, Foo y)
    {
        if (ReferenceEquals(x, y)) return true;
        return x.Name == y.Name;
    }

    public int GetHashCode(Foo obj)
    {
        unchecked
        {
            var hashCode = obj.Bar;
            hashCode = (hashCode * 397) ^ (obj.Name != null ? obj.Name.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (int) obj.Day; 0);
            return hashCode;
        }
    }
}

// Compares only in Foo.Day
public class FooDayComparer : IEqualityComparer<Foo>
{
    public bool Equals(Foo x, Foo y)
    {
        if (ReferenceEquals(x, y)) return true;
        return x.Day == y.Day;
    }

    public int GetHashCode(Foo obj)
    {
        unchecked
        {
            var hashCode = obj.Bar;
            hashCode = (hashCode * 397) ^ (obj.Name != null ? obj.Name.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (int) obj.Day; 0);
            return hashCode;
        }
    }
}

问题

我想让用户能够组合多个 Comparer 类型来评估类型 Foo 的两个实例。我不确定该怎么做。

想法

我想出的是这样的,我 AND 列表中所有比较器的比较结果:

bool CompareFoo(Foo a, Foo b, params IComparer[] comparers)
{
    bool isEqual = true;
    // Or the list and return;
    foreach (var comparer in comparers)
    {
        isEqual = isEqual && comparer.Equals(x,y);
    }
    return isEqual;
}

备注

你可以通过这样的方式实现:

class Program
{
    static bool CompareFoo(Foo a, Foo b, List<IEqualityComparer<Foo>> comparers)
    {
        return comparers.All(com => com.Equals(a, b));
    }

    static void Main(string[] args)
    {
        List<IEqualityComparer<Foo>> compares = new List<IEqualityComparer<Foo>>
        {
            new FooNameComparer(),
            new FooBarComparer()
        };

        var test1 = CompareFoo(new Foo { Name = "aio", Bar = 10 }, new Foo { Name = "aio", Bar = 10 }, compares);
        var test2 = CompareFoo(new Foo { Name = "Foo1", Bar = 10 }, new Foo { Name = "Foo2", Bar = 10 }, compares);
    }
}

注意:您必须在比较 class 中考虑所有可能的条件,例如在 "FooNameComparer" class 中,以下代码可能会成为错误:

return x.Name == y.Name;

因为如果两个class中的"Name" 属性传null,null == null return true!代码应该是:

public bool Equals(Foo x, Foo y)
{
    if (ReferenceEquals(x, y)) return true;
    if (string.IsNullOrEmpty(x?.Name) || string.IsNullOrEmpty(y?.Name))
        return false;

    return x.Name == y.Name; 
}

您可以通过定义一个将其他比较器作为构造函数参数的附加比较器来组合多个 IEqualityComparer<Foo> 对象:

public class CompositeFooComparer : IEqualityComparer<Foo>
{
    private IEqualityComparer<Foo>[] comparers;
    public CompositeFooComparer(params IEqualityComparer<Foo>[] comparers)
    {
        this.comparers = comparers;
    }

    public bool Equals(Foo x, Foo y)
    {
        foreach (var comparer in comparers)
        {
            if (!comparer.Equals(x, y))
            {
                return false;
            }
        }
        return true;
    }

    public int GetHashCode(Foo obj)
    {
        var hash = 0;
        foreach (var comparer in comparers)
        {
            hash = hash * 17 + (comparer.GetHashCode(obj));
        }
        return hash;
    }
}

然后你可以这样创建和使用它:

var fooA = new Foo
{
    Bar = 5,
    Day = Day.Fri,
    Name = "a"
};

var fooB = new Foo
{
    Bar = 5,
    Day = Day.Fri,
    Name = "b"
};

var barComparer = new FooBarComparer();
var dayComparer = new FooDayComparer();
var compositeComparer = new CompositeFooComparer(barComparer, dayComparer);

Console.WriteLine(compositeComparer.Equals(fooA, fooB)); // displays "true"

另一个想法是有一个比较器确实知道将比较哪些字段,而不是基于布尔参数。

public class ConfigurableFooComparer : IEqualityComparer<Foo>
{
    private readonly bool compareBar;
    private readonly bool compareName;
    private readonly bool compareDay;

    public ConfigurableFooComparer(bool compareBar, bool compareName, bool compareDay)
    {
        this.compareBar = compareBar;
        this.compareName = compareName;
        this.compareDay = compareDay;
    }

    public bool Equals(Foo x, Foo y)
    {
        if (ReferenceEquals(x, y))
        {
            return true;
        }
        if (x == null || y == null)
        {
            return false;
        }

        if (compareBar && x.Bar != y.Bar)
        {
            return false;
        }
        if (compareName && x.Name != y.Name)
        {
            return false;
        }
        if (compareDay && x.Day != y.Day)
        {
            return false;
        }

        return true;
    }

    public int GetHashCode(Foo obj)
    {
        unchecked
        {
            var hash = 0;

            if (compareBar)
            {
                hash = hash * 17 + obj.Bar.GetHashCode();
            }
            if (compareName)
            {
                hash = hash * 17 + (obj.Name == null ? 0 : obj.Name.GetHashCode());
            }
            if (compareDay)
            {
                hash = hash * 17 + obj.Day.GetHashCode();
            }

            return hash;
        }
    }

然后像这样使用它:

var barAndDayComparer = new ConfigurableFooComparer(compareBar: true, compareName: false, compareDay: true);
Console.WriteLine(barAndDayComparer.Equals(fooA, fooB));

在我看来,您想要实现的目标与 Chain-of-responsibility.

非常相似

那么,为什么不将所有 Foo 比较器安排在链状结构中并使链可扩展,以便可以在 运行 时添加新链接?

想法是这样的:

客户端将实现它想要的任何 Foo 比较器,所有的 Foo 比较器都将被整齐地排列,这样所有的比较器都将被一个一个地调用,如果任何一个 returns false 则整个比较 returns假的!

代码如下:

public abstract class FooComparer
{
    private readonly FooComparer _next;

    public FooComparer(FooComparer next)
    {
        _next = next;
    }

    public bool CompareFoo(Foo a, Foo b)
    {
        return AreFoosEqual(a, b) 
            && (_next?.CompareFoo(a, b) ?? true);
    }

    protected abstract bool AreFoosEqual(Foo a, Foo b);
}

public class FooNameComparer : FooComparer
{
    public FooNameComparer(FooComparer next) : base(next)
    {
    }

    protected override bool AreFoosEqual(Foo a, Foo b)
    {
        return a.Name == b.Name;
    }
}

public class FooBarComparer : FooComparer
{
    public FooBarComparer(FooComparer next) : base(next)
    {
    }

    protected override bool AreFoosEqual(Foo a, Foo b)
    {
        return a.Bar == b.Bar;
    }
}

FooComparer 摘要 class 的想法是拥有类似于连锁店经理的东西;它处理整个链的调用并强制其派生的 classes 实现代码来比较 Foo 的代码,同时公开客户端将使用的方法 CompareFoo

客户将如何使用它?它可以做这样的事情:

var manager = new FooManager();

manager.FooComparer
    = new FooNameComparer(new FooBarComparer(null));

manager.FooComparer.CompareFoo(fooA, fooB);

但是,如果他们可以将 FooComparer 链注册到 IoC 容器,那就更酷了!

编辑

这是一种我已经使用了一段时间的更简单的自定义比较方法:

public class GenericComparer<T> : IEqualityComparer<T> where T : class
{
    private readonly Func<T, object> _identitySelector;

    public GenericComparer(Func<T, object> identitySelector)
    {
        _identitySelector = identitySelector;
    }
    public bool Equals(T x, T y)
    {
        var first = _identitySelector.Invoke(x);
        var second = _identitySelector.Invoke(y);

        return first != null && first.Equals(second);
    }
    public int GetHashCode(T obj)
    {
        return _identitySelector.Invoke(obj).GetHashCode();
    }
}

public bool CompareFoo2(Foo a, Foo b, params IEqualityComparer<Foo>[] comparers)
{
    foreach (var comparer in comparers)
    {
        if (!comparer.Equals(a, b))
        {
            return false;
        }
    }

    return true;
}

让客户做:

var areFoosEqual = CompareFoo2(a, b, 
new GenericComparer<Foo>(foo => foo.Name), 
new GenericComparer<Foo>(foo => foo.Bar))

可能可以调整 GenericComparer 使其具有多个身份选择器,以便在单个 lambda 中将它们全部传递,但我们还需要更新其 GetHashCode 方法以使用所有身份正确计算 HashCode对象。