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


如果我在课堂上创建一个bool,就像是 bool check,默认为false。

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


140
2018-06-13 08:07


起源


评论不适用于扩展讨论;这次谈话已经开始了 转移到聊天。 - Martijn Pieters♦
问题很模糊。将“因为规范如此说明”是一个可以接受的答案吗? - Eric Lippert
因为这是他们复制时在Java中完成的方式。 :P - Alvin Thompson


答案:


Yuval和David的答案基本上是正确的;加起来:

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

大卫答案的评论者问为什么不可能通过静态分析来检测未分配场的使用;这是我想在这个答案中扩展的观点。

首先,对于任何变量,本地或其他变量,实际上无法确定 究竟 是否已分配变量或未分配变量。考虑:

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

问题“是x分配?”相当于“确实M()返回true?”现在,假设如果Fermat的Last Theorem对于小于elegyy的所有整数都为真,则M()返回true,否则返回false。为了确定x是否明确赋值,编译器必须基本上产生Fermat最后定理的证明。编译器并不那么聪明。

那么编译器为locals做的是实现一个算法 快速,和 高估 当一个本地没有明确分配。也就是说,它有一些误报,即使你和我知道它是“我无法证明这个本地被分配”。例如:

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

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

好的,到目前为止我们知道什么?对于当地人来说,获得一个确切的答案是不切实际的,但我们可以低估高估未分配,并得到一个非常好的结果,错误地“让你修复你不明确的程序”。那很好。为什么不对田地做同样的事情?也就是说,做一个低估的高估的任务检查员?

那么,有多少种方法可以对本地进行初始化?它可以在方法的文本中分配。它可以在方法文本中的lambda中分配;可能永远不会调用lambda,因此这些赋值不相关。或者它可以作为“out”传递给另一个方法,此时我们可以假设它在方法正常返回时被赋值。那些是非常明确的地方被分配的点,他们是 就在那里宣布本地的方法相同。确定当地人的明确分配只需要 局部分析。方法往往很短 - 方法中远不到一百万行代码 - 因此分析整个方法非常快。

现在呢?当然,可以在构造函数中初始化字段。或者字段初始化程序。或者构造函数可以调用初始化字段的实例方法。或者构造函数可以调用 虚拟 初始化字段的方法。或者构造函数可以调用方法 在另一堂课,可能是 在图书馆里,初始化字段。静态字段可以在静态构造函数中初始化。静态字段可以初始化 其他 静态构造函数。

基本上,字段的初始化程序可以是 在整个计划的任何地方包括里面 将在尚未编写的库中声明的虚方法

// 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正常返回时,需要一个结构构造函数来明确地指定所有字段;默认构造函数将所有字段初始化为其默认值。

课程怎么样?好, 你怎么知道一个构造函数初始化了一个字段? ctor可以打电话给 虚方法 初始化字段,现在我们回到了以前的位置。结构没有派生类;班级可能。是一个包含抽象类的库,它包含一个初始化其所有字段的构造函数吗?抽象类如何知道应该将字段初始化为什么值?

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

  • 使普通,安全,常用的编程习语成为非法。
  • 做一个昂贵的整个程序分析,使编译花费数小时,以寻找可能不存在的错误。
  • 依靠自动初始化为默认值。

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


178
2018-06-13 15:03



很好的答案,像往常一样。但我有个问题: 为什么不自动为局部变量分配默认值? 换句话说,为什么不做 bool x; 相当于 bool x = false;  甚至在一个方法里面? - durron597
@ durron597:因为经验表明忘记为本地分配值可能是一个错误。如果它可能是一个bug 和 它便宜且易于检测,因此有很好的动机使这种行为成为非法或警告。 - Eric Lippert


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

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

将变量初始化为 false 在这个特定的执行路径中改变什么?可能不是,考虑一下 default(bool) 无论如何都是假的,但它迫使你成为 知道的 这种情况正在发生。 .NET环境阻止您访问“垃圾内存”,因为它会将任何值初始化为其默认值。但是,假设这是一个引用类型,并且您将未初始化的(null)值传递给期望非null的方法,并在运行时获取NRE。编译器只是试图阻止它,接受这可能有时导致的事实 bool b = false 声明。

Eric Lippert谈到了这一点 在博客文章中

我们之所以想让这个非法,就像许多人一样   相信,因为局部变量将被初始化为   垃圾,我们想保护你免受垃圾。我们其实是这样做的   自动将locals初始化为默认值。 (虽然是C   和C ++编程语言没有,并且会愉快地允许你   从未初始化的本地读取垃圾。)相反, 这是因为   这样的代码路径的存在可能是一个bug,我们想抛出   你在质量的坑里;你应该努力写出来   错误。

为什么这不适用于课堂领域?好吧,我认为必须在某处绘制线,并且局部变量初始化更容易诊断和正确,而不是类字段。编译器 可以 这样做,但想想它需要进行的所有可能的检查(其中一些是独立于类代码本身的),以便评估一个类中的每个字段是否被初始化。我不是编译器设计师,但我相信它肯定会 更难 因为有很多案例被考虑在内,而且必须在一个案例中完成 及时时尚 同样。对于您必须设计,编写,测试和部署的每个功能,实现此功能的价值与投入的功能相反,将是不值得和复杂的。


28
2018-06-13 08:33



“想象这是一个引用类型,你将这个未初始化的对象传递给一个期望初始化的方法”你的意思是:“想象这是一个引用类型,你传递的是默认值(null)而不是引用目的”? - Deduplicator
@Deduplicator是的。期望非空值的方法。编辑那部分。希望现在更清楚了。 - Yuval Itzchakov
我不认为这是因为画线。每个类都假设有一个构造函数,至少是默认的构造函数。因此,当您坚持使用默认构造函数时,您将获得默认值(安静透明)。在定义构造函数时,您应该或者应该知道您在其中执行的操作以及您希望以何种方式初始化哪些字段,包括默认值的知识。 - Peter
相反:方法中的字段可以通过在不同的执行路径中声明和赋值。在您查看可能使用的框架的文档之前,甚至在您可能无法维护的代码的其他部分中,可能会有一些容易监视的异常。这可以引入非常复杂的执行路径。因此编译器提示。 - Peter
@Peter我真的不明白你的第二条评论。关于第一个,不需要初始化构造函数中的任何字段。这很常见 实践。编译器的工作不是强制执行这样的做法。你不能依赖运行的构造函数的任何实现,并说“好吧,所有字段都很好”。埃里克在他的答案中阐述了很多关于如何初始化一个类的领域的方法,并展示了它如何需要一个 很长时间 计算所有逻辑方式的初始化。 - Yuval Itzchakov


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

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

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

这只不过是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上花费时间的人都会知道这种显式/隐式初始化不一致会导致很多“为什么我得到一个”对象引用未设置为对象的实例“错误?”的问题。


26
2018-06-13 08:29



“编译器不能使用静态分析来确定通过代码的每条可能路径是否导致变量在我们之前被显式初始化。”我不相信这是真的。你能发布一个抗静态分析程序的例子吗? - John Kugelman
@JohnKugelman,考虑一下这个简单的例子 public interface I1 { string str {get;set;} }和方法 int f(I1 value) { return value.str.Length; }。如果这存在于库中,则编译器无法知道该库将链接到哪个,因此是否存在 set 将在之前被召唤 get,可能没有显式初始化支持字段,但它必须编译此类代码。 - David Arno
这是真的,但我不希望在编译时产生错误 f。它将在编译构造函数时生成。如果你留下一个可能未初始化的字段的构造函数,那将是一个错误。在初始化所有字段之前,可能还必须对调用类方法和getter进行限制。 - John Kugelman
@JohnKugelman:我会发一个答案,讨论你提出的问题。 - Eric Lippert
这不公平。我们试图在这里产生分歧! - John Kugelman


上面的答案很好,但我想我会发布一个更简单/更简短的答案,让人们懒得读长篇(像我一样)。

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


10
2018-06-22 16:22