Przeanalizowaliśmy już osobliwości struktur platformy .NET, które reprezentują typy wartości podczas porównywania obiektów według wartości – instancje struktur.
Teraz opiszę ten proces na konkretnym przykładzie, aby sprawdzić, czy pozwoli nam to ogólnie określić zastosowanie porównania obiektów po wartości, a tym samym uprościć próbkę porównywania obiektów po wartości – instancje klas reprezentujące referencję typy.
Struktura PersonStruct:
using System; namespace HelloEquatable { public struct PersonStruct : IEquatable<PersonStruct>, IEquatable<PersonStruct?> { private static int GetHashCodeHelper(int[] subCodes) { int result = subCodes[0]; for (int i = 1; i < subCodes.Length; i++) result = unchecked(result * 397) ^ subCodes[i]; return result; } 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() => GetHashCodeHelper( new int[] { 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); } }
Jak widać, ten przykład jest mniejszy i łatwiejszy ze względu na strukturę, ponieważ instancje struktur nie mają wartości null i nie można dziedziczyć ze struktur zdefiniowanych przez użytkownika. Omówiliśmy już osobliwości implementacji porównania według wartości dla instancji klas w moim poprzednim artykule.
Dodatkowo wyznaczyliśmy pola do porównania obiektów oraz zaimplementowaliśmy metodę GetHashCode().
Metody i operatory porównania zostały zaimplementowane w następującej kolejności:
- Aby porównać dwa wystąpienia struktur, zaimplementowaliśmy metodę statyczną PersonStruct.Equals(PersonStruct, PersonStruct). Użyjemy tej metody jako metody porównawczej referencji podczas implementacji innych metod i operatorów. Dodatkowo może być stosowany do porównywania wystąpień struktur w językach, które nie obsługują operatorów.
- Operatory PersonStruct.==(PersonStruct, PersonStruct) i PersonStruct.!=(PersonStruct, PersonStruct) również zostały zaimplementowane. Należy zauważyć, że kompilator C# ma następujące cechy szczególne:
- Możesz porównać z przeciążonymi operatorami T.==(T, T) i T.!=(T, T) w Nullable(Of T)
- Przed sprawdzeniem równości wartości kompilator może sprawdzić, czy wystąpienia struktur mają prawidłową wartość. Ponadto kompilator nie opakowuje instancji struktur w obiekty.
- Tak więc porównywanie wystąpień struktury Nullable(Of T) z niewpisaną wartością dopuszczającą wartość null prowadzi do wywołania operatorów ==(T, T) lub T.!=(T, T) podczas porównywania wystąpień Nullable( Of T) struktura bez przeciążonych operatorów T.==(T, T) i T.!=(T, T) powoduje wywołanie operatorów Object.==(Object, Object) lub Object.!=(Object, Object) i w rezultacie zawijając instancję w obiekt.
- Metoda PersonStruct.Equals(PersonStruct) (implementacja IEquatable(Of PersonStruct)) została zaimplementowana poprzez wywołanie metody PersonStruct.Equals(PersonStruct, PersonStruct).
- Aby uniknąć zawijania instancji struktur w obiekty, gdy mamy jedną lub dwie instancje Nullable(Of PersonStruct), możliwe jest zaimplementowanie następujących metod:
- PersonStruct.Equals(PersonStruct?, PersonStruct?), jako wywołanie operatora PersonStruct.==(PersonStruct, PersonStruct) służy do uniknięcia zawijania wystąpień struktur obu argumentów w obiekty i wywoływania Object.Equals( Metoda Object, Object), jeśli co najmniej jeden z argumentów jest wystąpieniem Nullable(Of PersonStruct). Ponadto można użyć tej metody do porównywania wystąpień Nullable(Of PersonStruct) w językach, które nie obsługują operatorów. W kodzie można znaleźć komentarze wyjaśniające, jak można zaimplementować tę metodę, jeśli kompilator C# nie był w stanie użyć operatorów T.==(T, T) i T.!=(T, T) dla wartości Nullable(Of T) argumenty.
- PersonStruct.Equals(PersonStruct?) – implementacja interfejsu IEquatable(Of PersonStruct?) służąca do uniknięcia zawijania argumentów Nullable(Of PersonStruct) w obiekty i wywoływania metody PersonStruct.Equals(Object). Jest zaimplementowany jako wywołanie operatora PersonStruct.==(PersonStruct, PersonStruct) z komentowanym kodem do użycia operatorów T.==(T, T) i T.!=(T, T) dla Nullable(Of T ) argumenty.
- PersonStruct.Equals(Object) – nadpisuje metodę Object.Equals(Object). Jest to implementowane przez sprawdzenie zgodności typu argumentu z typem bieżącego obiektu za pomocą operatora is przez rzutowanie argumentu na PersonStruct i wywołanie PersonStruct.Equals(PersonStruct, PersonStruct).
Uwagi:
- Implementacja interfejsu IEquatable(Of PersonStruct?) — IEquatable(Of Nullable(Of PersonStruct)) służy do pokazania konkretnych problemów na platformie podczas pracy ze strukturami, w których zawijanie instancji w obiekty odbywa się szybciej niż się spodziewamy.
- W realnych projektach, o ile nie jest konieczne poprawianie wydajności, implementacja IEquatable(Of Nullable(Of T)) nie ma zastosowania ze względów architektonicznych – nie powinniśmy implementować wpisanego IEquatable w typie T dla żadnego typu.
- Ogólnie rzecz biorąc, nie jest konieczne przytłaczanie kodu różnymi optymalizacjami.
W przypadku struktur możemy osiągnąć znacznie prostsze i bardziej wydajne porównywanie według wartości, unikając dziedziczenia struktur zdefiniowanych przez użytkownika i konieczności sprawdzania obiektów pod kątem wartości null. Ponadto możemy monitorować nową logikę, która obsługuje argumenty Nullable(Of T).
W mojej przyszłej publikacji podsumuję następujące punkty:
- Kiedy dobrym pomysłem jest zaimplementowanie porównywania obiektów według wartości;
- Jak możemy uprościć implementację porównania według wartości dla obiektów – instancje klas, które reprezentują typy referencyjne.
Przeczytaj także:
Porównywanie obiektów według wartości. Część 1:Początek
Porównywanie obiektów według wartości. Część 2:Uwagi dotyczące implementacji metody równości
Porównywanie obiektów według wartości. Część 3:Równa specyficzne dla typu i operatory równości
Porównywanie obiektów według wartości. Część 4:Operatory dziedziczenia i porównania
Porównywanie obiektów według wartości. Część 5:Kwestia równości struktury