12

一、简介:

这段代码:

class C
{
    int i = 5;
    byte[] s = new byte[i];
}

无法编译并出现以下错误:

字段初始值设定项不能引用非静态字段、方法或属性“Ci”

Resharper 说了类似的话:无法i在静态上下文中访问非静态字段。

这与C# 规范所说的一致——字段初始值设定项无法访问当前正在创建的实例 ( this),或者,通过扩展,任何实例字段:

实例字段的变量初始值设定项不能引用正在创建的实例。因此,在变量初始化程序中引用 this 是编译时错误,因为变量初始化程序通过简单名称引用任何实例成员是编译时错误。

但是,这在 Java 中工作得很好:

class C {
    int i = 5;
    byte s[] = new byte[i]; //no errors here
}

还在我这儿?好的,这是问题。错了,问题。

在一个假设的世界中,这在 C# 中是有效的,我想知道:它甚至可能吗?如果是这样,它将添加到表格中的利弊是什么?此外,既然 Java 确实支持它,那么 Java是否也有同样的优点/缺点 ?或者类型初始化器在两种语言中的工作方式是否存在根本区别?

4

5 回答 5

13

简而言之,在构造函数体运行之前访问接收器的能力是边际收益的一个特征,它使得编写有缺陷的程序变得更容易。因此,C# 语言设计者完全禁用了它。如果您需要使用接收器,则将该逻辑放入构造函数主体中。

至于为什么该功能在 Java 中是合法的,您将不得不询问 Java 设计人员。

于 2013-06-27T13:38:50.910 回答
5

在 C# 中,字段初始值设定项只是开发人员的方便语义。编译器将所有字段初始值设定项移动到构造函数ABOVE的主体中,在该主体中对基本构造函数进行调用。因此,字段在祖先链上被初始化,而类从基数向下初始化。

静态引用是可以的,因为它们在其他任何东西之前被初始化。

于 2013-06-27T11:54:39.897 回答
2

这绝不是一个权威的答案,但让我做一个有根据的猜测。

有一个根本的区别,我认为其他问题的答案与这个区别有关。
它存在于类型初始化的顺序中,尤其是在继承的上下文中。

那么,实例初始化是如何工作的呢?

在 C# 中:

  • 所有实例字段初始化程序首先运行,“向上”继承链,从大多数派生类到基类。

  • 然后ctors运行,从基础到派生“沿着”链。

ctor 相互调用或(明确)调用基类的 ctor 的可能性不会改变这种情况,所以我将把它排除在外。

基本上发生的是,这对链中的每个 chas 运行,从最衍生的开始:

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; //b is a field inherited from the base class, and NOT YET INITIALIZED!
    [...]
}

在 Java 中:

关于类型初始化的长篇有趣的文章在这里。总结一下:

它有点复杂,因为除了实例字段初始化器的概念之外,还有(可选)实例初始化器的概念,但这里是它的要点:

一切都沿着继承链运行。

  • 类的实例初始化程序运行
  • 基类的字段初始化器运行
  • 基类的 ctor(s) 运行

  • 对继承链中的下一个类重复上述步骤。

  • 重复上一步,直到到达最派生的类。

这是证明:(或自己在线运行

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

这意味着,在字段初始化程序运行时,所有继承的字段都已经初始化(通过基类中的初始化程序或 ctor),因此允许这种行为是足够安全的:

class C: B {
    int c = b; //b is inherited from the base class, and it's already initialized!
    [...]
}

在 Java 中,就像在 C# 中一样,字段初始值设定项按声明顺序运行。
Java 编译器甚至会检查字段初始值设定项是否被乱序调用*:

class C {
    int a = b; //compiler error: illegal forward reference
    int b = 5;
}

* 顺便说一句,如果初始化程序调用实例方法,您可以乱序访问字段:

class C {
    public int a = useB(); //after initializer completes, a == 0
    int b = 5;
    int useB(){
        return b;  //use b regardless if it was initialized or not.
    }
}
于 2013-06-27T14:11:08.667 回答
-1

这有点无法回答,但我喜欢将类主体中的任何内容视为与序列无关的。它不应该是需要以特定方式评​​估的顺序代码——它只是类的默认状态。如果你使用这样的代码,你期望 i 在 s 之前被评估。

无论如何,无论如何,您都可以使 ia const (应该是)。

于 2013-06-27T12:03:05.273 回答
-1

这是因为字段初始化器被编译器移动到构造函数中(除非是静态的),所以你需要在你的构造函数中显式,如下所示:

class C 
{
    int i = 5;
    byte[] s;

    public C()
    {
        s = new byte[i];
    }
}
于 2013-06-27T12:00:10.090 回答