На этом шаге мы рассмотрим "правильную" перегрузку операторов == и !=.
Теперь вернемся к методам Equals() и GetHashCode(), которые рекомендуется переопределять при перегрузке операторов == и !=. И сначала несколько слов о месте и роли этих методов.
Метод Equals() из класса Object предназначен для сравнения двух объектов на предмет равенства. Метод вызывается из одного объекта, а другой объект передается аргументом методу. При этом объекты могут относиться к разным классам. Сравнение объектов (того, из которого вызывается метод, и того, который передан аргументом методу) на предмет равенства выполняется на уровне сравнения ссылок на объекты. Метод Equals() возвращает значение true, если обе объектные переменные ссылаются на один и тот же объект. Если переменные ссылаются на разные объекты, метод Equals() возвращает значение false.
Метод Equals() описан в классе Object, и его аргумент объявлен как относящийся классу Object. Класс Object находится в вершине иерархии наследования, поэтому все классы, включая и описываемые нами в программе, неявно "получают в наследство" методы из класса Object. Это касается и метода Equals(). Мы можем вызывать этот метод из объекта описанного нами класса, даже если мы в классе такой метод не описали. Аргументом методу мы можем передавать объект любого класса, поскольку аргумент метода Equals() относится к классу Object, а объектная переменная класса Object может ссылаться на объект любого класса.
Метод GetHashCode() также описан в классе Object, поэтому наследуется во всех классах. У метода нет аргументов, а результатом метод возвращает целое число. Это целое число называется хэш-кодом объекта, из которого вызывается метод. Сам по себе хэш-код особого смысла не имеет. Хэш-коды используются для сравнения двух объектов на предмет равенства. Главное правило такое: если два объекта считаются одинаковыми (равными), то у них должны быть одинаковые хэш-коды.
Таким образом, каждый раз при описании класса, вне зависимости от нашего желания, в этом классе будут "неявно присутствовать" методы Equals() и GetHashCode() - мы можем вызвать эти методы из объекта класса, несмотря на то что лично мы методы в классе и не описывали. По умолчанию эти методы работают по определенным алгоритмам, но важно то, что анализируются ссылки для сравниваемых объектных переменных. По умолчанию операторы == и != реализуются в строгом соответствии с таким подходом. То есть мы имеем дело с "великолепной четверкой" (методы Equals() и GetHashCode() и операторы == и !=), которая функционирует в согласованном, "гармонизированном" режиме. И как только мы перегружаем операторы сравнения == и !=, меняя способ определения "равенства" объектов, то эта "гармония" нарушается. Поэтому, перегружая операторы == и !=, желательно переопределить и методы Equals() и GetHashCode(). Как это можно было бы сделать, показано далее.
В примере ниже вашему вниманию представлена программа, в которой наряду с перегрузкой операторов == и != еще и переопределяются методы Equals() и GetHashCode().
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace pr112_1 { class Program { // Класс с перегрузкой операторов сравнения == и != //и переопределением методов GetHashCode() и Equals(): class MyClass { // Целочисленное поле: public int code; // Символьное поле: public char symb; // Конструктор с двумя аргументами: public MyClass(int n, char s) { code = n; // Значение целочисленного поля symb = s; // Значение символьного поля } // Переопределение метода GetHashCode(): public override int GetHashCode() { // Вычисление хэш-кода: return code ^ symb; } // Переопределение метода Equals(): public override bool Equals(Object obj) { // Локальная объектная переменная: MyClass t = (MyClass)obj; // Результат метода: if ( code == t.code && symb == t.symb) return true; else return false; } // Перегрузка оператора "равно": public static bool operator==(MyClass a, MyClass b) { // Вызов метода Equals(): return a.Equals(b); } // Перегрузка оператора "не равно": public static bool operator!=(MyClass a, MyClass b) { // Использование оператора "равно": return !(a==b); } } // Главный метод: static void Main() { // Создание объектов: MyClass A = new MyClass(100, 'A'); MyClass B = new MyClass(100, 'B'); MyClass C = new MyClass(200, 'A'); MyClass D = new MyClass(100, 'A'); // Вычисление хэш-кодов: Console.WriteLine("Хэш-код A: {0}", A.GetHashCode()); Console.WriteLine("Хэш-код В: {0}", B.GetHashCode()); Console.WriteLine("Хэш-код C: {0}", C.GetHashCode()); Console.WriteLine("Хэш-код D: {0}", D.GetHashCode()); // Сравнение объектов на предмет // равенства и неравенства: Console.WriteLine("А==В дает {0}", A==B); Console.WriteLine("А!=В дает {0}", A!=B); Console.WriteLine("А==С дает {0}", A==C); Console.WriteLine("А!=С дает {0}", A!=C); Console.WriteLine("A==D дает {0}", A==D); Console.WriteLine("A!=D дает {0}", A!=D); // Задержка: Console.ReadLine(); } } }
Результат выполнения программы такой.
Рис.1. Результат выполнения программы
Основу нашего программного кода составляет класс MyClass, у которого есть целочисленное поле code и символьное поле symb. Класс оснащен конструктором с двумя аргументами. Также в классе перегружены операторы == и !=, а также переопределены методы Equals() и GetHashCode(). Два объекта класса мы будем полагать равными, если у них совпадают значения полей.
Метод GetHashCode() в конкретно нашем примере играет декоративную роль (но мы его все равно переопределяем). Относительно остальных участников процесса, то оператор == перегружается так, что вызывается метод Equals(), а перегрузка оператора != базируется на использовании оператора ==. Поэтому основа нашего кода - это код метода Equals().
Начнем мы с анализа кода GetHashCode(). Метод результатом возвращает целое число. Результат вычисляется как значение выражения code^symb. Здесь использован оператор "побитового исключающего или" ^, а операндами являются поля code и symb (для символьного поля берется код из кодовой таблицы) объекта, из которого вызывается метод. В данном случае не очень важно, какое именно получится число. Важно, чтобы для объектов с одинаковыми значениями полей code и symb хэш-коды были одинаковыми (при этом случайно могут совпадать хэш-коды разных объектов).
Метод Equals() переопределяется так. В теле метода объявляется локальная объектная переменная t класса МуСlass. Значение переменной присваивается командой
MyClass t = (MyClass)obj; ,
Нередко вместе с переопределением метода Equals() выполняется еще и его перегрузка (описывается еще одна версия метода) с аргументом пользовательского класса (в данном случае это класс MyClass).
Результат метода вычисляется с помощью условного оператора. Если совпадают значения полей объекта, из которого вызывается метод, и объекта, переданного аргументом методу, то метод Equals() результатом возвращает значение true, а в противном случае результат метода равен false.
Операторный метод для оператора "равно" описывается так, что для аргументов а и b класса MyClass результатом возвращается значение выражения а.Equals(b). А операторный метод для оператора "не равно" возвращает значением выражение !(a==b), что есть "противоположное" значение к результату сравнения объектов а и b на предмет равенства.
В главном методе программы создается несколько объектов, для этих объектов вычисляется хэш-код, а также объекты сравниваются на предмет равенства и неравенства. Легко заметить, что объекты с одинаковыми полями интерпретируются как равные.
На следующем шаге мы рассмотрим перегрузку операторов true и false.