Поделиться через


Практическое руководство. Определение равенства значений для типа (Руководство по программированию на C#)

При определении класса или структуры необходимо решить, имеет ли смысл создавать пользовательское определение равенства значений (или эквивалентности) для этого типа. Обычно правила определения равенства реализуются, если объекты этого типа будут добавляться в коллекции или если они в первую очередь предназначены для хранения набора полей или свойств. В основу определения равенства значений можно положить сравнение всех полей и свойств в типе или только их части. Но в любом случае (как для классов, так и для структур) реализация должна соответствовать следующим пяти гарантиям равенства.

  1. x.Equals(x) возвращает значение true. Это называется свойством рефлексивности.

  2. x. Equals (y) возвращает то же значение, что и y. Equals (x). Это называется свойством симметрии.

  3. Если (x.Equals(y) && y.Equals(z)) возвращает true, то x.Equals(z) возвращает true. Это называется свойством транзитивности.

  4. Последовательные вызовы x.Equals (y) возвращают одно и то же значение до тех пор, пока объекты, на которые ссылаются x и y, не будут изменены.

  5. x.Equals(null) возвращает значение false. Однако null.Equals(null) вызывает исключение; эта инструкция не удовлетворяет приведенному выше правилу номер 2.

У любой определяемой структуры уже имеется реализация равенства значений по умолчанию, наследуемая от переопределения ValueType метода Object.Equals(Object). Эта реализация использует отражение для проверки всех открытых и неоткрытых полей и свойств типа. Хотя эта реализация возвращает верный результат, она отличается невысокой скоростью по сравнению с пользовательской реализацией, которую можно написать специально для конкретного типа.

Детали реализации равенства значений для классов и структур различаются. Тем не менее для реализации равенства как для классов, так и для структур, необходимо выполнить одни и те же базовые действия.

  1. Переопределите виртуальный метод Object.Equals(Object). В большинстве случаев пользовательская реализация bool Equals( object obj ) должна вызывать относящийся к конкретному типу метод Equals, который является реализацией интерфейса IEquatable. (См. шаг 2.)

  2. Реализуйте интерфейс IEquatable, указав относящийся к конкретному типу метод Equals. Именно на этом этапе происходит фактическое сравнение значений. Например, функцию равенства можно определить путем сравнения только одного из двух полей в типе. Не создавайте исключений в методе Equals. Только для классов: этот метод должен проверять только те поля, которые были объявлены в классе. Для проверки полей базового класса следует вызывать метод base.Equals. (Не следует делать этого, если тип наследует напрямую классу Object, поскольку реализация Object метода Object.Equals(Object) выполняет проверку равенства ссылок.)

  3. Рекомендуется (хотя это и не обязательно) перегрузить операторы == и !=.

  4. Перегрузите метод Object.GetHashCode, чтобы два объекта с равными значениями возвращали один и тот же хэш-код.

  5. Необязательно: для поддержки определений "больше" и "меньше" реализуйте для типа интерфейс IComparable, а также перегрузите операторы <= и >=.

В первом из приведенных ниже примеров показана реализация класса. Во втором примере показана реализация структуры.

Пример

В следующем примере показана реализация равенства значений в классе (ссылочный тип).


    namespace ValueEquality
    {
        using System;
        class TwoDPoint : IEquatable<TwoDPoint>
        {
            // Readonly auto-implemented properties. 
            public int X { get; private set; }
            public int Y { get; private set; }

            // Set the properties in the constructor. 
            public TwoDPoint(int x, int y)
            {
                if ((x < 1) || (x > 2000) || (y < 1) || (y > 2000))
                    throw new System.ArgumentException("Point must be in range 1 - 2000");
                this.X = x;
                this.Y = y;
            }

            public override bool Equals(object obj)
            {
                return this.Equals(obj as TwoDPoint);
            }

            public bool Equals(TwoDPoint p)
            {
                // If parameter is null, return false. 
                if (Object.ReferenceEquals(p, null))
                {
                    return false;
                }

                // Optimization for a common success case. 
                if (Object.ReferenceEquals(this, p))
                {
                    return true;
                }

                // If run-time types are not exactly the same, return false. 
                if (this.GetType() != p.GetType())
                    return false;

                // Return true if the fields match. 
                // Note that the base class is not invoked because it is 
                // System.Object, which defines Equals as reference equality. 
                return (X == p.X) && (Y == p.Y);
            }

            public override int GetHashCode()
            {
                return X * 0x00010000 + Y;
            }

            public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
            {
                // Check for null on left side. 
                if (Object.ReferenceEquals(lhs, null))
                {
                    if (Object.ReferenceEquals(rhs, null))
                    {
                        // null == null = true. 
                        return true;
                    }

                    // Only the left side is null. 
                    return false;
                }
                // Equals handles case of null on right side. 
                return lhs.Equals(rhs);
            }

            public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs)
            {
                return !(lhs == rhs);
            }
        }

        // For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint. 
        class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
        {
            public int Z { get; private set; }

            public ThreeDPoint(int x, int y, int z)
                : base(x, y)
            {
                if ((z < 1) || (z > 2000))
                    throw new System.ArgumentException("Point must be in range 1 - 2000");
                this.Z = z;
            }

            public override bool Equals(object obj)
            {
                return this.Equals(obj as ThreeDPoint);
            }

            public bool Equals(ThreeDPoint p)
            {
                // If parameter is null, return false. 
                if (Object.ReferenceEquals(p, null))
                {
                    return false;
                }

                // Optimization for a common success case. 
                if (Object.ReferenceEquals(this, p))
                {
                    return true;
                }

                // Check properties that this class declares. 
                if (Z == p.Z)
                {
                    // Let base class check its own fields  
                    // and do the run-time type comparison. 
                    return base.Equals((TwoDPoint)p);
                }
                else 
                    return false;
            }

            public override int GetHashCode()
            {
                return (X * 0x100000) + (Y * 0x1000) + Z;
            }

            public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
            {
                // Check for null. 
                if (Object.ReferenceEquals(lhs, null))
                {
                    if (Object.ReferenceEquals(rhs, null))
                    {
                        // null == null = true. 
                        return true;
                    }

                    // Only the left side is null. 
                    return false;
                }
                // Equals handles the case of null on right side. 
                return lhs.Equals(rhs);
            }

            public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs)
            {
                return !(lhs == rhs);
            }
        }

        class Program
        {
            static void Main(string[] args)
            {
                ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);
                ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);
                ThreeDPoint pointC = null;
                int i = 5;

                Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
                Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
                Console.WriteLine("null comparison = {0}", pointA.Equals(pointC));
                Console.WriteLine("Compare to some other type = {0}", pointA.Equals(i));

                TwoDPoint pointD = null;
                TwoDPoint pointE = null;



                Console.WriteLine("Two null TwoDPoints are equal: {0}", pointD == pointE);

                pointE = new TwoDPoint(3, 4);
                Console.WriteLine("(pointE == pointA) = {0}", pointE == pointA);
                Console.WriteLine("(pointA == pointE) = {0}", pointA == pointE);
                Console.WriteLine("(pointA != pointE) = {0}", pointA != pointE);

                System.Collections.ArrayList list = new System.Collections.ArrayList();
                list.Add(new ThreeDPoint(3, 4, 5));
                Console.WriteLine("pointE.Equals(list[0]): {0}", pointE.Equals(list[0]));

                // Keep the console window open in debug mode.
                System.Console.WriteLine("Press any key to exit.");
                System.Console.ReadKey();
            }
        }

        /* Output:
            pointA.Equals(pointB) = True
            pointA == pointB = True
            null comparison = False
            Compare to some other type = False
            Two null TwoDPoints are equal: True
            (pointE == pointA) = False
            (pointA == pointE) = False
            (pointA != pointE) = True
            pointE.Equals(list[0]): False
        */
    }

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

К объектам класса можно применять операторы == и !=, даже если они не были перегружены в классе. Однако по умолчанию они служат для проверки равенства ссылок. При перегрузке в классе метода Equals необходимо перегрузить операторы == и !=, но это не обязательно.

В следующем примере показана реализация равенства значений в структуре (тип значения).

    struct TwoDPoint : IEquatable<TwoDPoint>
    {
        // Read/write auto-implemented properties. 
        public int X { get; private set; }
        public int Y { get; private set; }

        public TwoDPoint(int x, int y)
            : this()
        {
            X = x;
            Y = x;
        }

        public override bool Equals(object obj)
        {
            if (obj is TwoDPoint)
            {
                return this.Equals((TwoDPoint)obj);
            }
            return false;
        }

        public bool Equals(TwoDPoint p)
        {
            return (X == p.X) && (Y == p.Y);
        }

        public override int GetHashCode()
        {
            return X ^ Y;
        }

        public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
        {
            return lhs.Equals(rhs);
        }

        public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs)
        {
            return !(lhs.Equals(rhs));
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            TwoDPoint pointA = new TwoDPoint(3, 4);
            TwoDPoint pointB = new TwoDPoint(3, 4);
            int i = 5;

            // Compare using virtual Equals, static Equals, and == and != operators. 
            // True:
            Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
            // True:
            Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
            // True:
            Console.WriteLine("Object.Equals(pointA, pointB) = {0}", Object.Equals(pointA, pointB));
            // False:
            Console.WriteLine("pointA.Equals(null) = {0}", pointA.Equals(null));
            // False:
            Console.WriteLine("(pointA == null) = {0}", pointA == null);
            // True:
            Console.WriteLine("(pointA != null) = {0}", pointA != null);
            // False:
            Console.WriteLine("pointA.Equals(i) = {0}", pointA.Equals(i));
            // CS0019: 
            // Console.WriteLine("pointA == i = {0}", pointA == i); 

            // Compare unboxed to boxed.
            System.Collections.ArrayList list = new System.Collections.ArrayList();
            list.Add(new TwoDPoint(3, 4));
            // True:
            Console.WriteLine("pointE.Equals(list[0]): {0}", pointA.Equals(list[0]));


            // Compare nullable to nullable and to non-nullable.
            TwoDPoint? pointC = null;
            TwoDPoint? pointD = null;
            // False:
            Console.WriteLine("pointA == (pointC = null) = {0}", pointA == pointC);
            // True:
            Console.WriteLine("pointC == pointD = {0}", pointC == pointD);

            TwoDPoint temp = new TwoDPoint(3, 4);
            pointC = temp;
            // True:
            Console.WriteLine("pointA == (pointC = 3,4) = {0}", pointA == pointC);

            pointD = temp;
            // True:
            Console.WriteLine("pointD == (pointC = 3,4) = {0}", pointD == pointC);

            // Keep the console window open in debug mode.
            System.Console.WriteLine("Press any key to exit.");
            System.Console.ReadKey();
        }
    }

    /* Output:
        pointA.Equals(pointB) = True
        pointA == pointB = True
        Object.Equals(pointA, pointB) = True
        pointA.Equals(null) = False
        (pointA == null) = False
        (pointA != null) = True
        pointA.Equals(i) = False
        pointE.Equals(list[0]): True
        pointA == (pointC = null) = False
        pointC == pointD = True
        pointA == (pointC = 3,4) = True
        pointD == (pointC = 3,4) = True
    */
}

В структурах реализация по умолчанию метода Object.Equals(Object) (переопределенная версия ValueType) проверяет равенство всех полей типа за счет отражения. Когда разработчик переопределяет виртуальный метод Equals в структуре, его задача состоит в том, чтобы найти более эффективный способ проверки равенства значений и, если это возможно, реализовать сравнение только на основании части полей или свойств структуры.

Операторы == и != нельзя применять к структурам, если только они не были явным образом перегружены для конкретной структуры.

См. также

Основные понятия

Руководство по программированию на C#

Другие ресурсы

Сравнения на равенство (Руководство по программированию на C#)