关于c#:比较两种参考类型实例的“最佳实践”是什么?

What is “Best Practice” For Comparing Two Instances of a Reference Type?

我最近遇到了这个问题,直到现在,我都很高兴地重写了equality运算符(==)和/或equals方法,以查看两个引用类型是否实际包含相同的数据(即两个看起来相同的不同实例)。

自从我开始更多地使用自动化测试(将参考/预期数据与返回的数据进行比较)以来,我已经使用了更多。

在查阅了一些在msdn中的编码标准指南时,我遇到了一篇反对它的文章。现在我明白了为什么这篇文章会这样说(因为它们不是同一个实例),但它没有回答问题:

  • 比较两种引用类型的最佳方法是什么?
  • 我们应该实现IComparable吗?(我也看到过提到,这应该只为值类型保留)。
  • 有我不知道的界面吗?
  • 我们应该自己滚吗?!
  • 非常感谢^^

    更新

    看起来我误读了一些文档(这是一个漫长的一天),重写equals可能是一个很好的方法。

    If you are implementing reference
    types, you should consider overriding
    the Equals method on a reference type
    if your type looks like a base type
    such as a Point, String, BigNumber,
    and so on. Most reference types should
    not overload the equality operator,
    even if they override Equals. However,
    if you are implementing a reference
    type that is intended to have value
    semantics, such as a complex number
    type, you should override the equality
    operator.


    在.NET中正确、高效地实现平等,并且不需要重复代码是很困难的。具体地说,对于具有值语义的引用类型(即将相等性视为相等的不可变类型),应该实现System.IEquatable接口,并且应该实现所有不同的操作(EqualsGetHashCode==!=

    例如,下面是一个实现值相等的类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Point : IEquatable<Point> {
        public int X { get; }
        public int Y { get; }

        public Point(int x = 0, int y = 0) { X = x; Y = y; }

        public bool Equals(Point other) {
            if (other is null) return false;
            return X.Equals(other.X) && Y.Equals(other.Y);
        }

        public override bool Equals(object obj) => Equals(obj as Point);

        public static bool operator ==(Point lhs, Point rhs) => object.Equals(lhs, rhs);

        public static bool operator !=(Point lhs, Point rhs) => ! (lhs == rhs);

        public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
    }

    上面代码中唯一可移动的部分是粗体部分:Equals(Point other)中的第二行和GetHashCode()方法。其他代码应保持不变。

    对于不表示不可变值的引用类,不要实现操作符==!=。相反,使用它们的默认含义,即比较对象标识。

    代码有意地将派生类类型的对象等同起来。通常,这是不可取的,因为基类和派生类之间的相等性没有很好的定义。不幸的是,.NET和编码准则在这里不是很清楚。Resharper创建的代码,发布在另一个答案中,在这种情况下很容易受到不期望的行为的影响,因为Equals(object x)Equals(SecurableResourcePermission x)会对这种情况进行不同的处理。

    为了改变这种行为,必须在上面的强类型Equals方法中插入额外的类型检查:

    1
    2
    3
    4
    5
    public bool Equals(Point other) {
        if (other is null) return false;
        if (other.GetType() != GetType()) return false;
        return X.Equals(other.X) && Y.Equals(other.Y);
    }


    您似乎在用C编码,C中有一个方法调用等于您的类应该实现的方法,您是否希望使用其他度量比较两个对象,而不是"这两个指针(因为对象句柄就是指向同一内存地址的指针)"?.

    我从这里获取了一些示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    class TwoDPoint : System.Object
    {
        public readonly int x, y;

        public TwoDPoint(int x, int y)  //constructor
        {
            this.x = x;
            this.y = y;
        }

        public override bool Equals(System.Object obj)
        {
            // If parameter is null return false.
            if (obj == null)
            {
                return false;
            }

            // If parameter cannot be cast to Point return false.
            TwoDPoint p = obj as TwoDPoint;
            if ((System.Object)p == null)
            {
                return false;
            }

            // Return true if the fields match:
            return (x == p.x) && (y == p.y);
        }

        public bool Equals(TwoDPoint p)
        {
            // If parameter is null return false:
            if ((object)p == null)
            {
                return false;
            }

            // Return true if the fields match:
            return (x == p.x) && (y == p.y);
        }

        public override int GetHashCode()
        {
            return x ^ y;
        }
    }

    Java具有非常相似的机制。equals()方法是对象类的一部分,如果需要这种类型的功能,则类将重载它。

    重载"=="对于对象来说可能是一个坏主意,原因是,通常情况下,您仍然希望能够进行"这些是相同的指针"比较。例如,通常依赖于将元素插入到不允许重复的列表中,如果以非标准方式重载此运算符,则某些框架内容可能无法工作。


    下面,我总结了在实现IEquatable时需要做的事情,并从不同的msdn文档页面提供了理由。

    总结

    • 当需要测试值相等性时(例如在集合中使用对象时),您应该为类实现IEquatable接口、重写Object.Equals和GetHashCode。
    • 当需要测试引用相等性时,应使用operator==,operator!=和object.referenceequals。
    • 您应该只重写operator==和operator!=对于值类型和不可变引用类型。

    正当理由

    不可分割的

    The System.IEquatable interface is used to compare two instances of an object for equality. The objects are compared based on the logic implemented in the class. The comparison results in a boolean value indicating if the objects are different. This is in contrast to the System.IComparable interface, which return an integer indicating how the object values are different.

    The IEquatable interface declares two methods that must be overridden. The Equals method contains the implementation to perform the actual comparison and return true if the object values are equal, or false if they are not. The GetHashCode method should return a unique hash value that may be used to uniquely identify identical objects that contain different values. The type of hashing algorithm used is implementation-specific.

    IEquatable.Equals方法

    • You should implement IEquatable for your objects to handle the possibility that they will be stored in an array or generic collection.
    • If you implement IEquatable you should also override the base class implementations of Object.Equals(Object) and GetHashCode so that their behavior is consistent with that of the IEquatable.Equals method

    重写equals()和operator==(C编程指南)的指导原则

    • x.Equals(x) returns true.
    • x.Equals(y) returns the same value as y.Equals(x)
    • if (x.Equals(y) && y.Equals(z)) returns true, then x.Equals(z) returns true.
    • Successive invocations of x. Equals (y) return the same value as long as the objects referenced by x and y are not modified.
    • x. Equals (null) returns false (for non-nullable value types only. For more information, see Nullable Types (C# Programming Guide).)
    • The new implementation of Equals should not throw exceptions.
    • It is recommended that any class that overrides Equals also override Object.GetHashCode.
    • Is is recommended that in addition to implementing Equals(object), any class also implement Equals(type) for their own type, to enhance performance.

    By default, the operator == tests for reference equality by determining whether two references indicate the same object. Therefore, reference types do not have to implement operator == in order to gain this functionality. When a type is immutable, that is, the data that is contained in the instance cannot be changed, overloading operator == to compare value equality instead of reference equality can be useful because, as immutable objects, they can be considered the same as long as they have the same value. It is not a good idea to override operator == in non-immutable types.

    • Overloaded operator == implementations should not throw exceptions.
    • Any type that overloads operator == should also overload operator !=.

    ==操作员(C参考)

    • For predefined value types, the equality operator (==) returns true if the values of its operands are equal, false otherwise.
    • For reference types other than string, == returns true if its two operands refer to the same object.
    • For the string type, == compares the values of the strings.
    • When testing for null using == comparisons within your operator== overrides, make sure you use the base object class operator. If you don't, infinite recursion will occur resulting in a stackoverflow.

    object.equals方法(object)

    If your programming language supports operator overloading and if you choose to overload the equality operator for a given type, that type must override the Equals method. Such implementations of the Equals method must return the same results as the equality operator

    The following guidelines are for implementing a value type:

    • Consider overriding Equals to gain increased performance over that provided by the default implementation of Equals on ValueType.
    • If you override Equals and the language supports operator overloading, you must overload the equality operator for your value type.

    The following guidelines are for implementing a reference type:

    • Consider overriding Equals on a reference type if the semantics of the type are based on the fact that the type represents some value(s).
    • Most reference types must not overload the equality operator, even if they override Equals. However, if you are implementing a reference type that is intended to have value semantics, such as a complex number type, you must override the equality operator.

    号其他Gotchas

    • 在重写getHashCode()时,请确保在将引用类型用于哈希代码之前测试它们是否为空。
    • 我遇到了一个基于接口的编程和这里描述的操作符重载的问题:操作符重载和C语言中基于接口的编程#


    那篇文章只是建议不要重写相等运算符(对于引用类型),而不要重写equals。如果相等性检查将意味着比引用检查更多的内容,则应重写对象(引用或值)中的equals。如果需要接口,还可以实现IEquatable(由泛型集合使用)。但是,如果确实实现了IEquatable,则还应重写equals,因为IEquatable备注部分说明:

    If you implement IEquatable, you should also override the base class implementations of Object.Equals(Object) and GetHashCode so that their behavior is consistent with that of the IEquatable.Equals method. If you do override Object.Equals(Object), your overridden implementation is also called in calls to the static Equals(System.Object, System.Object) method on your class. This ensures that all invocations of the Equals method return consistent results.

    关于是否应实现equals和/或equality运算符:

    从实现equals方法

    Most reference types should not overload the equality operator, even if they override Equals.

    从实现equals和equality运算符(=)的准则

    Override the Equals method whenever you implement the equality operator (==), and make them do the same thing.

    这只表示在实现相等运算符时需要重写equals。它并不表示当您重写equals时需要重写equality运算符。


    对于将产生特定比较的复杂对象,那么实现IComparable并在比较方法中定义比较是一个很好的实现。

    例如,我们有"vehicle"对象,其中唯一的区别可能是注册号,我们使用它进行比较,以确保测试中返回的预期值是我们想要的值。


    微软似乎改变了他们的调子,或者至少有关于不重载相等运算符的冲突信息。根据这篇题为"如何:定义类型的值相等"的Microsoft文章:

    "和!=运算符可以与类一起使用,即使类没有重载它们。但是,默认行为是执行引用相等性检查。在类中,如果重载equals方法,则应重载==和!=运算符,但不是必需的。"

    根据埃里克·利珀特在回答我问的一个问题时所说的关于C中最小的平等准则,他说:

    "在这里遇到的危险是,您得到了一个为您定义的==运算符,它在默认情况下引用了相等。如果重载的equals方法执行值相等,而==执行引用相等,则很容易结束这种情况,然后在不引用值相等的相等项时意外使用引用相等。这是一个容易出错的实践,很难被人类代码审查发现。

    几年前,我研究了一种静态分析算法来统计检测这种情况,我们发现在我们研究的所有代码库中,每百万行代码中有大约两个实例的缺陷率。当只考虑在某个地方重写的代码基等于时,缺陷率显然要高得多!

    此外,考虑成本与风险。如果您已经有了IComparable的实现,那么编写所有的操作符是很简单的一行程序,它不会有错误,也不会被更改。这是你要写的最便宜的代码。如果在固定的写作和测试成本与无限制的查找和修复难以发现的错误成本之间做出选择,在使用引用相等而不是值相等的地方,我知道我会选择哪一个。"

    .NET框架将永远不会使用==或!=使用您编写的任何类型。但是,危险是如果有人这样做会发生什么。因此,如果该类是第三方的,那么我将始终提供==和!=运算符。如果该类只供组内部使用,我仍然可能实现==和!=运算符。

    如果实现了IComparable,则只实现<、<=、>和>=运算符。只有当类型需要支持排序(如排序时)或在排序的通用容器(如sortedset)中使用时,才应实现IComparable。

    如果集团或公司制定了不执行==和的政策!=操作员-那么我当然会遵循这个策略。如果有这样一个策略,那么用一个Q/A代码分析工具来执行它是明智的,它会标记出任何出现的==和!=与引用类型一起使用时的运算符。


    我倾向于使用Resharper自动生成的内容。例如,它为我的一个引用类型自动创建了这个:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        return obj.GetType() == typeof(SecurableResourcePermission) && Equals((SecurableResourcePermission)obj);
    }

    public bool Equals(SecurableResourcePermission obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        return obj.ResourceUid == ResourceUid && Equals(obj.ActionCode, ActionCode) && Equals(obj.AllowDeny, AllowDeny);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int result = (int)ResourceUid;
            result = (result * 397) ^ (ActionCode != null ? ActionCode.GetHashCode() : 0);
            result = (result * 397) ^ AllowDeny.GetHashCode();
            return result;
        }
    }

    如果您想覆盖==,但仍要进行引用检查,则仍然可以使用Object.ReferenceEquals


    上面的所有答案都不考虑多态性,通常您希望派生引用使用派生的equals,即使通过基引用进行比较也是如此。请参阅这里的问题/讨论/答案-平等和多态性


    我认为,在.NET的设计中,获得像检查对象是否正确等同性这样简单的方法有点困难。

    对于结构

    1)执行IEquatable。它显著提高了性能。

    2)由于您现在拥有自己的Equals,所以要覆盖GetHashCode,并且要与各种相等检查一致,也要覆盖object.Equals

    3)重载==!=运算符不需要认真地执行,因为如果无意中将一个结构与另一个结构等同于==!=,编译器会发出警告,但这样做有利于与Equals方法保持一致。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public struct Entity : IEquatable<Entity>
    {
        public bool Equals(Entity other)
        {
            throw new NotImplementedException("Your equality check here...");
        }

        public override bool Equals(object obj)
        {
            if (obj == null || !(obj is Entity))
                return false;

            return Equals((Entity)obj);
        }

        public static bool operator ==(Entity e1, Entity e2)
        {
            return e1.Equals(e2);
        }

        public static bool operator !=(Entity e1, Entity e2)
        {
            return !(e1 == e2);
        }

        public override int GetHashCode()
        {
            throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
        }
    }

    对于班级

    从MS:

    Most reference types should not overload the equality operator, even if they override Equals.

    对我来说,==感觉像是价值平等,更像是Equals方法的句法糖。写a == b比写a.Equals(b)更直观。很少我们需要检查引用是否相等。在处理物理对象的逻辑表示的抽象级别中,我们不需要检查这一点。我认为对==Equals使用不同的语义实际上可能会令人困惑。我认为首先应该是==代表价值平等,Equals代表参考(或者像IsSameAs这样更好的名字)。我不想在这里认真对待MS指南,不仅是因为它对我来说不自然,而且因为超载==不会造成任何重大伤害。这与不覆盖非通用的EqualsGetHashCode不同,后者可以回击,因为框架不在任何地方使用==,而只在我们自己使用的情况下使用。我从不超载==!=中获得的唯一真正好处是与我无法控制的整个框架的设计保持一致。这确实是件大事,所以很遗憾我会坚持下去。

    具有引用语义(可变对象)

    1)覆盖EqualsGetHashCode

    2)执行IEquatable不是必须的,但是如果你有一个会很好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class Entity : IEquatable<Entity>
    {
        public bool Equals(Entity other)
        {
            if (ReferenceEquals(this, other))
                return true;

            if (ReferenceEquals(null, other))
                return false;

            //if your below implementation will involve objects of derived classes, then do a
            //GetType == other.GetType comparison
            throw new NotImplementedException("Your equality check here...");
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as Entity);
        }

        public override int GetHashCode()
        {
            throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
        }
    }

    具有值语义(不可变对象)

    这是棘手的部分。如果不小心的话很容易弄乱。

    1)覆盖EqualsGetHashCode

    2)过载==!=以匹配Equals。确保它适用于空值。

    2)执行IEquatable不是必须的,但是如果你有一个会很好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class Entity : IEquatable<Entity>
    {
        public bool Equals(Entity other)
        {
            if (ReferenceEquals(this, other))
                return true;

            if (ReferenceEquals(null, other))
                return false;

            //if your below implementation will involve objects of derived classes, then do a
            //GetType == other.GetType comparison
            throw new NotImplementedException("Your equality check here...");
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as Entity);
        }

        public static bool operator ==(Entity e1, Entity e2)
        {
            if (ReferenceEquals(e1, null))
                return ReferenceEquals(e2, null);

            return e1.Equals(e2);
        }

        public static bool operator !=(Entity e1, Entity e2)
        {
            return !(e1 == e2);
        }

        public override int GetHashCode()
        {
            throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
        }
    }

    如果类可以继承,请特别注意查看它应该如何处理,在这种情况下,您必须确定基类对象是否可以等于派生类对象。理想情况下,如果没有派生类的对象用于相等性检查,那么基类实例可以等于派生类实例,在这种情况下,不需要检查基类的泛型Equals中的Type相等性。

    通常注意不要重复代码。我本可以创建一个通用抽象基类(IEqualizable左右)作为模板,以便更容易地重用,但遗憾的是,在C语言中,它阻止了我从其他类派生。