C#中访问`this`的字段初始化器是无效的,而在Java中是有效的?

12

首先,介绍一下:

这段代码:

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

编译错误如下:

字段初始化器无法引用非静态字段、方法或属性 `C.i'

Resharper 显示类似的信息:在静态上下文中无法访问非静态字段i

这符合C#规范的规定,即字段初始化器不能访问当前正在创建的实例(this),或者说不能访问任何实例字段:

实例字段的变量初始化器不能引用正在创建的实例。因此,在变量初始化器中引用 this 是一个编译时错误,就像通过简单名称引用任何实例成员一样。

然而在Java中,这完全没问题:

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

还在跟着我吗?好的,这里是问题。哦,问题们。

在一个假设的世界中,如果在C#中这是有效的,我想知道:这是否可能?如果是,它会为表格添加什么优点和缺点?同时,由于Java真正支持这一点,这些相同的优点/缺点是否也适用于Java?或者,在两种语言中类型初始化程序工作方式上是否存在根本区别


1
可能在任何方法的上下文之外,属性都被视为静态的,如果您没有提供其确切来源?我的建议:将您的初始化程序移动到构造函数中。 - Novak
@GuyDavid 你说的“props被视为静态”的意思是什么? - Cristian Diaconescu
当我看到编译器错误时,就有了这个假设——它正在寻找静态属性C.i而不是i - Novak
1
@GuyDavid 错误信息清楚地指出,字段 i 是 "相反的" 静态:不能引用 非静态 字段 [...] C.i。 - Cristian Diaconescu
感谢指出,我应该更仔细地阅读它(所以你应该忽略我的第二个评论)。不过,如果您将 i 添加为 static,则该错误应该消失,因为在方法的上下文之外,它只能引用非成员(与类相关而不是实例)属性。 - Novak
1
有人可以解释一下为什么规范将非法限制在通过“简单名称”引用实例成员的情况下吗?那到底是什么意思呢?看起来隐含的替代机制又会是合法的什么样子? - InBetween
5个回答

13
简而言之,在构造函数体运行之前访问接收器(receiver)的能力是具有边际利益的功能,它使编写错误程序变得更容易。因此,C#语言设计师完全禁用了它。如果需要使用接收器,则将逻辑放在构造函数体中。
至于为什么Java允许这个功能,你需要问Java设计师。

有趣的是,TypeScript刚刚启用了这种行为。 - MgSam
@MgSam:有趣;我已经不再与TypeScript的设计师们一起生活或工作了,所以对最新的变化不太了解。当然,我注意到TypeScript和C#有非常不同的设计原则。 - Eric Lippert

5
在C#中,字段初始化器只是为了方便开发人员的语法糖。编译器将所有字段初始化器移动到构造函数的上面,在调用基础构造函数之前。因此,字段从祖先链向上初始化,并且类从基础向下初始化。
静态引用是可以的,因为它们在其他任何东西之前初始化。

完全正确。但这意味着,在评估初始化程序时,使用尚未初始化的基本类型字段将是非法的。然而,类的先前声明的字段应该在范围内并初始化。因此,我看不出初始化程序调用顺序与我所询问的限制之间的联系。 - Cristian Diaconescu
1
@CristiDiaconescu,关于为什么该字段不在范围内,您可以阅读由尊敬的Lippert先生在此处提供的答案https://dev59.com/h0nSa4cB1Zd3GeqPNV8a - Gayot Fow

2
这绝不是一份权威的答案,但让我做一个有根据的猜测。
存在一个基本差异,我认为其他问题的答案与此差异有关。它在类型初始化顺序中,特别是在继承上下文中。
那么,实例初始化如何工作?
在 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; //b is a field inherited from the base class, and NOT YET INITIALIZED!
    [...]
}

在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; //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.
    }
}

-1

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

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

    public C()
    {
        s = new byte[i];
    }
}

你的回答提供了解决方法,这很明显。但它并没有回答“为什么”的问题。 - Cristian Diaconescu

-1
这可能不是一个确切的答案,但我认为类中的任何内容都是独立于顺序的。它不应该是需要按特定方式评估的顺序代码,而只是类的默认状态。如果您使用这样的代码,那么您期望先评估 i 再评估 s。
无论如何,您可以将i声明为const(应该这样做)。

1
实际上,初始化程序被保证按照它们声明的顺序进行评估:http://msdn.microsoft.com/en-us/library/aa645757%28v=VS.71%29.aspx(查找“文本顺序”短语)。话虽如此,我同意不使用这种(有点模糊)的假设编写代码更加清晰。 - Cristian Diaconescu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接