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

Ily*_*nov 81 c#

在观看网络研讨会Jon Skeet Inspects ReSharper之后,我开始尝试使用递归构造函数调用,并发现以下代码是有效的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()   { }
}
Run Code Online (Sandbox Code Playgroud)

我们都知道,字段初始化由编译器移动到构造函数中.所以,如果你有一个字段一样int a = 42;,你将不得不a = 42所有构造函数.但是如果你有构造函数调用另一个构造函数,那么只有被调用的构造函数才会有初始化代码.

例如,如果您使用带有参数调用默认构造函数的构造函数,则a = 42只能在默认构造函数中进行赋值.

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

class Foo
{
    int a = 42;

    Foo() :this(60)  { }
    Foo(int v)       { }
}
Run Code Online (Sandbox Code Playgroud)

编译成:

internal class Foo
{
    private int a;

    private Foo()
    {
        this.ctor(60);
    }

    private Foo(int v)
    {
        this.a = 42;
        base.ctor();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

正如您所看到的,编译器无法决定将字段初始化放在何处,因此不会将其放在任何位置.另请注意,没有base构造函数调用.当然,没有对象可以创建,StackOverflowException如果你试图创建一个实例,你总会得到Foo.

我有两个问题:

为什么编译器允许递归构造函数调用?

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


一些注意事项:ReSharper警告你Possible cyclic constructor calls.而且,在Java中,这样的构造函数调用不会进行事件编译,因此Java编译器在这种情况下更具限制性(Jon在网络研讨会上提到了这些信息).

这使得这些问题更加有趣,因为对于Java社区而言,C#编译器至少更加现代化.

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

Jep*_*sen 11

有趣的发现.

看来真的只有两种实例构造函数:

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

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

可能存在于类中的实例字段初始值设定项需要复制到上面类型2的所有实例构造函数的主体的开头,而类型1的实例构造函数不需要字段赋值代码.

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

现在,您的示例给出了所有实例构造函数都是类型1的情况.在这种情况下,字段initaializer代码不需要放在任何地方.看来,它没有得到很深入的分析.

事实证明,当所有实例构造函数都是类型1时,您甚至可以从没有可访问构造函数的基类派生.但是,基类必须是非密封的.例如,如果您编写的类只包含private实例构造函数,那么如果派生类中的所有实例构造函数都是类型1,那么人们仍然可以从您的类派生.但是,当然,新的对象创建表达式永远不会完成.要创建派生类的实例,必须"欺骗"并使用类似System.Runtime.Serialization.FormatterServices.GetUninitializedObject方法的东西.

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

最后,关于

Invalid<Method>Name<<Indeeed()
Run Code Online (Sandbox Code Playgroud)

句法.根据C#规则,这应该被理解为

(Invalid < Method) > (Name << Indeeed())
Run Code Online (Sandbox Code Playgroud)

因为左移运算符的<<优先级高于小于运算符<和大于运算符>.后两个操作数具有相同的优先级,因此通过左关联规则进行评估.如果类型是

MySpecialType Invalid;
int Method;
int Name;
int Indeed() { ... }
Run Code Online (Sandbox Code Playgroud)

如果MySpecialType引入了一个(MySpecialType, int)重载operator <,那么表达式

Invalid < Method > Name << Indeeed()
Run Code Online (Sandbox Code Playgroud)

是合法的,有意义的.


在我看来,如果编译器在这种情况下发出警告会更好.例如,它可以说unreachable code detected并指向从未转换为IL的字段初始值设定项的行号和列号.

  • @RoyiNamir是的.但是如果你看一下IL,就像提问者写的那样:_"我们都知道,编译器将字段初始化移到构造函数中."_这意味着,假设你用C#编写这个类: `class Example {int field = 42; 内部示例(){/*这里的一些代码*/field = 100; 然后,由它生成的IL将`42`赋值放入实例构造函数中,然后再将其它所有内容写成:`class Example {int field; 内部示例(){field = 42;/*这里的一些代码*/field = 100; } (2认同)

Dam*_*ver 5

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

从10.11.1开始:

所有实例构造函数(类的除外object)都隐式地包含在构造函数体之前的另一个实例构造函数的调用.隐式调用的构造函数由constructor-initializer确定

...

  • 表单的实例构造函数初始值设定项会导致调用类本身的实例构造函数...如果实例构造函数声明包含调用构造函数本身的构造函数初始值设定项,则会发生编译时错误this(argument-listopt)

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

Foo() : this() {}
Run Code Online (Sandbox Code Playgroud)

是非法的.


我承认 - 我看不出允许它的具体原因.当然,在IL级别允许这样的构造,因为我可以在运行时选择不同的实例构造函数 - 所以如果它终止,你可以进行递归.


我认为它没有标记或警告的另一个原因是因为它没有必要检测这种情况.想象一下,追逐数百个不同的构造函数,只是为了查看一个循环是否存在 - 当任何尝试的使用将很快(如我们所知)在运行时爆炸时,对于一个相当优势的情况.

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

  • 如果constructor-initializer是类本身的实例构造函数,则它不会发出字段初始值设定项 - 它会发出constructor-initializer调用,然后是正文.

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

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

  • @MatthewWatson由于类不完整,因此无法在分析时检测到错误.(也许你的类将定义名为`Invalid`等的成员,使其有效.)错误通常在代码生成时检测到,但是你找到了一种编写永远不会生成的代码的方法.你在编译器中找到了一个偷偷摸摸的漏洞(一种编写永远不会被编译的代码的方法),但不是一个严重的漏洞,因为无论如何都无法访问有问题的代码. (4认同)
  • 但是它允许这样的行编译:`int e =无效<Method> Name << Indeeed();`.我说那是编译错误. (2认同)