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

nac*_*ime 139 c# language-design local-variables

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

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

Eri*_*ert 177

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

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

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

首先,对于任何变量,本地或其他变量,实际上不可能确切地确定变量是分配还是未分配.考虑:

bool x;
if (M()) x = true;
Console.WriteLine(x);
Run Code Online (Sandbox Code Playgroud)

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

因此,编译器为本地人做的是实现一种快速的算法,并且高估了本地未明确分配的时间.也就是说,它有一些误报,即使你和我知道它是"我无法证明这个本地被分配".例如:

bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);
Run Code Online (Sandbox Code Playgroud)

假设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); 
    }
}
Run Code Online (Sandbox Code Playgroud)

编译这个库是错误的吗?如果是的话,BarCorp如何修复这个bug?通过为x分配默认值?但这就是编译器已经做的事情.

假设这个图书馆是合法的.如果FooCorp写道

public class Foo : Bar
{
    protected override void InitializeX() { } 
}
Run Code Online (Sandbox Code Playgroud)

那个错误?编译器应该如何解决这个问题?唯一的方法是进行整个程序分析,跟踪程序中每个可能路径每个字段的初始化静态,包括在运行时选择虚拟方法的路径.这个问题可以随心所欲 ; 它可能涉及数百万个控制路径的模拟执行.分析本地控制流需要几微秒,并取决于方法的大小.分析全局控制流可能需要数小时,因为它取决于程序和所有库每个方法的复杂性.

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

现在,评论者建议"要求构造函数初始化所有字段".这不是一个坏主意.事实上,C#已经为结构体提供了这个功能,这是一个不错的主意.在ctor正常返回时,需要一个结构构造函数来明确地指定所有字段; 默认构造函数将所有字段初始化为其默认值.

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

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

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

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

  • @ durron597:因为经验表明忘记为本地分配值可能是一个错误.如果它可能是一个错误*和*它便宜且易于检测,那么就有很好的动机使这种行为成为非法或警告. (8认同)

Yuv*_*kov 27

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

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

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

Eric Lippert 在一篇博文中谈到了这一点:

正如许多人所认为的那样,我们之所以想让这个非法,是因为本地变量将被初始化为垃圾,我们希望保护您免受垃圾侵害.事实上,我们会自动将locals初始化为默认值.(虽然C和C++编程语言没有,并且会愉快地允许你从未初始化的本地读取垃圾.)相反,这是因为这样的代码路径的存在可能是一个bug,我们想把你扔进去质量坑; 你应该努力写出那个bug.

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


Dav*_*rno 25

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

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

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

这不过是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;
Run Code Online (Sandbox Code Playgroud)

第二行代码不会编译,因为它试图读取未初始化的字符串变量.第四行代码编译得很好,虽然array已初始化,但只能使用默认值.由于字符串的默认值为null,因此我们在运行时获得异常.任何在Stack Overflow上花费时间的人都会知道这种显式/隐式初始化不一致会导致很多"为什么我得到一个"对象引用未设置为对象的实例"错误?" 的问题.

  • 这不公平.我们试图在这里产生分歧! (4认同)

Rea*_*lar 10

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

class Foo {
    private string Boo;
    public Foo() { /** bla bla bla **/ }
    public string DoSomething() { return Boo; }
}
Run Code Online (Sandbox Code Playgroud)

属性Boo可能已经或可能在构造函数中初始化.所以当它发现return Boo;它并不假设它已被初始化时.它只是抑制了错误.

功能

public string Foo() {
   string Boo;
   return Boo; // triggers error
}
Run Code Online (Sandbox Code Playgroud)

{ }字符定义的一个代码块的范围.编译器遍历这些{ }块的分支,跟踪内容.它可以很容易地告诉我Boo没有初始化.然后触发错误.

为什么错误存在?

引入该错误是为了减少使源代码安全所需的代码行数.如果没有错误,上面的内容将会是这样的.

public string Foo() {
   string Boo;
   /* bla bla bla */
   if(Boo == null) {
      return "";
   }
   return Boo;
}
Run Code Online (Sandbox Code Playgroud)

从手册:

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

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