我认为这是编译错误。
使用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;
}
奇怪的行为,不是吗?
我在这里开了一张票:http://github.com/dotnet/roslyn/issues/10126
答案 0 :(得分:17)
首先,分析这些问题以制作最小的复制器非常重要,这样我们就可以缩小问题所在。在原始代码中有三个红色鲱鱼:readonly
,static
和Nullable<T>
。没有必要重复这个问题。这是一个最小的复制品:
struct N<T> {}
struct M { public N<M> E; }
class P { static void Main() { var x = default(M); } }
这会在当前版本的VS中编译,但在运行时会抛出类型加载异常。
E
不会触发异常。任何尝试访问类型M
都会触发它。 (正如人们在类型加载异常的情况下所期望的那样。)现在让我们再做一些实验。如果我们制作N
和M
课程怎么办?我会告诉你结果:
我们可以继续讨论这个问题是否仅在某种意义上的M&#34;直接&#34;提到自己,或者是否是间接的&#34;循环也重现了这个bug。 (后者是真的。)正如Corey在他的回答中指出的那样,我们也可以问&#34;这些类型必须是通用的吗?&#34;没有;有一个复制器甚至比没有泛型的复制器更小。
然而,我认为我们已经足够完成对复制者的讨论,并继续讨论手头的问题,这是一个错误,如果是这样,在什么?&#34;
显然有些东西搞砸了,我今天没时间去理清责任应该落在哪里。以下是一些想法:
反对包含自己成员的结构的规则明显不适用于此。 (参见C#5规范的第11.3.1节,这是我手边的那个。我注意到这一部分可以受益于对泛型的仔细重写;这里的一些语言有点不精确。)如果E
是静态的,然后该部分不适用;如果它不是静态的,那么无论如何都可以计算N<M>
和M
的布局。
我知道C#语言中没有其他规则会禁止这种类型的安排。
可能是CLR规范禁止这种类型排列的情况,CLR在这里抛出异常是正确的。
现在让我们总结一下可能性:
CLR有一个错误。这种类型的拓扑结构应该是合法的,而CLR扔在这里是错误的。
CLR行为是正确的。这种类型的拓扑是非法的,并且在这里抛出CLR是正确的。 (在这种情况下,CLR可能存在规范错误,因为在规范中可能没有充分解释这个事实。我今天没有时间做CLR规范潜水。)
让我们假设为了论证,第二个是真的。我们现在可以对C#说些什么?一些可能性:
C#语言规范禁止此程序,但实现允许它。实现有一个bug。 (我认为这种情况是错误的。)
C#语言规范并未禁止此程序,但可以以合理的实施成本进行此操作。在这种情况下,C#规范有问题,应该修复,并且应该修复实现以匹配。
C#语言规范并未禁止该程序,但在编译时检测问题不能以合理的成本完成。几乎任何运行时崩溃就是这种情况;你的程序在运行时崩溃了,因为编译器无法阻止你编写一个错误的程序。这只是一个错误的程序;不幸的是,你没有理由知道它是马车。
总结一下,我们的可能性是:
这四个中的一个必须是真的。我不知道它是哪一个。如果我被要求猜测,我会选择第一个;我认为没有理由为什么CLR类型的装载机应该对这一点充满信心。但也许有一个我不知道的充分理由;希望CLR类型加载语义的专家能够参与其中。
更新:
此问题在此处进行了跟踪:
https://github.com/dotnet/roslyn/issues/10126
总结C#团队在该问题上的结论:
C#和CLR团队就是这样;跟进他们。如果您对此问题有任何疑虑,请发布跟踪问题,而不是此处。
答案 1 :(得分:10)
这不是2015年的错误,但可能是C#语言错误。下面的讨论涉及为什么实例成员不能引入循环,以及为什么Nullable<T>
会导致此错误,但不应该应用于静态成员。
我会将其作为语言错误提交,而不是编译错误。
在VS2013中编译此代码会产生以下编译错误:
结构成员&#39; ConsoleApplication1.Program.MyStruct.Empty&#39;类型&#39; System.Nullable&#39;导致结构布局中的循环
快速搜索会显示this answer,其中指出:
拥有一个包含自己作为成员的结构是不合法的。
不幸的是,用于值类型的可空实例的System.Nullable<T>
类型也是值类型,因此必须具有固定大小。将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?
不是引用,而是4字节(标准填充大小)的结构比Struct1
。
回应Julius Depulla的回答和评论,以下是访问static Nullable<T>
字段时实际发生的情况。从这段代码:
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<System.Int32>.get_HasValue
IL_000A: ldc.i4.0
IL_000B: ceq
IL_000D: call System.Console.WriteLine
IL_0012: ret
第一条指令获取静态字段foo.Empty
的地址并将其推送到堆栈中。此地址保证为非空,因为Nullable<Int32>
是结构而不是引用类型。
接下来,调用Nullable<Int32>
隐藏成员函数get_HasValue
来检索HasValue
属性值。这不会导致空引用,因为如前所述,值类型字段的地址必须为非null,而不管地址中包含的值。
其余的只是将结果与0进行比较并将结果发送到控制台。
在这个过程中,任何时候都不可能在类型上调用null。不管它是什么意思。值类型没有空地址,因此对值类型的方法调用不能直接导致空对象引用错误。这就是为什么我们不称它们为参考类型。
答案 2 :(得分:1)
现在我们已经讨论过什么以及为什么,这里有一个解决问题的方法,而不必等待各种.NET团队来追踪问题并确定如果有的话我会做的。
问题似乎仅限于作为值类型的字段类型,它们以某种方式引用回此类型,作为通用参数或静态成员。例如:
public struct A { public static B b; }
public struct B { public static A a; }
唉,我现在觉得很脏。坏OOP,但它表明存在问题而不以任何方式调用泛型。
因为它们是值类型,所以类型加载器确定存在涉及的圆度,应该忽略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
方法,所以我不会将它用于性能关键代码,但是解决方法至少可以帮助您完成工作,直到找到合适的解决方案。
如果事实证明这并没有得到解决,至少我们可以用它来绕过它。