Чтобы ответить на этот вопрос, мы должны описать четыре вида эквивалентности объектов:
Справочное равенство, object.ReferenceEquals (a, b) : две переменные указывают на один и тот же точный объект в ОЗУ. (Если бы это был C, обе переменные имели бы одинаковый точный указатель.)
Взаимозаменяемость, a == b : две переменные относятся к объектам, которые полностью взаимозаменяемы. Таким образом, когда a == b, Func (a, b) и Func (b, a) делают одно и то же.
Семантическое равенство, объект. Уравнения (a, b) : В этот точный момент времени два объекта означают одно и то же.
Равенство сущностей, a.Id == b.Id : два объекта ссылаются на одну и ту же сущность, например на строку базы данных, но не должны иметь одинаковое содержимое.
Как программист, при работе с объектом известного типа вам необходимо понимать тип эквивалентности, который подходит вашей бизнес-логике в конкретный момент кода, в котором вы находитесь.
Простейшим примером этого являются строки в сравнении с типами StringBuilder. Переопределение строк ==, StringBuilder не делает:
var aaa1 = "aaa";
var aaa2 = $"{'a'}{'a'}{'a'}";
var bbb = "bbb";
// False because aaa1 and aaa2 are completely different objects with different locations in RAM
Console.WriteLine($"Object.ReferenceEquals(aaa1, aaa2): {Object.ReferenceEquals(aaa1, aaa2)}");
// True because aaa1 and aaa2 are completely interchangable
Console.WriteLine($"aaa1 == aaa2: {aaa1 == aaa2}"); // True
Console.WriteLine($"aaa1.Equals(aaa2): {aaa1.Equals(aaa2)}"); // True
Console.WriteLine($"aaa1 == bbb: {aaa1 == bbb}"); // False
Console.WriteLine($"aaa1.Equals(bbb): {aaa1.Equals(bbb)}"); // False
// Won't compile
// This is why string can override ==, you can not modify a string object once it is allocated
//aaa1[0] = 'd';
// aaaUpdated and aaa1 point to the same exact object in RAM
var aaaUpdated = aaa1;
Console.WriteLine($"Object.ReferenceEquals(aaa1, aaaUpdated): {Object.ReferenceEquals(aaa1, aaaUpdated)}"); // True
// aaaUpdated is a new string, aaa1 is unmodified
aaaUpdated += 'c';
Console.WriteLine($"Object.ReferenceEquals(aaa1, aaaUpdated): {Object.ReferenceEquals(aaa1, aaaUpdated)}"); // False
var aaaBuilder1 = new StringBuilder("aaa");
var aaaBuilder2 = new StringBuilder("aaa");
// False, because both string builders are different objects
Console.WriteLine($"Object.ReferenceEquals(aaaBuider1, aaaBuider2): {Object.ReferenceEquals(aaa1, aaa2)}");
// Even though both string builders have the same contents, they are not interchangable
// Thus, == is false
Console.WriteLine($"aaaBuider1 == aaaBuilder2: {aaaBuilder1 == aaaBuilder2}");
// But, because they both have "aaa" at this exact moment in time, Equals returns true
Console.WriteLine($"aaaBuider1.Equals(aaaBuilder2): {aaaBuilder1.Equals(aaaBuilder2)}");
// Modifying the contents of the string builders changes the strings, and thus
// Equals returns false
aaaBuilder1.Append('e');
aaaBuilder2.Append('f');
Console.WriteLine($"aaaBuider1.Equals(aaaBuilder2): {aaaBuilder1.Equals(aaaBuilder2)}");
Чтобы получить более подробную информацию, мы можем работать в обратном направлении, начиная с равенства сущностей. В случае равенства сущностей, свойства сущности могут изменяться со временем, но первичный ключ сущности никогда не меняется. Это можно продемонстрировать с помощью псевдокода:
// Hold the current user object in a variable
var originalUser = database.GetUser(123);
// Update the user’s name
database.UpdateUserName(123, user.Name + "son");
var updatedUser = database.GetUser(123);
Console.WriteLine(originalUser.Id == updatedUser.Id); // True, both objects refer to the same entity
Console.WriteLine(Object.Equals(originalUser, updatedUser); // False, the name property is different
Переходя к семантическому равенству, пример слегка меняется:
var originalUser = new User() { Name = "George" };
var updatedUser = new User() { Name = "George" };
Console.WriteLine(Object.Equals(originalUser, updatedUser); // True, the objects have the same contents
Console.WriteLine(originalUser == updatedUser); // User doesn’t define ==, False
updatedUser.Name = "Paul";
Console.WriteLine(Object.Equals(originalUser, updatedUser); // False, the name property is different
А как насчет взаимозаменяемости? (переопределение ==) Это сложнее. Давайте немного воспользуемся приведенным выше примером:
var originalUser = new User() { Name = "George" };
var updatedUser = new User() { Name = "George" };
Console.WriteLine(Object.Equals(originalUser, updatedUser); // True, the objects have the same contents
// Does this change updatedUser? We don’t know
DoSomethingWith(updatedUser);
// Are the following equivalent?
// SomeMethod(originalUser, updatedUser);
// SomeMethod(updatedUser, originalUser);
В приведенном выше примере DoSomethingWithUser (updatedUser) может изменить updatedUser. Таким образом, мы больше не можем гарантировать, что объекты originalUser и updatedUser являются «Равными». Вот почему пользователь не переопределяет ==.
Хороший пример того, когда переопределять ==, - это неизменные объекты. Неизменяемый объект - это объект, публично видимое состояние (свойства) которого никогда не меняются. Все видимое состояние должно быть установлено в конструкторе объекта. (Таким образом, все свойства доступны только для чтения.)
var originalImmutableUser = new ImmutableUser(name: "George");
var secondImmutableUser = new ImmutableUser(name: "George");
Console.WriteLine(Object.Equals(originalImmutableUser, secondImmutableUser); // True, the objects have the same contents
Console.WriteLine(originalImmutableUser == secondImmutableUser); // ImmutableUser defines ==, True
// Won’t compile because ImmutableUser has no setters
secondImmutableUser.Name = "Paul";
// But this does compile
var updatedImmutableUser = secondImmutableUser.SetName("Paul"); // Returns a copy of secondImmutableUser with Name changed to Paul.
Console.WriteLine(object.ReferenceEquals(updatedImmutableUser, secondImmutableUser)); // False, because updatedImmutableUser is a different object in a different location in RAM
// These two calls are equivalent because the internal state of an ImmutableUser can never change
DoSomethingWith(originalImmutableUser, secondImmutableUser);
DoSomethingWith(secondImmutableUser, originalImmutableUser);
Стоит ли переопределять == изменяемым объектом? (То есть объект, чье внутреннее состояние может измениться?) Вероятно, нет. Вам потребуется создать довольно сложную систему событий, чтобы поддерживать взаимозаменяемость.
В общем, я работаю с большим количеством кода, который использует неизменяемые объекты, поэтому я переопределяю ==, потому что он более читабелен, чем object.Equals. Когда я работаю с изменяемыми объектами, я не переопределяю == и полагаюсь на object.Equals. Программист обязан знать, являются ли объекты, с которыми они работают, изменяемыми или нет, потому что знание того, может ли состояние чего-либо измениться, должно влиять на то, как вы разрабатываете свой код.
Реализацией по умолчанию == является object.ReferenceEquals, потому что с изменяемыми объектами взаимозаменяемость гарантируется только тогда, когда переменные указывают на один и тот же точный объект в ОЗУ. Даже если объекты имеют одинаковое содержимое в данный момент времени (Equals возвращает true), нет гарантии, что объекты будут продолжать оставаться равными; таким образом, объекты не являются взаимозаменяемыми. Таким образом, при работе с изменяемым объектом, который не переопределяет ==, реализация по умолчанию == работает, потому что если a == b, это один и тот же объект, а SomeFunc (a, b) и SomeFunc (b, a) точно так же.
Кроме того, еслизадница не определяет эквивалентность (например, представьте себе соединение с базой данных и открытый дескриптор файла, т. д.), тогда реализация по умолчанию == и Equals возвращаются к ссылочному равенству, поскольку две переменные типа подключения к базе данных, дескриптор открытого файла , ect, равны, только если они являются точным экземпляром соединения с базой данных, дескриптор открытого файла, ect. Равенство сущностей может иметь смысл в бизнес-логике, которая должна знать, что два разных подключения к базе данных ссылаются на одну и ту же базу данных или что два разных файловых дескриптора ссылаются на один и тот же файл на диске.
Теперь, для моего мыльного момента. На мой взгляд, C # обрабатывает эту тему в замешательстве. == должно быть для семантического равенства вместо метода Equals. Должен быть другой оператор, например ===, для взаимозаменяемости и, возможно, другой оператор, ====, для ссылочного равенства. Таким образом, кто-то, кто является новичком и / или пишет CRUD-приложения, должен понимать только ==, а не более тонкие детали взаимозаменяемости и ссылочного равенства.