Чому ми повинні визначати обидва == і! = У C #?


346

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

Чому?

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

Я розумію, що є дивні випадкові випадки, коли деякі сутності можуть бути ні рівними, ні нерівними (як IEEE-754 NaN), але такі здаються винятком, а не правилом. Отже, це не пояснює, чому дизайнери компіляторів C # зробили виняток із правила.

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

Примітка. Те саме стосується операторів <> <=> =. Я не уявляю випадків, коли вам потрібно буде це визначити неприродними способами. Lua дозволяє вам визначати лише <і <= і визначає> = і> природним шляхом через заперечення форматорів. Чому C # не робить те саме (принаймні, "за замовчуванням")?

EDIT

Мабуть, є вагомі причини, які дозволяють програмісту здійснювати перевірки рівності та нерівності, як би вони не хотіли. Деякі відповіді вказують на випадки, коли це може бути добре.

Ядро мого питання, однак, чому це вимагається насильно в 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 також може дати точну відповідь.
Юк

17
"Вчений - це не людина, яка дає правильні відповіді. Це той, хто ставить правильні запитання". Ось чому в ДП питання заохочуються. І це працює!
Адріано Карнейро

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

Таким чином, хоча сам C ++ не вимагає перевантажувати вас парами, він не дозволить вам використовувати оператор рівності, який ви не перевантажували в користувацькому класі. Це дійсно в .NET, тому що всі об'єкти мають один за замовчуванням; C ++ не робить.


1. Як бічна примітка, стандарт C # все ж вимагає перевантажити пару операторів, якщо ви хочете перевантажити будь-якого. Це частина стандарту, а не просто компілятор . Однак ті самі правила щодо визначення того, якого оператора потрібно викликати, застосовуються під час доступу до бібліотеки .net, написаної іншою мовою, яка не має однакових вимог.

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

3. І Java, але тут справді не сенс


75
" не потрібно повертати bool " .. Я думаю, що це наша відповідь саме там; молодець. Якщо! (O1 == o2) навіть не є чітко визначеним, то ви не можете очікувати, що стандарт буде на нього покладатися.
Брайан Гордон

10
За іронією долі в темі про логічних операторів ви використовували подвійний від’ємник у реченні: «C # не дозволяє вам перекрити лише один із них», що фактично логічно означає, що «C # дозволяє вам перекрити лише один із будь-яких».
Dan Diplo

13
@Dan Diplo, це добре, це не так, як мені не заборонено робити цього чи ні.
Крістофер Керренс

6
Якщо це правда, я підозрюю, що №2 правильний; але тепер стає питання, чому дозволити ==повернути що-небудь, окрім bool? Якщо ви повернете ан int, ви більше не зможете використовувати його як умовне твердження, наприклад. if(a == b)більше не збиратимуться. В чому справа?
BlueRaja - Danny Pflughoeft

2
Ви можете повернути об'єкт, який має неявне перетворення у bool (оператор true / false), але я все ще не бачу, де це було б корисно.
Стефан Драгнев

53

Можливо, якщо комусь потрібно реалізувати тризначну логіку (тобто null). У таких випадках - наприклад, стандартний SQL для ANSI - операторів не можна просто заперечувати залежно від введених даних.

У вас може бути випадок, коли:

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
@Fede вдарив цвяхом по голові. Це насправді не має нічого спільного з питанням - ви пояснюєте, чому хтось може захотіти переосмислити ==, а не чому їх слід змусити перекрити обидва.
BlueRaja - Danny Pflughoeft

6
@Yuck - Так, є хороша реалізація за замовчуванням: це звичайний випадок. Навіть у вашому прикладі реалізація за замовчуванням !=буде правильною. У вкрай рідкісному випадку, коли ми хотіли б x == yі x != yобидва були помилковими (я не можу придумати жодного прикладу, який має сенс) , вони все одно можуть перекрити реалізацію за замовчуванням і надати свою власну. Завжди робіть загальний випадок за замовчуванням!
BlueRaja - Danny Pflughoeft

25

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

Очевидно, зі порівнянням рядків, наприклад, ви можете просто перевірити рівність і returnпоза циклом, коли побачите не збігаються символи. Однак це може бути не таким чистим із складнішими проблемами. На думку приходить фільтр цвітіння ; це дуже легко і швидко сказати , якщо елемент НЕ в наборі, але важко сказати , якщо елемент знаходиться в наборі. Хоча ця returnтехніка може застосовуватися, код може бути не таким гарним.


6
Чудовий приклад; Я думаю, ти зрозумієш причину. Простий !фіксує вас в єдине визначення рівності, де інформований кодер може бути в змозі оптимізувати і те, == і інше!= .
Юк

13
Отже, це пояснює, чому їм слід давати можливість перекрити обидва, а не чому їх слід змушувати .
BlueRaja - Danny Pflughoeft

1
У цьому сенсі їм не потрібно оптимізувати обидва, вони просто повинні дати чітке визначення обох.
Jesper

22

Якщо ви дивитесь на реалізацію перевантажень == і! = У джерелі .net, вони часто не реалізують! = As! (Зліва == праворуч). Вони реалізують його повністю (як ==) з запереченою логікою. Наприклад, DateTime реалізує == як

return d1.InternalTicks == d2.InternalTicks;

і! = як

return d1.InternalTicks != d2.InternalTicks;

Якщо ви (або компілятор, якщо він це зробив неявно), повинні були реалізувати! = Як

return !(d1==d2);

то ви робите припущення про внутрішню реалізацію == і! = у речах, на які посилається ваш клас. Уникнути цього припущення може бути філософія їхнього рішення.


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 є семантично дорівнює b.


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 ...


1
Це відмінна практика впровадження одного з точки зору іншого. Я щойно бачив, що це робиться інакше - що схильне до помилок.
Стефан Драгнев

3
Це проблема програміста, а не проблема C #. Програмісти, які не зможуть реалізувати це право, зазнають невдач і в інших сферах.
GolezTrol

@GolezTrol: Чи можете ви пояснити цю посилання на Луа? Я зовсім не знайомий з Луа, але ти, мабуть, щось натякає.
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

Словом, вимушена послідовність.

'==' і '! =' - це завжди правдиві протилежності, незалежно від того, як ви їх визначаєте, визначені як такі їх словесним визначенням "дорівнює" і "не дорівнює". Визначивши лише одне з них, ви відкриваєтесь до невідповідності оператора рівності, де обидва '==' і '! =' Можуть бути істинними, або обидва помилковими для двох заданих значень. Ви повинні визначати обидва, оскільки, коли ви вирішите визначити одне, ви також повинні визначити інший відповідним чином, щоб було чітко зрозуміло, що таке ваше визначення "рівності". Іншим рішенням для компілятора є лише дозволити вам переосмислити '==' АБО! Очевидно, що це не так з компілятором C #, і я впевнений, що є "

Питання, яке ви повинні задати, - це "чому мені потрібно перекрити операторів?" Це сильне рішення, яке потрібно приймати, і вимагає вагомих міркувань. Для об'єктів '==' і '! =' Порівняйте за посиланням. Якщо ви перекриєте їх, щоб НЕ порівнювали за посиланням, ви створюєте загальну невідповідність оператору, що не видно жодному іншому розробнику, який би ознайомився з цим кодом. Якщо ви намагаєтеся задати питання "стан еквівалентних двох екземплярів?", То вам слід реалізувати IEquatible, визначити рівний () та використовувати цей виклик методу.

Нарешті, IEquatable () не визначає NotEquals () з тих же міркувань: потенціал відкрити невідповідності оператора рівності. ВЖЕ ВИНАГИ повертається NotEquals ()! Рівний (). Відкриваючи визначення класу NotEquals () для класу, що реалізує Equals (), ви знову змушуєте питання послідовності у визначенні рівності.

Редагувати: Це просто мої міркування.


Це нелогічно. Якщо причиною стала послідовність, то застосувало б зворотне: ви могли б лише реалізувати, operator ==і компілятор зробив би висновок operator !=. Зрештою, це те, що робить для operator +і operator +=.
Конрад Рудольф

1
foo + = бар; - складене призначення, скорочення для foo = foo + bar ;. Це не оператор.
blenderfreaky

Якщо вони справді є "завжди істинними протилежностями", то це буде приводом для автоматичного визначення a != bяк !(a == b)... (що не те, що робить C #).
andrewf

-3

Напевно, просто те, про що вони не думали, не встигло зробити.

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

Ви маєте рацію, маючи невелику кількість роботи, компілятор міг би нам це безкоштовно подати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.