30

在一个例子中,我的教授实现了 Equals,如下所示:

public class Person {
    private string dni;

    // ...

    public override bool Equals(object o) {
        if (o == null)
            return (this == null);
        else {
            return ((o is Person) && (this.dni == (o as Person).dni));
        }
    }
}

我没有使用 C# 的经验,但据我所知this,成员函数内部不能为空(至少在我所知道的语言 C++ 和 Java 中是这样),所以 if 对我来说似乎很奇怪。

我是对的还是c#中有任何组件我不知道哪个使测试变得this == null必要?

4

4 回答 4

58

我没有使用 C# 的经验,但据我所知,这在成员函数中不能为空(至少在 C++ 和 Java 中是这样,我知道的语言)

让我们首先指出您的陈述是错误的。

在 C++ 中,在 null 接收器上调度方法是未定义的行为未定义的行为意味着任何事情都可能发生。“任何事情”包括程序传递和继续,就好像没有任何问题一样NULLthis当然,在 C++ 中检查是否为 null 有点愚蠢,因为只有在您已经不知道程序在做什么的情况this下,检查才能为真,因为它的行为是未定义的。

在Java中是否this可以为空我不知道。

现在解决您关于 C# 的问题。让我们假设它==没有超载。我们稍后会回到这一点。

您的方法是用 C# 编写的。假设它是从具有空接收器的 C# 程序调用的。C# 编译器评估接收器是否可能为空;如果它可能为空,那么它确保它在调用该方法之前生成执行空检查的代码。因此,在这种情况下,此检查毫无意义。这当然是 99.9999% 的可能性。

假设它是通过反射调用的,就像在 mike z 的回答中一样。在这种情况下,执行调用的不是 C# 语言;相反,有人故意滥用反射。

假设它是从另一种语言调用的。我们有一个虚方法;如果它是通过虚拟调度从其他语言调用的,则必须执行空检查,因为我们怎么知道虚拟槽中的内容?在那种情况下,它不能为空。

但是假设它是使用非虚拟调度从另一种语言调用的。在这种情况下,其他语言不需要实现检查 null 的 C# 功能。它可以调用它并传递 null。

所以有几种方法this可以null在 C# 中使用,但它们都非常脱离主流。因此,很少有人像您的教授那样编写代码。C# 程序员习惯性地认为this不是null并且永远不会检查它。

现在我们已经解决了这个问题,让我们再批评一下这段代码。

public override bool Equals(object o) {
    if (o == null)
        return (this == null);
    else {
        return ((o is Person) && (this.dni == (o as Person).dni));
    }
}

首先有一个明显的错误。我们假设它this可能是空的,好的,让我们运行它。 什么停止this.dni抛出空引用异常???如果您要假设它this可以为空,那么至少要始终如一地这样做!(在 Coverity,我们将这种情况称为“前向空缺陷”。)

接下来:我们覆盖Equals然后使用==inside,大概是指引用相等。这条路是疯狂的!现在我们有一种情况,x.Equals(y)可以是真的,x==y也可以是假的!这太可怕了。请不要去那里。如果您要覆盖,Equals则同时重载==,并IEquatable<T>在您使用时实施。

(现在,有一个合理的论点是疯狂存在于任何一个方向;如果与值语义==一致,则可以不同于,这似乎也很奇怪。这里的要点是 C# 中的相等性相当混乱。)Equalspersonx == persony(object)personx == (object)persony

此外:如果==稍后被覆盖怎么办?当代码的作者明确希望进行参考比较时,现在Equals正在调用重写的运算符。==这是错误的秘诀。

我的建议是 (1) 编写一个做正确事情的静态方法,以及 (2)ReferenceEquals每次都使用可能对相等的含义有任何混淆的情况:

private static bool Equals(Person x, Person y)
{
    if (ReferenceEquals(x, y))
        return true;
    else if (ReferenceEquals(x, null))
        return false;
    else if (ReferenceEquals(y, null))
        return false;
    else 
        return x.dni == y.dni;
}

这很好地涵盖了所有情况。请注意,当引用相等语义的含义时,读者非常清楚。另请注意,此代码可以很容易地在每种可能性上放置断点,以进行调试。最后,请注意我们尽早采取最便宜的方式;如果对象的引用相等,那么我们就不必对字段进行潜在的昂贵比较!

现在其他方法很简单:

public static bool operator ==(Person x, Person y) 
{
  return Equals(x, y);
}
public static bool operator !=(Person x, Person y) 
{
  return !Equals(x, y);
}
public override bool Equals(object y)
{
  return Equals(this, y as Person);
}
public bool Equals(Person y)
{
  return Equals(this, y);
}

请注意,我的方式比您教授的方式更加优雅和清晰。请注意,我的方式处理 nullthis而不this直接与 null 进行比较。

再一次:所有这些都说明了折衷的立场,其中值和引用相等都是可能的,并且有四种(、、==和)实现相等的方法,即使不假设can 或 not也是非常复杂和混乱的。!=object.Equals(object)IEquatable<T>.Equals(T)thisnull

如果你对这个主题感兴趣,我本周在我的博客上描述了一个稍微困难的问题:如何实现一般的比较,包括不等式。

http://ericlippert.com/2013/10/07/math-from-scratch-part-six-comparisons/

作为对 C# 如何处理相等性的批评,这些评论特别有趣。

最后:不要忘记覆盖GetHashCode. 确保你做对了。

于 2013-10-10T15:59:18.210 回答
13

是的,在某些情况下。最常见的情况是,如果您使用反射创建的委托调用实例方法并传递空接收器。这将打印“真”:

public class Test
{
    public void Method()
    {
        Console.WriteLine(this == null);
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        object t = new Test();
        var methodInfo = t.GetType().GetMethod("Method");
        var a = (Action)Delegate.CreateDelegate(typeof(Action), null, methodInfo);
        a();
    }
}

老实说,虽然我从未见过有人真正this在生产代码中检查 null。

于 2013-10-10T15:46:25.900 回答
2

Yes, it is possible and in fact not entirely unlikely. The C# language gives an excellent guarantee that no method can be called on a null reference to a class object, it generates code that makes a null check at the call site. Which certainly does avoid a lot of head-scratching, a NullReferenceException inside the method can be pretty hard to debug without that check.

That check is however specific to C#, not all .NET languages implement it. The C++/CLI language doesn't for example. You can see it for yourself by creating a solution with a C++ CLR Console mode project and a C# class library. Add a reference in the CLR project to the C# project.

C# code:

using System;

namespace ClassLibrary1 {
    public class Class1 {
        public void NullTest() {
            if (this == null) Console.WriteLine("It's null");
        }
    }
}

C++/CLI code:

#include "stdafx.h"

using namespace System;

int main(array<System::String ^> ^args) {
    ClassLibrary1::Class1^ obj = nullptr;
    obj->NullTest();
    Console::ReadLine();
    return 0;
}

Output:

It's null

This is not otherwise undefined behavior or illegal in any way. It is not verboten by the CLI spec. And as long as the method doesn't access any instance members then nothing goes wrong. The CLR handles this as well, a NullReferenceException is also generated for pointers that are not null. Like it will be when NullTest() accesses a field that isn't the first field in the class.

于 2013-10-12T04:29:13.913 回答
2

一种方法是,如果您通过重载运算符来练习一些黑this魔法。例如,如果有人决定拥有一个 null 或空字符串等同于一个 null 人:== null==dni

public static bool operator ==(Person a, Person b)
{
    if(String.IsNullOrEmpty(a.dni) && (object)b == null)
        return true;

    if(String.IsNullOrEmpty(b.dni) && (object)a == null)
        return true;

    return Object.ReferenceEquals(a, b);
}

请注意,这可能是一个非常糟糕的主意。

于 2013-10-10T15:45:00.793 回答