82

在观看网络研讨会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()   { }
}

我们可能都知道,字段初始化是由编译器移到构造函数中的。因此,如果您有一个类似 的字段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构造函数调用。当然,不能创建任何对象,StackOverflowException如果您尝试创建Foo.

我有两个问题:

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

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


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

这使得这些问题更有趣,因为就 Java 社区而言,C# 编译器至少更现代。

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

4

4 回答 4

11

有趣的发现。

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

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

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

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

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

现在您的示例给出了所有实例构造函数都是类型1的情况。在这种情况下,字段初始化程序代码不需要放在任何地方。因此,似乎没有对其进行深入分析。

事实证明,当所有实例构造函数都是类型1.时,您甚至可以从没有可访问构造函数的基类派生。但是,基类必须是非密封的。例如,如果您编写一个只有private实例构造函数的类,如果人们将派生类中的所有实例构造函数都设为上面的1.类型,他们仍然可以从您的类派生。然而,一个新的对象创建表达式当然永远不会完成。要创建派生类的实例,必须“作弊”并使用System.Runtime.Serialization.FormatterServices.GetUninitializedObject方法之类的东西。

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

最后,关于

Invalid<Method>Name<<Indeeed()

句法。根据 C# 规则,这将被读作

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

因为左移运算符<<的优先级高于小于运算符<和大于运算符>。后两个运算符具有相同的优先级,因此由左结合规则评估。如果类型是

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

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

Invalid < Method > Name << Indeeed()

将是合法且有意义的。


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

于 2013-05-20T19:49:45.950 回答
5

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

从 10.11.1 开始:

所有实例构造函数(除了那些用于 classobject的构造函数)在构造函数主体之前隐式包含对另一个实例构造函数的调用。隐式调用的构造函数由构造函数初始化器确定

...

  • 形式的实例构造函数初始化程序会导致调用类本身的实例构造函数...如果实例构造函数声明包含调用构造函数本身的构造函数初始化程序,则会发生编译时错误this(argument-listopt)

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

Foo() : this() {}

是非法的。


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


我认为它没有对此进行标记或警告的另一个原因是因为它不需要检测这种情况。想象一下,追逐数百个不同的构造函数,只是为了看看是否确实存在一个循环——当任何尝试的使用将在运行时迅速(正如我们所知)爆炸时,对于一个相当边缘的情况。

当它为每个构造函数生成代码时,它只考虑constructor-initializer、字段初始化器和构造函数的主体 - 它不考虑任何其他代码:

  • Ifconstructor-initializer是类本身的实例构造函数,它不会发出字段初始值设定项 - 它发出constructor-initializer调用,然后发出主体。

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

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

于 2013-05-20T08:38:02.127 回答
2

你的例子

class Foo
{
    int a = 42;

    Foo() :this(60)  { }
    Foo(int v)       { }
}

可以正常工作,因为您可以毫无问题地实例化该 Foo 对象。但是,以下内容更像您要询问的代码

class Foo
{
    int a = 42;

    Foo() :this(60)     { }
    Foo(int v) : this() { }
}

这和你的代码都会创建一个stackoverflow(!),因为递归永远不会触底。因此,您的代码将被忽略,因为它永远不会执行。

换句话说,编译器无法决定将错误代码放在哪里,因为它可以判断递归永远不会触底。我认为这是因为它必须把它放在只会被调用一次的地方,但是构造函数的递归性质使得这不可能。

在构造函数的主体内创建自身实例的构造函数意义上的递归对我来说是有意义的,因为例如,这可用于实例化每个节点指向其他节点的树。但是通过这个问题说明的那种预构造函数的递归永远不会触底,所以如果不允许这样做对我来说是有意义的。

于 2013-05-20T08:28:02.753 回答
0

我认为这是允许的,因为您仍然可以(可以)捕获异常并用它做一些有意义的事情。

初始化永远不会运行,它几乎肯定会抛出 StackOverflowException。但这仍然可能是想要的行为,并不总是意味着进程应该崩溃。

如此处所述https://stackoverflow.com/a/1599236/869482

于 2013-05-22T12:31:05.850 回答