Почему C # и Java используют равенство ссылок по умолчанию для '=='?

32

Некоторое время я размышлял, почему Java и C # (и я уверен, что другие языки) по умолчанию ссылаются на равенство для ==.

В программировании, которое я делаю (что, безусловно, является лишь небольшим подмножеством проблем программирования), я почти всегда хочу логическое равенство при сравнении объектов вместо ссылочного равенства. Я пытался придумать, почему оба эти языка пошли по этому пути, вместо того, чтобы инвертировать его и ==иметь логическое равенство и использовать .ReferenceEquals()для ссылочного равенства.

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

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

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

Молния
источник
3
Потому что переменные являются ссылками? Поскольку переменные действуют как указатели, имеет смысл сравнивать их
Даниэль Гратцер
C # использует логическое равенство для типов значений, таких как структуры. Но каким должно быть «логическое равенство по умолчанию» для двух объектов разных ссылочных типов? Или для двух объектов, где один имеет тип A, унаследованный от B? Всегда "ложный", как для структур? Даже если на один и тот же объект ссылаются дважды, сначала как A, а затем как B? Не имеет особого смысла для меня.
Док Браун
3
Другими словами, вы спрашиваете, почему в C #, если вы переопределяете Equals(), это не меняет автоматически поведение ==?
svick

Ответы:

29

C # делает это, потому что Java сделал. Java сделала, потому что Java не поддерживает перегрузку операторов. Поскольку равенство значений должно быть переопределено для каждого класса, оно не может быть оператором, а должно быть методом. ИМО это было плохое решение. Писать и читать намного легче, a == bчем a.equals(b), и гораздо более естественно для программистов с опытом работы на C или C ++, но a == bэто почти всегда неправильно. Ошибки от использования ==там, где .equalsтребовалось, потратили бесчисленные тысячи часов программиста.

Кевин Клайн
источник
7
Я думаю, что сторонников перегрузки операторов так же много, как и хулителей, поэтому я бы не сказал «это было плохое решение» как абсолютное утверждение. Пример: в проекте C ++, в котором я работаю, мы перегружены ==для многих классов, и несколько месяцев назад я обнаружил, что несколько разработчиков не знали, что на ==самом деле делает. Всегда существует такой риск, когда семантика некоторой конструкции не очевидна. equals()Обозначение говорит мне , что я использую специальный метод , и что я должен искать его где - нибудь. Итог: я думаю, что перегрузка операторов - это вообще открытый вопрос.
Джорджио
9
Я бы сказал, что в Java нет пользовательских перегрузок операторов. Множество операторов имеют двойное (перегруженное) значение в Java. Посмотрите, +например, что делает сложение (числовых значений) и объединение строк одновременно.
Иоахим Зауэр
14
Как может a == bбыть более естественным для программистов с опытом работы с C, так как C не поддерживает пользовательскую перегрузку операторов? (Например, C-способ сравнения строк strcmp(a, b) == 0, а не a == b.)
svick
Это в основном то, что я думал, но я решил попросить тех, у кого больше опыта, убедиться, что я не упустил что-то очевидное.
молния
4
@svick: в C нет ни строкового типа, ни ссылочных типов. Строковые операции выполняются через char *. Мне кажется очевидным, что сравнение двух указателей на равенство - это не то же самое, что сравнение строк.
Кевин Клайн
15

Краткий ответ: последовательность

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

  • Ссылочное равенство : означает, что a = b истинно, если a и b ссылаются на один и тот же объект. Было бы неверно, если бы a и b ссылались на разные объекты, даже если все атрибуты a и b были одинаковыми.
  • Неглубокое равенство : означает, что a = b истинно, если все атрибуты объектов, на которые ссылаются a и b, идентичны. Неглубокое равенство может быть легко реализовано путем побитового сравнения пространства памяти, которое представляет два объекта. Обратите внимание, что ссылочное равенство подразумевает поверхностное равенство
  • Глубокое равенство : означает, что a = b истинно, если каждый атрибут в a и b либо идентичен, либо глубоко равен. Обратите внимание, что глубокое равенство подразумевается как ссылочным равенством, так и поверхностным равенством. В этом смысле глубокое равенство является самой слабой формой равенства, а ссылочное равенство является наиболее сильным.

Эти три типа равенства часто используются, потому что они удобны для реализации: все три проверки на равенство могут быть легко сгенерированы компилятором (в случае глубокого равенства, компилятору может понадобиться использовать биты тега для предотвращения бесконечных циклов, если структура сравнивать имеет круговые ссылки). Но есть другая проблема: ни один из них не может быть уместным.

В нетривиальных системах равенство объектов часто определяется как нечто между глубоким и эталонным равенством. Чтобы проверить, хотим ли мы считать два объекта равными в определенном контексте, нам может потребоваться сравнить некоторые атрибуты по тому, как они находятся в памяти, а другие по глубокому равенству, в то время как некоторым атрибутам может быть разрешено быть чем-то совершенно другим. Что нам действительно нужно, так это «четвертый тип равенства», действительно хороший, часто называемый в литературе семантическим равенством . Вещи равны, если они равны, в нашей области. знак равно

Итак, мы можем вернуться к вашему вопросу:

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

Что мы имеем в виду, когда пишем «a == b» на любом языке? В идеале всегда должно быть одно и то же: семантическое равенство. Но это невозможно.

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

var a = 1;
var b = a;
if (a == b){
    ...
}
a = 3;
b = 3;
if (a == b) {
    ...
}

В этом случае мы ожидаем, что «a равно b» в обоих утверждениях. Все остальное было бы безумием. Большинство (если не все) языков придерживаются этого соглашения. Следовательно, с помощью простых типов (или значений) мы знаем, как достичь семантического равенства. С объектами это может быть что-то совершенно другое. Увидеть ниже:

var a = new Something(1);
var b = a;
if (a == b){
    ...
}
b = new Something(1);
a.DoSomething();
b.DoSomething();
if (a == b) {
    ...
}

Мы ожидаем, что первое «если» будет всегда верным. Но что вы ожидаете от второго «если»? Это действительно зависит. Может ли DoSomething изменить (семантическое) равенство a и b?

Проблема с семантическим равенством состоит в том, что он не может быть автоматически сгенерирован компилятором для объектов, и это не очевидно из назначений . Для пользователя должен быть предусмотрен механизм определения семантического равенства. В объектно-ориентированных языках этот механизм является унаследованным методом: равно . Читая фрагмент ОО-кода, мы не ожидаем, что метод будет иметь одинаковую точную реализацию во всех классах. Мы привыкли к наследованию и перегрузкам.

Однако с операторами мы ожидаем того же поведения. Когда вы видите «a == b», вы должны ожидать одинаковый тип равенства (из 4 выше) во всех ситуациях. Таким образом, стремясь к согласованности, разработчики языков использовали равенство ссылок для всех типов. Это не должно зависеть от того, переопределил ли программист метод или нет.

PS: язык Dee немного отличается от языка Java и C #: оператор equals означает поверхностное равенство для простых типов и семантическое равенство для пользовательских классов (с ответственностью за реализацию операции =, лежащей у пользователя - по умолчанию не предусмотрено). Так как для простых типов неглубокое равенство - это всегда семантическое равенство, язык является последовательным. Однако цена, которую он платит, заключается в том, что оператор equals по умолчанию не определен для пользовательских типов. Вы должны реализовать это. И иногда это просто скучно.

Hbas
источник
2
When you see ‘a == b’ you should expect the same type of equality (from the 4 above) in all situations.Разработчики языка Java использовали ссылочное равенство для объектов и семантическое равенство для примитивов. Для меня не очевидно, что это было правильное решение или что это решение является более "последовательным", чем разрешение ==быть перегруженным для семантического равенства объектов.
Чарльз Сальвиа
Они использовали «эквивалент эталонного равенства» и для примитивов. Когда вы используете "int i = 3", для номера нет указателей, поэтому вы не можете использовать ссылки. Со строками, типа сортировки «примитива», это становится более очевидным: вам нужно использовать «.intern ()» или прямое присваивание (String s = «abc»), чтобы использовать == (равенство ссылок).
Hbas
1
PS: C #, с другой стороны, не соответствовал строкам. И ИМХО, в этом случае, это намного лучше.
Hbas
@CharlesSalvia: В Java, если aи один bи тот же тип, выражение a==bпроверяет aи bудерживает ли одно и то же. Если один из них содержит ссылку на объект № 291, а другой содержит ссылку на объект № 572, они не содержат одно и то же. В содержание объектно # 291 и # 572 могут быть эквивалентны, но сами переменные содержат разные вещи.
суперкат
2
@CharlesSalvia Он разработан таким образом, чтобы вы могли видеть a == bи знать, что он делает. Кроме того, вы можете увидеть a.equals(b)и предположить, что перегрузки equals. Если a == bзвонки a.equals(b)(если реализованы), это сравнение по ссылке или по содержанию? Не помнишь? Вы должны проверить класс А. Код уже не так быстро читается, если вы даже не уверены, что вызывается. Было бы так, как если бы методы с одинаковой сигнатурой были разрешены, а вызываемый метод зависит от текущей области видимости. Такие программы было бы невозможно прочитать.
Нил
0

Я пытался придумать, почему оба этих языка пошли по этому пути, вместо того, чтобы инвертировать его и иметь == быть логическим равенством и использовать .ReferenceEquals () для ссылочного равенства.

Потому что последний подход будет сбивать с толку. Рассмотреть возможность:

if (null.ReferenceEquals(null)) System.out.println("ok");

Должен ли этот код печататься "ok", или он должен бросить NullPointerException?

Atsby
источник
-2

Для Java и C # преимущество заключается в том, что они объектно-ориентированы.

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

С логической точки зрения - равенство объекта с другим не должно быть столь же очевидным, как сравнение свойств объекта на равенство (например, как логически интерпретируется null == null? Это может отличаться от случая к случаю).

Я думаю, что это сводится к тому, что вы наблюдаете, что «вы всегда хотите логического равенства, а не ссылочного равенства». Консенсус среди языковых дизайнеров был, вероятно, противоположным. Мне лично трудно это оценить, так как мне не хватает широкого спектра опыта программирования. Грубо говоря, я больше использую ссылочное равенство в алгоритмах оптимизации, а логическое равенство - больше в обработке наборов данных.

Рафаэль Эмшофф
источник
7
Ссылочное равенство не имеет ничего общего с объектной ориентацией. На самом деле, наоборот: одно из фундаментальных свойств объектной ориентации состоит в том, что объекты с одинаковым поведением неразличимы. Один объект должен иметь возможность имитировать другой объект. (В конце концов, ОО был изобретен для симуляции!) Ссылочное равенство позволяет вам различать два разных объекта с одинаковым поведением, оно позволяет вам различать симулируемый объект и реальный. Следовательно, Reference Equality нарушает объектную ориентацию. Программа OO не должна использовать эталонное равенство.
Йорг Миттаг
@ JörgWMittag: для правильного выполнения объектно-ориентированной программы необходимо наличие средства запроса у объекта X, равно ли его состояние состоянию Y (потенциально переходного состояния), а также средства запроса объекта X о его эквивалентности Y [X эквивалентно Y, только если его состояние гарантированно будет вечно равным Y]. Было бы хорошо иметь отдельные виртуальные методы для эквивалентности и равенства состояний, но для многих типов ссылочное неравенство подразумевает неэквивалентность, и нет причин тратить время на диспетчеризацию виртуальных методов, чтобы доказать это.
суперкат
-3

.equals()сравнивает переменные по их содержанию. вместо ==этого сравнивает объекты по их содержанию ...

использование объектов более точное использование .equals()

Нуно Диас
источник
3
Ваше предположение неверно. .equals () делает то, что было закодировано .equals (). Обычно это по содержанию, но это не обязательно. Также не точнее использовать .equals (). Это зависит только от того, чего вы пытаетесь достичь.
Молния