- PVSM.RU - https://www.pvsm.ru -
По умолчанию два объекта считаются равными, если соответствующие переменные содержат одну и ту же ссылку.
В противном случае объекты считаются неравными.
Однако, может возникнуть ситуация, когда необходимо считать объекты некоторого класса равными, если они определенным образом совпадают по своему содержимому (тождественны).
На примере этого класса рассмотрим:
Для каждого случая рассмотрим, каким именно образом лучше реализовать сравнение объектов по значению, чтобы получился согласованный и, насколько это возможно, компактный, copy-paste free, производительный код.
Задача является не настолько тривиальной, насколько это может показаться на первый взгляд.
А также рассмотрим, какие улучшения могли бы быть внесены в платформу, чтобы упростить реализацию этой задачи.
Класс Person:
using System;
namespace HelloEquatable
{
public class Person
{
private static string NormalizeName(string name) => name?.Trim() ?? string.Empty;
private static DateTime? NormalizeDate(DateTime? date) => date?.Date;
public string FirstName { get; }
public string LastName { get; }
public DateTime? BirthDate { get; }
public Person(string firstName, string lastName, DateTime? birthDate)
{
this.FirstName = NormalizeName(firstName);
this.LastName = NormalizeName(lastName);
this.BirthDate = NormalizeDate(birthDate);
}
}
}
Если два объекта класса Person сравнивать любым способом:
то объекты будут считаться равными, только если соответствующие переменные содержат одну и ту же ссылку;
при помещении в хеш-наборы (хеш-карты) [6] и словари [7], объекты так же будут считаться равными только в случае совпадения ссылок.
В этом случае для сравнения объектов по значению в клиентском коде потребуется написать строки вида:
var p1 = new Person("John", "Smith", new DateTime(1990, 1, 1));
var p2 = new Person("John", "Smith", new DateTime(1990, 1, 1));
bool personEquals =
p1.BirthDate == p2.BirthDate &&
p1.FirstName == p2.FirstName &&
p1.LastName == p2.LastName;
Примечания:
Для того, чтобы объекты класса Person можно было сравнивать по значению следующими способами:
у класса Person необходимо перекрыть методы Object.Equals(Object) [2] и Object.GetHashCode() [13], причем таким образом, чтобы:
Стоит обратить особое внимание, что в документации [2] к методу Equals(Object) [2] приведены специальные требования:
Также стоит обратить внимание, что в документации [13] к методу GetHashCode() [13] приведено предупреждение, что значение, возвращаемое методом, не является постоянным значением, и поэтому его не следует сохранять на диск или в базу данных, использовать в качестве ключа, и т.д.
Класс Person с перекрытыми методами Equals(Object) [2] и GetHashCode() [13]:
using System;
namespace HelloEquatable
{
public class Person
{
private static string NormalizeName(string name) => name?.Trim() ?? string.Empty;
private static DateTime? NormalizeDate(DateTime? date) => date?.Date;
public string FirstName { get; }
public string LastName { get; }
public DateTime? BirthDate { get; }
public Person(string firstName, string lastName, DateTime? birthDate)
{
this.FirstName = NormalizeName(firstName);
this.LastName = NormalizeName(lastName);
this.BirthDate = NormalizeDate(birthDate);
}
public override int GetHashCode() =>
this.FirstName.GetHashCode() ^
this.LastName.GetHashCode() ^
this.BirthDate.GetHashCode();
public override bool Equals(object obj)
{
if ((object)this == obj)
return true;
var other = obj as Person;
if ((object)other == null)
return false;
return
this.BirthDate == other.BirthDate &&
this.FirstName == other.FirstName &&
this.LastName == other.LastName;
}
}
}
Примечания к методу GetHashCode() [13]:
public override int GetHashCode() =>
this.FirstName?.GetHashCode() ?? 0 ^
this.LastName?.GetHashCode() ?? 0 ^
this.BirthDate?.GetHashCode() ?? 0;
Рассмотрим детально, как именно реализован метод Equals(Object) [2]:
Обратим внимание на требование к методу Equals(Object) [2]:
x.Equals(null) returns false.
Когда-то меня заинтересовало, почему некоторые экземплярные методы в стандартной библиотеке .NET проверяют this [16] на null [8] — например, так реализован метод String.Equals(Object) [18]:
public override bool Equals(Object obj) {
//this is necessary to guard against reverse-pinvokes and
//other callers who do not use the callvirt instruction
if (this == null)
throw new NullReferenceException();
String str = obj as String;
if (str == null)
return false;
if (Object.ReferenceEquals(this, obj))
return true;
if (this.Length != str.Length)
return false;
return EqualsHelper(this, str);
}
Первым делом в методе выполняется проверка this [16] на null [8] и, в случае положительного результата проверки, генерируется исключение NullReferenceException [10].
В комментарии указано, в каких случаях this [16] может принимать null [8]-значение.
(Кстати, сравнение this [16] на null [8] выполнено с помощью оператора == [4], который у класса String [19] перегружен [20], поэтому с точки зрения производительности проверку лучше сделать, явно приведя this [16] к object [21]: (object)this == null, или же воспользоваться методом Object.ReferenceEquals(Object, Object) [1], как это сделано во втором сравнении в этом же методе.)
А затем появилась статья, где об этом можно прочитать подробнее: Когда this == null: невыдуманная история из мира CLR [22].
Однако, в таком случае, если вызвать перегруженный метод Person.Equals(Object) без создания экземпляра, передав в качестве входного параметра null [8], то первая же строчка метода (if ((object)this == obj) return true;) возвратит true [14], что фактически будет правильно, но формально будет противоречить требованиям к реализации метода.
При этом в документации [2] к методу не указано, что первым делом нужно проверять this [16] на null [8] и генерировать исключение в случае успешной проверки.
Да и в таком случае следовало бы вообще во всех экземплярных методах всех классов первой строчкой проверять this [16] на null [8], что является абсурдом.
Поэтому представляется, что официальные требования к реализации метода Equals(Object) [2] должны быть уточнены следующим образом:
Он касается того, как наиболее корректно реализовать требование:
x.Equals(y) returns the same value as y.Equals(x).
И того, полностью и непротиворечиво ли изложены в документации [2] требования и примеры к реализации метода в этой части, и есть ли альтернативные походы к реализации этого требования.
Об этом, так же как и о реализации полного набора доработок класса для сравнения его объектов по значению, поговорим в следующих публикациях.
Автор: sand14
Источник [23]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/205995
Ссылки в тексте:
[1] Object.ReferenceEquals(Object, Object): https://msdn.microsoft.com/library/system.object.referenceequals.aspx
[2] Object.Equals(Object): https://msdn.microsoft.com/library/bsc2ak47.aspx
[3] Object.Equals(Object, Object): https://msdn.microsoft.com/library/w4hkze5k.aspx
[4] ==: https://msdn.microsoft.com/library/53k8ybth.aspx
[5] !=: https://msdn.microsoft.com/library/3tz250sf.aspx
[6] хеш-наборы (хеш-карты): https://msdn.microsoft.com/library/bb359438.aspx
[7] словари: https://msdn.microsoft.com/library/xfhwa508.aspx
[8] null: https://msdn.microsoft.com/library/edakx9da.aspx
[9] пустая строка: https://msdn.microsoft.com/library/system.string.empty.aspx
[10] NullReferenceException: https://msdn.microsoft.com/library/system.nullreferenceexception.aspx
[11] Nullable(Of T): https://msdn.microsoft.com/library/b3h38hb0.aspx
[12] MinValue: https://msdn.microsoft.com/library/system.datetime.minvalue.aspx
[13] Object.GetHashCode(): https://msdn.microsoft.com/library/system.object.gethashcode.aspx
[14] true: https://msdn.microsoft.com/library/6x6y6z4d.aspx
[15] «исключающее или»: https://msdn.microsoft.com/library/zkacc7k1.aspx
[16] this: https://msdn.microsoft.com/library/dk1507sz.aspx
[17] false: https://msdn.microsoft.com/library/67bxt5ee.aspx
[18] String.Equals(Object): https://msdn.microsoft.com/library/fkfd9eh8.aspx
[19] String: https://msdn.microsoft.com/library/system.string.aspx
[20] перегружен: https://msdn.microsoft.com/library/system.string.op_equality.aspx
[21] object: https://msdn.microsoft.com/library/9kkx3h3c.aspx
[22] Когда this == null: невыдуманная история из мира CLR: https://habrahabr.ru/company/enterra/blog/252249/
[23] Источник: https://habrahabr.ru/post/314328/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.