简体   繁体   English

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

[英]Maybe a C# compiler bug in Visual Studio 2015

I think this is a compiler bug.我认为这是一个编译器错误。

The following console application compiles und executes flawlessly when compiled with VS 2015:以下控制台应用程序在使用 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();
        }
    }
}

But now it's getting weird: This code compiles, but it throws a TypeLoadException when executed.但现在它变得奇怪了:这段代码可以编译,但在执行时会抛出TypeLoadException

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

        public struct MyStruct
        {
            public static readonly MyStruct? Empty = null;
        }
    }
}

Do you experience the same issue?您是否遇到同样的问题? If so, I will file an issue at Microsoft.如果是这样,我将向 Microsoft 提交问题。

The code looks senseless, but I use it to improve readability and to achieve disambiguation.代码看起来毫无意义,但我用它来提高可读性并消除歧义。

I have methods with different overloads like我有不同重载的方法,比如

void DoSomething(MyStruct? arg1, string arg2)

void DoSomething(string arg1, string arg2)

Calling a method this way...以这种方式调用方法...

myInstance.DoSomething(null, "Hello world!")

... does not compile. ...不编译。

Calling打电话

myInstance.DoSomething(default(MyStruct?), "Hello world!")

or或者

myInstance.DoSomething((MyStruct?)null, "Hello world!")

works, but looks ugly.有效,但看起来很丑。 I prefer it this way:我更喜欢这样:

myInstance.DoSomething(MyStruct.Empty, "Hello world!")

If I put the Empty variable into another class, everything works okay:如果我将Empty变量放入另一个类,则一切正常:

public static class MyUtility
{
    public static readonly MyStruct? Empty = null;
}

Strange behavior, isn't it?奇怪的行为,不是吗?


UPDATE 2016-03-29更新 2016-03-29

I opened a ticket here: http://github.com/dotnet/roslyn/issues/10126我在这里开了一张票: http : //github.com/dotnet/roslyn/issues/10126


UPDATE 2016-04-06更新 2016-04-06

A new ticket has been opened here: https://github.com/dotnet/coreclr/issues/4049这里已经开了一张新票: https : //github.com/dotnet/coreclr/issues/4049

First off, it is important when analyzing these issues to make a minimal reproducer, so that we can narrow down where the problem is.首先,在分析这些问题时制作一个最小的复制器很重要,这样我们就可以缩小问题所在。 In the original code there are three red herrings: the readonly , the static and the Nullable<T> .在原始代码中有三个红鲱鱼: readonlystaticNullable<T> None are necessary to repro the issue.没有必要重现问题。 Here's a minimal repro:这是一个最小的重现:

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

This compiles in the current version of VS, but throws a type load exception when run.这在当前版本的 VS 中编译,但在运行时抛出类型加载异常。

  • The exception is not triggered by use of E .使用E不会触发异常。 It is triggered by any attempt to access the type M .它由任何访问类型M尝试触发。 (As one would expect in the case of a type load exception.) (正如人们在类型加载异常的情况下所期望的那样。)
  • The exception reproduces whether the field is static or instance, readonly or not;异常再现字段是静态的还是实例的,只读与否; this has nothing to do with the nature of the field.这与该领域的性质无关。 (However it must be a field! The issue does not repro if it is, say, a method.) (但是它必须是一个字段!如果它是一个方法,这个问题就不会重现。)
  • The exception has nothing whatsoever to do with "invocation";异常与“调用”没有任何关系; nothing is being "invoked" in the minimal repro.在最小再现中没有“调用”任何内容。
  • The exception has nothing whatsoever to do with the member access operator ".".该异常与成员访问运算符“.”没有任何关系。 It does not appear in the minimal repro.它没有出现在最小再现中。
  • The exception has nothing whatsoever to do with nullables;该异常与可空值没有任何关系; nothing is nullable in the minimal repro.在最小重现中没有任何东西可以为空。

Now let's do some more experiments.现在让我们再做一些实验。 What if we make N and M classes?如果我们创建NM类会怎样? I will tell you the results:我会告诉你结果:

  • The behaviour only reproduces when both are structs.该行为仅在两者都是结构时重现。

We could go on to discuss whether the issue reproduces only when M in some sense "directly" mentions itself, or whether an "indirect" cycle also reproduces the bug.我们可以继续讨论问题是否仅在 M 在某种意义上“直接”提及自己时才会重现,或者“间接”循环是否也会重现错误。 (The latter is true.) And as Corey notes in his answer, we could also ask "do the types have to be generic?" (后者是真的。)正如 Corey 在他的回答中指出的那样,我们还可以问“类型是否必须是通用的?” No;不; there is a reproducer even more minimal than this one with no generics.有一个比这个没有泛型的复制器更小。

However I think we have enough to complete our discussion of the reproducer and move on to the question at hand, which is "is it a bug, and if so, in what?"但是,我认为我们已经足够完成对复制器的讨论并继续讨论手头的问题,即“这是一个错误,如果是,是什么?”

Plainly something is messed up here, and I lack the time today to sort out where the blame ought to fall.显然这里有些事情搞砸了,我今天没有时间理清应该归咎于哪里。 Here are some thoughts:以下是一些想法:

  • The rule against structs containing members of themselves plainly does not apply here.针对包含自身成员的结构的规则显然不适用于此处。 (See section 11.3.1 of the C# 5 specification, which is the one I have present at hand. I note that this section could benefit from a careful rewriting with generics in mind; some of the language here is a bit imprecise.) If E is static then that section does not apply; (请参阅 C# 5 规范的第 11.3.1 节,这是我手头的那个。我注意到本节可以从考虑泛型的仔细重写中受益;这里的某些语言有点不精确。)如果E是静态的,则该部分不适用; if it is not static then the layouts of N<M> and M can both be computed regardless.如果它不是静态的,那么N<M>M的布局无论如何都可以计算。

  • I know of no other rule in the C# language that would prohibit this arrangement of types.我知道 C# 语言中没有其他规则会禁止这种类型的排列。

  • It might be the case that the CLR specification prohibits this arrangement of types, and the CLR is right to throw an exception here.可能是 CLR 规范禁止这种类型的排列,而 CLR 在这里抛出异常是正确的。

So now let us sum up the possibilities:所以现在让我们总结一下可能性:

  • The CLR has a bug. CLR 有一个错误。 This type topology should be legal, and it is wrong of the CLR to throw here.这种类型的拓扑应该是合法的,在这里抛出是 CLR 的错误。

  • The CLR behaviour is correct. CLR 行为是正确的。 This type topology is illegal, and it is correct of the CLR to throw here.这种类型的拓扑是非法的,CLR 在这里抛出是正确的。 (In this scenario it may be the case that the CLR has a spec bug, in that this fact may not be adequately explained in the specification. I don't have time to do CLR spec diving today.) (在这种情况下,CLR 可能存在规范错误,因为规范中可能没有充分解释这一事实。我今天没有时间进行 CLR 规范潜水。)

Let us suppose for the sake of argument that the second is true.为了论证的缘故,让我们假设第二个是真的。 What can we now say about C#?我们现在可以对 C# 说些什么? Some possibilities:一些可能性:

  • The C# language specification prohibits this program, but the implementation allows it. C# 语言规范禁止此程序,但实现允许它。 The implementation has a bug.实现有一个错误。 (I believe this scenario to be false.) (我认为这种情况是错误的。)

  • The C# language specification does not prohibit this program, but it could be made to do so at a reasonable implementation cost. C# 语言规范并未禁止该程序,但可以以合理的实现成本来实现。 In this scenario the C# specification is at fault, it should be fixed, and the implementation should be fixed to match.在这种情况下,C# 规范有问题,应该修复它,并且应该修复实现以匹配。

  • The C# language specification does not prohibit the program, but detecting the problem at compile time cannot be done at reasonable cost. C# 语言规范并没有禁止该程序,但是在编译时检测问题无法以合理的成本完成。 This is the case with pretty much any runtime crash;几乎任何运行时崩溃都是这种情况; your program crashed at runtime because the compiler couldn't stop you from writing a buggy program.您的程序在运行时崩溃,因为编译器无法阻止您编写有缺陷的程序。 This is just one more buggy program;这只是又一个有问题的程序; unfortunately, you had no reason to know it was buggy.不幸的是,你没有理由知道它有问题。

Summing up, our possibilities are:总结一下,我们的可能性是:

  • The CLR has a bug CLR 有一个错误
  • The C# spec has a bug C# 规范有一个错误
  • The C# implementation has a bug C# 实现有一个错误
  • The program has a bug程序有bug

One of these four must be true.这四个中的一个必须是真的。 I do not know which it is.我不知道是哪个。 Were I asked to guess, I'd pick the first one;如果让我猜,我会选第一个; I see no reason why the CLR type loader ought to balk on this one.我看不出为什么 CLR 类型加载器应该回避这个。 But perhaps there is a good reason that I do not know;但也许有一个我不知道的充分理由; hopefully an expert on the CLR type loading semantics will chime in.希望 CLR 类型加载语义方面的专家会插话。


UPDATE:更新:

This issue is tracked here:此问题在此处跟踪:

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

To sum up the conclusions from the C# team in that issue:总结一下 C# 团队在该问题上的结论:

  • The program is legal according to both the CLI and C# specifications.根据 CLI 和 C# 规范,该程序是合法的。
  • The C# 6 compiler allows the program, but some implementations of the CLI throw a type load exception. C# 6 编译器允许该程序,但 CLI 的某些实现会引发类型加载异常。 This is a bug in those implementations.这是这些实现中的一个错误。
  • The CLR team is aware of the bug, and apparently it is hard to fix on the buggy implementations. CLR 团队知道这个错误,显然很难修复有问题的实现。
  • The C# team is considering making the legal code produce a warning, since it will fail at runtime on some, but not all, versions of the CLI. C# 团队正在考虑让合法代码产生警告,因为它会在运行时在某些(但不是所有)CLI 版本上失败。

The C# and CLR teams are on this; C# 和 CLR 团队正在解决这个问题; follow up with them.跟进他们。 If you have any more concerns with this issue please post to the tracking issue, not here.如果您对此问题有任何更多疑虑,请发布到跟踪问题,而不是这里。

This is not a bug in 2015 but a possibly a C# language bug.这不是 2015 年的错误,而是可能是 C# 语言错误。 The discussion below relates to why instance members cannot introduce loops, and why a Nullable<T> will cause this error, but should not apply to static members.下面的讨论涉及为什么实例成员不能引入循环,以及为什么Nullable<T>会导致此错误,但不应适用于静态成员。

I would submit it as a language bug, not a compiler bug.我会将其作为语言错误提交,而不是编译器错误。


Compiling this code in VS2013 gives the following compile error:在 VS2013 中编译此代码会出现以下编译错误:

Struct member 'ConsoleApplication1.Program.MyStruct.Empty' of type 'System.Nullable' causes a cycle in the struct layout “System.Nullable”类型的结构成员“ConsoleApplication1.Program.MyStruct.Empty”导致结构布局中出现循环

A quick search turns up this answer which states:快速搜索会找到这个答案,其中指出:

It's not legal to have a struct that contains itself as a member.将自身包含为成员的结构是不合法的。

Unfortunately the System.Nullable<T> type which is used for nullable instances of value types is also a value type and must therefore have a fixed size.不幸的是,用于值类型的可为空实例的System.Nullable<T>类型也是值类型,因此必须具有固定大小。 It's tempting to think of MyStruct?很容易想到MyStruct? as a reference type, but it really isn't.作为引用类型,但实际上不是。 The size of MyStruct? MyStruct?的大小MyStruct? is based on the size of MyStruct ... which apparently introduces a loop in the compiler.基于MyStruct的大小......这显然在编译器中引入了一个循环。

Take for instance:举个例子:

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

public struct Struct2
{
    public Struct1? s;
}

Using System.Runtime.InteropServices.Marshal.SizeOf() you'll find that Struct2 is 16 bytes long, indicating that Struct1?使用System.Runtime.InteropServices.Marshal.SizeOf()你会发现Struct2是 16 字节长,表明Struct1? is not a reference but a struct that is 4 bytes (standard padding size) longer than Struct1 .不是引用,而是比Struct1长 4 个字节(标准填充大小)的Struct1


What's not happening here这里没有发生什么

In response to Julius Depulla's answer and comments, here is what is actually happening when you access a static Nullable<T> field.为了回应 Julius Depulla 的回答和评论,以下是您访问static Nullable<T>字段时实际发生的情况。 From this code:从这个代码:

public struct foo
{
    public static int? Empty = null;
}

public void Main()
{
    Console.WriteLine(foo.Empty == null);
}

Here is the generated IL from LINQPad:这是从 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         

The first instruction gets the address of the static field foo.Empty and pushes it on the stack.第一条指令获取静态字段foo.Empty的地址并将其压入堆栈。 This address is guaranteed to be non-null as Nullable<Int32> is a structure and not a reference type.该地址保证非空,因为Nullable<Int32>是一个结构而不是引用类型。

Next the Nullable<Int32> hidden member function get_HasValue is called to retrieve the HasValue property value.接下来调用Nullable<Int32>隐藏成员函数get_HasValue以检索HasValue属性值。 This cannot result in a null reference since, as mentioned previously, the address of a value type field must be non-null, regardless of the value contained at the address.这不会导致空引用,因为如前所述,值类型字段的地址必须是非空的,无论地址中包含的值如何。

The rest is just comparing the result to 0 and sending the result to the console.剩下的只是将结果与 0 进行比较并将结果发送到控制台。

At no point in this process is it possible to 'invoke a null on a type' whatever that means.在此过程中,任何时候都不能“在类型上调用 null”,无论这意味着什么。 Value types do not have null addresses, so method invocation on value types cannot directly result in a null object reference error.值类型没有空地址,因此对值类型的方法调用不能直接导致空对象引用错误。 That's why we don't call them reference types.这就是我们不称它们为引用类型的原因。

Now that we've had a lengthy discussion about what and why, here's a way to work around the issue without having to wait on the various .NET teams to track down the issue and determine what if anything will be done about it.既然我们已经对内容和原因进行了长时间的讨论,那么这里有一种解决问题的方法,而无需等待各个 .NET 团队来追踪问题并确定是否会对此采取任何措施。

The issue appears to be restricted to field types that are value types which reference back to this type in some way, either as generic parameters or static members.该问题似乎仅限于作为值类型以某种方式引用回此类型的字段类型,无论是作为泛型参数还是静态成员。 For instance:例如:

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

Ugh, I feel dirty now.呃,我现在觉得很脏。 Bad OOP, but it demonstrates that the problem exists without invoking generics in any way.糟糕的 OOP,但它表明在不以任何方式调用泛型的情况下存在问题。

So because they are value types the type loader determines that there is a circularity involved that should be ignored because of the static keyword.因此,因为它们是值类型,类型加载器确定涉及的循环性应该被忽略,因为static关键字。 The C# compiler was smart enough to figure it out. C# 编译器足够聪明,可以解决这个问题。 Whether it should have or not is up to the specs, on which I have no comment.是否应该有取决于规格,对此我没有评论。

However, by changing either A or B to class the problem evaporates:但是,通过将AB更改为class ,问题就消失了:

public struct A { public static B b; }
public class B { public static A a; }

So the problem can be avoided by using a reference type to store the actual value and convert the field to a property:因此可以通过使用引用类型来存储实际值并将字段转换为属性来避免该问题:

public struct MyStruct
{
    private static class _internal { public static MyStruct? empty = null; }
    public static MyStruct? Empty => _internal.empty;
}

This is a bunch slower because it's a property instead of a field and calls to it will invoke the get method, so I wouldn't use it for performance-critical code, but as a workaround it at least lets you do the job until a proper solution is available.这是一堆慢,因为它是一个属性而不是一个字段,调用它会调用get方法,所以我不会将它用于性能关键代码,但作为一种解决方法,它至少可以让你完成这项工作,直到有适当的解决方案。

And if it turns out that this doesn't get resolved, at least we have a kludge we can use to bypass it.如果事实证明这没有得到解决,至少我们有一个可以用来绕过它的kludge。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM