[英]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.