观看网络研讨会后 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
结束。
我有两个问题:
为什么编译器完全允许递归构造函数调用?
为什么我们观察到编译器对在此类中初始化的字段的这种行为?
一些注意事项:ReSharper用 Possible cyclic constructor calls
警告您。此外,在 Java 中,此类构造函数调用不会进行事件编译,因此 Java 编译器在这种情况下具有更多限制(Jon 在网络研讨会上提到了这一信息)。
这让这些问题变得更加有趣,因为就 Java 社区的所有方面而言,C# 编译器至少更现代。
最佳答案
有趣的发现。
看起来实际上只有两种实例构造函数:
- 一个实例构造函数,它链接了另一个实例构造函数相同类型,与
: this( ...)
语法。 - 一个实例构造函数,它链接基类的实例构造函数。这包括未指定 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/