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

Pet*_*rot 28 c# compiler-bug roslyn visual-studio-2015 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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但是现在它变得很奇怪了:这个代码编译,但它会TypeLoadException在执行时抛出.

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var x = MyStruct.Empty;
        }

        public struct MyStruct
        {
            public static readonly MyStruct? Empty = null;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

你遇到同样的问题吗?如果是这样,我将在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;
}
Run Code Online (Sandbox Code Playgroud)

奇怪的行为,不是吗?


更新2016-03-29

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


更新2016-04-06

这里打开了一张新票:https://github.com/dotnet/coreclr/issues/4049

Eri*_*ert 18

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

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

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

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

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

  • 只有两个结构时,行为才会重现.

我们可以继续讨论这个问题是否仅在M在某种意义上"直接"提到自己,或者"间接"循环是否也能重现这个bug时再现.(后者是真的.)正如科里在他的回答中指出的那样,我们也可以问"类型必须是通用的吗?" 没有; 有一个复制器甚至比这个更简洁,没有泛型.

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

显然有些东西搞砸了,我今天没时间去理清责任应该落在哪里.以下是一些想法:

  • 反对包含自己成员的结构的规则明显不适用于此.(参见C#5规范的第11.3.1节,这是我手边的那个.我注意到这一部分可以从对泛型的仔细重写中受益;这里的一些语言有点不精确.)如果E是静态然后该部分不适用; 如果它不是静态的,则无论如何都可以计算N<M>和的布局M.

  • 我知道C#语言中没有其他规则可以禁止这种类型的安排.

  • 可能是CLR的规范禁止的类型的这种安排的情况下,和CLR是正确的,在这里抛出异常.

那么现在让我们总结一下可能性:

  • 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的某些实现会抛出类型加载异常.这是这些实现中的错误.
  • CLR团队意识到了这个错误,显然很难修复错误的实现.
  • C#团队正在考虑让合法代码产生警告,因为它会在运行时在某些(但不是全部)CLI版本上失败.

C#和CLR团队就是这样; 跟进他们.如果您对此问题有任何疑虑,请发布跟踪问题,而不是此处.

  • @mikez:哦,天哪,如果贾里德已经在上面了,那么我在这里搞砸了什么?让他解决一下,这就是他们为他付出的巨大代价!:-)(这就是我在博客阅读中落后的原因.) (2认同)

Cor*_*rey 10

这不是2015年的错误,但可能是C#语言错误.下面的讨论涉及为什么实例成员不能引入循环,以及为什么a Nullable<T>会导致此错误,但不应该应用于静态成员.

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


在VS2013中编译此代码会产生以下编译错误:

类型为"System.Nullable"的struct成员'ConsoleApplication1.Program.MyStruct.Empty'导致struct布局中的循环

快速搜索出现了这个答案,其中指出:

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

不幸的System.Nullable<T>是,用于值类型的可空实例的类型也是值类型,因此必须具有固定大小.很容易将其MyStruct?视为参考类型,但事实并非如此.大小MyStruct?基于...的大小,MyStruct这显然在编译器中引入了一个循环.

举个例子:

public struct Struct1
{
    public int a;
    public int b;
    public int c;
}

public struct Struct2
{
    public Struct1? s;
}
Run Code Online (Sandbox Code Playgroud)

使用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);
}
Run Code Online (Sandbox Code Playgroud)

这是从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         
Run Code Online (Sandbox Code Playgroud)

第一条指令获取静态字段的地址foo.Empty并将其推送到堆栈上.保证该地址为非空,因为Nullable<Int32>它是结构而不是引用类型.

接下来,调用Nullable<Int32>隐藏的成员函数get_HasValue来检索HasValue属性值.这不能导致空引用,因为如前所述,值类型字段的地址必须为非null,而不管地址中包含的值.

其余的只是将结果与0进行比较并将结果发送到控制台.

在这个过程中,无论如何都可以"在类型上调用null".值类型没有空地址,因此对值类型的方法调用不能直接导致空对象引用错误.这就是为什么我们不称它们为引用类型.