Search code examples
c#classstructdelegates

Delegate With Null Instance Encounters Exception With Yield Return


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.


Solution

  • 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.