65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (15投票s)

2016 年 7 月 5 日

CPOL

7分钟阅读

viewsIcon

24383

在本文中,您将了解 .NET 中的 IEquatable 接口以及它如何解决值类型的一些相等性问题。

引言

阅读完

后,您会发现 Object.Equals 方法存在两个问题。一个问题是它缺乏强类型,对于值类型,需要进行装箱。在本文中,我们将探讨 IEquatable<T> 接口,它为这些问题提供了解决方案。

系列文章

如果您想阅读本系列的上一篇文章,如果您愿意,以下是与此相关的上一篇内容的链接:

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>。如果您还记得第二部分,当我们演示 stringEquals 方法时,我们显式地将 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 版本,那么使用您类型的开发者将会感到非常困惑。

您可能还想阅读

摘要

我们看到,我们可以为我们的类型实现 IEquatable<T>,以提供一个强类型的 Equals 方法,该方法还可以避免值类型的装箱。IEquatable<T> 已为原始数值类型实现,但不幸的是,微软在 Framework Class Library 中并未积极为其实现其他值类型。

.NET 中的相等性故事 - 第三部分 - CodeProject - 代码之家
© . All rights reserved.