Как стать автором
Обновить

Комментарии 39

Какой самый неожиданный баг у вас возникал из-за разницы между == и .Equals()?

Таких багов не припомню, но был баг из-за разницы ... == null и ... Is null. Вернее, из-за переопределенных операторов равно и не равно. Foo == null давало иногда фолс, хотя Foo определенно был нулл. В процессе обдумывания причины я механически поменял на Foo is null, так как по моим личным меркам (и нашим соглашениям кодинга) равно и не равно нулл - это code smell. После этого баг пропал. Кто-то переопределил операторы равно и не равно, и там первым делом шла проверка типов, которая выбрасывала фолс еще до логики сравнения как таковой. Помогла бы обработка бордер кейса, когда оба параметра равны нулл, как по учебнику от МС. Код был не наш, поэтому мы тоже эти операторы не пофиксили, наши проблемы решил is, а я в очередной раз позанудствовал команде про пользу код стайла.

Вообще избегаю равно и не равно в конструкциях условного перехода. Кроме тех случаев, когда операторы явно переопределены в нашем коде. То есть на сегодня это означает реализацию типами IEquatable. Мало ли что там кто накрутил в этих операторах. В методах конечно тоже можно накрутить, но в метод даже джун при первом же дебаге лезет смотреть по ф12, а переопределенные операторы проверить приходит в голову не всем. Не так очевидно. Неявные вызовы в конвенционном коде - зло. Ну а с нулл сравнение только через is и is not, с пятого дотнета. Или их в шестом ввели? Склероз одолел.

Рекомендация по поводу переопределения операторов сравнения и всех связанных методов не ваша личная абсолютно. Решарпер не только ругается на это, но имеет встроенный сниппет чтобы ускорить кодинг. И как любой ворнинг в решарпере, этот имеет подробную документацию на сайте джетбрейнс. И даже реализовать IEquatable рекомендует. Не уверен на 100%, но вроде рослинатор - расширенная пачка анализаторов от команды рослин, тоже имеет и анализатор и код фикс для этого. Лень лезть проверять. Так что не стоит узурпировать эту рекомендацию ) Она вполне широко известна.

Спасибо

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

Иногда мы берём какую‑нибудь внешнюю библиотеку, где класс кажется обычным. Но авторы библиотеки перегрузили ==.

А если внешних библиотек несколько, и каждая по-своему перегружает ==, то что происходит, и как это разруливается?

А если внешних библиотек несколько, и каждая по-своему перегружает ==, то что происходит, и как это разруливается?

Перегрузка же для своего класса/типа определяется - нечего разруливать.

Потому что это удобно.

Классы сравниваемые по == сравниваются на равенство вида "это одно и то же?" и каждый класс сам решает, как это должно работать. Т.е. класс знает, как его сравнить. В общем случае это всё таки сравнение ссылок для ссылочных типов и сравнение значений для значимых типов. Строки исключение.

Классы сравниваемые по Equals обычно сравниваются по значению, причем опять таки бизнесово чаще. Т.е. у сущности можно сравнить только Id и тип и сказать "да, они эквивалентны".

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

Объекты разных типов(из разных библиотек) будут по-разному реагировать на ==.

Согласен. По-умолчанию программист использует == для сравнения ссылок. (если в контексте объектов класса). Если кто переопределит этот оператор, то становится совсем неочевидным это и скорее всего приведёт к путанице.

В Java такая же ерунда, очень бесит после плюсов.

Нет, не такая же. Оператор == в яве нельзя переопределить, у него постоянная семантика и этим он принципиально отличается от .equals().

А в шарпе как раз тот случай, когда содрали "как у явы", но не поняли зачем именно там сделали два разных оператора сравнения.

На самом деле, я не думаю, что дизайнеры шарпы были настолько глупы, но, кажется всё же, немного недальновидны. Или в шарпах есть третий оператор, который сравнивает уже только ссылки? ;) upd: а уже увидел, что есть ))

Есть такой, .ReferenceEquals()

Нет, не такая же.

Такая же, такая же... "только ещё хужее" (с)

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

?! Хотя, если имеется ввиду, что (его семантика) - это всегда т.н. equality check, то да - постоянная. Но, с этой точки зрения - она и в C# ровно такая же "постоянная".

А, на самом деле, как раз с equality check в java всё настолько просто и понятно (нет), что сразу вспоминается "самый популярный вопрос" и некоторые JEP'ы.

И да... если под "постоянная" подразумевается, что в Java == для ссылочных типов всегда проверяет "ссылки" - то это не так. Внезапно, не всегда.

А в шарпе как раз тот случай, когда содрали "как у явы", но не поняли зачем именно там сделали два разных оператора сравнения.

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

Простите, что вы имеете ввиду? В Java "ссылочные типы" всегда сравниваются по значению. Возможно вы про эффекты наличия объектов в Constant Pool? Или про кэши Integer? Это ничего не меняет, это всё ещё равенство ссылок.

В Java "ссылочные типы" всегда сравниваются по значению.

А с этим никто и не спорит. :-)

Возможно вы про эффекты наличия объектов в Constant Pool? Или про кэши Integer? Это ничего не меняет, это всё ещё равенство ссылок.

Нет, не про них (хотя это тож из разряда "красивое"). Я - скорее - про то, что equality operators в Java:

  1. Не имеют привязки (явной) к типу левого операнда, как обычно мы привыкли и ожидаем. К типу правого - если что - оно тож не привязано;

  2. Конкретное "поведение" выбирается исходя из спецификации (часть языка). И оно не такое простое и понятное, как хотелось бы;

  3. В этой спецификации как раз есть пункт, который описывает когда "значение" ссылочного типа - которое будет использовано для сравнения - это не значение его "ссылки".

Вот такая вот "постоянная семантика" (tm)

Я вас не понимаю. Моим ожиданиям сравнение в java полностью соответствует. Можете пожалуйста привести один-два конкретных примера кода, где оно не соответствует вашим?

Я вас не понимаю.

Ну вы чего?!

Integer.valueOf(x) == Integer.valueOf(x);

Ожидаем вычисление в false. Так?

Integer.valueOf(x) == Integer.valueOf(x).intValue();

А тут? А что поменялось? Ссылочный же тип :-)

... где оно не соответствует вашим?

Если речь про п. 1, то речь о том, что в языках, где есть перегрузка операторов, поведение оператора - конечно, сильно упрощая - определяется типом левого операнда.

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

Во втором случае всё совсем полностью понятно: сравнение ссылочного типа с примитивным принципиально невозможно, значит должен быть анбоксинг.

Замечу, что первый пример в реальной жизни я встречал 0 раз за 20 лет. Второй примерно 1.5 раза.

Во втором случае всё совсем полностью понятно: сравнение ссылочного типа с примитивным принципиально невозможно, значит должен быть анбоксинг.

В каком смысле "принципиально невозможно"? То, что в Java "принципиально невозможно" - в принципе, не компилируется :-)

В том-то и дело, что этот вариант вполне себе возможен, и то, что при разрешении там к операнду применяется (ко всему прочему) unboxing conversion, а не - к примеру - boxing conversion (а почему, собственно, нет?) - четко прописано в спецификации на equality operators.

И там же, кстати, есть за а почему этого не происходит в первом случае (хотя - казалось бы, что мешает?!)

Замечу, что первый пример в реальной жизни я встречал 0 раз за 20 лет.

Дык он же примечателен не тем, что "могут быть кэши". А тем, что есть ("неуловимая", ага) разница между ним и

Integer.valueOf(x) == new Integer(x);

Всё "просто и понятно" (tm)

Не просто же так конструкторы у этих "штук" помечены depricated начиная, емнип, с "девятки"? Ведь так? :-)

В "реальной жизни" оно вообще обычно стреляет не через if'ы, а, например, через упрятанный "в глубинах JDK" synchronized, в которой прилетает "такое" и всё внезапно ломается.

Второй примерно 1.5 раза.

Ну это просто самый простой способ показать:

  1. Что == у ссылочных типов не всегда берет "ссылку" для сравнения;

  2. Что не "просто и понятно" и - самое главное - сильно непросто контролировать.

Или вы реально считаете, что тот же JEP 390 на пустом месте родился, и его зря в "стандарт" потянули?!

А вы видели чтобы кто нибудь когда нибудь синхронизировался на value-based типах. Что это может быть за кейс? Или может быть на реальный баг натыкались? Я просто за 15 лет такого не встречал.

JEP 390 для подготовки к valhalla, там собственно про это и написано.

А вы видели чтобы кто нибудь когда нибудь синхронизировался на value-based типах.

Ну т.е. таки надо за не "на пустом месте"? :-) Если не лень - ну можете начать, например, с осени 2014 года. Ссылку сознательно дал на начало ветки, чтоб был ясен контекст. И даже в этой ветке уже есть упоминания о synchronized на строке, которая "внезапно была интернирована".

Что это может быть за кейс?

А что необычного-то?! Есть экземпляр ссылочного типа. Где-то было сказано, что его нельзя использовать для? До "восьмерки", в смысле. Да и в "восьмерке" это было сделано настолько, скажем так, "деликатно", что многие и не заметили даже. :-)

JEP 390 для подготовки к valhalla ...

Это что-то меняет?! Да львиная доля jep'ов - это "для подготовки к valhalla". Тем не менее, тот же 390й уже целиком и полностью в "стандарте" начиная с 16ой редакции (а "кусками" - так и раньше).

Я просто оставлю тут этот старый мем с бора

#define TRUE FALSE //счастливой отладки суки

Компилятор в любом случае выбросит warning, если не переопределять Equals при переопределенном == (но не наоборот). Ну и про GetHashCode напоминает. А переопределить только один из == и != вообще не дает, это ошибка, которую нельзя игнорировать.

Забавно, что в коде примеров здесь используется is null, но ни разу не упоминается, что оператор is не переопределяется и ВСЕГДА делает реальную проверку ссылки на null. Поэтому я, когда нужна проверка объекта на истинный null, чтобы не огрести NullReferenceException при вызове метода, например, всегда пишу is null, а не == null.

Поэтому я, когда нужна проверка объекта на истинный null, чтобы не огрести NullReferenceException при вызове метода, например, всегда пишу is null, а не == null.

Не везде поможет.
В Unity для своих объектов оператор == переопределен, и получается is null может быть false, а == null будет true.

В юнити ещё это выстреливает при myComponent?.DoIt() или для оператора ??

как ниже писали. для UnityEngine.Object переопределен операторы ==/!=

var go = new GameObject();

// уничтожаем объект, но ссылка не меняется на null
GameObject.Destroy(go);

 // тут переопределеннй оператор вернет false и SendMessage не вызовется
if (go != null) go.SendMessage(...);

// А будет честная проверка ReferenceEquals и программа упадет, вызвав SendMessage
go?.SendMessage(""); 

А как эта ссылка должна "поменяться на null"? Только через ref, но ее же легко прихранить где угодно, она же копируется в каждую новую переменную/поле.

Поэтому они пошли по пути ef core, где ты делаешь table.delete(entity), но entity у тебя не становится внезапно null. Почему же тогда у людей не возникает с этим проблем?

Если мне == null вернет true, то это не так страшно, NullReferenceException в любом случае не выскочит. А вот если он вернет false, а там на самом деле null, то вот здесь проблема, которую как раз is null решает, делая истинную проверку на нулевую ссылку.

Unity написан на двух языках C++ (runtime) и C#/.NET (scripting API).
Объекты Unity (классы унаследованные от UnityEngine.Object) существуют и в C++ части (реализуют базовый функционал движка) и в C# (как объект C# содержащий ссылку на объект C++ и обертка для доступа к этому объекту).
И в этих частях разное управление памятью: в C++ прямое, в C# уборка мусора.

Проблема: в C++ объект можно уничтожить, но объект в C# при этом остается живым (поскольку это ссылка на объект в C++) и попытка использовать его выдаст NullReferenceException.
В Unity это решили перезагрузив == null: если соответствующий объект в C++ уничтожен, то вернет true.
Но проверку is null перезагрузить нельзя - это проверки объекта (reference type) C# на null, и через неё работают null-conditional и null-coalescing операторы.

var obj = new GameObject("Test");
Destroy(obj);
Debug.Log("obj == null: " + (obj == null)); // true
Debug.Log("obj is null: " + (obj is null)); // false
// попытка использовать функционал движка выдаст ошибку: obj.name = "renamed";
// но методы C# не вызывающие функционал движка будут работать нормально

Поэтому проверка is null не гарантирует, что с объектом можно безопасно работать.

Проверка is null гарантирует, что там есть живой шарповый объект. То, что у него под капотом неуправляемые ресурсы закончились (привет disposable) - это уже не его задача знать.

А я и не говорил, что is null гарантирует, что с объектом можно безопасно работать. Но она гарантирует, что там действительно null и с ним работать нельзя. А Destroy в Unity - это некий аналог Dispose. И в обычном C# приложении так же может вывалится ObjectDisposedException при ненулевой и, вроде бы, рабочей ссылке. Проблема тут в том, что мелкомягкие не предусмотрели штатное свойство IsDisposed в интерфейсе IDisposable (сделали его только для контролов ну и некоторых других типов объектов, но не об этом речь сейчас). Поэтому разработчикам библиотек и фреймворков приходится извращаться, как в Unity.

через неё работают null-conditional и null-coalescing операторы

Я тут выше дал ссылку на SharpLab, который показывает низкоуровневый код, в котором видно, что эти операторы используют == null (внезапно).

Надо смотреть IL код для типа с перегруженным оператором ==, а не странную расшифровку C# для типа object.
И результат будет разный:

object? obj = rnd.Next(1) == 0 ? null : new();
var a0 = obj is null; // IL: ceq (прямое сравнение с null)
var a1 = obj == null; // IL: ceq

TestClass? obj2 = rnd.Next(1) == 0 ? null : new(5);
var a2 = obj2 is null; // IL: ceq
var a3 = obj2 == null; // IL: call bool TestClass::op_Equality(class TestClass, class TestClass)

Реальный вызов оператора == (op_Equality) будет только в случае == null для типа с перегруженным ==.

странную расшифровку

Это не "расшифровка", это один из проходов компилятора C#, сначала весь синтаксический сахар преобразуется в low level C# код, а уже потом происходит компиляция в IL.

Не очень понимаю, что вы своим примером хотите мне показать.

Разные экземпляры, но результат true

Что, простите? Какие ещё разные экземпляры? После интернирования это один и тот же экземпляр, естественно любое сравнение даёт True.

string s1 = "Hello";
string s2 = String.Intern("Hello");
Console.WriteLine(s1 == s2); // True
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // True

вырастает в целое шапито

Шапито, это когда на Хабре на серьезных щах обсуждают то что написано на первых страницах рихтеровской "C# via CLR"

Почти все посты, написанные на Хабре от имени компаний - чистое SEO. Здесь автор ничего не собирается обсуждать и кому-то что-то доказывать. Посмотрите, у него 500+ статей и при этом чуть больше 40 комментариев, и это еще много, обычно комментариев вообще не бывает. Главное - статью с ключевыми словами со ссылкой на компанию дать, а качество и полезность текста автора не волнует.

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

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

Правда и там был кот в мешке. Nothing, местный нан, был объектом, и каждый nothing - отдельный обьект....

Зарегистрируйтесь на Хабре, чтобы оставить комментарий