Search code examples
c#reflectionstatic-constructor

Why is FieldInfo.GetValue(null) not working in static constructor


See the code below. I want a class that automatically enumerates all the defined static readonly instances of its own type (see TestClass as an example, it defines 3 static readonly instances of its own type).

I want this automation because I want to loop over the defined types and not risk the change of forgetting to add a new instance to the list of All.

Ok, I have it working, that is not the point. But why doesn't FillAll work when called from a static constructor? See the commented static constructor in DefinedInstancesBase<T> code. I mean FieldInfo.GetValue(null) returns null in the static constructor, though the debugger has already hit creating the static readonly instances before the FieldInfo.GetValue(null) is called.

I'm very curious why it doesn't work. Is this by design?

public abstract class DefinedInstancesBase<T>
{
    public static IList<T> All
    {
        get
        {
            if (_All == null)
            {
                FillAll();
            }
            return _All;
        }
    }

    //Why this doesn't work? No idea.
    //static DefinedInstancesBase()
    //{
    //    FillAll();
    //}

    private static void FillAll()
    {
        var typeOfT = typeof(T);
        var fields = typeOfT.GetFields(BindingFlags.Public | BindingFlags.Static);
        var fieldsOfTypeT = fields.Where(f => f.FieldType == typeOfT);
        _All = new List<T>();
        foreach (var fieldOfTypeT in fieldsOfTypeT)
        {
            _All.Add((T)fieldOfTypeT.GetValue(null));
        }
    }

    private static List<T> _All = null;
}

[TestClass]
public class DefinedInstancesTest
{
    [TestMethod]
    public void StaticReadOnlyInstancesAreEnumerated()
    {
        //Given
        var expectedClasses = new List<TestClass>
        {
            TestClass.First,
            TestClass.Second,
            TestClass.Third,
        };

        //When
        var actualClasses = TestClass.All;

        //Then
        for (var i=0; i<expectedClasses.Count; i++)
        {
            Assert.AreEqual(expectedClasses[i].Id, actualClasses[i].Id);
        }
    }

    private class TestClass : DefinedInstancesBase<TestClass>
    {
        public static readonly TestClass First = new TestClass(1);
        public static readonly TestClass Second = new TestClass(2);
        public static readonly TestClass Third = new TestClass(3);

        public int Id { get; private set; }

        private TestClass(int pId)
        {
            Id = pId;
        }
    }
}

Solution

  • There are two separate issues at work here.

    1. There is a typo in your static constructor in the code above. Try changing static DefinedInstances() to static DefinedInstancesBase(), because currently it is just specified as a private static function.
    2. The second and more important issue is to understand the order that the various constructors are being called in. What is happening is that the static constructor on the base abstract class is getting triggered by the instantiation (during member initializer) of the First field in the derived class. Therefore, First is still null when the static constructor of DefinedInstancesBase class is being called (and thus the FindAll() method).

    See the following code (slightly modified to better illustrate the issue) and output:

    public void Main()
    {
        DefinedInstancesTest dit = new DefinedInstancesTest();
        dit.StaticReadOnlyInstancesAreEnumerated();
    }
    
    public abstract class DefinedInstancesBase<T>
    {
        public static IList<T> All
        {
            get
            {
                //if (_All == null)
                //    FillAll();
                return _All;
            }
        }
    
        // correctly named static ctor
        static DefinedInstancesBase() { FillAll(); }
    
        private static void FillAll()
        {
            Console.WriteLine("FillAll() called...");
            var typeOfT = typeof(T);
            var fields = typeOfT.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
            var fieldsOfTypeT = fields.Where(f => f.FieldType == typeOfT);
            _All = new List<T>();
            foreach (var fieldOfTypeT in fieldsOfTypeT)
            {
                _All.Add((T)fieldOfTypeT.GetValue(null));
            }
        }
    
        private static List<T> _All = null;
    }
    
    //[TestClass]
    public class DefinedInstancesTest
    {
        //[TestMethod]
        public void StaticReadOnlyInstancesAreEnumerated()
        {
            //Given
            var expectedClasses = new List<TestClass>
            {
                TestClass.First,
                TestClass.Second,
                TestClass.Third,
            };
    
            //When
            var actualClasses = TestClass.All;
    
            //Then
            for (var i=0; i<expectedClasses.Count; i++)
            {
                //Assert.AreEqual(expectedClasses[i].Id, actualClasses[i].Id);
                if (expectedClasses[i].Id != actualClasses[i].Id)
                  Console.WriteLine("not equal!");
            }
        }
    
        private class TestClass : DefinedInstancesBase<TestClass>
        {
            public static readonly TestClass First;
            public static readonly TestClass Second;
            public static readonly TestClass Third;
    
            public int Id { get; private set; }
    
          static TestClass()
          {
            Console.WriteLine("TestClass() static ctor called...");
            First = new TestClass(1);
            Second = new TestClass(2);
            Third = new TestClass(3);
          }
    
            private TestClass(int pId)
            {
              Console.WriteLine("TestClass({0}) instance ctor called...", pId);
              Id = pId;
            }
        }
    }
    
    TestClass() static ctor called...
    // the line "First = new TestClass(1);" now triggers the base class static ctor to be called,
    // but the fields First, Second, and Third are all still equal to null at this point!
    FillAll() called...
    TestClass(1) instance ctor called...
    TestClass(2) instance ctor called...
    TestClass(3) instance ctor called...
    // this null reference exception to be expected because the field value actually was null when FindAll() added it to the list
    Unhandled Expecption: 
    System.NullReferenceException: Object reference not set to an instance of an object.