结构体和类中的等号运算符重载

5

如果我对一个类进行operator ==的方法重载,那么在比较字段之前必须执行一些检查:

  • 如果两个参数都为null或者两个参数是同一个实例,则返回true。

    例如:if (System.Object.ReferenceEquals(arg1, arg2)) return true;

  • 如果一个参数为null,但不是全部,则返回false。

    例如:if (((object)arg1 == null) || ((object)arg2 == null)) return false;

事实上,如果我有一个结构体并且想要对operator ==进行重载,这些检查就不是必需的,因为它们是无用的。原因如下:结构体是值类型,因此它不能为null,例如DateTime date = null;是无效的,因为DateTime(它是一个结构体)不是引用类型,因此您不能比较两个DateTime,其中一个设置为null

我创建了一个简单的结构体Point2D,带有operator ==,然后将Point2D的实例与null进行比较:

Point2D point = new Point2D(0,0);
Console.WriteLine((point == null));
  1. 显然,没有调用operator ==运算符,但比较结果返回False。那么调用了哪个方法?

  2. 文档指出,在不可变类型中重载此运算符是不推荐的。为什么?


1
你应该尽量一次只问一个问题。如果你有两个问题,分别提出来。 - svick
@svick:抱歉,我会尽量避免类似的问题。 - enzom83
2个回答

16

Chris Shain的答案是正确的,但没有解释为什么这是合法的

当您重写等号运算符,并且两个操作数是非可空值类型,并且返回类型是bool时,我们会为您提供一个升级版运算符。也就是说,如果您有:

public static bool operator ==(S s1, S s2) { ... }

然后无需额外费用,您就可以获得

public static bool operator ==(S? s1, S? s2) { ... }

被调用的是那个运算符。 当然,编译器知道结果始终为false,因为一个操作数为null,另一个操作数则不为null。

以前有一个警告,指出您的代码始终返回false,但我们在几个版本之前意外禁用了它,并且从未真正重新启用它。 我明天将在Roslyn编译器中处理这个代码,看看能否将其恢复到良好状态。


4
阅读您的回答总是有教育意义的,即使我已经写了近十年.NET代码。谢谢 :-) - Chris Shain
Eric,你是说这里有两个类型为“Point”和“null”的操作数;它们之间没有隐式转换,但编译器调用了另一种类型的运算符,可以将两个操作数隐式转换为该类型。这似乎与条件运算符的处理方式不一致,在条件运算符中,你不能说int? x = someBool ? 7 : null;。我从C#的第二个版本开始学习;在早期版本中,像1 == null这样的表达式是否会编译失败? - phoog
@phoog:这不是一个恰当的比喻。对于运算符重载解析,正确的比喻是方法重载解析。如果您有一个静态方法int?Operators.Add(int?, int?)并调用Operator.Add(1, null),那么您期望选择该方法,即使int不能转换为null,null也不能转换为int,对吧?将所有内置和提升的运算符视为类Operators上的静态方法;运算符重载解析只是对这些方法进行重载解析。 - Eric Lippert
那么,为了非常明确,这是否意味着对于一个非空结构体(non-nullable struct)与重载运算符==的情况,自动生成的提升运算符在调用重载运算符==之前执行空值检查,这就是为什么重载运算符==永远不需要执行这个检查本身的原因? - Neutrino
@Neutrino:正确。自动生成的提升运算符的语义是首先检查是否为空,只有在两个操作数都已知为非空时才调用未提升的运算符。 - Eric Lippert

5

因为编译器似乎会优化掉它。我尝试了这段代码:

System.Drawing.Point point = new System.Drawing.Point(0,0);
Console.WriteLine((point == null));

然后它生成了以下的IL代码:

IL_0000:  ldloca.s    00 
IL_0002:  ldc.i4.0    
IL_0003:  ldc.i4.0    
IL_0004:  call        System.Drawing.Point..ctor
IL_0009:  ldc.i4.0    
IL_000A:  call        System.Console.WriteLine

这实际上归结为“创建一个点,然后将false写入命令行”。
这也解释了为什么它不调用您的运算符。 结构体永远不可能为空,在编译器可以保证您始终会得到false结果的情况下,它根本不费力地发出调用运算符的代码。
尽管String是一个类并重载了==运算符,但在这段代码中也发生了同样的事情。
System.Drawing.Point point = new System.Drawing.Point(0,0);
Console.WriteLine("foo" == null);

关于不可变性...在C#中,==运算符通常被解释为“引用相等”,例如这两个变量指向同一个类的实例。 如果您正在重载它,则通常意味着两个类的实例,即使它们不是同一实例,在其数据相同时应该表现得像它们是同一实例。 经典的例子是字符串。 即便 GiveMeAnA() 返回的实际字符串引用可能与由字面字符串"A"表示的字符串引用不同,"A" == GiveMeAnA()仍然成立。
如果您在不可变的类上重载了==操作符,那么在求值==之后对类进行的更改可能会导致许多微妙的错误。

3
我使用LINQPad(http://linqpad.net)或Reflector(http://reflector.net)。前者可以显示任意代码片段的IL,后者则将程序集反编译成IL,并能够从该IL重新生成等效的C#代码。还有一个名为ILDASM的内置工具(http://msdn.microsoft.com/en-us/library/f7dy01k1(v=vs.80).aspx)。理解IL是一件棘手的事情-请参阅http://codebetter.com/raymondlewallen/2005/02/07/getting-started-understanding-msil-assembly-language/ - Chris Shain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接