再次讨论平等,我偶然发现了EqualityComparer<T>.Default.Equals()
.我更喜欢为引用类型调用此方法,而不是object.Equals()
.
现在我想我大错特错了。
object.Equals()
使用可重写的实例Equals()
方法提供正确的多态行为,而EqualityComparer<T>.Default.Equals()
调用IEquatable<T>.Equals()
如果它已实现。
现在考虑这个小程序:
public class Class1 : IEquatable<Class1>
{
public int Prop1 { get; set; }
public bool Equals(Class1 other)
{
if (other == null)
return false;
return Prop1 == other.Prop1;
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
return Equals(obj as Class1);
}
}
public class Class2 : Class1, IEquatable<Class2>
{
public int Prop1 { get; set; }
public int Prop2 { get; set; }
public bool Equals(Class2 other)
{
if (other == null)
return false;
return Prop1 == other.Prop1 && Prop2 == other.Prop2;
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
return Equals(obj as Class2);
}
}
class Program
{
static void Main(string[] args)
{
var c1 = new Class1 {Prop1 = 10};
var c2 = new Class2 {Prop1 = 10, Prop2 = 5};
var c3 = new Class2 {Prop1 = 10, Prop2 = 15};
Console.WriteLine("Object.Equals()");
Console.WriteLine("C1=C2 {0}",Equals(c1,c2));
Console.WriteLine("C2=C1 {0}",Equals(c2, c1));
Console.WriteLine("C2=C3 {0}",Equals(c2, c3));
Console.WriteLine("C3=C2 {0}", Equals(c3, c2));
var dec1 = EqualityComparer<Class1>.Default;
Console.WriteLine();
Console.WriteLine("EqualityComparer<Class1>.Default.Equals");
Console.WriteLine("C1=C2 {0}", dec1.Equals(c1, c2));
Console.WriteLine("C2=C1 {0}", dec1.Equals(c2, c1));
Console.WriteLine("C2=C3 {0} BUG?", dec1.Equals(c2, c3));
Console.WriteLine("C3=C2 {0} BUG?", dec1.Equals(c3, c2));
Console.ReadKey();
}
}
它显示了在相等语义中引入不一致是多么容易:
Object.Equals((
C1=C2 假
C2=C1 假
C2=C3 假
C3=C2 假
平等比较器<类1>。默认.等于
C1=C2 假
C2=C1 假
C2=C3 真正的错误?
C3=C2 真正的错误?
类1>
但是,MSDN 文档建议:
实施者注意事项 如果实现 Equals,则还应 重写 Object.Equals(Object( 和 GetHashCode,以便它们的行为与 IEquatable
.等于方法。如果您确实覆盖了 Object.Equals(Object(, 在对静态的调用中也会调用被重写的实现 类上的 Equals(System.Object, System.Object( 方法。在 此外,您应该重载op_Equality并op_Inequality 运营商。这可确保所有相等性测试返回一致 结果,示例说明。
从这一刻开始,我认为没有理由为引用类型实现IEquatable<T>
。谁能告诉我什么时候有任何意义?当我们以不同的方式看待类型(作为基本类型(时,我真的应该将不同的相等行为视为不一致吗?
今天我问自己在类中添加IEquatable<T>
会产生什么后果,我发现了你的问题。
然后我测试了你的代码。对于其他阅读本文的人,这里有一个答案,而不仅仅是"这样做让它工作"。
首先,这不是一个错误。
您的问题是,您指定了一个EqualityComparer<Class1>
,该public bool Equals(Class1 other)
仅在class1
中实现。
因此,dec1.Equals(c2, c3)
将调用此函数,其中仅比较class1
的内容。
从你的评论BUG?
我可以看到你也希望class2
的内容也能被比较,就像其他人所期望的那样。为此,您需要更改public bool Equals(Class1 other)
到public virtual bool Equals(Class1 other)
并在 class2
中覆盖此函数,然后您还可以在其中比较 class2
的内容。
但这可能会导致一个非常奇怪的结构。因此,为了完整起见,这是我的实现方式:
在基类中,只有类型检查:
//--------------------------------------------------------------------------
public static bool operator == (CClass1 i_value1, CClass1 i_value2)
{
if (ReferenceEquals (i_value1, i_value2))
return true;
if (ReferenceEquals (null, i_value1))
return false;
return (i_value1.Equals (i_value2));
}
//--------------------------------------------------------------------------
public static bool operator != (CClass1 i_value1, CClass1 i_value2)
{
return !(i_value1 == i_value2);
}
///-------------------------------------------------------------------------
public sealed override bool Equals (object i_value)
{
if (ReferenceEquals (null, i_value))
return false;
if (ReferenceEquals (this, i_value))
return true;
if (i_value.GetType () != GetType ())
return false;
return Equals_EXEC ((CClass1)i_value);
}
///-------------------------------------------------------------------------
public bool Equals (CClass1 i_value) // not virtual, don't allow overriding!
{
if (ReferenceEquals (null, i_value))
return false;
if (ReferenceEquals (this, i_value))
return true;
if (i_value.GetType () != GetType ())
return false;
return Equals_EXEC (i_value);
}
仍在基类中,内容检查:
///-------------------------------------------------------------------------
protected override bool Equals_EXEC (CClass1 i_value)
{
return Equals_exec (i_value);
}
//--------------------------------------------------------------------------
private bool Equals_exec (CClass1 i_value)
{
return variable1 == i_value.variable1
&& variable2 == i_value.variable2
&& ... ;
}
在派生类中,内容检查:
///-------------------------------------------------------------------------
protected override bool Equals_EXEC (CClassN i_value)
{
return base.Equals_EXEC (i_value)
&& Equals_exec (i_value as CClassN);
}
//--------------------------------------------------------------------------
private bool Equals_exec (CClassN i_value)
{
return variable5 == i_value.variable5
&& variable6 == i_value.variable6
&& ... ;
}
无论对错,以下是我倾向于在基类和派生类上实现Equals(Object)
和IEquatable<T>.Equals(T)
的方式。
public class Class1 : IEquatable<Class1>
{
public sealed override bool Equals(object obj)
{
return Equals(obj as Class1);
}
public virtual bool Equals(Class1 obj)
{
if(ReferenceEquals(obj, null))
return false;
// Some property checking
}
}
public class Class2 : Class1, IEquatable<Class2>
{
public sealed override bool Equals(Class1 obj)
{
return Equals(obj as Class2);
}
public virtual bool Equals(Class2 obj)
{
if(!base.Equals(obj))
return false;
// Some more property checking
}
}
public class Class3 : Class2, IEquatable<Class3>
{
public sealed override bool Equals(Class2 obj)
{
return Equals(obj as Class3);
}
public virtual bool Equals(Class3 obj)
{
if(!base.Equals(obj))
return false;
// Some more property checking
}
}
对于引用类型,实现IEquatable<T>
的好处是微不足道的,如果你有两个类型T
的实例,你可以直接调用T.Equals(T)
.而不是随后需要对参数执行类型检查的T.Equals(Object)
。
IEquatable<T>
的主要用途是值类型,其中对实例进行装箱会产生开销。