Шаг 112.
Язык программирования C#. Начала
Перегрузка операторов. Перегрузка операторов сравнения (окончание)

    На этом шаге мы рассмотрим "правильную" перегрузку операторов == и !=.

    Теперь вернемся к методам 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() наследуются в пользовательском классе, и мы их можем переопределить (описать новые версии этих методов). При переопределении методы описываются с ключевым словом override.

    В примере ниже вашему вниманию представлена программа, в которой наряду с перегрузкой операторов == и != еще и переопределяются методы 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().


В данном случае перегрузка оператора == через вызов метода Equals() не есть инструкция к действию. Это просто иллюстрация совместного использования нескольких методов.

    Начнем мы с анализа кода GetHashCode(). Метод результатом возвращает целое число. Результат вычисляется как значение выражения code^symb. Здесь использован оператор "побитового исключающего или" ^, а операндами являются поля code и symb (для символьного поля берется код из кодовой таблицы) объекта, из которого вызывается метод. В данном случае не очень важно, какое именно получится число. Важно, чтобы для объектов с одинаковыми значениями полей code и symb хэш-коды были одинаковыми (при этом случайно могут совпадать хэш-коды разных объектов).

    Метод Equals() переопределяется так. В теле метода объявляется локальная объектная переменная t класса МуСlass. Значение переменной присваивается командой

  MyClass t = (MyClass)obj;    ,
где через obj обозначен аргумент метода Equals(). В этой команде выполняется явное приведение переменной obj класса Object к ссылке класса MyClass. Это означает, что мы собираемся обрабатывать аргумент метода как объектную переменную класса MyClass.


Мы не определяем "с нуля" метод Equals(), а переопределяем его версию, унаследованную из класса Object. А там аргумент метода описан как такой, что относится к классу Object. При этом мы исходим из того, что при вызове метода аргументом будет передаваться объектная переменная класса MyClass. Поэтому в теле метода выполняется явное приведение аргумента к классу MyClass. Если при вызове метода передать аргумент другого типа, в общем случае возникает ошибка.

    Нередко вместе с переопределением метода Equals() выполняется еще и его перегрузка (описывается еще одна версия метода) с аргументом пользовательского класса (в данном случае это класс MyClass).


    Результат метода вычисляется с помощью условного оператора. Если совпадают значения полей объекта, из которого вызывается метод, и объекта, переданного аргументом методу, то метод Equals() результатом возвращает значение true, а в противном случае результат метода равен false.

    Операторный метод для оператора "равно" описывается так, что для аргументов а и b класса MyClass результатом возвращается значение выражения а.Equals(b). А операторный метод для оператора "не равно" возвращает значением выражение !(a==b), что есть "противоположное" значение к результату сравнения объектов а и b на предмет равенства.

    В главном методе программы создается несколько объектов, для этих объектов вычисляется хэш-код, а также объекты сравниваются на предмет равенства и неравенства. Легко заметить, что объекты с одинаковыми полями интерпретируются как равные.

    На следующем шаге мы рассмотрим перегрузку операторов true и false.




Предыдущий шаг Содержание Следующий шаг