Search code examples
c#genericsstructtypeloadexception

Why can't generic structs have static members which specify the generic type in C#?


Apologies if this is a duplicate! I searched around but wasn't able to find an explanation. The following toy example gives me a TypeLoadException as soon as I try to instantiate this struct. It works just fine if I use a class or if instead do not specify the generic type in the static member (leave it as T.)

public struct Point<T>
{
    static Point<int> IntOrigin = new Point<int>(0, 0);

    T X { get; }
    T Y { get; }

    public Point(T x, T y)
    {
        this.X = x;
        this.Y = y;
    }
}

My rather more complicated, real situation boils down to something like this, so I'd really like to understand why it emits a TypeLoadException.


Solution

  • This comment, and another comment, on Github come closest to addressing the current state of affairs, pointing out why the sort of self-referential struct definition hasn't been allowed, and likely won't be for the foreseeable future.

    Even the static member needs the type to be initialized before it can be included in the type layout, but the type initialization needs that static member to be initialized. This cycle of initialization dependencies creates the Catch-22 that results in the runtime exception.

    According to this comment, .NET Core works fine with this pattern. But I found the same failure when I tried your example in a .NET Core project. So either that comment is in error, or there's some subtle difference between the instance-member scenario and your static-member scenario (I didn't bother to investigate any further than that).

    It is interesting that the compiler used on dotNETFiddle.net emits a compile-time error, "Struct member 'struct2 field' of type 'struct1' causes a cycle in the struct layout". I don't know why the Visual Studio compiler no longer seems to produce this error (checked in 2017 and 2019). It seems like another bug to me. But the discussion on Github around this issue seems to accept that the code is technically valid (i.e. according to the C# specification), so probably there was at some point a conscious decision to remove the compiler error, and let the CLR do the complaining at runtime.

    Note that the advice in the error's reference page suggests changing to a class instead of a struct. Of course, this is often not feasible where a struct is being used; it may be important to have a value type. However, in your specific example, there's actually a simple work-around based on that idea. Since your field isn't part of the actual layout of an instance of the struct, you can move it to a static class used specifically for such values. E.g.:

    public struct Point<T>
    {
        public static class Constants
        {
            static Point<int> IntOrigin = new Point<int>(0, 0);
        }
    
        T X { get; }
        T Y { get; }
    
        public Point(T x, T y)
        {
            this.X = x;
            this.Y = y;
        }
    }
    
    

    Then instead of (for example) Point<double>.IntOrigin, you'll need to use Point<double>.Constants.IntOrigin. Since the type initialization for each type can be done independently, the cycle in the initialization does not occur and the code runs fine.