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

时间:2016-03-25 14:43:50

标签: c# visual-studio-2015 roslyn compiler-bug coreclr

我认为这是编译错误。

使用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

3 个答案:

答案 0 :(得分:17)

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

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

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

  • 使用E不会触发异常。任何尝试访问类型M都会触发它。 (正如人们在类型加载异常的情况下所期望的那样。)
  • 异常表示字段是静态还是实例,只读或不;这与该领域的性质无关。 (但它必须是一个字段!如果它是一个方法,问题不会重现。)
  • 该例外与&#34; invocation&#34 ;;没有任何关系。什么都没有被调用&#34;在最小的复制品中。
  • 该例外与会员访问运营商没有任何关系&#34;。&#34;。它没有出现在最小的复制品中。
  • 这个例外与nullables毫无关系;在最小的复制品中没有任何东西可以为空。

现在让我们再做一些实验。如果我们制作NM课程怎么办?我会告诉你结果:

  • 只有两种结构都会重现行为。

我们可以继续讨论这个问题是否仅在某种意义上的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有错误
  • C#规范有错误
  • C#实现有一个错误
  • 程序有错误

这四个中的一个必须是真的。我不知道它是哪一个。如果我被要求猜测,我会选择第一个;我认为没有理由为什么CLR类型的装载机应该对这一点充满信心。但也许有一个我不知道的充分理由;希望CLR类型加载语义的专家能够参与其中。

更新:

此问题在此处进行了跟踪:

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

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

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

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#编译器很聪明,可以搞清楚。它是否应该具体取决于规格,我没有评论。

但是,通过将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方法,所以我不会将它用于性能关键代码,但是解决方法至少可以帮助您完成工作,直到找到合适的解决方案。

如果事实证明这并没有得到解决,至少我们可以用它来绕过它。