I've encountered some strange behavior trying to create a delegate from a function that returns an IEnumerable. In the first three instances I can pass in a null "this" and receive valid results, however in the combination of a struct and yield return I hit a runtime NullReferenceException. See the code below to replicate the issue.
class Program
{
public delegate IEnumerable<int> test();
static void Main(string[] args)
{
var method2 = typeof(TestClass).GetMethod("testReturn");
var test2 = (test)Delegate.CreateDelegate(typeof(test), null, method2);
var results2 = test2.Invoke();
Console.WriteLine("This works!");
var method = typeof(TestClass).GetMethod("testYield");
var test = (test)Delegate.CreateDelegate(typeof(test), null, method);
var results = test.Invoke();
Console.WriteLine("This works!");
var method3 = typeof(TestStruct).GetMethod("testReturn");
var test3 = (test)Delegate.CreateDelegate(typeof(test), null, method3);
var results3 = test3.Invoke();
Console.WriteLine("This works!");
var method4 = typeof(TestStruct).GetMethod("testYield");
var test4 = (test)Delegate.CreateDelegate(typeof(test), null, method4);
var results4 = test4.Invoke();
Console.WriteLine("This doesn't work...");
}
public class TestClass
{
public IEnumerable<int> testYield()
{
for (int i = 0; i < 10; i++)
yield return i;
}
public IEnumerable<int> testReturn()
{
return new List<int>();
}
}
public struct TestStruct
{
public IEnumerable<int> testYield()
{
for (int i = 0; i < 10; i++)
yield return i;
}
public IEnumerable<int> testReturn()
{
return new List<int>();
}
}
}
It does work when I pass in default(TestStruct) instead of null, however I will not be able to reference the proper type in this way at runtime.
EDIT: I was able to fix this issue by using Activator.CreateInstance instead of null to create a dummy object dynamically. I'm still interested as to what is different about the yield return that is creating this issue, though.
Iterator methods that use yield return
create a state machine, which means that local variables, including this
, are lifted into fields of a hidden class.
In the case of iterator methods of classes, the this
is obviously an object reference. But for structs, this
is a ref
of the struct.
Looking at the compiler-generated in Sharplab, you can see why TestStruct.testYield
fails and not TestClass.testYield
.
The only reference that TestClass.testYield
makes to its this
argument is:
IL_0008: ldarg.0
IL_0009: stfld class C/TestClass C/TestClass/'<testYield>d__0'::'<>4__this'
This does not involve a dereference of this
, which in your case is null
.
Why does Reflection not throw an exception? Because it is not required to do so. An object reference is allowed to be null
, even if it is the this
parameter. C# will throw on a direct call because it always generates a callvirt
instruction.
Whereas TestStruct.testYield
actually de-references its this
argument, this is because of the inherent difficulty in lifting a ref struct
into a field:
IL_0008: ldarg.0
IL_0009: ldobj C/TestStruct
IL_000e: stfld valuetype C/TestStruct C/TestStruct/'<testYield>d__0'::'<>3__<>4__this'
Technically speaking, a managed ref
pointer is not allowed to be null (see ECMA-335 Section II.14.4.2), so it is somewhat surprising that Reflection even allows the call. Obviously it can always be done with unsafe code.