Search code examples
c#visual-studio-2015roslyncompiler-bugcoreclr

Maybe a C# compiler bug in Visual Studio 2015


I think this is a compiler bug.

The following console application compiles und executes flawlessly when compiled with VS 2015:

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var x = MyStruct.Empty;
        }

        public struct MyStruct
        {
            public static readonly MyStruct Empty = new MyStruct();
        }
    }
}

But now it's getting weird: This code compiles, but it throws a TypeLoadException when executed.

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var x = MyStruct.Empty;
        }

        public struct MyStruct
        {
            public static readonly MyStruct? Empty = null;
        }
    }
}

Do you experience the same issue? If so, I will file an issue at Microsoft.

The code looks senseless, but I use it to improve readability and to achieve disambiguation.

I have methods with different overloads like

void DoSomething(MyStruct? arg1, string arg2)

void DoSomething(string arg1, string arg2)

Calling a method this way...

myInstance.DoSomething(null, "Hello world!")

... does not compile.

Calling

myInstance.DoSomething(default(MyStruct?), "Hello world!")

or

myInstance.DoSomething((MyStruct?)null, "Hello world!")

works, but looks ugly. I prefer it this way:

myInstance.DoSomething(MyStruct.Empty, "Hello world!")

If I put the Empty variable into another class, everything works okay:

public static class MyUtility
{
    public static readonly MyStruct? Empty = null;
}

Strange behavior, isn't it?


UPDATE 2016-03-29

I opened a ticket here: http://github.com/dotnet/roslyn/issues/10126


UPDATE 2016-04-06

A new ticket has been opened here: https://github.com/dotnet/coreclr/issues/4049


Solution

  • This is not a bug in 2015 but a possibly a C# language bug. The discussion below relates to why instance members cannot introduce loops, and why a Nullable<T> will cause this error, but should not apply to static members.

    I would submit it as a language bug, not a compiler bug.


    Compiling this code in VS2013 gives the following compile error:

    Struct member 'ConsoleApplication1.Program.MyStruct.Empty' of type 'System.Nullable' causes a cycle in the struct layout

    A quick search turns up this answer which states:

    It's not legal to have a struct that contains itself as a member.

    Unfortunately the System.Nullable<T> type which is used for nullable instances of value types is also a value type and must therefore have a fixed size. It's tempting to think of MyStruct? as a reference type, but it really isn't. The size of MyStruct? is based on the size of MyStruct... which apparently introduces a loop in the compiler.

    Take for instance:

    public struct Struct1
    {
        public int a;
        public int b;
        public int c;
    }
    
    public struct Struct2
    {
        public Struct1? s;
    }
    

    Using System.Runtime.InteropServices.Marshal.SizeOf() you'll find that Struct2 is 16 bytes long, indicating that Struct1? is not a reference but a struct that is 4 bytes (standard padding size) longer than Struct1.


    What's not happening here

    In response to Julius Depulla's answer and comments, here is what is actually happening when you access a static Nullable<T> field. From this code:

    public struct foo
    {
        public static int? Empty = null;
    }
    
    public void Main()
    {
        Console.WriteLine(foo.Empty == null);
    }
    

    Here is the generated IL from LINQPad:

    IL_0000:  ldsflda     UserQuery+foo.Empty
    IL_0005:  call        System.Nullable<System.Int32>.get_HasValue
    IL_000A:  ldc.i4.0    
    IL_000B:  ceq         
    IL_000D:  call        System.Console.WriteLine
    IL_0012:  ret         
    

    The first instruction gets the address of the static field foo.Empty and pushes it on the stack. This address is guaranteed to be non-null as Nullable<Int32> is a structure and not a reference type.

    Next the Nullable<Int32> hidden member function get_HasValue is called to retrieve the HasValue property value. This cannot result in a null reference since, as mentioned previously, the address of a value type field must be non-null, regardless of the value contained at the address.

    The rest is just comparing the result to 0 and sending the result to the console.

    At no point in this process is it possible to 'invoke a null on a type' whatever that means. Value types do not have null addresses, so method invocation on value types cannot directly result in a null object reference error. That's why we don't call them reference types.