Подтвердить что ты не робот

Преимущества/Недостатки различных реализаций для сравнения объектов

Эти вопросы включают в себя две различные реализации по существу одного и того же кода.

Сначала, используя делегат для создания метода сравнения, который можно использовать в качестве параметра при сортировке коллекции объектов:

class Foo
{
    public static Comparison<Foo> BarComparison = delegate(Foo foo1, Foo foo2)
    {
        return foo1.Bar.CompareTo(foo2.Bar);
    };
}

Я использую вышеуказанное, когда хочу иметь способ сортировки коллекции объектов Foo по-другому, чем предлагает функция CompareTo. Например:

List<Foo> fooList = new List<Foo>();
fooList.Sort(BarComparison);

Во-вторых, используя IComparer:

public class BarComparer : IComparer<Foo>
{
    public int Compare(Foo foo1, Foo foo2)
    {
        return foo1.Bar.CompareTo(foo2.Bar);
    }
}

Я использую вышеуказанное, когда хочу сделать двоичный поиск объекта Foo в коллекции объектов Foo. Например:

BarComparer comparer = new BarComparer();
List<Foo> fooList = new List<Foo>();
Foo foo = new Foo();
int index = fooList.BinarySearch(foo, comparer);

Мои вопросы:

  • Каковы преимущества и недостатки каждой из этих реализаций?
  • Какие еще способы использовать преимущества каждой из этих реализаций?
  • Есть ли способ объединить эти реализации таким образом, что мне не нужно дублировать код?
  • Могу ли я получить двоичный поиск и альтернативную сортировку, используя только одну из этих реализаций?
4b9b3361

Ответ 1

Вероятно, самым большим преимуществом для принятия Comparison<T> в отличие от IComparer<T> является возможность писать анонимные методы. Если у меня есть, скажем, a List<MyClass>, где MyClass содержит свойство ID, которое должно использоваться для сортировки, я могу написать:

myList.Sort((c1, c2) => c1.ID.CompareTo(c2.ID));

Это намного удобнее, чем писать целую реализацию IComparer<MyClass>.

Я не уверен, что принятие IComparer<T> действительно имеет важные преимущества, за исключением совместимости с устаревшим кодом (включая классы .NET Framework). Свойство Comparer<T>.Default действительно полезно для примитивных типов; все остальное обычно требует дополнительной работы для кодирования.

Чтобы избежать дублирования кода, когда мне нужно работать с IComparer<T>, одна вещь, которую я обычно делаю, это создать общий сравнитель, например:

public class AnonymousComparer<T> : IComparer<T>
{
    private Comparison<T> comparison;

    public AnonymousComparer(Comparison<T> comparison)
    {
        if (comparison == null)
            throw new ArgumentNullException("comparison");
        this.comparison = comparison;
    }

    public int Compare(T x, T y)
    {
        return comparison(x, y);
    }
}

Это позволяет писать код, например:

myList.BinarySearch(item,
    new AnonymousComparer<MyClass>(x.ID.CompareTo(y.ID)));

Это не совсем красиво, но это экономит некоторое время.

Другой полезный класс, который у меня есть, следующий:

public class PropertyComparer<T, TProp> : IComparer<T>
    where TProp : IComparable
{
    private Func<T, TProp> func;

    public PropertyComparer(Func<T, TProp> func)
    {
        if (func == null)
            throw new ArgumentNullException("func");
        this.func = func;
    }

    public int Compare(T x, T y)
    {
        TProp px = func(x);
        TProp py = func(y);
        return px.CompareTo(py);
    }
}

Для чего вы можете написать код, предназначенный для IComparer<T>, как:

myList.BinarySearch(item, new PropertyComparer<MyClass, int>(c => c.ID));

Ответ 2

На самом деле нет никакого преимущества ни по одному из вариантов с точки зрения производительности. Это действительно вопрос удобства и надежности кода. Выберите вариант, который вы предпочитаете. При этом указанные методы ограничивают ваш выбор незначительно.

Вы можете использовать интерфейс IComparer<T> для List<T>.Sort, который позволит вам не дублировать код.

К сожалению, BinarySearch не реализует параметр с помощью Comparison<T>, поэтому вы не можете использовать делегат Comparison<T> для этого метода ( по крайней мере, не напрямую).

Если вы действительно хотели использовать Comparison<T> для обоих, вы могли бы создать общую реализацию IComparer<T>, которая приняла делегата Comparison<T> в своем конструкторе и реализовала IComparer<T>.

public class ComparisonComparer<T> : IComparer<T>
{
    private Comparison<T> method;
    public ComparisonComparer(Comparison<T> comparison)
    {
       this.method = comparison;
    }

    public int Compare(T arg1, T arg2)
    {
        return method(arg1, arg2);
    }
}

Ответ 3

Метод делегата очень короткий (лямбда-выражения могут быть еще короче), поэтому, если более короткий код является вашей целью, то это преимущество.

Однако реализация IComparer (и его эквивалента общего назначения) делает ваш код более подверженным тестированию: вы можете добавить некоторое модульное тестирование к вашему классу/методу сравнения.

Кроме того, вы можете повторно использовать реализацию компаратора при составлении двух или более сопоставлений и комбинировать их как новый сопоставитель. Повторное использование кода с анонимными делегатами сложнее.

Итак, подведем итог:

Анонимные делегаты: более короткий (и, возможно, более чистый) код

Явная реализация: тестируемость и повторное использование кода.

Ответ 4

В вашем случае преимущество наличия делегата IComparer<T> over Comparision<T> заключается в том, что вы также можете использовать его для метода Sort, так что вам не нужна версия делегата Comparison вообще.

Еще одна полезная вещь, которую вы можете сделать, - реализовать делегированную реализацию IComparer<T> следующим образом:

public class DelegatedComparer<T> : IComparer<T>
{
  Func<T,T,int> _comparision;
  public DelegatedComparer(Func<T,T,int> comparision)
  {
    _comparision = comparision;
  }
  public int Compare(T a,T b) { return _comparision(a,b); }
}

list.Sort(new DelegatedComparer<Foo>((foo1,foo2)=>foo1.Bar.CompareTo(foo2.Bar));

и более продвинутая версия:

public class PropertyDelegatorComparer<TSource,TProjected> : DelegatedComparer<TSource>
{
  PropertyDelegatorComparer(Func<TSource,TProjected> projection)
    : base((a,b)=>projection(a).CompareTo(projection(b)))
}

Ответ 5

Они действительно решают разные потребности:

IComparable полезен для упорядоченных объектов. Реальные числа должны быть сопоставимы, но комплексные числа не могут - они не определены.

IComparer позволяет определять повторно используемые, хорошо инкапсулированные компараторы. Это особенно полезно, если для сравнения необходимо знать дополнительную информацию. Например, вы можете сравнить даты и время из разных часовых поясов. Это может быть сложным, и для этой цели следует использовать отдельный компаратор.

Используется метод сравнения для простых операций сравнения, которые не являются достаточно сложными для обеспечения возможности повторного использования, например, сортировка списка клиентов по их имени. Это простая операция, поэтому не нужны дополнительные данные. Аналогично, это не присуще объекту, потому что объекты не упорядочены естественным образом.

Наконец, существует IEquatable, что может быть важно, если ваш метод Equals может решить только, если два объекта равны или нет, но если нет понятия "больше" и "меньше", например. комплексные числа или векторы в пространстве.