Почему мы должны определять оба == и! = В C #?

346

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

Зачем?

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

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

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

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

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

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

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

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

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

Стефан Драгнев
источник
24
+1 за отличный вопрос. Кажется, нет никакой причины вообще ... конечно, компилятор может предположить, что a! = B может быть переведено в! (A == b), если не указано иное. Мне любопытно узнать, почему они тоже решили это сделать.
Patrick87
16
Это самый быстрый вопрос, за который я проголосовал и получил одобрение.
Кристофер Уок
26
Я уверен, что @Eric Lippert также может дать здравый ответ.
Yuck
17
«Ученый не тот, кто дает правильные ответы, он тот, кто задает правильные вопросы». Вот почему в SE вопросы приветствуются. И это работает!
Адриано Карнейру
4
Тот же вопрос для Python: stackoverflow.com/questions/4969629/…
Джош Ли

Ответы:

161

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

Глядя на этот основной код 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): бинарный '! =': оператор не найден, который принимает левый операнд типа 'Test' (или нет приемлемого преобразования) `.

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


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

2. EMCA-334 (pdf) ( http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf )

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

Christopher Currens
источник
75
« Вы не должны возвращать бул ». Я думаю, что это наш ответ прямо там; отлично сработано. Если! (O1 == o2) даже четко не определено, то вы не можете ожидать, что стандарт будет полагаться на него.
Брайан Гордон
10
По иронии судьбы в теме о логических операторах вы использовали двойное отрицание в предложении: «C # не позволяет вам не переопределять только один из обоих», что фактически логически означает, что «C # позволяет вам переопределить только один из обоих».
Дэн Диплом
13
@ Дэн Диплом, все хорошо, я не запрещаю не делать этого или нет.
Кристофер Уок
6
Если это правда, я подозреваю, что № 2 является правильным; но теперь возникает вопрос, зачем позволять ==возвращать что-либо, кроме как bool? Если вы возвращаете int, вы больше не можете использовать его как условный оператор, например. if(a == b)больше не будет компилироваться. В чем смысл?
BlueRaja - Дэнни Пфлюгофт
2
Вы можете вернуть объект, который имеет неявное преобразование в bool (оператор true / false), но я все еще не вижу, где это будет полезно.
Стефан Драгнев
53

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

Вы можете иметь случай, когда:

var a = SomeObject();

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

дрянь
источник
1
Я бы сказал, что в этом случае, поскольку aэто не логическое значение, вы должны сравнивать его с перечислением из трех состояний, а не с реальным trueили false.
Брайан Гордон
18
Кстати, я не могу поверить, что никто не ссылался на шутку FileNotFound ..
Брайан Гордон
28
Если a == trueэто так, falseто я всегда ожидал a != trueбы быть true, несмотря на то, a == falseчтоfalse
Фед
20
@ Феде ударил ногтем по голове. Это действительно не имеет ничего общего с вопросом - вы объясняете, почему кто-то может захотеть переопределить ==, а не почему он должен быть вынужден переопределить оба.
BlueRaja - Дэнни Пфлюгофт
6
@Yuck - Да, есть хорошая реализация по умолчанию: это общий случай. Даже в вашем примере реализация по умолчанию !=будет правильной. В крайне необычном случае , когда мы хотим x == yи x != yкак к ложному (я не могу вспомнить ни одного примера , который имеет смысл) , они еще могут переопределить реализацию по умолчанию и предоставить свои собственные. Всегда делайте общий случай по умолчанию!
BlueRaja - Дэнни Пфлугхофт
25

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

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

Брайан Гордон
источник
6
Отличный пример; Я думаю, ты понимаешь причину почему. Простое !запирает вас в едином определении равенства, где информированный кодер может оптимизировать и то, == и другое!= .
Yuck
13
Таким образом, это объясняет, почему им должна быть предоставлена возможность переопределить оба, а не то, почему они должны быть вынуждены .
BlueRaja - Дэнни Пфлюгофт
1
В этом смысле им не нужно оптимизировать оба, они просто должны дать четкое определение обоих.
Джеспер
22

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

return d1.InternalTicks == d2.InternalTicks;

и! = как

return d1.InternalTicks != d2.InternalTicks;

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

return !(d1==d2);

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

топор - сделано с SOverflow
источник
17

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

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

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

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

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

Keiths
источник
14

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

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

Крис Маллинс
источник
Вы вдохновили этот вопрос: stackoverflow.com/questions/6919259/…
Брайан Гордон
2
Причина гибкости также может быть применена ко многим функциям, которые они решили опустить, например, blogs.msdn.com/b/ericlippert/archive/2011/06/23/…. Таким образом, это не объясняет их выбор для реализация операторов равенства.
Стефан Драгнев
Это очень интересно. Кажется, что это было бы полезно. Я понятия не имею, почему они оставили бы это.
Крис Маллинс
11

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

  • Что, если тестирование неравенства намного быстрее, чем тестирование равенства?
  • Что делать, если в некоторых случаях вы хотите вернуть falseи для ==и!= (то есть, если они не могут быть сопоставлены по какой-то причине)
Дан Абрамов
источник
5
В первом случае вы могли бы реализовать проверку на равенство с точки зрения проверки на неравенство.
Стефан Драгнев
Во втором случае будут нарушены структуры, например, Dictionaryи Listесли вы используете свой собственный тип с ними.
Стефан Драгнев
2
@Stefan: Dictionaryиспользует GetHashCodeи Equals, а не операторы. Listиспользует Equals.
Дан Абрамов
6

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

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

GolezTrol
источник
1
Это отличная практика реализации одного с точки зрения другого. Я только что видел, что сделано иначе - что подвержено ошибкам.
Стефан Драгнев
3
Это проблема программиста, а не проблема C #. Программисты, которые не смогут реализовать это право, потерпят неудачу и в других областях.
GolezTrol
@GolezTrol: Не могли бы вы объяснить, что ссылка Lua? Я совсем не знаком с Луа, но вы, похоже, намекаете на что-то.
zw324
@Ziyao Wei: OP указывает, что Lua делает это не так, как C # (Lua, очевидно, добавляет аналоги по умолчанию для упомянутых операторов). Просто пытаюсь упростить такое сравнение. Если различий нет вообще, то хотя бы один из них не имеет права на существование. Должен сказать, я тоже не знаю Луа.
GolezTrol
6

Ключевыми словами в вашем вопросе являются « почему » и « должны ».

В следствии:

Отвечать так, потому что они разработали так, правда ... но не отвечать на вопрос «почему» на ваш вопрос.

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

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

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

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

Грег Хендершотт
источник
5

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

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

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

Эндрю Рассел
источник
3

Языки программирования представляют собой синтаксические перестановки исключительно сложных логических выражений. Имея это в виду, можете ли вы определить случай равенства без определения случая неравенства? Ответ - нет. Чтобы объект a был равен объекту b, обратное значение объекта a, отличного от b, также должно быть истинным. Еще один способ показать это

if a == b then !(a != b)

это обеспечивает определенную способность языка определять равенство объектов. Например, сравнение NULL! = NULL может бросить рывок в определение системы равенства, которая не реализует оператор неравенства.

Теперь, что касается идеи! = Просто заменяемого определения, как в

if !(a==b) then a!=b

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

мистифицировать
источник
Nullбудет только проблемой, потому что nullэто допустимый вход ==, чего не должно быть, хотя, очевидно, это очень удобно. Лично я просто думаю, что это допускает огромное количество расплывчатого, неаккуратного программирования.
nicodemus13
2

Короче говоря, принудительная согласованность.

'==' и '! =' всегда являются истинными противоположностями, независимо от того, как вы их определяете, как таковые определяются их словесным определением «равно» и «не равно». Определяя только одно из них, вы открываете себя для несогласованности операторов равенства, когда оба «==» и «! =» Могут быть оба истинными или оба ложными для двух заданных значений. Вы должны определить и то и другое, поскольку, когда вы решите определить одно, вы должны также определить другое соответствующим образом, чтобы было совершенно ясно, каково ваше определение «равенства». Другое решение для компилятора - позволить вам только переопределить '==' OR '! =' И оставить другое как неотъемлемо отрицательное для другого. Очевидно, что это не относится к компилятору C #, и я уверен, что

Вопрос, который вы должны задать: «зачем мне переопределять операторы?» Это сильное решение, которое требует серьезных аргументов. Для объектов '==' и '! =' Сравните по ссылке. Если вы хотите переопределить их, чтобы НЕ сравнивать по ссылке, вы создаете общую несогласованность операторов, которая не очевидна для любого другого разработчика, который будет просматривать этот код. Если вы пытаетесь задать вопрос «является ли состояние этих двух экземпляров эквивалентным?», То вам следует реализовать IEquatible, определить Equals () и использовать этот вызов метода.

Наконец, IEquatable () не определяет NotEquals () по той же причине: потенциальная возможность раскрыть несоответствия операторов равенства. NotEquals () должен ВСЕГДА возвращать! Equals (). Открыв определение NotEquals () для класса, реализующего Equals (), вы вновь решаете проблему согласованности в определении равенства.

Изменить: это просто мои рассуждения.

Emoire
источник
Это нелогично. Если бы причиной была согласованность, то применилось бы обратное: вы могли бы только реализовать, operator ==и компилятор сделал бы вывод operator !=. В конце концов, это то , что он делает для operator +и operator +=.
Конрад Рудольф
1
foo + = bar; является составным назначением, сокращением для foo = foo + bar ;. Это не оператор.
blenderfreaky
Если на самом деле они являются «истинными всегда противоположны», то это будет основанием для автоматического определения , a != bкак !(a == b)... (это не то , что делает C #).
Андрей
-3

Возможно, что-то, о чем они не подумали, не успело сделать.

Я всегда использую твой метод, когда перегружаю ==. Тогда я просто использую это в другом.

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

Rhyous
источник