为什么recursion构造函数调用使无效的C#代码编译?

在观看networking研讨会之后, Jon Skeet检查了ReSharper ,我已经开始用recursion构造函数调用一下,发现下面的代码是有效的C#代码(通过有效的我的意思是编译)。

class Foo { int a = null; int b = AppDomain.CurrentDomain; int c = "string to int"; int d = NonExistingMethod(); int e = Invalid<Method>Name<<Indeeed(); Foo() :this(0) { } Foo(int v) :this() { } } 

大家都知道,编译器会将字段初始化移入构造函数中。 所以如果你有像int a = 42;这样的字段int a = 42;所有构造函数中都有a = 42 。 但是,如果你有构造函数调用另一个构造函数,你将只有被调用的初始化代码。

例如,如果你有构造函数的参数调用默认的构造函数,你将只有在默认的构造函数中赋值a = 42

为了说明第二种情况,下一个代码:

 class Foo { int a = 42; Foo() :this(60) { } Foo(int v) { } } 

编译成:

 internal class Foo { private int a; private Foo() { this.ctor(60); } private Foo(int v) { this.a = 42; base.ctor(); } } 

所以主要的问题是,我的代码,在这个问题的开始,被编译成:

 internal class Foo { private int a; private int b; private int c; private int d; private int e; private Foo() { this.ctor(0); } private Foo(int v) { this.ctor(); } } 

正如你所看到的,编译器不能决定把字段初始化的位置,因此不能放在任何地方。 还要注意,没有base构造函数调用。 当然,不会创build任何对象,如果您尝试创buildFoo的实例,您将始终以StackOverflowException结束。

我有两个问题:

为什么编译器允许recursion构造函数呢?

为什么我们观察编译器的这种行为,在这样的类中初始化?


一些注意: ReSharper用Possible cyclic constructor calls警告你。 此外,在Java中,这样的构造函数调用不会事件编译,所以Java编译器在这种情况下更具有限制性(Jon在networking研讨会上提到了这个信息)。

这使得这些问题更加有趣,因为在Java社区方面,C#编译器至less是更现代的。

这是使用C#4.0和C#5.0编译器编译的,并使用dotPeek进行反编译。

有趣的发现。

看来,实际上只有两种实例构造函数:

  1. 一个实例构造函数,它使用: this( ...)语法链接相同types的另一个实例构造函数。
  2. 链接基类的实例构造函数的实例构造函数。 这包括没有指定chainig的实例构造函数,因为: base()是默认值。

(我忽略了System.Object的实例构造函数,这是一个特例, System.Object没有基类,但是System.Object也没有字段)。

可能存在于类中的实例字段初始值设定项需要被复制到上面types2的所有实例构造函数体的开头,而没有types1的实例构造函数需要字段赋值代码。

所以显然,C#编译器不需要对types1的构造函数进行分析,以查看是否存在循环。

现在你的例子给出了所有实例构造函数都是1的情况 。 在这种情况下,字段initaializer代码不需要放在任何地方。 所以它似乎并没有被深入分析。

事实certificate,当所有实例构造函数都是types1时 ,甚至可以从没有可访问构造函数的基类派生。 虽然基类必须是非密封的。 例如,如果您仅使用private实例构造函数编写一个类,那么如果派生类中的所有实例构造函数都是types1 ,则仍然可以从您的类派生类。 然而,当然,一个新的对象创buildexpression式永远也不会完成。 要创build派生类的实例,必须“欺骗”并使用诸如System.Runtime.Serialization.FormatterServices.GetUninitializedObject方法之类的东西。

另一个例子: System.Globalization.TextInfo类只有一个internal实例构造函数。 但是你仍然可以通过这个技术从mscorlib.dll以外的程序集中派生出这个类。

最后,关于

 Invalid<Method>Name<<Indeeed() 

句法。 根据C#规则,这被认为是

 (Invalid < Method) > (Name << Indeeed()) 

因为左移运算符<<优先于小于运算符<和大于运算符> 。 后两种操作具有相同的优先级,因此用左联合规则进行评估。 如果types是

 MySpecialType Invalid; int Method; int Name; int Indeed() { ... } 

如果MySpecialType引入了operator < (MySpecialType, int)重载,则expression式

 Invalid < Method > Name << Indeeed() 

将是合法和有意义的。


在我看来,如果编译器在这种情况下发出警告会更好。 例如,它可以说unreachable code detected并指向永远不会被翻译成IL的字段初始值设定项的行号和列号。

我认为,因为语言规范只是直接调用正在定义的相同构造函数。

从10.11.1:

所有实例构造函数(除了类object实例构造object )隐式地包含紧接在构造函数体之前的另一个实例构造函数的调用。 隐式调用的构造函数由constructor-initializer决定

  • 一个forms为this( argument-list opt )的实例构造函数初始值设定项会引起类自身的实例构造函数被调用…如果一个实例构造函数声明包含调用构造函数本身的构造函数初始化函数,则会发生编译时错误

最后一句似乎只是排除了直接调用自己产生编译时错误,例如

 Foo() : this() {} 

是非法的。


我承认虽然 – 我不明白允许它的具体原因。 当然,在IL级这样的构造是允许的,因为可以在运行时select不同的实例构造函数,我相信 – 所以你可以有recursion提供它终止。


我认为其他原因并不标志或警告,因为它不需要检测这种情况。 想象一下,追逐数百个不同的构造函数,只是为了看看是否存在一个循环 – 当任何尝试的用法将会在运行时迅速(正如我们所知)在相当边缘的情况下爆炸。

当它为每个构造函数做代码生成时,它考虑的只是constructor-initializer ,字段初始值设定项和构造函数的主体 – 它不考虑任何其他代码:

  • 如果constructor-initializer是类本身的实例构造函数,它不会发出字段初始化程序 – 它会发出constructor-initializer调用,然后发出正文。

  • 如果constructor-initializer是直接基类的实例构造函数,它将发出字段初始值设定项,然后是constructor-initializer调用,然后是body。

在任何情况下,都不需要去其他地方寻找 – 所以它不是“无法”决定在哪里放置字段初始值设定项 – 它只是遵循一些简单的规则,只考虑当前的构造函数。

你的例子

 class Foo { int a = 42; Foo() :this(60) { } Foo(int v) { } } 

将工作正常,从这个意义上说,你可以没有问题实例化Foo对象。 不过,以下内容更像是你所问的代码

 class Foo { int a = 42; Foo() :this(60) { } Foo(int v) : this() { } } 

这两个和你的代码将创build一个计算器(!),因为recursion永远不会落空。 所以你的代码被忽略,因为它永远不会被执行。

换句话说,编译器不能决定在哪里放置错误的代码,因为它可以告诉recursion永远不会落空。 我认为这是因为它必须把它放在只能被调用一次的地方,但是构造器的recursion性使得这是不可能的。

在构造函数体内创build实例的构造函数的recursion对我来说是有意义的,因为例如可以用来实例化每个节点指向其他节点的树。 但是通过这个问题所阐述的预构造函数的recursion是不可能达到的,所以如果这是不被允许的话。

我认为这是允许的,因为你可以(可以)仍然捕捉exception,并做一些有意义的事情。

初始化将永远不会运行,几乎肯定会抛出一个StackOverflowException。 但是这仍然可以是被通缉的行为,并不总是意味着这个过程会崩溃。

正如这里所解释的https://stackoverflow.com/a/1599236/869482