访问“this”的字段初始值设定项:在C#中无效,在Java中有效

本文关键字:无效 有效 Java this 字段 访问 | 更新日期: 2023-09-27 18:20:10

首先介绍:

此代码:

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

编译失败,出现以下错误:

字段初始值设定项不能引用非静态字段、方法或属性"C.i"

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

这与C#规范所说的一致——字段初始化器不能访问当前正在创建的实例(this),或者扩展到任何实例字段:

实例字段的变量初始值设定项不能引用正在创建的实例。因此,引用它是一个编译时错误这在变量初始值设定项中,因为这是变量初始值设定项通过简单的名称。

然而,这在Java:中运行得很好

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

还和我在一起吗?好的,问题来了。呃,问题

在一个假设的世界里,这在C#中是有效的,我想知道:这是否可能?如果是这样的话,它将向表中添加哪些优点和缺点?此外,由于它确实受到Java的支持,是否也支持?或者类型初始化程序在两种语言中的工作方式是否存在根本差异

访问“this”的字段初始值设定项:在C#中无效,在Java中有效

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

至于为什么该功能在Java中是合法的,您必须询问Java设计师。

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

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

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

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

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

在C#中:

  • 所有实例字段初始值设定项都首先运行,"向上"继承链,从大多数派生到基类。

  • 然后ctors运行,沿着链"向下"运行,从基础到派生。

actor相互调用或(明确地)调用基类的actor的可能性不会改变这种情况,所以我将省略它。

基本上发生的是,这会为链中的每个chass运行,从最派生的开始:

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运行

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

  • 重复前面的步骤,直到到达派生程度最高的类

证据如下:(或者自己在网上运行)

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

这意味着,当字段初始化器运行时,所有继承的字段都已经初始化(由基类中的初始化器OR 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.
    }
}

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

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

这有点不回答,但我喜欢认为类主体中的任何东西都是序列无关的。它不应该是需要以特定方式求值的顺序代码——它只是类的默认状态。如果你使用这样的代码,你希望我在s之前被评估。

不管怎样,你可以让我成为一个常量(应该是),不管怎样。

相关文章: