在.NET实例方法中,为什么可能出现"this == null"的情况?

37

我一直认为在实例方法体内,this不可能为null。但下面这个简单的程序表明它是可能的。这是某种已记录的行为吗?

class Foo
{
    public void Bar()
    {
        Debug.Assert(this == null);
    }
}

public static void Test()
{            
    var action = (Action)Delegate.CreateDelegate(typeof (Action), null, typeof(Foo).GetMethod("Bar"));
    action();
}

更新

我同意那些指出这是文档中所描述的方法的回答。然而,我真的不明白这种行为。特别是因为这不是C#的设计方式。

我们收到了一个报告,来自一些使用C#(虽然当时它还没有被命名为C#)的.NET团体中的某个人编写了调用空指针上的方法的代码,但是他们并没有获得异常,因为该方法没有访问任何字段(即“this”为空,但是方法中没有使用它)。然后该方法调用了另一个方法,该方法确实使用了此指针并抛出了异常,于是有点摸不着头脑。在他们解决问题后,他们给我们发了一张便条。 我们认为能够在空实例上调用方法有点奇怪。Peter Golde进行了一些测试,以查看始终使用callvirt的性能影响如何,结果影响很小,因此我们决定进行更改。

http://blogs.msdn.com/b/ericgu/archive/2008/07/02/why-does-c-always-use-callvirt.aspx


看看我的答案,你会知道CLR在.NET 1.0中就是有意设计成这样的。你引用的文章是关于另一种(非委托)情况的,这也应该是编译器优化——当实例的类型可以静态确定时,用直接调用替换callvirt。请注意,CLR必须在callvirt期间抛出NullReferenceException的原因是需要基于这个引用进行VMT查找,而不仅仅是想要检查引用的语义。 - Jirka Hanika
相关链接:https://dev59.com/0HA75IYBdhLWcg3wuLjD - Brian Gideon
在我看来,很遗憾没有一种标准的方式(也许是通过属性)来指定一个方法应该使用call而不是callvirt进行调用,因为这将允许封装值类型[如string]的类型具有可以操作默认值存储位置的成员。我认为,说if (someString.IsNullOrEmpty)if (String.IsNullOrEmpty(someString))更加简洁。 - supercat
稍微偏离一下话题 - 使用Unity游戏引擎的人可能熟悉this == null,因为Unity重载了==运算符并使用自己的实现方式,这使得this可以为空。 - Bip901
5个回答

23
由于您在使用 Delegate.CreateDelegate 时将 null 传递给 firstArgument 参数,所以您在对空对象调用实例方法。
参考链接:http://msdn.microsoft.com/en-us/library/74x8f551.aspx

如果 firstArgument 是 null 引用且 method 是实例方法,则结果取决于委托类型 type 和方法 method 的签名:

如果 type 的签名明确包括方法 method 的隐藏第一个参数,则委托被称为表示开放实例方法的委托。当调用委托时,参数列表中的第一个参数将传递给方法的隐藏实例参数。

如果 method 和 type 的签名匹配(即,所有参数类型都兼容),则委托被称为闭包 null 引用。调用委托就像在 null 实例上调用实例方法一样,这不是特别有用的事情。


12

如果你使用call IL指令或委托方法,你可以调用一个方法。只有在尝试访问成员字段时,这个陷阱才会被触发,从而引发NullReferenceException异常。

尝试

 int x;
 public void Bar()
 {
        x = 1; // NullRefException
        Debug.Assert(this == null);
 }
BCL甚至没有显式的this == null检查来帮助那些不总是使用callvirt(如C#)的语言进行调试。有关更多信息,请参见此问题
例如,String类就有这种检查。它们并没有什么神秘之处,只是在像C#这样的语言中,你看不到需要它们的情况。
// Determines whether two strings match. 
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] 
public override bool Equals(Object obj)
{
    //this is necessary to guard against reverse-pinvokes and
    //other callers who do not use the callvirt instruction
    if (this == null)
        throw new NullReferenceException();

    String str = obj as String;
    if (str == null) 
        return false;

    if (Object.ReferenceEquals(this, obj)) 
        return true;

    return EqualsHelper(this, str);
}

5
尝试查看 Delegate.CreateDelegate() 的文档,在 msdn 上

您正在“手动”调用所有内容,因此没有为 this 指针传递实例,而是传递了 null。 因此,这可能发生,但您必须非常努力才能做到。


请参考以下示例,这是一种非常简单的方法。http://bradwilson.typepad.com/blog/2008/01/c-30-extension.html - Jirka Hanika
扩展方法可能“看起来”像类方法,但其实它们并不是。它们只是在类实例上执行某些额外的易用性静态方法。因此我不会称它为同一种东西。 - Kevin Anderson
我同意你的观点,但是这个列表并不仅限于扩展方法。C++/CLI 直接调用非虚拟的 C# 方法,使得你可以像使用扩展方法一样轻松地编写源代码,同时在 C# 端产生真正的 this == null - Jirka Hanika
就像在C ++中一样,this在设计良好的代码中很少为 null,但语言并不试图严格防止这种情况(否则它将更加努力并在调用方添加一个检查)。 - Jirka Hanika

5

this是一个引用,因此从类型系统的角度来看,它可以是null而不会有问题。

你可能会问为什么没有抛出NullReferenceException。 CLR抛出该异常的所有情况的完整列表在文档中记录。 你的情况没有列出。 是的,这是一个callvirt,但是调用的是Delegate.Invoke参见此处),而不是调用Bar,因此this引用实际上是非null的委托!

您看到的行为对CLR具有有趣的实现后果。 委托具有Target属性(对应于您的this引用),当委托是静态的时,它经常是空的(想象一下Bar是静态的)。 现在,自然地,该属性有一个称为_target的私有后备字段。 对于静态委托,_target是否包含null? 不包含。 它包含对委托本身的引用。 为什么不是空? 因为像您的示例所示,空引用是委托的合法目标,CLR没有两种类型的null指针来区分静态委托。

这个小知识点证明了使用委托时,实例方法的空目标不是事后想出来的。 您可能仍然会问最终问题:但是他们为什么必须得到支持?

早期的CLR有一个雄心勃勃的计划,即成为即使是宣誓的C++开发人员选择的平台之一,首先通过受管理的C++,然后通过C++/CLI来实现。某些具有挑战性的语言特性被省略了,但是在C++中执行没有实例的实例方法并不真正具有挑战性,这在C++中是完全正常的。 包括委托支持。

因此,最终答案是:因为C#和CLR是两个不同的世界。

更多好文章更好的阅读材料,以显示允许空实例的设计即使在非常自然的C#语法上下文中也会留下其痕迹。


0

在C#类中,这是一个只读引用。因此,正如预期的那样,它可以像任何其他引用一样使用(以只读模式)...

this == null // readonly - possible
this = new this() // write - not possible

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