.NET 中的相等性故事 - 第三部分






4.96/5 (15投票s)
在本文中,您将了解 .NET 中的 IEquatable 接口以及它如何解决值类型的一些相等性问题。
引言
阅读完
后,您会发现 Object.Equals
方法存在两个问题。一个问题是它缺乏强类型,对于值类型,需要进行装箱。在本文中,我们将探讨 IEquatable<T>
接口,它为这些问题提供了解决方案。
系列文章
如果您想阅读本系列的上一篇文章,如果您愿意,以下是与此相关的上一篇内容的链接:
- NET 中相等性的故事 - 第一部分
- C# 中的相等性和 Object 类(第 2 部分)
- C# 中的相等性和 IEquatable<T> 接口(第 3 部分)
- C# 中的 == 运算符和原始类型(第 4 部分)
- C# 中的 == 运算符和引用类型(第 5 部分)
- C# 中的 == 运算符和 String 类(第 6 部分)
- C# 中的 == 运算符和值类型(第 7 部分)
- C# 中继承和泛型下的相等运算符 (==) 问题 (第八部分)
- 在 C# 中为值类型实现相等性 (第 9 部分)
- 在 C# 中为引用类型实现相等性 - 第十部分
IEquatable<T> 接口
泛型 IEquatable <T>
是为了解决 Equals
方法的一个略有不同的问题而存在的。Object
类型上的 Equals
方法接受 Object
类型的参数。我们知道,如果希望 Object.Equals
对所有类型都起作用,这是唯一可能的参数类型。
但是 Object
是一个引用类型,这意味着如果您想将值类型作为参数传递,该值类型将被装箱,这将产生性能损失,这是不好的。通常,当我们选择值类型而不是引用类型时,是因为我们关心性能,所以我们总是希望避免这种装箱和拆箱的性能开销。
还有一个问题,将参数类型设置为 object 意味着没有类型安全性。例如,以下代码可以毫无问题地编译:
class Program
{
static void Main(String[] args)
{
Person p1 = new Person("Ehsan Sajjad");
Program p = new Program();
Console.WriteLine(p1.Equals(p));
Console.ReadKey();
}
}
没有任何东西可以阻止我比较两个不同类型实例的 Equals
方法。我们将 Person
类的实例与 Program
类的实例进行比较,编译器不会阻止我这样做,这显然是一个问题,因为它们是完全不同的类型,而且它们之间不可能有任何有意义的相等性。
这只是一个例子,您不应该在代码中进行此类比较,显然,如果编译器能够检测到这种情况,那将是很好的,但目前它无法做到,因为 Object.Equals
方法不具备强类型安全性。
我们可以通过一个接受被比较类型作为参数的 Equals
方法来解决装箱和类型安全问题。例如,我们可以在 String
上有一个接受 string
作为参数的 Equals
方法,并且可以在 Person
类上有一个接受 Person
变量作为参数的 Equals
方法。这将很好地解决装箱和类型安全问题。
我们在上一篇文章中讨论了上述方法在继承方面存在的问题。但是,无法在 System.Object
上有效地定义这些强类型方法,因为 System.Object
不知道将会有哪些类型继承它。
那么,我们如何才能使一个强类型的 Equals
方法普遍可用呢?微软通过提供 IEquatable<T>
接口解决了这个问题,任何想要提供强类型 Equals
方法的类型都可以暴露这个接口。如果我们查看文档,可以看到 IEquatable<T>
只暴露了一个名为 Equals
的方法,该方法返回一个布尔值。
这与 Object.Equals
的作用完全相同,但它接受泛型类型 T
的实例作为参数,因此它是强类型的,这意味着对于值类型,将不会进行装箱。
IEquatable<T> 和值类型
我们可以用最简单的类型整数来举例说明 IEquatable<T>
接口。
static void Main(String[] args)
{
int num1 = 5;
int num2 = 6;
int num3 = 5;
Console.WriteLine(num1.Equals(num2));
Console.WriteLine(num1.Equals(num3));
}
我们有三个整数变量,我们使用 Equals
方法进行比较并将结果打印到控制台。如果我们查看 intellisense,可以看到 int 有两个 Equals
方法,其中一个接受 object 作为参数,这是重写的 Object.Equals
方法,另一个接受一个整数作为参数,这个 Equals
方法是 IEquatable<int>
的实现,这个实现是由整数类型提供的,并且上面的示例代码会使用这个重载进行比较,因为在两个 Equals
调用中,我们传递的都是整数而不是 object,所以编译器会选择为 IEquatable<int>
定义的重载,因为它最匹配签名。
当然,用 Equals
方法比较整数的方式非常不自然,通常我们直接这样写:
Console.WriteLine(num1 == num2);
我们通过 Equals
方法编写代码是为了让您看到有两个 Equals
方法。所有原始类型都提供了 IEquatable<T>
接口的实现。以我们上面的例子为例,int
实现了 IEquatable<int>
。
同样,其他原始类型也实现了 IEquatable<T>
。一般来说,IEquatable<T>
对于值类型非常有用。不幸的是,微软在 Framework Class Library 中并没有在非原始值类型上一致地实现它,所以您不能总是依赖这个接口。
IEquatable<T> 和引用类型
IEquatable<T>
对于引用类型不像对于值类型那么有用。因为对于引用类型,并没有像值类型那样需要解决的性能问题(装箱),而且 IEquatable<T>
与继承不能很好地协同工作。
但值得注意的是,String
(一种引用类型)确实实现了 IEquatable<T>
。如果您还记得第二部分,当我们演示 string
的 Equals
方法时,我们显式地将 string
变量强制转换为 object
。
static void Main(String[] args)
{
string s1 = "Ehsan Sajjad";
string s2 = string.Copy(s1);
Console.WriteLine(s1.Equals((object)s2));
}
那样做是为了确保它调用接受 object 作为参数的 Object.Equals
重载。如果我们不这样做,编译器就会选择强类型的 Equals
方法,而该方法实际上是 String
实现的 IEquatable<string>
。String
是一个密封类,您无法继承它,所以相等性和继承冲突的问题不会出现。
显然,当一个类型同时提供这两个 Equals
方法时,虚拟的 Object.Equals
方法和 IEquatable<T> Equals
方法应该始终返回相同的结果。对于微软的所有实现来说都是如此,这也是您在自己实现该接口时需要做的事情之一。
如果您想实现 IEquatable<T>
接口,那么您应该确保重写 Object.Equals
方法,使其行为与您的接口方法完全一致,这是有意义的,因为如果一个类型实现了两个行为不同的 Equals
版本,那么使用您类型的开发者将会感到非常困惑。
您可能还想阅读
- igualdad.NET 故事 - 第一部分
- NET 中相等性的故事(虚拟 Object.Equals、静态 Object.Equals 和 Reference.Equals)– 第二部分
- NET 中相等性的故事(IEquatable<T> 接口简介)– 第三部分
- NET 中相等性的故事(C# 中的 == 运算符和原始类型)– 第四部分
- .NET 中的相等性故事 (C# 中的 == 运算符和引用类型) – 第五部分
- .NET 中的相等性故事 (C# 中的 == 运算符和 String 类型) – 第六部分
- .NET 中的相等性故事 (C# 中的相等运算符 (==) 和值类型) – 第七部分
- .NET 中的相等性故事 (C# 中继承和泛型下的相等运算符 (==) 问题) – 第八部分
- 在 C# 中为值类型实现相等性 - 第九部分
- 在 C# 中为引用类型实现相等性 - 第十部分
摘要
我们看到,我们可以为我们的类型实现 IEquatable<T>
,以提供一个强类型的 Equals
方法,该方法还可以避免值类型的装箱。IEquatable<T>
已为原始数值类型实现,但不幸的是,微软在 Framework Class Library 中并未积极为其实现其他值类型。