54

我在这里关于 SO 的许多问题都与 IEquatable 实现有关。我发现它很难正确实现,因为在幼稚的实现中有很多隐藏的错误,而且我找到的关于它的文章也很不完整。我想找到或写一份明确的参考资料,其中必须包括:

  • 如何正确实现 IEquatable
  • 如何正确覆盖 Equals
  • 如何正确覆盖 GetHashCode
  • 如何正确实现 ToString 方法
  • 如何正确实现运算符 ==
  • 如何正确实现运算符 !=

这么完整的参考文献已经存在了?

PS:即使是MSDN 参考对我来说似乎也有缺陷

4

5 回答 5

28

实现IEquatable<T>一个值类型

值类型的实现IEquatable<T>与引用类型的实现略有不同。假设我们有 Implement-Your-Own-Value-Type 原型,一个复数结构。

public struct Complex
{
    public double RealPart { get; set; }
    public double ImaginaryPart { get; set; }
}

我们的第一步是实现IEquatable<T>和覆盖Object.Equalsand Object.GetHashCode

public bool Equals(Complex other)
{
    // Complex is a value type, thus we don't have to check for null
    // if (other == null) return false;

    return (this.RealPart == other.RealPart)
        && (this.ImaginaryPart == other.ImaginaryPart);
}

public override bool Equals(object other)
{
    // other could be a reference type, the is operator will return false if null
    if (other is Complex)
        return this.Equals((Complex)other);
    else
        return false;
}

public override int GetHashCode()
{
    return this.RealPart.GetHashCode() ^ this.ImaginaryPart.GetHashCode();
}

只需很少的努力,我们就有一个正确的实现,除了操作符。添加运算符也是一个简单的过程:

public static bool operator ==(Complex term1, Complex term2)
{
    return term1.Equals(term2);
}

public static bool operator !=(Complex term1, Complex term2)
{
    return !term1.Equals(term2);
}

精明的读者会注意到我们可能应该实现IEquatable<double>,因为Complex数字可以与底层值类型互换。

public bool Equals(double otherReal)
{
    return (this.RealPart == otherReal) && (this.ImaginaryPart == 0.0);
}

public override bool Equals(object other)
{
    // other could be a reference type, thus we check for null
    if (other == null) return base.Equals(other);

    if (other is Complex)
    {
        return this.Equals((Complex)other);
    }
    else if (other is double)
    {
        return this.Equals((double)other);
    }
    else
    {
        return false;
    }
}

如果我们添加IEquatable<double>,我们需要四个运算符,因为您可以拥有Complex == doubleor double == Complex(对于 也是如此operator !=):

public static bool operator ==(Complex term1, double term2)
{
    return term1.Equals(term2);
}

public static bool operator ==(double term1, Complex term2)
{
    return term2.Equals(term1);
}

public static bool operator !=(Complex term1, double term2)
{
    return !term1.Equals(term2);
}

public static bool operator !=(double term1, Complex term2)
{
    return !term2.Equals(term1);
}

所以你有了它,我们用最少的努力IEquatable<T>为值类型提供了一个正确且有用的实现:

public struct Complex : IEquatable<Complex>, IEquatable<double>
{
}
于 2009-08-20T17:48:13.070 回答
16

我相信使用 .NET 的设计来获得像检查对象是否相等这样简单的东西有点棘手。

对于结构

1)实施IEquatable<T>。它显着提高了性能。

2)既然你现在有自己的Equals,覆盖GetHashCode,并与各种相等检查覆盖保持一致object.Equals

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

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...");
    }
}

上课

来自女士:

大多数引用类型不应该重载相等运算符,即使它们覆盖了 Equals。

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

使用引用语义(可变对象)

1) 覆盖EqualsGetHashCode

2)实施IEquatable<T>不是必须的,但如果你有一个会很好。

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确保它适用于 nulls

2)实施IEquatable<T>不是必须的,但如果你有一个会很好。

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...");
    }
}

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

通常注意不要重复代码。我本可以制作一个通用抽象基类(IEqualizable<T>或其他)作为模板,以便更轻松地重用,但遗憾的是在 C# 中这阻止了我从其他类派生。

于 2012-12-16T22:31:36.443 回答
3

在阅读 MSDN 后,我很确定正确实现的最佳示例在IEquatable.Equals 方法页面中。我唯一的偏差如下:

public override bool Equals(Object obj)
{
   if (obj == null) return base.Equals(obj);

   if (! (obj is Person))
      return false; // Instead of throw new InvalidOperationException
   else
      return Equals(obj as Person);   
}

对于那些想知道偏差的人,它来自Object.Equals(Object) MSDN 页面:

Equals 的实现不能抛出异常。

于 2009-08-20T17:07:08.347 回答
3

我找到了另一个参考,它是 .NET Anonymous Type 实现。对于具有 int 和 double 作为属性的匿名类型,我反汇编了以下 C# 代码:

public class f__AnonymousType0
{
    // Fields
    public int A { get; }
    public double B { get; }

    // Methods
    public override bool Equals(object value)
    {
        var type = value as f__AnonymousType0;
        return (((type != null)
            && EqualityComparer<int>.Default.Equals(this.A, type.A))
            && EqualityComparer<double>.Default.Equals(this.B, type.B));
    }

    public override int GetHashCode()
    {
        int num = -1134271262;
        num = (-1521134295 * num) + EqualityComparer<int>.Default.GetHashCode(this.A);
        return ((-1521134295 * num) + EqualityComparer<double>.Default.GetHashCode(this.B);
    }

    public override string ToString()
    {
        StringBuilder builder = new StringBuilder();
        builder.Append("{ A = ");
        builder.Append(this.A);
        builder.Append(", B = ");
        builder.Append(this.B);
        builder.Append(" }");
        return builder.ToString();
    }
}
于 2009-09-16T01:19:04.683 回答
1

我只需要从这个类派生

public abstract class DataClass : IEquatable<DataClass>
{
    public override bool Equals(object obj)
    {
        var other = obj as DataClass;
        return this.Equals(other);
    }

    public bool Equals(DataClass other)
    {
        return (!ReferenceEquals(null, other))
            && this.Execute((self2, other2) =>
                other2.Execute((other3, self3) => self3.Equals(other3), self2)
                , other);
    }

    public override int GetHashCode()
    {
        return this.Execute(obj => obj.GetHashCode());
    }

    public override string ToString()
    {
        return this.Execute(obj => obj.ToString());
    }

    private TOutput Execute<TOutput>(Func<object, TOutput> function)
    {
        return this.Execute((obj, other) => function(obj), new object());
    }

    protected abstract TOutput Execute<TParameter, TOutput>(
        Func<object, TParameter, TOutput> function,
        TParameter other);
}

然后像这样实现抽象方法

public class Complex : DataClass
{
    public double Real { get; set; }

    public double Imaginary { get; set; }

    protected override TOutput Execute<TParameter, TOutput>(
        Func<object, TParameter, TOutput> function,
        TParameter other)
    {
        return function(new
        {
            Real = this.Real,
            Imaginary = this.Imaginary,
        }, other);
    }
}
于 2011-12-22T14:51:39.173 回答