了解 C# 字段初始化要求

本文关键字:初始化 字段 了解 | 更新日期: 2023-09-27 18:34:15

考虑以下代码:

public class Progressor
{
    private IProgress<int> progress = new Progress<int>(OnProgress);
    private void OnProgress(int value)
    {
        //whatever
    }
}

这会在编译时给出以下错误:

字段

初始值设定项不能引用非静态字段、方法或属性"Progressor.OnProgress(int(">

我理解它抱怨的限制,但我不明白为什么这是一个问题,但可以在构造函数中初始化该字段,如下所示:

public class Progressor
{
    private IProgress<int> progress;
    public Progressor()
    {
         progress =  new Progress<int>(OnProgress);
    }
    private void OnProgress(int value)
    {
        //whatever
    }
}

C# 中关于字段初始化与需要此限制的构造函数初始化有什么区别?

了解 C# 字段初始化要求

字段初始化在基类构造函数调用之前,因此它不是有效的对象。此时,任何以 this 作为参数的方法调用都会导致无法验证的代码,如果不允许不可验证的代码,则会引发VerificationException。例如:在安全透明代码中。

  • 10.11.2 实例变量初始值设定项
    当实例构造函数没有构造函数初始值设定项,或者它具有窗体 base(...( 的构造函数初始值设定项时,该构造函数将隐式执行由其类中声明的实例字段的变量初始值设定项指定的初始化。这对应于在进入构造函数时和隐式调用直接基类构造函数之前立即执行的一系列赋值。变量初始值设定项按它们在类声明中显示的文本顺序执行。
  • 10.11.3 构造函数执行
    变量初始值设定项将转换为赋值语句,这些赋值语句在调用基类实例构造函数之前执行。此排序可确保在执行有权访问该实例的任何语句之前,所有实例字段都由其变量初始值设定项初始化。

我回答中的所有内容都只是我对"为什么允许这种访问会很危险"的想法。我不知道这是否是它受到限制的真正原因。

C# 规范说,字段初始化按类中声明字段的顺序进行:

10.5.5.2. 实例字段初始化

变量初始值设定项按文本顺序执行,其中 它们出现在类声明中。

现在,假设您提到的代码是可能的 - 您可以从字段初始化调用实例方法。这将使以下代码成为可能:

public class Progressor
{
    private string _first = "something";
    private string _second = GetMyString();
    private string GetMyString()
    {
        return "this is really important string";
    }
}

目前为止,一切都好。但是,让我们稍微滥用一下这种权力:

public class Progressor
{
    private string _first = "something";
    private string _second = GetMyString();
    private string _third = "hey!";
    private string GetMyString()
    {
        _third = "not hey!";
        return "this is really important string";
    }
}

因此,_second_third之前初始化。 GetMyString运行,_third得到分配的"not hey!"值,但稍后它自己的字段初始化运行,并设置为"hey!"。不是真的有用,也不是可读性,对吧?

您还可以在GetMyString方法中使用_third

public class Progressor
{
    private string _first = "something";
    private string _second = GetMyString();
    private string _third = "hey!";
    private string GetMyString()
    {
        return _third.Substring(0, 1);
    }
}

你期望_second的价值是多少?好吧,在字段初始化运行之前,所有字段都会获得默认值。对于string来说,它会null,所以你会得到意想不到的NullReferenceException

所以,设计师们认为,防止人们犯这种错误更容易。

你可以说,好吧,让我们禁止访问属性和调用方法,但让我们允许使用在你想要从中访问它的字段上方声明的字段。像这样:

public class Progressor
{
    private string _first = "something";
    private string _second = _first.ToUpperInvariant();
}

但不是

public class Progressor
{
    private string _first = "something";
    private string _second = _third.ToUpperInvariant();
    private string _third = "another";
}

这似乎有用且安全。但是还是有办法滥用它!

public class Progressor
{
    private Lazy<string> _first = new Lazy<string>(GetMyString);
    private string _second = _first.Value;
    private string GetMyString()
    {
        // pick one from above examples
    }
}

所有方法的问题碰巧又回来了。

第 10.5.5.2 节:实例字段初始化描述了以下行为:

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

此行为适用于您的代码,因为它是对正在创建的实例的隐式引用OnProgress

答案或多或少是,C# 的设计者更喜欢这种方式。

由于所有字段初始值设定项都转换为构造函数中的指令,这些指令位于构造函数中的任何其他语句之前,因此没有技术原因说明这是不可能的。所以这是一个设计选择。

构造函数的好处是它清楚地说明了赋值的完成顺序。

请注意,对于static成员,C# 设计器的选择不同。例如:

static int a = 10;
static int b = a;

是允许的,与此不同(也允许(:

static int b = a;
static int a = 10;

这可能会令人困惑。

如果您做出:

partial class C
{
    static int b = a;
}

和其他地方(在其他文件中(:

partial class C
{
    static int a = 10;
}

我什至不认为将会发生什么是明确的。

当然,对于实例字段中具有委托的特定示例字段初始值设定项:

Action<int> progress = OnProgress; // ILLEGAL (non-static method OnProgress)

实际上没有问题,因为它不是非静态成员的读取或调用。而是使用方法信息,它不依赖于任何初始化。但根据 C# 语言规范,它仍然是一个编译时错误。