可能是 Visual Studio 2015 中的 C# 编译器错误

Posted

技术标签:

【中文标题】可能是 Visual Studio 2015 中的 C# 编译器错误【英文标题】:Maybe a C# compiler bug in Visual Studio 2015 【发布时间】:2016-03-25 14:43:50 【问题描述】:

我认为这是一个编译器错误。

以下控制台应用程序在使用 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 年 3 月 29 日更新

我在这里开票:http://github.com/dotnet/roslyn/issues/10126


2016 年 4 月 6 日更新

此处已开新票:https://github.com/dotnet/coreclr/issues/4049

【问题讨论】:

可以是reproduced with .NET fiddle,乍一看像个bug。注意:请删除您的 using 指令,示例不需要它们,这会不必要地使您的代码示例混乱。 就我个人而言,我称之为MyStruct.Null。在结构的上下文中考虑“空”时,人们会期待一些不同的东西。 我认为它可能是一个允许编译的错误。 VS2013 编译时出现同样的错误,异常 CS0523: Struct member 'ConsoleApplication1.Program.MyStruct.Empty' of type 'System.Nullable<ConsoleApplication1.Program.MyStruct>' causes a cycle in the struct layout,我有一半的预期 不错的发现。那肯定看起来像一个错误。 首先,是否是错误取决于 C# 团队,我不再是该团队的成员。其次,用户“coderealm”对bug的分析是无稽之谈;我怀疑那里的 coderealm 和这里的 Julius 是同一个人。点运算符不是调用运算符,它是成员访问运算符。 调用仅在访问的成员是属性、被调用的方法或被调用的委托类型的字段时才会发生。 (或其他一些类似的情况。)在这种情况下,以上都不是真的;这里没有调用。 【参考方案1】:

首先,重要的是在分析这些问题时制作一个最小的复制器,这样我们就可以缩小问题所在。在原始代码中有三个红鲱鱼:readonlystaticNullable<T>。没有必要重现这个问题。这是一个最小的复制:

struct N<T> 
struct M  public N<M> E; 
class P  static void Main()  var x = default(M);  

这在当前版本的VS中编译,但运行时抛出类型加载异常。

使用E 不会触发异常。它由任何访问类型M 的尝试触发。 (正如人们在类型加载异常的情况下所期望的那样。) 异常重现字段是静态还是实例,只读与否;这与该领域的性质无关。 (但是它必须是一个字段!如果它是一个方法,那么这个问题就不会重现。) 异常与“调用”无关;最小复制中没有“调用”任何内容。 该异常与成员访问运算符“.”无关。它不会出现在最小复制中。 异常与可空值无关;在最小复制中没有任何内容可以为空。

现在让我们做更多的实验。如果我们创建NM 类怎么办?我会告诉你结果:

只有当两者都是结构时,该行为才会重现。

我们可以继续讨论问题是否仅在 M 在某种意义上“直接”提及自身时才会重现,或者“间接”循环是否也会重现错误。 (后者是正确的。)正如 Corey 在他的回答中指出的那样,我们也可以问“类型必须是通用的吗?”不;有一个比没有泛型的复制器更小的复制器。

但是我认为我们已经足够完成对复制器的讨论并继续讨论手头的问题,即“这是一个错误,如果是,在什么地方?”

显然这里出了点问题,我今天没有时间理清应该归咎于哪里。以下是一些想法:

禁止包含自身成员的结构的规则显然不适用于此处。 (请参阅 C# 5 规范的第 11.3.1 节,这是我手头的那个。我注意到,在考虑泛型的情况下仔细重写本节可能会受益;这里的一些语言有点不精确。)如果E 是静态的,则该部分不适用;如果它不是静态的,那么N&lt;M&gt;M 的布局都可以计算出来。

我知道 C# 语言中没有其他规则会禁止这种类型排列。

可能CLR 规范禁止这种类型的排列,CLR 在这里抛出异常是正确的。

所以现在让我们总结一下可能性:

CLR 存在错误。这种类型的拓扑应该是合法的,CLR扔在这里是不对的。

CLR 行为正确。这种类型的拓扑是非法的,CLR扔在这里是正确的。 (在这种情况下,可能是 CLR 存在规范错误,因为规范中可能没有充分解释这一事实。我今天没有时间进行 CLR 规范潜水。)

李>

让我们假设第二个是真的。我们现在可以对 C# 说些什么?一些可能性:

C# 语言规范禁止该程序,但实现允许它。该实现有一个错误。 (我认为这种情况是错误的。)

C# 语言规范并未禁止此程序,但可以以合理的实施成本来禁止此程序。在这种情况下,C# 规范有问题,应该修复它,并且应该修复实现以匹配。

C# 语言规范并未禁止该程序,但无法以合理的成本在编译时检测问题。几乎所有运行时崩溃都是这种情况。您的程序在运行时崩溃了,因为编译器无法阻止您编写有缺陷的程序。这只是一个错误的程序;不幸的是,你没有理由知道它有问题。

总结一下,我们的可能性是:

CLR 有一个错误 C# 规范有一个错误 C# 实现存在错误 程序有错误

这四个中的一个必须为真。我不知道它是哪个。如果让我猜,我会选第一个;我看不出为什么 CLR 类型加载器应该拒绝这个。但也许有一个我不知道的充分理由;希望 CLR 类型加载语义方面的专家能够参与进来。


更新:

在此处跟踪此问题:

https://github.com/dotnet/roslyn/issues/10126

总结 C# 团队在该问题中的结论:

根据 CLI 和 C# 规范,该程序是合法的。 C# 6 编译器允许该程序,但 CLI 的一些 实现会引发类型加载异常。这是这些实现中的一个错误。 CLR 团队意识到了这个错误,显然很难修复有错误的实现。 C# 团队考虑让合法代码产生警告,因为它会在运行时在某些(但不是所有)CLI 版本上失败。

C# 和 CLR 团队正在处理这个问题;跟进他们。如果您对此问题有任何疑问,请发布到跟踪问题,而不是此处。

【讨论】:

有趣。几周前@JaredPar 提供了一个内容丰富的article,他指出这种构造在 C# 中是合法的,但在 CLR 中是非法的,但让我们不加评论地讨论这是否正确/可取。可以这么说,我从没想过结构中的私有字段是他们合同的一部分。 @mikez: 哦,看在上帝的份上,如果 Jared 已经在上面了,那我在这儿搞什么鬼?让他解决,这就是他们付给他大笔钱的原因! :-) (这就是我在阅读博客时落后的原因。) 我相信这里的最小复制存在错误。您的最小复制与 OP 使用 Nullable&lt;T&gt; 之间的一个区别是 Nullable&lt;T&gt; 还包含一个类型为 T 的字段。在您的情况下,您没有这样做,这几乎使您认为这是合法的大多数论点无效(关于原始帖子)。在该字段存在的情况下,您的最小复制结构M 将包含一个结构N&lt;M&gt;,其中还包含M,这反过来意味着间接结构M 包含自身,因此具有无限大小。 具有讽刺意味的是,您可以使用 Microsoft 编译器编译此代码,但无法使用 .NET 运行时运行它——而您无法使用Mono 编译器,您可以使用 Mono 运行时运行它。 (在 Windows 10、.NET 3.5 和 Unity 3D 5.4.0f3 附带的 Mono 2.0 版本上测试。) C++ 也有一些限制。可怕的“错误:字段类型不完整”【参考方案2】:

这不是 2015 年的错误,但可能是 C# 语言错误。下面的讨论涉及为什么 instance members 不能引入循环,以及为什么 Nullable&lt;T&gt; 会导致此错误,但不应应用于静态成员。

我会将其作为语言错误而不是编译器错误提交。


在 VS2013 中编译这段代码会出现以下编译错误:

“System.Nullable”类型的结构成员“ConsoleApplication1.Program.MyStruct.Empty”导致结构布局中出现循环

快速搜索出现this answer,其中指出:

拥有一个包含自身作为成员的结构是不合法的。

不幸的是,用于值类型的可为空实例的System.Nullable&lt;T&gt; 类型也是值类型,因此必须具有固定大小。很容易将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 个字节(标准填充大小)的结构。


这里没有发生了什么

响应 Julius Depulla 的回答和 cmets,当您访问 static Nullable&lt;T&gt; 字段时,实际上会发生什么。从此代码:

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&lt;Int32&gt; 是一个结构而不是引用类型。

接下来调用Nullable&lt;Int32&gt;隐藏成员函数get_HasValue来检索HasValue属性值。这不会导致空引用,因为如前所述,值类型字段的地址必须是非空的,无论地址中包含的值如何。

剩下的只是将结果与 0 进行比较并将结果发送到控制台。

无论这意味着什么,在此过程中都无法“在类型上调用 null”。值类型没有空地址,因此对值类型的方法调用不能直接导致空对象引用错误。这就是我们不称它们为引用类型的原因。

【讨论】:

是的,你是对的。这个问题在这里讨论:***.com/questions/9296251/…。但由于它是静态的,所以这不是内存布局问题。 @PeterPerot 是的,这就是为什么在我的回答结束时我建议您将其作为语言错误提交。这篇文章的其余部分本质上是讨论为什么这对成员来说是个问题……如果不清楚,请抱歉。 +1。我将提交错误重新发布。顺便说一句,这里有另一个讨论:social.msdn.microsoft.com/Forums/vstudio/en-US/… @PeterPerot 更新了答案以更好地反映这一点。 我认为这个答案不正确。我已经解释了为什么它不是下面的错误。【参考方案3】:

既然我们已经就什么和原因进行了长时间的讨论,这里有一种解决问题的方法,而无需等待各个 .NET 团队来追踪问题并确定是否会采取任何措施.

问题似乎仅限于作为值类型的字段类型,这些值类型以某种方式引用回此类型,无论是作为泛型参数还是静态成员。例如:

public struct A  public static B b; 
public struct B  public static A a; 

呃,我现在觉得很脏。糟糕的 OOP,但它表明问题存在而无需以任何方式调用泛型。

因此,因为它们是值类型,所以类型加载器确定存在循环性,由于 static 关键字,应该忽略该循环性。 C# 编译器足够聪明,可以弄清楚。它是否应该取决于规格,我对此没有评论。

但是,通过将 AB 更改为 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 方法,所以我不会将它用于性能关键代码,但作为一种解决方法,它至少可以让你完成这项工作,直到找到合适的解决方案。

如果事实证明这个问题没有得到解决,至少我们有一个可以用来绕过它的组合。

【讨论】:

很酷的解决方法,@Cory。但是,由于Nullable&lt;MyStruct&gt; 本身是一个结构体并且没有引用类型,因此每次调用属性 getter 都会创建一个副本。所以你可以简单地写public struct MyStruct public static MyStruct? Empty =&gt; null; ;无需在内部变量中备份可空类型的null @PeterPerot 当然可以。我总是忘记值类型的复制效果。我猜我年轻时是一名 C 程序员

以上是关于可能是 Visual Studio 2015 中的 C# 编译器错误的主要内容,如果未能解决你的问题,请参考以下文章

std库中的Visual Studio编译错误

visual studio 2015怎样升级

如何捕获在Visual Studio 2015中单击“保存工作项”按钮时触发的事件?

Visual Studio 2015/2017 中的 TSLint?

Visual Studio 2015 中的递归包含路径

Visual Studio 中的源文件包含