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?
I opened a ticket here: http://github.com/dotnet/roslyn/issues/10126
A new ticket has been opened here: https://github.com/dotnet/coreclr/issues/4049
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
.
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.