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

标签 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()   { }
}

众所周知,字段初始化由编译器移至构造函数中。因此,如果您有一个像 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 构造函数调用。当然,无法创建任何对象,如果您尝试创建 Foo 的实例,您将始终以 StackOverflowException 结束。

我有两个问题:

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

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


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

这让这些问题变得更加有趣,因为就 Java 社区的所有方面而言,C# 编译器至少更现代。

这是使用 C# 4.0 编译的和 C# 5.0编译器和反编译使用 dotPeek .

最佳答案

有趣的发现。

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

  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 的字段初始值设定项的行号和列号。

关于c# - 为什么递归构造函数调用会使无效的 C# 代码编译?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16645267/

相关文章:

c# - 如何正确地异步编写 Socket/NetworkStream?

c# - 什么 C# 命名方案可用于符合 CLS 的属性和成员?

c# - XNA 4.0 中的 2D BoundingRectangle 旋转

c# - 如何调试单个 ASPX 文件?

c# - 忽略 "Unauthorized Access" "function Directory.GetDirectories()"

c# - 用c#检查年份是否为闰年

c# - 是否可以在OnInit事件中设置cookie并在同一页面读取?

c# - 使用 COM 将字符串从 C# 传递到 cpp

c# - 一个套接字实例可以多次发送和接收吗?

c# - ASP.Net C# 帮助器,返回一个数组