C #: Как оценить несколько классов Comparer для объекта при проверке равенства? - PullRequest
0 голосов
/ 15 сентября 2018

Мотивация

Я собираюсь реализовать IComparer<> аналогично демонстрационному коду ниже.Где Foo - это тип объектов, которые мне нужно сравнить.Он не реализует IComparable, но я предоставляю класс IComparer для каждого поля, чтобы пользователь мог выбрать приравнивание к экземплярам на основе значения одного поля.

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

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

Классы сравнения:

// 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;
}

Примечания

  • Моя целевая версия .NET 4.5.
  • Возможно, я застрял с C# 5.0.
  • Кроме того, может зависнуть `MSBuild 12.0
  • . Я впервые использую IComparer.
.

Ответы [ 3 ]

0 голосов
/ 15 сентября 2018

Вы можете объединить несколько 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));
0 голосов
/ 15 сентября 2018

Мне кажется, что то, что вы пытаетесь достичь, очень похоже на цепочку ответственности .

Так почему бы вам не расположить всех своих сравнителей Foo вцепочечная структура и делает цепочку расширяемой, чтобы новые ссылки могли быть добавлены во время выполнения?

Вот идея:

клиент будет реализовывать любые средства сравнения Foo, которые он хочет, и всеони будут аккуратно организованы таким образом, что все они будут вызываться один за другим, и если кто-то вернет false, тогда все сравнение возвращает false!

Вот код:

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 заключается в том, чтобы иметь что-то вроде менеджера цепочки;он обрабатывает вызов всей цепочки и вынуждает его производные классы реализовывать код для сравнения 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 должен иметь несколько селекторов идентификаторов, чтобы передавать их все в одну лямбду, но нам также необходимо обновить его GetHashCode метод для правильного вычисления HashCode, используя все объекты идентификаторов.

0 голосов
/ 15 сентября 2018

Вы можете достичь чего-то вроде этого:

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);
    }
}

Примечание: вы должны учитывать все возможные условия в ваших классах сравнения, например, в классе "FooNameComparer" приведенный ниже код может стать ошибкой:

return x.Name == y.Name;

, потому что если свойство «Имя» двух классов принимает значение null, null == null возвращает 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; 
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...