if (this == null) Console.WriteLine("this is null"); 这句话一写,大家一定觉得荒谬,然而 if 内代码的执行却是可能的!本文讲介绍到底发生了什么。
制造一个 this 可以为 null 的程序
请看代码,这是我们的库函数:
1
2
3
4
5
6
7
8
9
10
11
namespace Walterlv.Demo
{
    public class Foo
    {
        public void Test()
        {
            if (this == null) Console.WriteLine("this is null");
            else Console.WriteLine("this is not null");
        }
    }
}
外面是这样调用的:
1
2
3
4
5
6
7
8
9
10
11
namespace Walterlv.Demo
{
    public class Program
    {
        private static void Main()
        {
            Foo p = null;
            p.Test();
        }
    }
}
这代码写出来,当然毫不犹豫地说——这会发生 NullReferenceException!
然而……
现在我们改一改 Program 的 IL:

将关注重点放在图中红框标注的部分,那是调用 p.Test 的地方。
现在,我们将它从 callvirt 修改成 call。
第一步:反编译 exe 成 IL:
第二步:修改 IL,将 callvirt 修改成 call
第三步:重新编译 IL 成 exe
结果,现在再执行程序时,输出是 this is null:

为什么此时 this 是 null
从名字上看,call 是为了调用非虚方法、静态方法或者基类方法的;而 callvirt 是为了调用虚方法的。前者在编译时就将确认调用了某个类的某个方法,而后者将在运行时动态决定应该调用哪个。
然而,当 IL 试图调用某个变量实例的一个方法时,由于不确定这个变量到底是不是实际的类型(还是基类型),所以都采用 callvirt 进行调用。call 在编译时就已确定调用,所以也没有加入 null 的判断;callvirt 却需要,因为通常都是实例使用。
于是,此次便出现了 null.Test() 这样诡异的调用。
一些建议和总结
虽然我们制造出了一个 this 可能为 null 的情况,即便库和调用方是分开开发的,但实际开发中其实并不需要考虑这样的问题。
参考资料
- Easy way to modify IL code – I know the answer (it’s 42)
- .net - Call and Callvirt - Stack Overflow
- Observing a null this value
本文会经常更新,请阅读原文: https://blog.walterlv.com/post/this-could-be-null.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected]) 。
