Почему мы должны определять оба == и! = В C #? - PullRequest
339 голосов
/ 02 августа 2011

Компилятор C # требует, чтобы всякий раз, когда пользовательский тип определял оператор ==, он также определял != (см. здесь ).

Почему?

Мне любопытно узнать, почему дизайнеры посчитали это необходимым и почему компилятор не может по умолчанию использовать разумную реализацию для одного из операторов, когда присутствует только другой.Например, Lua позволяет вам определять только оператор равенства, а другой вы получаете бесплатно.C # мог бы сделать то же самое, попросив вас определить либо ==, либо оба == и! =, А затем автоматически скомпилировать отсутствующий оператор! = Как !(left == right).

Я понимаю, что существуют странные угловые случаи, когда некоторыесущности не могут быть ни равными, ни неравными (как, например, NaN IEEE-754), но они кажутся исключением, а не правилом.Так что это не объясняет, почему разработчики компилятора C # сделали исключение из правила.

Я видел случаи плохого качества изготовления, когда оператор равенства определен, тогда оператор неравенства является копией-вставкой с каждым икаждое сравнение отменяется, и каждый && переключается на ||(Вы понимаете, в чем дело ... в основном! (a == b) расширено по правилам Де Моргана).Это плохая практика, которую компилятор может исключить при разработке, как в случае с Lua.

Примечание: То же самое верно для операторов <> <=> =.Я не могу представить случаи, когда вам нужно будет определить их неестественным образом.Lua позволяет вам определять только <и <= и определяет> = и> естественно через отрицание прежних.Почему C # не делает то же самое (по крайней мере, «по умолчанию»)?

РЕДАКТИРОВАТЬ

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

Суть моего вопроса, однако, заключается в том, почему это принудительно требуется в C #, когда обычно это не логически необходимо?

Это также поразительно отличается от выбора дизайна для .NET-интерфейсов, таких как Object.Equals, IEquatable.Equals IEqualityComparer.Equals, где отсутствие аналога NotEquals показывает, что платформа учитывает !Equals() объекты как неравные и это все.Кроме того, классы, подобные Dictionary, и методы, подобные .Contains(), зависят исключительно от вышеупомянутых интерфейсов и не используют операторы напрямую, даже если они определены.Фактически, когда ReSharper генерирует элементы равенства, он определяет и ==, и != в терминах Equals(), и даже тогда, только если пользователь вообще решает генерировать операторы.Операторы равенства не нужны платформе для понимания равенства объектов.

По сути, .NET Framework не заботится об этих операторах, он только заботится о нескольких Equals методах.Решение требовать, чтобы операторы == и! = Были определены пользователем в тандеме, связано исключительно с языковым дизайном, а не с семантикой объектов в отношении .NET.

Ответы [ 13 ]

155 голосов
/ 03 августа 2011

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

Глядя на этот базовый код F #, вы можете скомпилировать его в рабочую библиотеку. Это допустимый код для F #, и он перегружает только оператор равенства, а не неравенство:

module Module1

type Foo() =
    let mutable myInternalValue = 0
    member this.Prop
        with get () = myInternalValue
        and set (value) = myInternalValue <- value

    static member op_Equality (left : Foo, right : Foo) = left.Prop = right.Prop
    //static member op_Inequality (left : Foo, right : Foo) = left.Prop <> right.Prop

Это именно так и выглядит. Он создает компаратор равенства только на == и проверяет, равны ли внутренние значения класса.

Хотя вы не можете создать такой класс в C #, вы можете использовать класс, скомпилированный для .NET. Очевидно, он будет использовать наш перегруженный оператор для == Итак, что же использует среда выполнения для !=?

Стандарт C # EMCA содержит целый набор правил (раздел 14.9), объясняющих, как определить, какой оператор использовать при оценке равенства. Говоря слишком упрощенно и, следовательно, не вполне точно, если сравниваемые типы имеют одинаковый тип и , если присутствует перегруженный оператор равенства, он будет использовать эту перегрузку, а не стандартное ссылочное равенство оператор, унаследованный от Object. Поэтому неудивительно, что если присутствует только один из операторов, он будет использовать оператор равенства ссылок по умолчанию, который есть у всех объектов, перегрузка для него отсутствует. 1

Зная, что это так, реальный вопрос заключается в следующем: почему это было разработано таким образом, и почему компилятор не может понять это самостоятельно? Многие люди говорят, что это не было дизайнерским решением, но мне нравится думать, что оно было продумано таким образом, особенно учитывая тот факт, что все объекты имеют оператор равенства по умолчанию.

Итак, почему компилятор не создает автоматически оператор !=? Я не могу знать наверняка, если кто-то из Microsoft не подтвердит это, но это то, что я могу определить, исходя из фактов.


Для предотвращения неожиданного поведения

Возможно, я хочу сделать сравнение значений на ==, чтобы проверить равенство. Однако, когда дело дошло до !=, мне было все равно, равны ли значения, если только ссылки не равны, потому что моя программа считает их равными, меня интересует только совпадение ссылок. В конце концов, это фактически обозначено как поведение C # по умолчанию (если оба оператора не были перегружены, как было бы в случае некоторых библиотек .net, написанных на другом языке). Если компилятор автоматически добавлял код, я больше не мог полагаться на то, что компилятор будет выводить код, который должен быть совместимым. Компилятор не должен писать скрытый код, который меняет ваше поведение, особенно если код, который вы написали, соответствует стандартам C # и CLI.

С точки зрения этого заставляет перегрузить его, вместо того, чтобы перейти к поведению по умолчанию, я могу только твердо сказать, что оно в стандарте (EMCA-334 17.9.2) 2 . Стандарт не уточняет почему. Я считаю, что это связано с тем, что C # заимствует много поведения из C ++. Подробнее об этом см. Ниже.


Когда вы переопределяете != и ==, вам не нужно возвращать bool.

Это еще одна вероятная причина. В C # эта функция:

public static int operator ==(MyClass a, MyClass b) { return 0; }

действует так же, как этот:

public static bool operator ==(MyClass a, MyClass b) { return true; }

Если вы возвращаете что-то отличное от bool, компилятор не может автоматически вывести противоположный тип. Кроме того, в случае, когда ваш оператор возвращает bool, им просто не имеет смысла создавать генерируемый код, который будет существовать только в этом конкретном случае, или, как я уже говорил выше, код, который скрывает Поведение CLR по умолчанию.


C # многое заимствует из C ++ 3

Когда появился C #, в журнале MSDN была статья, в которой говорилось о C #:

Многие разработчики хотели бы, чтобы существовал язык, который было бы легко писать, читать и поддерживать, как Visual Basic, но который по-прежнему обеспечивал мощь и гибкость C ++.

Да, цель разработки для C # заключалась в том, чтобы дать почти такое же количество энергии, что и в C ++, и лишь немного пожертвовать такими удобствами, как жесткая безопасность типов и сборка мусора.C # был строго смоделирован после C ++.

Возможно, вы не удивитесь, узнав, что в C ++ операторы равенства не должны возвращать bool , как показано в в этом примере программы

Теперь C ++ напрямую не требует перегрузки дополнительного оператора.Если вы скомпилировали код в примере программы, вы увидите, что он работает без ошибок.Однако, если вы попытались добавить строку:

cout << (a != b);

, вы получите

ошибка компилятора C2678 (MSVC): бинарный '! =': Оператор не найден, который занимает левуюоперанд типа «Тест» (или нет приемлемого преобразования) `.

Итак, хотя сам C ++ не требует перегрузки в парах, он не будет позволяет использовать оператор равенства, который вы не перегружали в пользовательском классе.Это действительно в .NET, потому что все объекты имеют объект по умолчанию;C ++ не делает.


1.В качестве примечания, стандарт C # по-прежнему требует перегрузки пары операторов, если вы хотите перегрузить один из них.Это часть стандарта , а не просто компилятор .Однако при доступе к библиотеке .net, написанной на другом языке, который не соответствует этим требованиям, применяются те же правила, что и при определении того, какой оператор вызывать.

2. EMCA-334 (pdf) (http://www.ecma -international.org / публикации / файлы / ECMA-ST / Ecma-334.pdf )

3.И Java, но на самом деле не в этом дело

47 голосов
/ 02 августа 2011

Вероятно, если кому-то нужно реализовать трехзначную логику (т.е. null). В таких случаях, например, в стандарте ANSI SQL, операторы нельзя просто отменить, в зависимости от ввода.

У вас может быть случай, когда:

var a = SomeObject();

И a == true возвращает false, а a == false также возвращает false.

24 голосов
/ 02 августа 2011

Кроме того, что во многих областях C # откладывается на C ++, лучшее объяснение, которое я могу придумать, заключается в том, что в некоторых случаях вы можете использовать слегка иной подход к доказательству "не равенства", чем к доказательству«равенство».

Очевидно, что при сравнении строк, например, вы можете просто проверить равенство и return вне цикла, когда вы видите несопоставимые символы.Тем не менее, это может быть не так чисто с более сложными проблемами.На ум приходит фильтр Блума ;очень легко быстро определить, является ли элемент не в наборе, но трудно определить, является ли элемент в наборе.Хотя может применяться та же техника return, код может быть не таким красивым.

20 голосов
/ 02 августа 2011

Если вы посмотрите на реализации перегрузок == и! = В источнике .net, они часто не реализуют! = As! (Left == right). Они реализуют это полностью (как ==) с отрицательной логикой. Например, DateTime реализует == как

return d1.InternalTicks == d2.InternalTicks;

и! = As

return d1.InternalTicks != d2.InternalTicks;

Если вы (или компилятор, если он сделал это неявно) должны были реализовать! = As

return !(d1==d2);

тогда вы делаете предположение о внутренней реализации == и! = В вещах, на которые ссылается ваш класс. Избежание этого предположения может быть философией, стоящей за их решением.

16 голосов
/ 03 августа 2011

Чтобы ответить на ваши изменения относительно того, почему вы вынуждены переопределить оба, если вы переопределите один, все это в наследстве.

Если вы переопределите ==, скорее всего, для обеспечения какого-то семантического или структурного равенства (например, DateTimes равны, если их свойства InternalTicks равны, даже если они могут быть разными экземплярами), тогда вы меняете поведение по умолчанию оператор из Object, который является родителем всех объектов .NET. Оператор == в C # является методом, базовая реализация которого Object.operator (==) выполняет референциальное сравнение. Object.operator (! =) - это другой, другой метод, который также выполняет ссылочное сравнение.

Практически в любом другом случае переопределения метода было бы нелогично предполагать, что переопределение одного метода также приведет к изменению поведения антонимического метода. Если вы создали класс с методами Increment () и Decrement () и переопределили Increment () в дочернем классе, ожидаете ли вы, что Decrement () также будет переопределен с противоположностью вашего переопределенного поведения? Компилятор не может быть достаточно умным, чтобы генерировать обратную функцию для любой реализации оператора во всех возможных случаях.

Однако операторы, хотя и реализованные очень похоже на методы, концептуально работают парами; == и! =, <и>, и <= и> =. В этом случае было бы нелогично с точки зрения потребителя думать, что! = Работает иначе, чем ==. Таким образом, компилятор нельзя заставить предполагать, что a! = B ==! (A == b) во всех случаях, но обычно ожидается, что == и! = Должны работать аналогичным образом, поэтому компилятор заставляет Вы должны реализовать в парах, однако на самом деле вы в конечном итоге делать это. Если для вашего класса a! = B ==! (A == b), то просто реализуйте оператор! =, Используя! (==), но если это правило не выполняется во всех случаях для вашего объекта (например, (если сравнение с определенным значением, равным или неравным, недопустимо), то вы должны быть умнее, чем в IDE.

НАСТОЯЩИЙ вопрос, который следует задать, заключается в том, почему <и> и <= и> = являются парами для сравнительных операторов, которые должны быть реализованы одновременно, когда они выражены в числовом выражении! (a> b) == a <= b. Вы должны быть обязаны реализовать все четыре, если вы переопределяете один, и, вероятно, вы должны также переопределить == (и! =), Потому что (a <= b) == (a == b), если a семантически равно б. </p>

12 голосов
/ 02 августа 2011

Если вы перегрузите == для своего пользовательского типа, а не! =, Тогда он будет обработан оператором! = Для объекта! = Объекта, поскольку все происходит от объекта, и это будет сильно отличаться от CustomType! = CustomType,

Кроме того, создатели языка, вероятно, хотели, чтобы это было максимально возможным для программистов, а также чтобы они не делали предположений относительно того, что вы намереваетесь делать.

9 голосов
/ 03 августа 2011

Это то, что приходит мне в голову:

  • Что если проверка неравенства намного быстрее, чем проверка равенства?
  • Что если в некоторых случаях вы хотите вернуть false как для ==, так и для != (т. Е. Если по каким-то причинам их невозможно сравнить)
5 голосов
/ 03 августа 2011

Ключевыми словами в вашем вопросе являются " Why " и " must ".

В результате:

Ответьте именно такпотому что они спроектировали так, чтобы это было правдой ... но не отвечая на вопрос "почему" на ваш вопрос.

Ответ на то, что иногда может оказаться полезным переопределить оба этих вопроса независимо, правда ...но не отвечая на обязательную часть вашего вопроса.

Я думаю, что простой ответ заключается в том, что не какая-либо убедительная причина, почему C # требует отменитьоба.

Язык должен позволять вам переопределять только == и предоставлять вам реализацию по умолчанию !=, то есть !.Если вам захочется переопределить !=, имейте это в виду.

Это не было хорошим решением.Люди проектируют языки, люди не совершенны, C # не совершенен.Пожав плечами и QED

4 голосов
/ 02 августа 2011

Ну, это, вероятно, просто выбор дизайна, но, как вы говорите, x!= y не обязательно должен совпадать с !(x == y).Не добавляя реализацию по умолчанию, вы уверены, что не сможете забыть реализовать конкретную реализацию.И если это действительно так тривиально, как вы говорите, вы можете просто реализовать одно, используя другое.Я не понимаю, как это «плохая практика».

Могут быть и другие различия между C # и Lua тоже ...

3 голосов
/ 03 августа 2011

Просто чтобы добавить к отличным ответам здесь:

Подумайте, что произойдет в отладчике , когда вы попытаетесь войти в оператор != и в итоге получите оператор ==! Разговор о путанице!

Имеет смысл, что CLR предоставит вам свободу исключать одного или другого оператора - так как он должен работать со многими языками. Но есть множество примеров того, как C # не раскрывает возможности CLR (например, ref возвраты и локальные значения), и множество примеров реализации функций, отсутствующих в самом CLR (например: using, lock, foreach и т. д.).

...