Почему мне приходится перегружать операторы при реализации CompareTo?

Допустим, у меня есть тип, который реализует IComparable.

Я бы подумал, что разумно ожидать, что операторы ==, !=, >, <, >= и <= будут "просто работать" автоматически при вызове CompareTo, но вместо этого я должен переопределить их все, если я хочу их использовать.

С точки зрения языкового дизайна, есть ли веская причина, по которой это было сделано именно так? Есть ли случаи, когда вам действительно полезно, чтобы A>B вел себя не так, как Compare(A,B)>0?


person Andy    schedule 15.12.2013    source источник
comment
Вы не звоните Compare(A, B), вы звоните A.CompareTo(B). И это означает, что он никогда не сможет работать, если A равно null, что вы можете захотеть поддерживать с помощью перегруженного оператора (x == null и null == x должны проверять одно и то же). Я вовсе не уверен, что это основная причина.   -  person    schedule 15.12.2013


Ответы (2)


Вся ситуация неприятная. В C# слишком много способов выразить равенство и неравенство:

  • операторы == != > ‹ >= ‹= (которые являются логически статическими методами)
  • статический метод Equals (который вызывает виртуальный метод), виртуальный метод Equals, метод ReferenceEquals
  • Интерфейсы IComparable и IEquatable

Все они имеют немного разную семантику, и, за исключением статического Equals, ни один из них не использует автоматически другой, и ни один из них на самом деле не имеет нужного мне поведения. Статические методы отправляются на основе типа времени компиляции обоих операндов; виртуальные методы/методы интерфейса отправляются на основе типа времени выполнения одного из операндов, что делает операцию асимметричной; тип одной стороны имеет большее значение, чем тип другой.

Я не могу себе представить, чтобы кто-то думал, что положение, в котором мы находимся, прекрасно; при отсутствии ограничений это не то, что могло бы развиться. Но у разработчиков управляемых языков есть ограничения: CLR не реализует статические методы в контрактах интерфейса или двойную виртуальную диспетчеризацию, или возможность наложить ограничение оператора на параметр универсального типа. И поэтому появилось множество решений для решения проблемы равенства/неравенства.

Я думаю, что если бы разработчики CLR и C# вернулись в прошлое и рассказали себе в прошлом, какие функции должны быть в версии 1 CLR, некоторые формы статических методов в интерфейсах были бы первыми в списке. Если бы в интерфейсе были статические методы, то мы могли бы определить:

interface IComparable<in T, in U> 
{
    static bool operator <(T t, U u);
    static bool operator >(T t, U u);
    ... etc

И тогда, если у вас есть:

static void Sort<T>(T[] array) where T : IComparable<T, T>

Затем вы можете использовать операторы < и == и так далее для сравнения элементов.

person Eric Lippert    schedule 15.12.2013
comment
Это не будет интерфейс в нынешнем понимании этого термина, не так ли? Я не понимаю, как можно было бы осмысленно использовать предложенный вами IComparable<T, T> за пределами ограничения универсального типа. Мне это больше похоже на предложенные концепции С++. - person ; 15.12.2013
comment
@hvd: Предлагаемая функция в настоящее время не реализована, поэтому по определению она меняет значение интерфейса. Возможно, лучше было бы использовать другой термин, например понятие; Я не знаю. Попытка разработать функцию в этом комментарии из 500 символов кажется плохой идеей. - person Eric Lippert; 15.12.2013
comment
О, я, конечно, не прошу об этом. Я просто думаю, что то, что вы можете сделать с этим интерфейсом, коренным образом отличается от того, что вы можете делать с интерфейсами в текущем C#, и от того, что вы можете делать с ними в других языках, в которых они были до появления C#. Я, наверное, просто слишком много думаю об этом. - person ; 15.12.2013
comment
Спасибо, Эрик, это действительно отличная дискуссия. Как обычно, ответ таков, что это сложнее, чем я думал :) - person Andy; 16.12.2013
comment
Идея предоставления статических методов в интерфейсе, возможно, переименованных в контракт, была бы невероятно полезной. Например, при создании архитектуры, в которой вы действительно хотите, чтобы статические классы реализовывали один и тот же контракт, в конечном итоге вам придется создавать синглтоны, реализующие интерфейс. Таким образом, они выставляют свойство Instance или eek, создавая набор статических методов для имитации методов экземпляра, которые используют свойство Instance самостоятельно. В любом случае, Эрик, это была бы феноменальная функция, но с вашим объяснением я вижу препятствия, которые им теперь придется преодолеть. - person Mike Perrenoud; 17.12.2013
comment
Различные способы сравнения вещей полезны в разных контекстах. Я бы сказал, что в хорошем языке вызовы ReferenceEquals никогда не должны быть необходимы (должен быть оператор, который служит этой цели во всех случаях, когда оба операнда являются ссылочными типами, и запрещен во всех других), но в противном случае лучше иметь больше способов сравнения вещей, каждый из которых может иметь согласованное поведение, когда это допустимо во время компиляции (независимо от того, полезен он во всех контекстах или нет), чем пытаться втиснуть больше значений в меньшее количество способов сравнения. - person supercat; 14.01.2014

Две основные причины:

  1. Это общая структура для всех операторов. Хотя операторы сравнения могут никогда не иметь альтернативной семантики, очень полезна структура, допускающая очень различную семантику для некоторых других операторов. Реализация отдельной структуры только для операторов сравнения потребовала бы отказа от некоторых других, возможно, гораздо более полезных функций. Взгляните на эту элегантную реализацию BNF в C# в качестве примера.
  2. Реализации по умолчанию для типов значений, которые имеют его, по необходимости полагаются на отражение и, следовательно, ужасно неэффективны. Только вы действительно знаете наиболее эффективный способ реализации этих операторов для ваших классов. Во многих случаях не все поля структуры нужно сравнивать для проверки на равенство, и не всегда нужно объединять все поля в подходящей реализации GetHashCode. Никакая реализация по умолчанию не может определить это для всех типов, потому что это сводится к проблеме остановки.

Обновление в соответствии с Eric Lippert среди прочего, ниже приведена подходящая стандартная реализация операторов сравнения в C# для UDT типа:

public int  CompareTo(UDT x) { return CompareTo(this, x); }
public bool Equals(UDT x)    { return CompareTo(this, x) == 0; }
public static bool operator  < (UDT x, UDT y) { return CompareTo(x, y)  < 0; }
public static bool operator  > (UDT x, UDT y) { return CompareTo(x, y)  > 0; }
public static bool operator <= (UDT x, UDT y) { return CompareTo(x, y) <= 0; }
public static bool operator >= (UDT x, UDT y) { return CompareTo(x, y) >= 0; }
public static bool operator == (UDT x, UDT y) { return CompareTo(x, y) == 0; }
public static bool operator != (UDT x, UDT y) { return CompareTo(x, y) != 0; }
public override bool Equals(object obj)
{
    return (obj is UDT) && (CompareTo(this, (UDT)obj) == 0);
}

Просто добавьте пользовательское определение для private static int CompareTo(UDT x, UDT y) и перемешайте.

person Pieter Geerkens    schedule 15.12.2013