Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
Переопределение метода System.Object.GetHashCode()
В случае переопределения в классе метода Equals() вы также должны переопределить стандартную реализацию метода GetHashCode(). Выражаясь упрощенно, хеш-код — это числовое значение, которое представляет объект как специфическое состояние. Например, если вы создадите две переменные типа string, хранящие значение Hello, то они должны давать один и тот же хеш-код. Однако если одна из них хранит строку в нижнем регистре (hello), то должны получаться разные хеш-коды.
Для выдачи хеш-значения метод System.Object.GetHashCode() по умолчанию применяет адрес текущей ячейки памяти, где расположен объект. Тем не менее, если вы строите специальный тип, подлежащий хранению в экземпляре типа Hashtable (из пространства имен System.Collections), тогда всегда должны переопределять данный член, потому что для извлечения объекта тип Hashtable будет вызывать методы Equals() и GetHashCode().
На заметку! Говоря точнее, класс System.Collections.Hashtable внутренне вызывает метод GetHashCode(), чтобы получить общее представление о местоположении объекта, а с помощью последующего (внутреннего) вызова метода Equals() определяет его точно.
Хотя в настоящем примере мы не собираемся помещать объекты Person внутрь System.Collections.Hashtable, ради полноты изложения давайте переопределим метод GetHashCode(). Существует много алгоритмов, которые можно применять для создания хеш-кода, как весьма изощренных, так и не очень. В большинстве ситуаций есть возможность генерировать значение хеш-кода, полагаясь на реализацию метода GetHashCode() из класса System.String.
Учитывая, что класс String уже имеет эффективный алгоритм хеширования, использующий для вычисления хеш-значения символьные данные объекта String, вы можете просто вызвать метод GetHashCode() с той частью полей данных, которая должна быть уникальной во всех экземплярах (вроде номера карточки социального страхования), если ее удается идентифицировать. Таким образом, если в классе Person определено свойство SSN, то вы могли бы написать следующий код:
// Предположим, что имеется свойство SSN.
class Person
{
public string SSN {get; } = "";
public Person(string fName, string lName, int personAge,
string ssn)
{
FirstName = fName;
LastName = lName;
Age = personAge;
SSN = ssn;
}
// Возвратить хеш-код на основе уникальных строковых данных.
public override int GetHashCode() => SSN.GetHashCode();
}
В случае использования в качестве основы хеш-кода свойства, допускающего чтение и запись, вы получите предупреждение. После того, как объект создан, хеш-код должен быть неизменяемым. В предыдущем примере свойство SSN имеет только метод get, что делает его допускающим только чтение, и устанавливать его можно только в конструкторе.
Если вы не можете отыскать единый фрагмент уникальных строковых данных, но есть переопределенный метод ToString(), который удовлетворяет соглашению о доступе только по чтению, тогда вызывайте GetHashCode() на собственном строковом представлении:
// Возвратить хеш-код на основе значения, возвращаемого
// методом ToString() для объекта Person.
public override int GetHashCode() => ToString().GetHashCode();
Тестирование модифицированного класса Person
Теперь, когда виртуальные члены класса Object переопределены, обновите операторы верхнего уровня, чтобы протестировать внесенные изменения:
Console.WriteLine("***** Fun with System.Object *****n");
// ПРИМЕЧАНИЕ: мы хотим, чтобы эти объекты были идентичными
// в целях тестирования методов Equals() и GetHashCode().
Person p1 = new Person("Homer", "Simpson", 50, "111-11-1111");
Person p2 = new Person("Homer", "Simpson", 50, "111-11-1111");
// Получить строковые версии объектов.
Console.WriteLine("p1.ToString() = {0}", p1.ToString());
Console.WriteLine("p2.ToString() = {0}", p2.ToString());
// Протестировать переопределенный метод Equals().
Console.WriteLine("p1 = p2?: {0}", p1.Equals(p2));
// Протестировать хеш-коды.
// По-прежнему используется хеш-значение SSN
Console.WriteLine("Same hash codes?: {0}", p1.GetHashCode() == p2.GetHashCode());
Console.WriteLine();
// Изменить значение Age объекта p2 и протестировать снова.
p2.Age = 45;
Console.WriteLine("p1.ToString() = {0}", p1.ToString());
Console.WriteLine("p2.ToString() = {0}", p2.ToString());
Console.WriteLine("p1 = p2?: {0}", p1.Equals(p2));
// По-прежнему используется хеш-значение SSN
Console.WriteLine("Same hash codes?: {0}", p1.GetHashCode() == p2.GetHashCode());
Console.ReadLine();
Ниже показан вывод:
***** Fun with System.Object *****
p1.ToString() = [First Name: Homer; Last Name: Simpson; Age: 50]
p2.ToString() = [First Name: Homer; Last Name: Simpson; Age: 50]
p1 = p2?: True
Same hash codes?: True
p1.ToString() = [First Name: Homer; Last Name: Simpson; Age: 50]
p2.ToString() = [First Name: Homer; Last Name: Simpson; Age: 45]
p1 = p2?: False
Same hash codes?: True
Использование статических членов класса System.Object
В дополнение к только что рассмотренным членам уровня экземпляра класс System.Object определяет два статических члена, которые также проверяют эквивалентность на основе значений или на основе ссылок. Взгляните на следующий код:
static void StaticMembersOfObject()
{
// Статические члены System.Object.
Person p3 = new Person("Sally", "Jones", 4);
Person p4 = new Person("Sally", "Jones", 4);
Console.WriteLine("P3 and P4 have same state: {0}",
object.Equals(p3, p4));
// Р3 и P4 имеют то же самое состояние
Console.WriteLine("P3 and P4 are pointing to same object: {0}",
object.ReferenceEquals(p3, p4));
// Р3 и P4 указывают на тот же самый объект
}
Здесь вы имеете возможность просто отправить два объекта (любого типа) и позволить классу System.Object выяснить детали автоматически. Ниже показан вывод, полученный в результате вызова метода StaticMembersOfObject() в операторах верхнего уровня:
***** Fun with System.Object *****
P3 and P4 have the same state: True
P3 and P4 are pointing to the same object: False
Резюме
В настоящей главе объяснялась роль и детали наследования и полиморфизма. В ней были представлены многочисленные новые ключевые слова и лексемы для поддержки каждого приема. Например, вспомните, что с помощью двоеточия указывается родительский класс для создаваемого типа. Родительские типы способны определять любое количество виртуальных и/или абстрактных членов для установления полиморфного интерфейса. Производные типы переопределяют эти члены с применением ключевого слова override.
Вдобавок к построению множества иерархий классов в главе также исследовалось явное приведение между базовыми и производными типами. В завершение главы рассматривались особенности главного родительского класса в библиотеках базовых классов .NET Core