Чому 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) {
    ...
}

У цьому випадку ми очікуємо, що "дорівнює 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?

Проблема смислової рівності полягає в тому, що вона не може бути автоматично сформована компілятором для об'єктів, і це не очевидно з призначення . Користувачеві повинен бути передбачений механізм визначення смислової рівності. У об'єктно-орієнтованих мовах цей механізм є успадкованим методом: дорівнює . Читаючи фрагмент коду OO, ми не очікуємо, що метод має однакову точну реалізацію у всіх класах. Ми звикли успадковувати і перевантажувати.

Однак, з операторами ми очікуємо такої ж поведінки. Коли ви бачите "a == b", ви повинні очікувати однакового типу рівності (з 4 вище) у всіх ситуаціях. Отже, прагнучи до узгодженості, дизайнери мов використовували еталонну рівність для всіх типів. Це не повинно залежати від того, програміст перекрив метод чи ні.

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


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") для використання == (рівність відліку).
Хбас

1
PS: C #, з іншого боку, не відповідав його рядкам. І IMHO, у цьому випадку, це набагато краще.
Хбас

@CharlesSalvia: У Java, якщо aі bє одного типу, вираз a==bперевіряє, чи є aта чи bє те саме. Якщо один з них містить посилання на об’єкт №291, а інший містить посилання на об’єкт № 572, вони не містять однакової речі. У зміст об'єктно # 291 і # 572 можуть бути еквівалентні, але самі змінні містять різні речі.
supercat

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?


-2

Перевага для Java та C # полягає в тому, що вони орієнтовані на об'єкти.

З точки зору продуктивності - простіший код також повинен бути швидшим: оскільки OOP має намір логічно відрізняти елементи, представлені різними об'єктами, перевірка еталонної рівності була б швидшою, враховуючи, що об'єкти можуть стати досить великими.

З логічної точки зору - рівність об'єкта іншому не повинна бути такою очевидною, як порівняння властивостей об'єкта щодо рівності (наприклад, як логічно інтерпретується null == null? Це може відрізнятися від конкретного випадку).

Я думаю, що це зводиться до вашого зауваження, що "ви завжди хочете, щоб логічна рівність була порівняно з еталонною рівністю". Консенсус серед мовних дизайнерів, ймовірно, був протилежним. Мені особисто важко це оцінити, оскільки мені бракує широкого спектру досвіду програмування. Приблизно я більше використовую опорну рівність більше в алгоритмах оптимізації, а логічна рівність більше в обробці наборів даних.


7
Довідкова рівність не має нічого спільного з орієнтацією на об'єкт. Насправді навпаки: одна з основних властивостей об'єктної орієнтації полягає в тому, що об'єкти, що мають однакову поведінку, не відрізняються. Один об’єкт повинен бути в змозі імітувати інший об’єкт. (Зрештою, ОО було винайдено для моделювання!) Довідкова рівність дозволяє розрізняти два різних об'єкти, які мають однакову поведінку, дозволяє розрізняти модельований об’єкт і реальний. Тому довідкова рівність порушує орієнтацію на об'єкт. Програма ОО не повинна використовувати еталонну рівність.
Йорг W Міттаг

@ JörgWMittag: Щоб правильно виконати об'єктно-орієнтовану програму, потрібно мати засоби запитання об’єкта X, чи його стан дорівнює стану Y [потенційно перехідна умова], а також засіб запитати об'єкт X, чи він еквівалентний Y [X еквівалентний Y лише за умови, що його стан вічно дорівнює Y]. Окремі віртуальні методи еквівалентності та рівності стану було б добре, але для багатьох типів посилання нерівності означатиме нееквівалентність, і немає причин витрачати час на відправлення віртуального методу, щоб довести це.
supercat

-3

.equals()порівнює змінні за їх змістом. замість ==цього порівнює об’єкти за їх вмістом ...

використання об'єктів більш точне використання ту .equals()


3
Ви припускаєте, що це неправильно. .equals () робить все, що .equals () було закодовано. Зазвичай це за змістом, але цього не повинно бути. Також не більш точно використовувати .equals (). Це просто залежить від того, що ви намагаєтеся зробити.
блискавка
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.