C# 中相等运算符 (==) 在继承和泛型方面的问题






4.87/5 (29投票s)
本文重点介绍在使用 == 运算符与泛型或继承时可能面临的问题,这些问题可能导致意外结果。
引言
在这篇文章中,我们将探讨为什么使用 ==
运算符进行相等性比较并不总是好的选择,我们将看到 ==
运算符在继承和泛型方面表现不佳,并将看到更好的选择是依赖于 virtual Equals
方法。
背景
在上一篇文章中,我们了解到== 运算符不适用于非基本值类型,除非我们为该值类型重载运算符,并且我们看到了如何重载相等运算符以启用非基本值类型的 == 运算符比较。我们还比较了== 运算符和Object.Equals
方法,以了解它们对于基本类型、值类型和引用类型的行为有何不同。
== 运算符与 Object.Equals 的比较
以下是两者的比较
- 对于基本类型,例如
int
、float
、long
、bool
等,== 运算符和Object.Equals
方法都将比较值,即 1 等于 1,但 1 不等于 0。 - 对于大多数引用类型,
==
运算符和Object.Equals
方法默认都会比较引用,您可以通过重载==
运算符或重写Object.Equals
方法来修改此行为,但如果您希望两者的行为保持一致,并且不想让其他开发人员和您自己感到惊讶,则必须同时进行(重载==
运算符和重写Equals
方法)。 - 对于非基本值类型,
Object.Equals
方法将使用反射进行值相等性比较,这很慢,当然这是重写行为,但相等运算符默认不适用于值类型,除非您为该类型重载==
运算符,这我们在上面的示例中已经看到了。 - 还有一个小区别,就是对于引用类型,如果第一个参数是
null
,virtual Equals
方法就无法工作,但这无关紧要,作为一种变通方法,可以使用static Equals
方法,该方法接受两个要比较的对象作为参数。
本系列之前的文章
如果您想阅读本系列的上一篇文章,如果您愿意,以下是与此相关的上一篇内容的链接:
- NET 中相等性的故事 - 第一部分
- C# 中的相等性和 Object 类(第 2 部分)
- C# 中的相等性和 IEquatable<T> 接口(第 3 部分)
- C# 中的 == 运算符和原始类型(第 4 部分)
- C# 中的 == 运算符和引用类型(第 5 部分)
- C# 中的 == 运算符和 String 类(第 6 部分)
- C# 中的 == 运算符和值类型(第 7 部分)
- C# 中相等运算符 (==) 在继承和泛型方面的问题 (第 8 部分)
- 在 C# 中为值类型实现相等性 (第 9 部分)
- 在 C# 中为引用类型实现相等性 - 第十部分
相等运算符和继承问题
让我们创建一些示例代码,这些代码将说明当代码中涉及继承时 ==
运算符的行为方式,因此我们声明两个 string
类型变量,我们将通过 C# 中可用的 4 种不同方式进行相等性检查,再次检查结果
public class Program
{
public static void Main(string[] args)
{
string str = "Ehsan Sajjad";
string str1 = string.Copy(str);
Console.WriteLine(ReferenceEquals(str, str1));
Console.WriteLine(str.Equals(str1));
Console.WriteLine(str == str1);
Console.WriteLine(object.Equals(str, str1));
}
}
输出
以下是上述执行代码的输出
首先,我们使用 ReferenceEquals
方法检查两个 string
变量是否引用同一个 string
对象,接下来我们使用 String
类型的实例方法 Equals
进行检查,在第三行,我们再次检查相等性,但这次使用 ==
运算符,最后,我们使用 Object
的 static
Equals
方法进行检查,以便我们可以比较这 4 种技术的结果。我们应该能够从我们迄今为止完成的先前文章中判断出结果会是什么
ReferenceEquals
肯定会返回false
,因为两者引用的是不同的对象,而不是同一个对象。String
类型的Equals
方法也将返回true
,因为两个string
相同(即,相同的字符序列)。==
运算符也将返回true
,因为两个string
值相等。virtual Object.Equals
调用也将返回true
,因为将调用String
的重写实现,并且它会检查string
值的相等性。
到目前为止,以上所有内容都说得通,现在我们将稍微修改上面的示例,我们所做的唯一更改是,不使用 String
类型的变量,而是使用 Object
类型的变量,我们将看到输出与上面的代码有何不同,这是将其转换为使用 Object
类型而不是 String
后的代码
static void Main(string[] args)
{
Object str = "Ehsan Sajjad";
Object str1 = string.Copy((string)str);
Console.WriteLine(ReferenceEquals(str, str1));
Console.WriteLine(str.Equals(str1));
Console.WriteLine(str == str1);
Console.WriteLine(object.Equals(str, str1));
}
输出
所以我们可以看到结果与之前我们使用 String
类型变量时不同。其他三种方法仍然返回相同的结果,但是 ==
运算符的相等性检查现在返回 false
而不是 true
,这表明两个 string
不相等,这与它们实际上相等的事实相矛盾,也与其他两种方法的结果相冲突。
为什么会这样?
这背后的原因是 ==
运算符等同于一个 static
方法,而 static
方法不能是 virtual
方法,当这里使用 ==
运算符进行比较时,实际上发生的是我们试图比较两个 Object
类型的变量,我们知道它们实际上是 String
类型,但编译器并不知道这一点,我们知道对于非虚方法,在编译时决定需要调用哪个实现,并且由于变量已被声明为 Object
类型,编译器将发出用于比较 Object
类型实例的代码,如果您导航到 Object
的源代码,您会发现它没有 ==
运算符的重载,因此这里会发生的是 ==
运算符将做它一直做的(即,它会检查引用相等性)当没有为该类型找到重载时用于比较两个引用类型,并且由于这两个 string
对象是单独的实例,引用相等性将被评估为 false
,表示两个对象不相等。
应首选 Object Equals 方法
上述问题永远不会出现在其他两个 Equals
方法中,因为它们是 virtual
的,并且特定类型将为其提供重写,调用它们将始终调用重写实现以正确评估它们的相等性,因此在上述情况下,将调用 String
类型的重写方法。
对于 static Equals
方法,我们已经在之前的一篇文章中讨论过,它内部调用相同的 virtual Equals
方法,它只是在我们想要避免 NRE(空引用异常)并且有可能我们将在其上调用 Equals
方法的对象可能为 null
的情况下使用。
因此,在继承的情况下,在检查相等性时,应首选 Equals
方法重写,而不是使用 ==
运算符。
== 运算符和引用相等性
另一个需要注意的是,在比较之前将相等运算符的操作数转换为对象将始终给出与调用 ReferenceEquals
方法相同的结果。
一些开发人员以这种方式编写代码来检查引用相等性。我们应该小心地以这种方式进行引用相等性检查,因为将来阅读该代码的其他人可能不理解或不知道转换为对象会导致比较基于引用。
但是,如果我们显式调用 ReferenceEquals
方法,那将澄清我们想要进行引用相等性的意图,并消除对代码需要做什么的所有疑问。
== 运算符和泛型问题
避免==
运算符的另一个原因是,如果我们在代码中使用泛型。为了说明这一点,让我们创建一个简单的方法,它接受两个 T
类型的泛型参数,并比较以检查两个对象是否相等并在 Console
上打印结果,代码如下
static void Equals<T>(T a, T b)
{
Console.WriteLine(a == b);
}
上面的例子显然非常简单,人们很容易就能看出它实际在做什么。我们正在使用相等运算符来比较两个 T
类型的对象,但是如果您尝试在 Visual Studio 中构建上面的例子,它将无法构建,并且会出现编译时错误,显示为
我们看到上面的错误是因为 T
可以是任何类型,它可以是引用类型或值类型或基本类型,并且不能保证正在传递的类型参数提供了 ==
运算符的实现。
在 C# 泛型中,无法对泛型类型或方法应用约束,这可能强制过去的类型参数提供 ==
运算符的重载实现,我们可以通过在类型 T
上放置类约束来成功构建上述代码,例如
static void Equals<t>(T a, T b) where T : class
{
Console.WriteLine(a == b);
}
因此,我们对泛型类型 T
施加了引用类型的约束,因此我们现在能够成功编译代码,因为 ==
运算符可以毫无问题地用于引用类型,并且它将检查两个引用类型对象的引用相等性。
临时解决方案
我们现在能够构建代码,但存在一个问题,因为如果我们想将值类型参数或原始类型传递给泛型 Equals
方法,我们将无法做到,因为我们已经将此方法限制为只能用于引用类型。
让我们在 main
方法中编写一些代码,它将创建两个相同的 string
,就像我们之前在几篇文章中做过的那样
class Program
{
static void Main(string[] args)
{
Object str = "Ehsan Sajjad";
Object str1 = string.Copy((string)str);
Equals(str, str1);
}
static void Equals<T>(T a, T b) where T : class
{
Console.WriteLine(a == b);
}
}
那么猜猜运行上述代码的输出会是什么,它会评估这两个 string
是否相等?如果我们回想一下之前看到的,对于 String
类型,由 String
定义的==
运算符重载比较对象的值,因此上述代码应该在 Console
上打印 true
,但如果运行代码,我们会看到相反的结果,即它打印了 false
因此,上述代码给了我们意想不到的结果,我们期望在 Console
上打印 True
。立即想到的问题是,为什么它评估为 false
,看起来 ==
运算符正在检查两个对象的引用相等性而不是值相等性,但问题是为什么它进行引用相等性检查。
之所以发生这种情况,是因为即使编译器知道它可以将 ==
运算符应用于传递的任何类型 T
(在上面的例子中是 String
),并且 String
具有 ==
运算符重载,但编译器不知道使用该方法时使用的泛型类型是否重载了 ==
运算符,因此它假定 T
不会重载它,并编译代码时认为 ==
运算符是在 Object
类型实例上调用的,这显然是它检查引用相等性的原因。
在使用 ==
运算符和泛型时,这一点非常重要。==
运算符将不使用类型 T
定义的相等运算符重载,而会将其视为 Object
。
再次使用 Object.Equals 解决问题
现在让我们修改我们的泛型方法,改用 Equals
方法而不是相等运算符,代码如下
static void Equals<t>(T a, T b)
{
Console.WriteLine(object.Equals(a,b));
}
我们已经删除了 class
约束,因为 Object.Equals
可以对任何类型调用,我们使用 static
方法也是出于同样的原因,如果其中一个参数传递 null
,我们的代码也不会失败并会按预期工作,现在此方法将适用于值类型和引用类型。
现在,如果再次运行代码,我们会看到它按预期打印了结果,因为 object.Equals
会在运行时调用 Equals
方法的适当重写实现,因为 static
方法会调用 virtual Equals
方法,并且我们看到了预期的结果 True
,因为两个 string
值相等。
摘要
==
运算符在继承方面表现不佳,并且在与继承一起使用时可能会产生意想不到的结果,因为==
运算符不是virtual
的,因此在可能的情况下应首选virtual Equals
或static Equals
方法。- 当在
Generic
类或方法中使用==
运算符时,它的工作方式也不尽如人意,应使用Equals
方法与generic
结合使用,以避免程序中出现意外行为。