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

Why do local variables require initialization, but fields do not?

如果我在我的 class 中创建一个布尔值,就像 bool check 一样,它默认为 false。

当我在我的方法 bool check(而不是 class 中)创建相同的 bool 时,我得到一个错误 "use of unassigned local variable check"。为什么?

Why do local variables require initialization, but fields do not?

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

Why do local variables require initialization?

这只不过是 C# 语言的设计决定,如 explained by Eric Lippert。 CLR 和 .NET 环境不需要它。例如,VB.NET 将使用未初始化的局部变量编译得很好,实际上 CLR 将所有未初始化的变量初始化为默认值。

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

Why don't fields require initialization?

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

What about collection types?

C# 对局部变量初始化的执行是有限的,这常常让开发人员望而却步。考虑以下四行代码:

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

第二行代码无法编译,因为它试图读取一个未初始化的字符串变量。第四行代码编译得很好,因为 array 已经初始化,但只有默认值。由于字符串的默认值为 null,我们在 运行 时得到一个异常。任何花时间在 Stack Overflow 上的人都会知道这种 explicit/implicit 初始化不一致会导致很多 "Why am I getting a “Object reference not set to an instance of an object” error?" 问题。

When I create the same bool within my method, bool check(instead of within the class), i get an error "use of unassigned local variable check". Why?

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

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

Eric Lippert 谈到这个 in a blog post:

The reason why we want to make this illegal is not, as many people believe, because the local variable is going to be initialized to garbage and we want to protect you from garbage. We do in fact automatically initialize locals to their default values. (Though the C and C++ programming languages do not, and will cheerfully allow you to read garbage from an uninitialized local.) Rather, it is because the existence of such a code path is probably a bug, and we want to throw you in the pit of quality; you should have to work hard to write that bug.

为什么这不适用于 class 字段?好吧,我假设必须在某处画线,并且与 class 字段相反,局部变量初始化更容易诊断和正确。编译器 可以 这样做,但是想一想它需要进行的所有可能的检查(其中一些检查独立于 class 代码本身)以便评估如果 class 中的每个字段都已初始化。我不是编译器设计者,但我确信它肯定 更难 因为有很多情况需要考虑,并且必须及时 完成时尚也是。对于您必须设计、编写、测试和部署的每个功能,与付出的努力相比,实现这些功能的价值将是不值得和复杂的。

Yuval和David的回答基本正确;总结:

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

David 回答的评论者询问为什么无法通过静态分析检测未分配字段的使用;这是我想在这个答案中展开的重点。

首先,对于任何变量,无论是局部变量还是其他变量,实际上不可能准确地确定一个变量是赋值还是未赋值。考虑:

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

问题 "is x assigned?" 等同于 "does M() return true?" 现在,假设 M() returns 如果费马大定理对所有小于 110 亿的整数都为真,则为真,否则为假。为了确定 x 是否明确赋值,编译器本质上必须产生费马大定理的证明。编译器没那么聪明

因此,编译器为局部变量所做的是实现一种算法,该算法快速,并且当局部变量未明确分配时高估 .也就是说,它有一些误报,它说 "I can't prove that this local is assigned" 即使你我都知道。例如:

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

假设 N() returns 是一个整数。你我都知道 N() * 0 将是 0,但编译器并不知道。 (注意:C# 2.0 编译器 did 知道,但我删除了那个优化,因为规范没有 编译器知道。)

好吧,到目前为止我们知道了什么?当地人得到一个确切的答案是不切实际的,但我们可以廉价地高估未分配的度,并得到一个相当不错的结果,但在 "make you fix your unclear program" 方面出错。那挺好的。为什么不对字段做同样的事情?也就是说,做一个便宜的高估的明确赋值检查器?

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

现在字段呢?当然,可以在构造函数中初始化字段。或字段初始值设定项。或者构造函数可以调用初始化字段的实例方法。或者构造函数可以调用初始化字段的 virtual 方法。或者构造函数可以调用另一个 class 中的方法 ,它可能是 中的方法,用于初始化字段。静态字段可以在静态构造函数中初始化。静态字段可以由 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() { } 
}

是个错误吗? 编译器应该如何解决这个问题?唯一的方法是做一个整个程序分析来跟踪[=50=的初始化静态]每个字段 通过程序的每条可能路径,包括涉及运行时虚拟方法选择的路径。这道题可以任意难;它可能涉及数百万条控制路径的模拟执行。分析本地控制流需要几微秒,并且取决于方法的大小。分析全局控制流可能需要几个小时,因为它取决于 程序和所有库中每个方法的复杂性

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

现在,评论者建议"require that a constructor initialize all fields"。这不是一个坏主意。事实上,C# 已经为结构 提供了该功能,这是一个不错的主意。通常需要结构构造函数在 ctor returns 之前明确分配所有字段;默认构造函数将所有字段初始化为其默认值。

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

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

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

设计组选择了第三个方案。

上面的答案很好,但我想我会 post 很多 simpler/shorter 答案,因为人们懒得阅读长篇文章(比如我)。

Class

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

来自手册:

The C# compiler does not allow the use of uninitialized variables. If the compiler detects the use of a variable that might not have been initialized, it generates compiler error CS0165. For more information, see Fields (C# Programming Guide). Note that this error is generated when the compiler encounters a construct that might result in the use of an unassigned variable, even if your particular code does not. This avoids the necessity of overly-complex rules for definite assignment.

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