我认为在.NET的设计中,即使是检查对象是否相等这样简单的事情也有点棘手。
对于结构体
1)实现IEquatable<T>
。它可以显著提高性能。
2)由于你现在有了自己的Equals
,所以要重写GetHashCode
,并且为了与各种相等性检查保持一致,还要重写object.Equals
。
3)不需要过分地重载==
和!=
运算符,因为如果你无意中用==
或!=
将一个结构体与另一个结构体相等,编译器会发出警告,但最好还是这样做,以便与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
)的语义。
我不想在这里严格遵循微软的准则,不仅因为它对我来说不自然,而且因为重载==
不会造成太大的伤害。这与不覆盖非泛型
Equals
或
GetHashCode
不同,后者可能会反咬一口,因为框架不会在任何地方使用
==
,只有当我们自己使用它时才会使用。
不重载==
和!=
唯一真正的好处就是与整个框架的设计一致,而我对此没有任何控制。这确实是一件大事,所以遗憾地我会坚持这一点。
关于引用语义(可变对象)
1)重写Equals
和GetHashCode
方法。
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;
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)重写Equals
和GetHashCode
。
2)重载==
和!=
以匹配Equals
。确保它适用于null。
3)实现IEquatable<T>
不是必须的,但如果有一个会很好。
public class Entity : IEquatable<Entity>
{
public bool Equals(Entity other)
{
if (ReferenceEquals(this, other))
return true;
if (ReferenceEquals(null, other))
return false;
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<T>
),以便更容易地重用,但遗憾的是,在C#中,这样做会禁止我从其他类派生。
==
运算符的一个主要问题在于它实际上是两个运算符;当它与存在重载的类型一起使用时,它使用重载;否则,如果操作数是引用类型,则进行引用相等性检查。由于==
是静态绑定而不是虚拟绑定,即使在使用泛型时,这种行为也可能导致意外结果。在vb.net中,为可重写的相等性和引用相等性使用单独的运算符,避免了这种歧义。 - supercat