В предыдущей публикации мы рассмотрели особенности устройства и работы структур платформы .NET, являющихся "типами по значению" (Value Types) в разрезе сравнения по значению объектов — экземпляров структур.
Теперь рассмотрим готовый пример реализации сравнения по значению объектов — экземпляров структур.
Поможет ли пример для структур более точно определить с предметной (доменной) точки зрения область применимости сравнения объектов по значению в целом, и тем самым упростить образец сравнения по значению объектов — экземпляров классов, являющихся ссылочными типами (Reference Types), выведенный в одной из предыдущих публикаций?
using System;
namespace HelloEquatable
{
public struct PersonStruct : IEquatable<PersonStruct>, IEquatable<PersonStruct?>
{
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 PersonStruct(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 static bool Equals(PersonStruct first, PersonStruct second) =>
first.BirthDate == second.BirthDate &&
first.FirstName == second.FirstName &&
first.LastName == second.LastName;
public static bool operator ==(PersonStruct first, PersonStruct second) =>
Equals(first, second);
public static bool operator !=(PersonStruct first, PersonStruct second) =>
!Equals(first, second);
public bool Equals(PersonStruct other) =>
Equals(this, other);
public static bool Equals(PersonStruct? first, PersonStruct? second) =>
first == second;
// Alternate version:
//public static bool Equals(PersonStruct? first, PersonStruct? second) =>
// first.HasValue == second.HasValue &&
// (
// !first.HasValue || Equals(first.Value, second.Value)
// );
public bool Equals(PersonStruct? other) => this == other;
// Alternate version:
//public bool Equals(PersonStruct? other) =>
// other.HasValue && Equals(this, other.Value);
public override bool Equals(object obj) =>
(obj is PersonStruct) && Equals(this, (PersonStruct)obj);
// Alternate version:
//public override bool Equals(object obj) =>
// obj != null &&
// this.GetType() == obj.GetType() &&
// Equals(this, (PersonStruct)obj);
}
}
Пример с реализацией сравнения объектов по значению для структур меньше по объему и проще по структуре благодаря тому, что экземпляры структур не могут принимать null-значения и тому, что от структур, определенных пользователем (User defined structs), нельзя унаследоваться (особенности реализации сравнения по значению объектов — экземпляров классов, с учетом наследования, рассмотрены в четвертой публикации данного цикла).
Аналогично предыдущим примерам, определены поля для сравнения и реализован метод GetHashCode().
Методы и операторы сравнения реализованы последовательно следующим образом:
-
Реализован статический метод PersonStruct.Equals(PersonStruct, PersonStruct) для сравнения двух экземпляров структур.
Этот метод будет использован как эталонный способ сравнения при реализации других методов и операторов.
Также этот метод может использоваться для сравнения экземпляров структур в языках, не поддерживающих операторы. -
Реализованы операторы PersonStruct.==(PersonStruct, PersonStruct) и PersonStruct.!=(PersonStruct, PersonStruct).
Следует отметить, что компилятор C# имеет интересную особенность:
при наличии у структуры T перегруженных операторов T.==(T, T) и T.!=(T, T), для структур Nullable(Of T) также появляется возможность сравнения с помощью операторов T.==(T, T) и T.!=(T, T).
Вероятно, это "магия" компилятора, проверяющая наличие значения у экземпляров структуры, перед проверкой равенства непосредственно значений, и не приводящая к упаковке экземпляров структур в объекты.
Что характерно, в этом случае сравнение экземпляра структуры Nullable(Of T) с нетипизированным null также приводит к вызову оператора T.==(T, T) или T.!=(T, T), в то время как аналогичное сравнение экземпляра структуры Nullable(Of T), не имеющей перегруженных операторов T.==(T, T) и T.!=(T, T), приводит к вызову оператора Object.==(Object, Object) или Object.!=(Object, Object) и, как следствие, к упаковке экземпляра структуры объект. -
Реализован метод PersonStruct.Equals(PersonStruct) (реализация IEquatable(Of PersonStruct)), путем вызова метода PersonStruct.Equals(PersonStruct, PersonStruct).
- Для предотвращения упаковки экземпляров структур в объект, если в сравнении участвует один или два экземпляра Nullable(Of PersonStruct), реализованы:
-
Метод PersonStruct.Equals(PersonStruct?, PersonStruct?) — для предотвращения упаковки экземпляров структуробоих аргументов в объекты и вызова метода Object.Equals(Object, Object), если хотя бы один из аргументов является экземпляром Nullable(Of PersonStruct).
Также этот метод может быть использован при сравнении экземпляров Nullable(Of PersonStruct) в языках, не поддерживающих операторы.
Метод реализован как вызов оператора PersonStruct.==(PersonStruct, PersonStruct).
Рядом с методом приведен закомментированный код, показывающий, каким образом нужно было бы реализовать этот метод, если бы компилятор C# не поддерживал вышеупомянутую "магию" использования операторов T.==(T, T) и T.!=(T, T) для Nullable(Of T)-аргументов. -
Метод PersonStruct.Equals(PersonStruct?) (реализация интерфейса IEquatable(Of PersonStruct?)) — для предотвращения упаковки Nullable(Of PersonStruct)-аргумента в объект и вызова метода PersonStruct.Equals(Object).
Метод также реализован как вызов оператора PersonStruct.==(PersonStruct, PersonStruct), с закомментированным кодом реализации при отсутствии "магии" компилятора. - И наконец, реализован метод PersonStruct.Equals(Object), перекрывающий метод Object.Equals(Object).
Метод реализован путем проверки совместимости типа аргумента с типом текущего объекта с помощью оператора is, с последующими приведением аргумента к PersonStruct и вызовом PersonStruct.Equals(PersonStruct, PersonStruct).
Для структур исчерпывающая реализация сравнения экземпляров по значению получилась существенно проще и компактнее благодаря отсутствию наследования у User defined structs, а также благодаря отсутствию необходимости проверок на null.
(Однако, по сравнению с реализацией для классов, появилась и новая логика, поддерживающая Nullable(Of T)-аргументы).
В следующей публикации мы подведем итоги данного цикла на тему "Object Equality", в т.ч. рассмотрим:
- в каких случаях, с предметной и технической точек зрения, действительно целесообразно реализовывать сравнение значений объектов по значению;
- каким образом в этих случаях возможно упростить реализацию сравнения по значению для объектов — экземпляров классов, являющихся ссылочными типами (Reference Types), с учетом опыта упрощенной реализации для структур.
Автор: sand14