一尘不染

为什么局部变量需要初始化,而字段却不需要初始化?

c#

如果我在课堂上创建了布尔bool check值,则类似,则默认为false。

当我在方法中bool check(而不是在类中)创建相同的布尔值时,出现错误“使用未分配的局部变量检查”。为什么?


阅读 480

收藏
2020-05-19

共1个答案

一尘不染

尤瓦尔和戴维的答案基本上是正确的。加起来:

  • 使用未分配的局部变量是一个可能的错误,编译器可以低成本检测到该错误。
  • 使用未分配的字段或数组元素不太可能产生错误,并且更难在编译器中检测到这种情况。因此,编译器不会尝试检测对字段使用未初始化的变量,而是依靠初始化为默认值来使程序行为具有确定性。

David的评论者问,为什么不可能通过静态分析检测到未分配字段的使用;这就是我要在此答案中扩展的要点。

首先,对于任何变量,无论是局部变量还是其他变量,在实践中都无法 确切 确定变量是已分配还是未分配。考虑:

bool x;
if (M()) x = true;
Console.WriteLine(x);

问题“ x被分配了?” 等效于“ M()返回true吗?”
现在,假设如果Fermat的Last定理对于所有小于等于零的十亿亿个整数的整数为true,则M()返回true,否则为false。为了确定x是否被明确赋值,编译器必须从本质上产生费马最后定理的证明。编译器不是那么聪明。

因此,编译器为locals做的事情是实现一种算法 fast ,并在未明确分配local时 高估
了算法。也就是说,它具有一些误报,即使您和我都知道,它也会说“我无法证明此本地人已分配”。例如:

bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);

假设N()返回一个整数。您和我都知道N()* 0将为0,但是编译器并不知道。(注:C#2.0编译器 确实 知道,但我删除了优化,规范没有
的是,编译器知道。)

好吧,那么到目前为止我们知道什么?对于当地人来说,要获得确切的答案是不切实际的,但是我们可以廉价地高估未分配的值,并获得相当不错的结果,使您犯了“使您修复不清楚的程序”的错误。那很好。为什么不对字段做同样的事情?就是说,做一个确定的检查器来便宜地高估?

那么,本地有几种初始化方式?可以在方法的文本中分配它。可以在方法文本中的lambda中分配它;该lambda可能永远不会被调用,因此这些分配是不相关的。或者可以将它作为“输出”传递给anothe方法,此时我们可以假定在方法正常返回时分配了它。这些是分配本地变量的非常清楚的点,并且就在
那里,与声明本地变量的方法相同 。确定本地人的确定分配只需要 本地分析 。方法往往很短-远远少于一种方法中的一百万行代码-因此分析整个方法非常快。

现在字段呢?字段当然可以在构造函数中初始化。或字段初始化程序。或者构造函数可以调用实例方法来初始化字段。或者,构造函数可以调用 虚拟化 字段的 虚拟
方法。或者构造函数可以 在另一个类 (可能 在库中)中 调用方法,以初始化字段。静态字段可以在静态构造函数中初始化。静态字段可以由 其他
静态构造函数初始化。

本质上,字段的初始化 程序可以在整个程序中的任何位置 ,包括 将在尚未编写的库中声明的虚拟方法 内部:

// 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#已经具有针对struct的功能
,这是一个不错的主意。需要一个struct构造函数在ctor正常返回时明确分配所有字段。默认构造函数将所有字段初始化为其默认值。

那班呢?好吧, 您怎么知道构造函数已经初始化了一个字段 ?ctor可以调用一个 虚拟方法
来初始化字段,现在我们回到了以前的位置。结构没有派生类。类可能。一个包含抽象类的库是否需要包含一个初始化其所有字段的构造函数?抽象类如何知道应将字段初始化为什么值?

John建议在字段初始化之前简单地禁止在ctor中调用方法。综上所述,我们的选择是:

  • 将常见,安全,常用的编程习惯用法定为非法。
  • 进行昂贵的全程序分析,使编译过程耗时数小时,以查找可能不存在的错误。
  • 依靠自动初始化为默认值。

设计团队选择了第三个选项。

2020-05-19