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


140

如果我在班级内创建布尔bool check值,则类似,则默认为false。

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


评论不作进一步讨论;此对话已转移至聊天
马丁·彼得斯

14
问题很模糊。是否会因为“规范如此说明”而接受?
埃里克·利珀特

4
因为那是他们复制Java时在Java中完成的方式。:P
Alvin Thompson

Answers:


177

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

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

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可能永远不会被调用,因此这些分配是不相关的。或者可以将其作为“输出”传递给另一个方法,此时我们可以假定在方法正常返回时已对其进行分配。这些是分配了本地的非常清楚的点,并且就在那里,与声明本地的方法相同。确定本地人的确定分配只需要本地分析。方法往往很短-远远少于一种方法中的一百万行代码-因此分析整个方法非常快。

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

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

// 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的功能,这是一个不错的主意。要求构造函数构造函数在ctor正常返回之前明确分配所有字段;默认构造函数将所有字段初始化为其默认值。

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

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

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

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


1
像往常一样好答案。但是我有一个问题:为什么还不自动将默认值分配给局部变量?换句话说,为什么不bool x;等于bool x = false; 在方法内部
durron597

8
@ durron597:因为经验表明,忘记为本地变量赋值可能是一个错误。如果它可能是一个错误并且便宜且易于检测,则有很好的动机将这种行为设为非法或警告。
Eric Lippert 2015年

27

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

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

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

埃里克·利珀特(Eric Lippert)在博客文章中谈到了这一点:

正如许多人所相信的那样,我们之所以要使它非法是不合法的,因为该局部变量将被初始化为垃圾,而我们希望保护您免受垃圾的侵害。实际上,我们会自动将本地变量初始化为其默认值。(尽管C和C ++编程语言没有,并且会很乐意让您从未初始化的本地读取垃圾。)相反,这是因为存在这样的代码路径可能是一个错误,我们希望将您扔到质量坑 您应该努力编写该错误。

为什么这不适用于课程领域?好吧,我认为这条线必须绘制在某个地方,并且与类字段相反,局部变量初始化更容易诊断和正确处理。编译器可以执行此操作,但是要考虑它可能需要进行的所有可能的检查(其中一些检查与类代码本身无关),以便评估是否初始化了类中的每个字段。我不是编译器设计人员,但是我敢肯定,因为要考虑很多情况,而且一定要及时完成,这肯定会很困难。对于您必须设计,编写,测试和部署的每个功能,与付出的努力相比,实现此功能的价值将是不值得且复杂的。


“想象这是一个引用类型,您将把这个未初始化的对象传递给一个期望一个已初始化对象的方法。”您的意思是:“想象这是一个引用类型,并且您正在传递默认值(null)而不是一个引用。目的”?
Deduplicator

@Deduplicator是的。一种期待非空值的方法。编辑了该部分。希望现在更加清楚。
Yuval Itzchakov

我不认为这是因为画线。每个类都假定有一个构造函数,至少是默认构造函数。因此,当您坚持使用默认构造函数时,您将获得默认值(安静透明)。在定义构造函数时,您应该或应该知道您在其中进行的操作以及要以何种方式初始化哪些字段(包括默认值的知识)。
Peter Peter

相反:方法中的字段可以通过在不同的执行路径中声明和分配值来实现。在您查看可能使用的框架的文档甚至是您可能不维护的代码的其他部分之前,可能会有容易监督的异常。这会引入非常复杂的执行路径。因此,编译器会提示。
彼得

@Peter我不太明白你的第二条评论。关于第一个,不需要初始化构造函数中的任何字段。这是一种常见的做法。编译器的工作不是强制执行这种做法。您不能依靠正在运行的构造函数的任何实现来说“好,所有字段都可以使用”。埃里克(Eric)在有关如何初始化类的字段的方式中详细阐述了答案,并展示了计算所有逻辑方式初始化将花费很长时间的方法。
Yuval Itzchakov

25

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

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

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

正如Eric Lippert解释的,这仅仅是C#语言的设计决策。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上花费时间的人都会知道,这种显式/隐式初始化不一致会导致很多“为什么我会收到“对象引用未设置为对象实例”的错误?” 问题。


“编译器不能使用静态分析来确定通过代码的所有可能路径是否导致变量在我们之前被显式初始化。” 我不相信这是真的。您可以发布一个可以抵抗静态分析的程序示例吗?
John Kugelman 2015年

@JohnKugelman,考虑public interface I1 { string str {get;set;} }和方法的简单情况int f(I1 value) { return value.str.Length; }。如果库中存在此库,则编译器将无法知道该库将链接至哪个库,因此是否set会在之前调用get,将不会显式初始化backing字段,但必须编译此类代码。
David Arno

没错,但是我不希望编译时会产生错误f。它将在编译构造函数时生成。如果您将可能未初始化的字段留给构造函数,那将是一个错误。在初始化所有字段之前,可能还必须限制调用类方法和获取方法。
John Kugelman 2015年

@JohnKugelman:我将发布一个答案,讨论您提出的问题。
埃里克·利珀特

4
这不公平。我们试图在这里有分歧!
John Kugelman 2015年

10

上面有不错的答案,但我想我会发布一个更简单/简短的答案,让人们懒于阅读较长的答案(例如我自己)。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.