Yuval 和 David 的回答基本正确;总结:
- 使用未分配的局部变量可能是一个错误,编译器可以以低成本检测到这一点。
- 使用未分配的字段或数组元素不太可能是错误,并且更难在编译器中检测到条件。因此,编译器不会尝试检测未初始化变量对字段的使用,而是依赖于初始化为默认值以使程序行为具有确定性。
David 的回答的评论者询问为什么无法通过静态分析检测未分配字段的使用;这是我想在这个答案中展开的重点。
首先,对于任何变量,无论是局部变量还是其他变量,实际上都无法准确地确定变量是被赋值还是未赋值。考虑:
bool x;
if (M()) x = true;
Console.WriteLine(x);
问题“分配了 x 吗?”相当于“M() 是否返回 true?”现在,假设如果费马大定理对于所有小于 110 万亿的整数都为真,则 M() 返回真,否则返回假。为了确定 x 是否被明确赋值,编译器本质上必须产生一个费马大定理的证明。编译器没那么聪明。
因此,编译器为局部变量所做的是实现一种快速的算法,并且当局部变量未明确分配时高估。也就是说,它有一些误报,它说“我无法证明这个本地是被分配的”,即使你我都知道它是。例如:
bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);
假设 N() 返回一个整数。你和我都知道 N() * 0 将是 0,但编译器不知道这一点。 (注意:C# 2.0 编译器确实知道这一点,但我删除了该优化,因为规范没有说编译器知道这一点。)
好的,那么到目前为止我们知道什么?对于当地人来说,得到一个准确的答案是不切实际的,但我们可以廉价地高估未分配性,并得到一个相当不错的结果,即“让你修复你不清楚的程序”。那挺好的。为什么不对字段做同样的事情?也就是说,做一个明确的高估便宜的分配检查器?
那么,有多少种方法可以初始化局部变量?它可以在方法的文本中分配。它可以在方法文本中的 lambda 内赋值;那个 lambda 可能永远不会被调用,所以这些分配是不相关的。或者它可以作为“out”传递给另一个方法,此时我们可以假设它是在方法正常返回时分配的。这些是分配局部变量的非常明确的点,它们就在与声明局部变量相同的方法中。确定本地人的明确分配只需要本地分析。方法往往很短——一个方法中的代码远少于一百万行——因此分析整个方法非常快。
现在字段呢?当然,字段可以在构造函数中初始化。或字段初始化程序。或者构造函数可以调用初始化字段的实例方法。或者构造函数可以调用初始化字段的 virtual 方法。或者构造函数可以调用一个方法在另一个类中,它可能在一个库中,初始化字段。静态字段可以在静态构造函数中初始化。静态字段可以由 other 静态构造函数初始化。
基本上,字段的初始化器可以在整个程序中的任何地方,包括将在尚未编写的库中声明的虚拟方法:
// Library written by BarCorp
public abstract class Bar
{
// Derived class is responsible for initializing x.
protected int x;
protected abstract void InitializeX();
public void M()
{
InitializeX();
Console.WriteLine(x);
}
}
编译这个库会出错吗?如果是,BarCorp 应该如何修复这个错误?通过为 x 分配一个默认值?但这就是编译器已经做的事情。
假设这个库是合法的。如果 FooCorp 写
public class Foo : Bar
{
protected override void InitializeX() { }
}
那是个错误吗? 编译器应该如何解决这个问题?唯一的方法是进行整个程序分析,跟踪每个字段的初始化静态通过程序的所有可能路径,包括涉及在运行时选择虚拟方法的路径。这个问题可以任意难度;它可能涉及数百万条控制路径的模拟执行。分析本地控制流需要几微秒,并且取决于方法的大小。分析全局控制流可能需要数小时,因为它取决于程序中每个方法和所有库的复杂性。
那么,为什么不进行更便宜的分析,不必分析整个程序,而只是更严重地高估呢?好吧,提出一个可行的算法,它不会使编写一个实际编译的正确程序变得太难,设计团队可以考虑它。我不知道有什么这样的算法。
现在,评论者建议“要求构造函数初始化所有字段”。这不是一个坏主意。事实上,C# 已经为结构提供了该功能,这是一个不错的主意。在ctor正常返回时,需要一个struct构造函数来明确分配所有字段;默认构造函数将所有字段初始化为其默认值。
课程呢?那么,你怎么知道构造函数已经初始化了一个字段? ctor 可以调用 虚拟方法 来初始化字段,现在我们又回到了之前的位置。结构没有派生类;类可能。包含抽象类的库是否需要包含初始化其所有字段的构造函数?抽象类如何知道字段应该初始化为什么值?
John 建议在字段初始化之前简单地禁止调用 ctor 中的方法。所以,总而言之,我们的选择是:
- 使常见、安全、常用的编程习惯成为非法。
- 进行昂贵的整个程序分析,这使得编译需要几个小时才能找到可能不存在的错误。
- 依靠自动初始化为默认值。
设计团队选择了第三个选项。