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

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

Почему?

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

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

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

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

ИЗМЕНИТЬ

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

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

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

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

Ответ 1

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

Глядя на этот базовый код 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

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

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

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

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

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


Чтобы предотвратить непредвиденное поведение

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

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


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

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

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

так же справедлив, как и этот:

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

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


С# занимает большую часть из С++ 3

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

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

Да, цель дизайна для С# заключалась в том, чтобы дать почти такую ​​же мощность, как С++, жертвуя лишь небольшим количеством удобств, таких как жесткая безопасность типов и сбор мусора. С# был сильно смоделирован после С++.

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

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

cout << (a != b);

вы получите

ошибка компилятора C2678 (MSVC): двоичный '! =': оператор не найден, который принимает левый операнд типа "Test" (или нет приемлемого преобразования) `.

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


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

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

3. И Java, но это действительно не так.

Ответ 2

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

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

var a = SomeObject();

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

Ответ 3

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

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

Ответ 4

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

return d1.InternalTicks == d2.InternalTicks;

и!= as

return d1.InternalTicks != d2.InternalTicks;

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

return !(d1==d2);

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

Ответ 5

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

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

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

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

РЕАЛЬНЫЙ вопрос, который следует задать, - это почему < и > и <= и >= являются парами для сравнительных операторов, которые должны быть реализованы одновременно, когда в числовых выражениях (a < b) == a >= b и! (a > b) == a <= б. Вам нужно будет выполнить все четыре, если вы переопределите их, и вам, вероятно, придется переопределить == (и! =), Потому что (a <= b) == (a == b), если a семантически равным b.

Ответ 6

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

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

Ответ 7

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

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

Ответ 8

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

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

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

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

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

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

Это не было хорошим решением. Люди разрабатывают языки, люди не идеальны, С# не идеален. Shrug и Q.E.D.

Ответ 9

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

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

Ответ 10

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

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

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

Ответ 11

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

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

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

Теперь, в отношении идеи!= просто быть заменяемым определением, как в

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

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

Ответ 12

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

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

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

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

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

Ответ 13

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

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

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