KBa*_*Bar 2 c# delegates system.reflection eventhandler .net-6.0
我正在使用该答案中的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);
}
}
Run Code Online (Sandbox Code Playgroud)
有什么解释吗?
您正在使用未记录的内部 api,更糟糕的是该 api 接受原始指针。因此,如果您滥用此类 api(并且您永远无法确定您是否正确使用它,因为它没有记录),那么如果出现(可怕的)错误,这并不奇怪。
请注意,AddEventHandler第三个参数是EventHandler,它是这种类型的委托:
delegate void EventHandler(object sender, EventArgs e);
Run Code Online (Sandbox Code Playgroud)
您的MyEvent委托类型是:
delegate void EventHandler(object sender, int e);
Run Code Online (Sandbox Code Playgroud)
AddEventHandler使用内部未记录的编译器生成的委托构造函数,该构造函数接受两个参数:委托目标和指向方法的原始指针。然后,它只是将原始指针传递给该构造函数的委托方法,handler而不进行任何检查。您传递的委托和正在创建的委托可能完全不兼容,但您不会注意到,直到运行时才会尝试调用它。
在这种情况下,运行时认为它有指向 method 的委托void (object, bool),但实际上它指向 method void (object, EventArgs)。您通过调用它MyEvent(null, true),运行时将true布尔值作为第二个参数传递(它是值类型,因此直接传递值),但您的委托实际上指向期望的方法EventArgs,这是一个引用类型。所以它需要一个类型为某个对象的地址EventArgs。它获取布尔值,就好像它是指向 的指针一样EventArgs。
现在,== null一般情况下基本上只是检查引用是否为零(所有字节均为 0)。True布尔值不由 0 表示,因此 null 检查通过。
然后,它尝试访问位于该“地址”的对象。它无法工作,您访问受保护的内存并收到访问冲突错误。然而,正如这个答案中所解释的:
但如果 (A) 访问冲突发生在低于 0x00010000 的地址处,并且 (B) 发现这种冲突是由 jitted 代码发生的,则它会变成 NullReferenceException,否则它会变成 AccessViolationException
所以它变成了NullReferenceException你观察。
有趣的是,如果您像这样更改代码:
MyEvent(null, false);
Run Code Online (Sandbox Code Playgroud)
然后它将运行而不会出现错误,因为false它是由零字节表示的,因此e == null检查将返回true。
您可以更多地使用此代码,例如将事件类型更改为 int:
public static event EventHandler<int> MyEvent;
Run Code Online (Sandbox Code Playgroud)
然后执行以下操作:
MyEvent(null, 0x00010001);
Run Code Online (Sandbox Code Playgroud)
现在它将抛出AccessViolationException而不是NullReferenceException链接答案声明的那样(现在我们正在尝试访问高于 0x00010000 的位置的内存,因此运行时不会将此访问冲突转换为空引用异常)。
这是另一个有趣的事情,我们使用此答案中的代码在运行时获取 .NET 对象的内存地址,然后将该地址传递给处理程序:
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);
}
Run Code Online (Sandbox Code Playgroud)
出于上述原因,此代码输出“Hello world!”。
长话短说 - 不要在实际代码中使用这种危险的未记录的内部 api。