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

如果我在类中创建一个 bool,就像 bool check一样,它默认为 false。

当我在我的方法 bool check(而不是在类中)中创建相同的 bool 时,我得到一个错误“ use of un 使用未分配的局部變量 check”。为什么?

18095 次浏览

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

简而言之,编译器可以使用静态分析以可靠的方式检测到访问未初始化局部变量的代码。而字段不是这种情况。因此编译器强制执行第一种情况,而不是第二种情况。

为什么局部变量需要初始化?

这不过是 C # 语言的一个设计决策,如 由 Eric Lippert 解释。CLR 和。NET 环境不需要它。例如,使用未初始化的本地变量,VB.NET 就可以很好地进行编译,而实际上,CLR 将所有未初始化的变量初始化为默认值。

同样的情况也可能发生在 C # 上,但是语言设计人员选择不这样做。原因是初始化的变量是错误的巨大来源,因此,通过强制初始化,编译器有助于减少意外错误。

为什么字段不需要初始化?

那么,为什么这种强制的显式初始化不会发生在类中的字段中呢?仅仅因为显式的初始化可能在构造期间发生,通过对象初始化程序调用的属性,甚至在事件发生很久之后调用的方法。编译器不能使用静态分析来确定是否每个可能的路径通过代码导致变量被显式地初始化在我们之前。如果出现错误就会很烦人,因为开发人员可能会得到无法编译的有效代码。所以 C # 根本不会强制执行它,如果没有显式设置,CLR 会自动将字段初始化为默认值。

那收集类型呢?

C # 对本地变量初始化的执行是有限的,这经常会让开发人员措手不及:

string str;
var len1 = str.Length;
var array = new string[10];
var len2 = array[0].Length;

第二行代码无法编译,因为它试图读取一个未初始化的字符串变量。第四行代码编译得很好,因为 array已经初始化,但是只使用默认值。因为字符串的默认值为 null,所以我们在运行时会得到一个异常。任何在 Stack Overflow 上花时间的人都会知道,这种显式/隐式初始化不一致会导致很多“为什么我得到一个‘对象引用没有设置为对象的实例’”的错误问题。

当我在我的方法中创建相同的 bool 时,bool check (而不是 在类中) ,我得到一个错误“使用未赋值的局部变量 为什么?

因为编译器试图防止您犯错误。

将变量初始化为 false是否会改变这个特定执行路径中的任何内容?可能不会,考虑到 default(bool)是错误的无论如何,但它迫使你是 意识到了,这是正在发生的。那个。NET 环境阻止您访问“垃圾内存”,因为它将初始化任何值到它们的默认值。但是,仍然可以假设这是一个引用类型,并且将一个未初始化的(null)值传递给一个期望非 null 的方法,并在运行时获得一个 NRE。编译器只是试图阻止这种情况,接受这种情况有时可能导致 bool b = false语句的事实。

埃里克•利伯特(Eric Lippert)谈到了 在一篇博客文章里:

我们之所以要把这个定为非法,不是因为有很多人 相信,因为局部变量将被初始化为 垃圾,我们想保护你远离垃圾。事实上,我们做的 自动将局部变量初始化为它们的默认值 和 C + + 编程语言没有,并将乐于让您 从未初始化的本地读取垃圾。)更确切地说,< strong > 是因为 存在这样的代码路径可能是一个错误,我们想抛出 你陷入了质量的泥潭; 你应该努力工作才能写出那样的东西 错误

为什么这不适用于类字段?我假设这条线必须画在某个地方,局部变量初始化比类字段更容易诊断和获得正确的结果。编译器 可以执行此操作,但是想想它需要进行的所有可能的检查(其中一些检查与类代码本身无关) ,以便计算类中的每个字段是否已初始化。我不是编译器设计者,但是我确信它肯定是 用力点,因为有很多情况需要考虑,而且也必须在 时尚中完成。对于您必须设计、编写、测试和部署的每个特性,与投入的精力相比,实现这些特性的价值将是毫无价值和复杂的。

尤瓦尔和大卫的回答基本上是正确的,总结如下:

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

一个评论者对 David 的回答提出疑问,为什么不可能通过静态分析来检测未分配字段的使用; 这就是我想在这个答案中进一步阐述的观点。

首先,对于任何变量,无论是局部变量还是其他变量,实际上都不可能确定 没错是否赋值或未赋值变量。考虑一下:

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

问题是“ x 被分配了吗?”等价于“ does M () return true?”现在,假设 M ()返回 true,如果费马最后定理对所有小于110亿的整数都为真,否则为假。为了确定 x 是否被明确赋值,编译器必须从本质上证明费马最后定理。编译器没有那么聪明。

因此,编译器对局部变量所做的就是实现一个算法,即 很快高估了,当局部变量没有被明确赋值时。也就是说,它有一些假阳性,它说“我不能证明这个本地分配”,即使你和我都知道它是。例如:

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

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

好吧,目前我们知道些什么?局部变量得到一个精确的答案是不切实际的,但是我们可以低成本地高估非分配性,并且得到一个相当好的结果,这个结果错误在“让你修复你不清楚的程序”的一边。很好。为什么不对田地做同样的事情呢?也就是说,做一个明确的高估低估的任务检查器?

有多少种方法可以初始化局部变量?它可以在方法的文本中分配。它可以在方法文本的 lambda 中分配; 该 lambda 可能永远不会被调用,因此这些分配不相关。或者它可以作为“ out”传递给另一个方法,这时我们可以假设它在方法正常返回时被赋值。这些是非常清楚的本地分配点,它们是 在声明 local 的同一个方法中。确定局部变量的明确分配只需要 局部分析局部分析。方法往往很短——远远少于方法中的100万行代码——因此分析整个方法非常快。

那田地呢?字段当然可以在构造函数中初始化。或字段初始值设定项。或者构造函数可以调用初始化字段的实例方法。或者构造函数可以调用初始化字段的 虚拟的方法。或者构造函数可以调用方法 在另一个班(可能是 在图书馆里)来初始化字段。静态字段可以在静态构造函数中初始化。静态字段可以由 其他静态构造函数初始化。

本质上,字段的初始化器可以是 在整个项目中的任何地方,包括 将在尚未编写的库中声明的虚方法内部:

// 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);
}
}

编译这个库是一个错误吗?如果是的话,巴克公司该如何修复漏洞?通过给 x 赋一个默认值?但编译器已经这么做了。

假设这个库是合法的,如果 FooCorp 写

public class Foo : Bar
{
protected override void InitializeX() { }
}

那个是一个错误吗?唯一的方法是做一个 整个程序分析整个程序分析,跟踪 所有可能的途径每一个领域的静态初始化,包括涉及 在运行时选择虚方法的路径。这个问题可以是 非常难; 它可以涉及数百万个控制路径的模拟执行。分析本地控制流需要花费微秒,这取决于方法的大小。分析全局控制流可能需要几个小时,因为这取决于 程序中的每一个方法和所有的库的复杂性。

那么,为什么不做一个更便宜的分析,而不必分析整个程序,只是高估更严重?那么,提出一个可行的算法,不会让编写一个实际编译的正确程序变得太困难,并且设计团队可以考虑它。我不知道任何这样的算法。

现在,评论者建议“要求构造函数初始化所有字段”。这主意不错。事实上,这是一个不错的想法,C # 已经为结构提供了这个特性。当 ctor 正常返回时,struct 构造函数需要明确地分配所有字段,缺省构造函数将所有字段初始化为它们的默认值。

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

John 建议在字段初始化之前禁止调用 ctor 中的方法。所以,总的来说,我们的选择是:

  • 使常见的、安全的、经常使用的编程习惯用法成为非法。
  • 进行一次昂贵的全程序分析,使编译花费数小时,以便查找可能不存在的 bug。
  • 依赖于默认值的自动初始化。

设计团队选择了第三种方案。

上面的答案不错,但我觉得我应该给那些懒得读长篇大论的人(比如我)提供一个更简单、更短的答案。

同学们

class Foo {
private string Boo;
public Foo() { /** bla bla bla **/ }
public string DoSomething() { return Boo; }
}

属性 Boo可能或可能已经在构造函数中初始化了 没有。所以当它找到 return Boo;的时候,它并没有初始化 假设。它只是 压抑的错误。

功能

public string Foo() {
string Boo;
return Boo; // triggers error
}

{ }字符定义代码块的作用域。编译器遍历这些 { }块的分支以保持对内容的跟踪。它可以告诉 很容易Boo没有被初始化。然后触发错误。

错误为什么存在?

引入该错误是为了减少确保源代码安全所需的代码行数。如果没有这个错误,上面看起来就是这样的。

public string Foo() {
string Boo;
/* bla bla bla */
if(Boo == null) {
return "";
}
return Boo;
}

手册上说:

C # 编译器不允许使用未初始化的变量。如果编译器检测到使用了一个可能尚未初始化的变量,它将生成编译器错误 CS0165。有关更多信息,请参见字段(C # 编程指南)。请注意,当编译器遇到可能导致使用未赋值变量的结构时,即使您的特定代码没有遇到该结构,也会生成此错误。这就避免了为确定的赋值而使用过于复杂的规则的必要性。

参考资料: https://msdn.microsoft.com/en-us/library/4y7h161d.aspx