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

时间:2015-06-13 08:07:48

标签: c# language-design local-variables

如果我在班级中创建一个bool,就像<div class="container" ng-app="myApp"> <div class="content" ng-controller="count"> <h1 ng-click="animate()">Click ME</h1> <h2>Let me Fade</h2> </div> </div> <div class="container" ng-app="myApp"> <div class="content" ng-controller="count"> <h1 ng-click="animate()">Click ME</h1> <h2>Let me Fade</h2> </div> </div> 那样,它默认为false。

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

4 个答案:

答案 0 :(得分:178)

Yuval和David的答案基本上是正确的;总结:

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

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

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

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

问题&#34;是否已分配x?&#34;相当于&#34; M()是否返回true?&#34;现在,假设如果费马的最后定理对于小于十一万亿的所有整数都为真,则M()返回true,否则返回false。为了确定x是否明确赋值,编译器必须基本上产生费马最后定理的证明。编译器并不那么聪明。

因此,编译器为本地人做的是实现 fast 的算法,并且当没有明确分配本地时,高估。也就是说,它有一些误报,它表示&#34;我不能证明这个本地被分配了#34;即使你和我知道它是。例如:

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

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

好的,到目前为止我们知道什么?对于当地人来说,获得一个确切的答案是不切实际的,但是我们可以低估高估未分配的结果并获得一个非常好的结果,这样可以让你修复你不明确的程序&#34;。这很好。为什么不对田地做同样的事情?也就是说,制作一个低估的过高价格的明确作业检查员?

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

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

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

// 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如何修复这个bug?通过为x分配默认值?但这就是编译器已经做的事情。

假设此库是合法的。如果FooCorp写

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

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

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

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

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

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

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

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

答案 1 :(得分:28)

  

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

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

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

Eric Lippert谈到这个in a blog post

  

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

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

答案 2 :(得分:26)

  

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

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

  

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

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

答案 3 :(得分: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