这绝不是一份权威的答案,但让我做一个有根据的猜测。
存在一个基本差异,我认为其他问题的答案与此差异有关。它在类型初始化顺序中,特别是在继承上下文中。
那么,实例初始化如何工作?
在 C# 中:
所有实例字段初始化器都会首先运行,从最派生到基类“向上”沿着继承链运行。
然后构造函数运行,“向下”沿着链从基类到派生类运行。
构造函数互相调用或(显式地)调用基类的构造函数并不改变情况,因此我将其排除在外。
基本上发生的事情是:对于链中的每个类,从最派生开始运行:
Derived.initialize(){
derivedInstance.field1 = field1Initializer();
[...]
Base.Initialize();
Derived.Ctor();
}
一个简单的例子可以说明这一点:
void Main()
{
new C();
}
class C: B {
public int c = GetInt("C.c");
public C(){
WriteLine("C.ctor");
}
}
class B {
public int b = GetInt("B.b");
public static int GetInt(string _var){
WriteLine(_var);
return 6;
}
public B(){
WriteLine("B.ctor");
}
public static void WriteLine(string s){
Console.WriteLine(s);
}
}
输出:
C.c
B.b
B.ctor
C.ctor
这意味着,如果在字段初始化程序中访问字段是有效的,那么我就可以做出这样的灾难:
class C: B {
int c = b;
[...]
}
在Java中:
关于类型初始化的长篇有趣文章在这里。总结如下:
它有点复杂,因为除了实例字段初始化程序的概念外,还有一个(可选的)实例初始化程序的概念,但是这是要点:
所有内容都沿着继承链向下运行。
- 基类的实例初始化程序运行
- 基类的字段初始化程序运行
基类的构造函数运行
重复上述步骤以处理继承链中下一个类。
- 重复上一步直到达到最派生的类。
以下是证明:或在线运行)
class Main
{
public static void main (String[] args) throws java.lang.Exception
{
new C();
}
}
class C extends B {
{
WriteLine("init C");
}
int c = GetInt("C.c");
public C(){
WriteLine("C.ctor");
}
}
class B {
{
WriteLine("init B");
}
int b = GetInt("B.b");
public static int GetInt(String _var){
WriteLine(_var);
return 6;
}
public B(){
WriteLine("B.ctor");
}
public static void WriteLine(String s){
System.out.println(s);
}
}
输出:
init B
B.b
B.ctor
init C
C.c
C.ctor
这意味着,在字段初始化程序运行时,所有
继承的字段都已经被初始化(由基类中的初始化程序或构造函数),因此允许这种行为是足够安全的:
class C: B {
int c = b;
[...]
}
在Java中,与C#类似,字段初始化器按照声明顺序运行。
Java编译器甚至会检查字段初始化器是否按照顺序调用。
class C {
int a = b; //compiler error: illegal forward reference
int b = 5;
}
* 顺便提一下,如果初始化器调用实例方法来访问字段,则可以按任意顺序访问字段:
class C {
public int a = useB();
int b = 5;
int useB(){
return b;
}
}
C.i
而不是i
。 - Novaki
是 "相反的" 静态:不能引用 非静态 字段 [...] C.i。 - Cristian Diaconescui
添加为static
,则该错误应该消失,因为在方法的上下文之外,它只能引用非成员(与类相关而不是实例)属性。 - Novak