繁体   English   中英

抛出 NullReferenceException 但 object 通过了 null 检查,这怎么可能?

[英]NullReferenceException thrown but the object passed the null check, how is that possible?

我正在使用该答案中的AddEventHandler方法,但是在带有值类型参数的EventHandler上执行此操作时:

using System.Reflection;

public class Program
{
    public static event EventHandler<bool> MyEvent;

    public static void Main()
    {
        EventInfo eventInfo = typeof(Program).GetEvent(nameof(MyEvent));
        AddEventHandler(eventInfo, null, (s, e) => {
            if (e == null) return; // either if condition or null conditional operator
            Console.WriteLine(e?.ToString());
        });
        MyEvent(null, true);
    }

    public static void AddEventHandler(EventInfo eventInfo, object client, EventHandler handler)
    {
        object eventInfoHandler = eventInfo.EventHandlerType
            .GetConstructor(new[] { typeof(object), typeof(IntPtr) })
            .Invoke(new[] { handler.Target, handler.Method.MethodHandle.GetFunctionPointer() });

        eventInfo.AddEventHandler(client, (Delegate)eventInfoHandler);
    }
}

在此处输入图像描述

有什么解释吗?

您正在使用未记录的内部 api,更糟糕的是这个 api 接受原始指针。 因此,如果您滥用此类 go(可怕的)错误,这并不奇怪(并且您永远无法确定您是否正确使用它,因为它没有记录)。

请注意, AddEventHandler第三个参数是EventHandler ,它是这种类型的委托:

delegate void EventHandler(object sender, EventArgs e);

你的MyEvent委托类型是:

delegate void EventHandler(object sender, int e);

AddEventHandler使用内部未记录的编译器生成的委托构造函数,它接受两个参数:委托目标和指向方法的原始指针 然后它只是将指向handler委托方法的原始指针传递给该构造函数而不进行任何检查。 您传递的委托和正在创建的委托可能完全不兼容,但您不会注意到这一点,直到运行时尝试调用它。

在这种情况下,运行时认为它有委托指向方法void (object, bool) ,但实际上它指向方法void (object, EventArgs) 您通过MyEvent(null, true)调用它,运行时将true boolean 值作为第二个参数传递(它是值类型,因此直接传递值),但您的委托实际上指向期望EventArgs的方法,这是一个引用类型。 所以它需要一个EventArgs类型的地址 object 。 它获得 boolean 值,就好像它是指向EventArgs的指针一样。

现在, == null在一般情况下基本上只是检查引用是否为零(所有字节均为 0)。 True的 boolean 值没有用 0 表示,所以 null 检查通过。

然后,它尝试访问位于该“地址”的 object。 它无法工作,您访问受保护的 memory 并收到访问冲突错误。 但是,正如这个答案中所解释的那样:

但是,如果 (A) 访问冲突发生在低于 0x00010000 的地址,并且 (B) 发现这种冲突是由被 jitted 的代码发生的,那么它会变成 NullReferenceException,否则会变成 AccessViolationException

所以它变成了你观察到的NullReferenceException

有趣的是,如果您像这样更改代码:

MyEvent(null, false);

然后它会运行没有错误,因为false由零字节表示,所以e == null check 将返回 true。

您可以多玩一下这段代码,例如将事件类型更改为 int:

public static event EventHandler<int> MyEvent; 

然后做:

MyEvent(null, 0x00010001);

现在它将抛出AccessViolationException而不是NullReferenceException作为链接的答案声明(现在我们试图在高于 0x00010000 的位置访问 memory,因此运行时不会将此访问冲突转换为 null 引用异常)。

这是另一个有趣的事情,我们使用这个答案中的代码在运行时获取 .NET object 的 memory 地址,然后将该地址传递给处理程序:

public static event EventHandler<IntPtr> MyEvent;

public static unsafe void Main() {
    // it's not even EventArgs, it's string
    var fakeArgument = "Hello world!";
    // some black magic to get address
    var typedRef = __makeref(fakeArgument);
    IntPtr ptr = **(IntPtr**)(&typedRef);
    EventInfo eventInfo = typeof(Program).GetEvent(nameof(MyEvent));
    AddEventHandler(eventInfo, null, (object s, EventArgs e) => {
        if (e == null) return;
        // e is actually a string here, not EventArgs...
        Console.WriteLine(e?.ToString());
    });
    MyEvent(null,  ptr);
}

此代码输出“Hello world”。 出于上述原因。

长话短说 - 不要在真实代码中使用这种危险的未记录的内部 api。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM