如何:为类型定义值相等性(C# 编程指南)

在定义类或结构时,您将决定为类型创建值相等性(或等效性)的自定义定义是否有意义。 通常,当类型的对象预期要添加到某类集合时,或者当这些对象主要用于存储一组字段或属性时,您将实现值相等性。 您可以基于类型中所有字段和属性的比较来定义值相等性,也可以基于子集进行定义。 但在任何一种情况下,类和结构中的实现均应遵循五个等效性保证条件:

  1. x.Equals(x) 返回 true. 。这称为自反属性。

  2. x.Equals(y) 返回与 Equals(x) 相同的值。 这称为对称属性。

  3. 如果 (x.Equals(y) && y.Equals(z)) 返回 true,则 x.Equals(z) 返回 true。 这称为可传递属性。

  4. 只要不修改 x 和 y 所引用的对象,x.Equals(y) 的后续调用就返回相同的值。

  5. x.Equals(null) 返回 false。 但是,null.Equals(null) 会引发异常;它不遵循上面的第二条规则。

您定义的任何结构已经具有它从 Object.Equals(Object) 方法的 ValueType 重写中继承的默认值相等性实现。 此实现使用反射来检查类型中的所有公共和非公共字段以及属性。 尽管此实现可生成正确的结果,但与您专门为类型编写的自定义实现相比,它的速度相对较慢。

类和结构的值相等性的实现详细信息不同。 但是,类和结构都需要相同的基础步骤来实现相等性:

  1. 重写 Object.Equals(Object) 方法。 大多数情况下,您的 bool Equals( object obj ) 实现应只调入作为 IEquatable 接口的实现的类型特定 Equals 方法。 (请参见步骤 2。)

  2. 通过提供类型特定的 Equals 方法实现 IEquatable 接口。 实际的等效性比较将在此接口中执行。 例如,您可能决定通过仅比较类型中的一两个字段来定义相等性。 不要从 Equals 中引发异常。 仅适用于类:此方法应仅检查类中声明的字段。 它应调用 base.Equals 来检查基类中的字段。 (如果类型直接从 Object 中继承,则不要这样做,因为 Object.Equals(Object)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# 编程指南)