可能是Visual Studio 2015中的C#编译器错误
我认为这是一个编译错误。
使用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(); } } }
但现在它变得很奇怪:这段代码编译,但是在执行时会抛出一个TypeLoadException
。
namespace ConsoleApplication1 { class Program { static void Main(string[] args) { var x = MyStruct.Empty; } public struct MyStruct { public static readonly MyStruct? Empty = null; } } }
你遇到同样的问题吗? 如果是这样,我将在Microsoft提出问题。
代码看起来毫无意义,但我用它来提高可读性并实现消歧。
我有不同的重载方法,如
void DoSomething(MyStruct? arg1, string arg2)
void DoSomething(string arg1, string arg2)
以这种方式调用方法……
myInstance.DoSomething(null, "Hello world!")
……不编译。
调用
myInstance.DoSomething(default(MyStruct?), "Hello world!")
要么
myInstance.DoSomething((MyStruct?)null, "Hello world!")
工作,但看起来很难看。 我更喜欢这样:
myInstance.DoSomething(MyStruct.Empty, "Hello world!")
如果我将Empty
变量放入另一个类中,一切正常:
public static class MyUtility { public static readonly MyStruct? Empty = null; }
奇怪的行为,不是吗?
更新2016-03-29
我在这里开了一张票: http : //github.com/dotnet/roslyn/issues/10126
更新2016-04-06
这里打开了一张新票: https : //github.com/dotnet/coreclr/issues/4049
这不是2015年的错误,但可能是C#语言错误。 下面的讨论涉及为什么实例成员不能引入循环,以及为什么Nullable
会导致此错误,但不应该应用于静态成员。
我会将其作为语言错误提交,而不是编译器错误。
在VS2013中编译此代码会产生以下编译错误:
类型为“System.Nullable”的struct成员“ConsoleApplication1.Program.MyStruct.Empty”导致struct布局中的循环
快速搜索出现了这个答案 :
拥有一个包含自己作为成员的结构是不合法的。
不幸的是,用于值类型的可空实例的System.Nullable
类型也是值类型,因此必须具有固定大小。 想到MyStruct?
很诱人MyStruct?
作为参考类型,但它确实不是。 MyStruct?
的大小MyStruct?
基于MyStruct
的大小……这显然在编译器中引入了一个循环。
举个例子:
public struct Struct1 { public int a; public int b; public int c; } public struct Struct2 { public Struct1? s; }
使用System.Runtime.InteropServices.Marshal.SizeOf()
你会发现Struct2
是16个字节,表明Struct1?
不是引用,而是一个比Struct1
长4个字节(标准填充大小)的Struct1
。
这里没有发生什么
回应Julius Depulla的回答和评论,这是访问static Nullable
字段时实际发生的情况。 从这段代码:
public struct foo { public static int? Empty = null; } public void Main() { Console.WriteLine(foo.Empty == null); }
这是从LINQPad生成的IL:
IL_0000: ldsflda UserQuery+foo.Empty IL_0005: call System.Nullable.get_HasValue IL_000A: ldc.i4.0 IL_000B: ceq IL_000D: call System.Console.WriteLine IL_0012: ret
第一条指令获取静态字段foo.Empty
的地址并将其推送到堆栈上。 由于Nullable
是结构而不是引用类型,因此该地址保证为非null 。
接下来,调用Nullable
隐藏成员函数get_HasValue
来检索HasValue
属性值。 这不会导致空引用,因为如前所述,值类型字段的地址必须为非null,而不管地址中包含的值。
其余的只是将结果与0进行比较并将结果发送到控制台。
在这个过程中,无论如何都可以“在类型上调用null”。 值类型没有空地址,因此对值类型的方法调用不能直接导致空对象引用错误。 这就是为什么我们不称它们为引用类型。
首先,分析这些问题以制作最小的复制器非常重要,这样我们就可以缩小问题所在。 在原始代码中有三个红色鲱鱼: readonly
, static
和Nullable
。 没有必要重复这个问题。 这是一个最小的repro:
struct N {} struct M { public N E; } class P { static void Main() { var x = default(M); } }
这将在当前版本的VS中编译,但在运行时会抛出类型加载exception。
- 使用
E
不会触发exception。 它是由任何访问类型M
尝试触发的。 (正如人们在类型加载exception的情况下所期望的那样。) - 该exception重现该字段是静态还是实例,只读或不; 这与该领域的性质无关。 (但它必须是一个字段!如果它是一个方法,问题不会重现。)
- 该例外与“调用”无关; 在最小的repro中没有被“调用”。
- 该例外与成员访问运算符“。”没有任何关系。 它不会出现在最小的复制品中。
- 例外与nullables毫无关系; 在最小的复制品中没有任何东西可以为空。
现在让我们做一些实验。 如果我们制作N
和M
课程怎么办? 我会告诉你结果:
- 只有结构时,行为才会重现。
我们可以继续讨论这个问题是否仅在M在某种意义上“直接”提到自己,或者“间接”循环是否也能重现这个错误时才会重现。 (后者是真的。)正如科里在他的回答中指出的那样,我们也可以问“类型必须是通用的吗?” 没有; 有一个复制器甚至比这个更简洁,没有generics。
但是我认为我们已经足够完成对复制器的讨论,并继续讨论手头的问题,这是“它是一个错误,如果是这样,在什么?”
很明显,这里有些东西搞砸了,我今天没时间去理清责任应该落在哪里。 以下是一些想法:
-
反对包含自己成员的结构的规则明显不适用于此。 (参见C#5规范的第11.3.1节,这是我手边的那个。我注意到这一部分可以受益于对generics的仔细重写;这里的一些语言有点不精确。)如果
E
是静态的,那么该部分不适用; 如果它不是静态的,则无论如何都可以计算N
和M
的布局。 -
我知道C#语言中没有其他规则可以禁止这种类型的安排。
-
可能是CLR规范禁止这种类型的安排,CLR在这里抛出exception是正确的。
那么现在让我们总结一下可能性:
-
CLR有一个bug。 这种类型的拓扑结构应该是合法的,而CLR在这里抛出是错误的。
-
CLR行为是正确的。 这种类型的拓扑是非法的,并且在这里抛出CLR是正确的。 (在这种情况下,CLR可能存在规范错误,因为在规范中可能没有充分解释这个事实。我今天没有时间做CLR规范潜水。)
让我们假设为了论证第二个是真的。 我们现在可以对C#说些什么? 一些可能性:
-
C#语言规范禁止此程序,但实现允许它。 实现有一个bug。 (我认为这种情况是错误的。)
-
C#语言规范并未禁止此程序,但可以以合理的实施成本进行此操作。 在这种情况下,C#规范有问题,应该修复,并且应该修复实现以匹配。
-
C#语言规范不禁止该程序,但在编译时检测问题不能以合理的成本完成。 几乎任何运行时崩溃就是这种情况; 你的程序在运行时崩溃了,因为编译器无法阻止你编写一个错误的程序。 这只是一个错误的程序; 不幸的是,你没有理由知道它是马车。
总结一下,我们的可能性是:
- CLR有一个bug
- C#规范有一个bug
- C#实现有一个bug
- 该程序有一个错误
这四个中的一个必须是真的。 我不知道它是哪一个。 如果我被要求猜测,我会选择第一个; 我认为没有理由为什么CLR类型的装载机应该对这个装载机不屑一顾。 但也许有一个我不知道的充分理由; 希望CLR类型加载语义的专家能够参与其中。
更新:
此问题在此处进行了跟踪:
https://github.com/dotnet/roslyn/issues/10126
总结该问题中C#团队的结论:
- 根据CLI和C#规范,该程序是合法的。
- C#6编译器允许该程序,但CLI的某些实现会抛出类型加载exception。 这是这些实现中的错误。
- CLR团队意识到了这个错误,显然很难修复错误的实现。
- C#团队正在考虑让合法代码产生警告,因为它会在运行时在某些(但不是全部)CLI版本上失败。
C#和CLR团队就是这样; 跟进他们。 如果您对此问题有任何疑虑,请发布跟踪问题,而不是此处。
现在我们已经讨论了什么以及为什么,这里有一种解决问题的方法,而不必等待各种.NET团队追踪问题并确定如何处理它。
该问题似乎仅限于作为值类型的字段类型,它们以某种方式引用回此类型,作为通用参数或静态成员。 例如:
public struct A { public static B b; } public struct B { public static A a; }
呃,我现在觉得很脏。 坏OOP,但它表明问题存在而不以任何方式调用generics。
因此,因为它们是值类型,所以类型加载器确定存在由于static
关键字而应该忽略的循环。 C#编译器很聪明,可以搞清楚。 它是否应该具有的是规格,我没有评论。
但是,通过将A
或B
更改为class
,问题就会消失:
public struct A { public static B b; } public class B { public static A a; }
因此,可以通过使用引用类型来存储实际值并将字段转换为属性来避免此问题:
public struct MyStruct { private static class _internal { public static MyStruct? empty = null; } public static MyStruct? Empty => _internal.empty; }
这是一堆较慢,因为它是一个属性而不是字段,调用它将调用get
方法,所以我不会将它用于性能关键代码,但作为一种解决方法,它至少让你做到这一点,直到有适当的解决方案。
如果事实certificate这没有得到解决,至少我们有一个可以用来绕过它的kludge。