为什么局部variables需要初始化,但字段不?
如果我在我的类中创build一个布尔,就像bool check
,它默认为false。
当我在我的方法中创build相同的布尔, bool check
(而不是在类中),我得到一个错误“使用未分配的局部variables检查”。 为什么?
Yuval和David的回答基本上是正确的; 加起来:
- 使用未分配的局部variables是一个可能的错误,编译器可以以低成本检测到这一点。
- 使用未分配的字段或数组元素不太可能是一个错误,并且在编译器中检测到这种情况更加困难。 因此,编译器不会尝试检测字段的未初始化variables的使用情况,而是依赖初始化设置为默认值,以确保程序行为的确定性。
David的回答的一位评论者问,为什么通过静态分析检测未分配字段的使用是不可能的。 这是我想在这个答案中扩展的一点。
首先,对于任何variables,本地或其他variables,实际上不可能确定variables是否被分配或未被分配。 考虑:
bool x; if (M()) x = true; Console.WriteLine(x);
问题是“是否分配?” 相当于“M()是否返回true” 现在,如果费马最后定理对于所有小于十亿gajillion的整数是真的,则M()返回真,否则假。 为了确定x是否被明确赋值,编译器必须基本上产生费马大定理的certificate。 编译器不是那么聪明。
那么编译器为本地代码所做的是实现一个快速的algorithm,并且高估当一个本地没有明确分配的时候。 也就是说,它有一些误报,它说“我不能certificate这个地方是分配的”,即使你我知道它是。 例如:
bool x; if (N() * 0 == 0) x = true; Console.WriteLine(x);
假设N()返回一个整数。 你和我知道N()* 0将是0,但编译器不知道。 (注意:C#2.0编译器确实知道,但是我删除了这个优化,因为规范并没有说编译器知道这一点)。
好,那么到目前为止我们知道什么? 当地人得到一个确切的答案是不切实际的,但是我们可以低估低估未被分配的东西,并得到一个相当好的结果,而这个结果是“让你修正你不清楚的程序”的错误。 那很好。 为什么不为领域做同样的事情呢? 也就是说,做一个明确的分配检查,便宜地高估?
那么有多less方法可以初始化本地? 它可以在方法的文本中分配。 它可以在方法文本中的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() { } }
是一个错误? 编译器应该如何解决这个问题? 唯一的方法是做一个完整的程序分析 ,跟踪程序中 每个可能path上每个 字段的初始化静态,包括在运行时select虚拟方法的path。 这个问题可以是任意的 ; 它可能涉及模拟执行数百万条控制path。 分析本地控制stream量需要几微秒,并取决于方法的大小。 分析全局控制stream可能需要几个小时,因为它取决于程序中所有方法和所有库的复杂性。
那么,为什么不做一个更便宜的分析,而不必分析整个项目,而只是更高估呢? 那么,提出一个可以编写一个正确的程序并不难,而devise团队可以考虑的algorithm。 我不知道有这样的algorithm。
现在,评论者build议“要求build设者初始化所有领域”。 这不是一个坏主意。 实际上, C#已经具有结构的这个特性 ,这是一个非常不错的主意。 一个结构构造函数需要在Ctor正常返回时明确指定所有的字段; 默认构造函数将所有字段初始化为默认值。
什么类? 那么, 你怎么知道一个构造函数已经初始化了一个字段呢? Ctor可以调用一个虚拟的方法来初始化这些字段,现在我们回到了之前的状态。 结构没有派生类; 类可能。 包含抽象类的库是否需要包含初始化其所有字段的构造函数? 抽象类如何知道字段应该初始化为什么值?
Johnbuild议在字段初始化之前禁止调用ctor中的方法。 所以,总结一下,我们的select是:
- 制作常见,安全,常用的编程习语非法。
- 做一个昂贵的整个程序分析,使编译花费几个小时,以查找可能不存在的错误。
- 依靠自动初始化为默认值。
devise团队select了第三个选项。
当我在我的方法中创build相同的布尔,布尔检查(而不是在类中),我得到一个错误“使用未分配的局部variables检查”。 为什么?
因为编译器试图阻止你犯错。
将variables初始化为false
改变这个特定执行path中的任何内容吗? 可能不会,考虑到default(bool)
是错误的,但它迫使你意识到这一切正在发生。 .NET环境阻止你访问“垃圾内存”,因为它会将任何值初始化为默认值。 但是,想象一下,这是一个引用types,并且将一个未初始化的(null)值传递给期望非null的方法,并在运行时获得NRE。 编译器只是试图阻止,接受这样的事实,有时这可能会导致bool b = false
语句。
Eric Lippert 在一篇博客文章中谈到:
我们想要做到这一点非法的原因并不像许多人所认为的那样,因为局部variables将被初始化为垃圾,我们希望保护您免于垃圾。 事实上,我们自动将当地人初始化为默认值。 (尽pipeC和C ++编程语言没有,并且会高兴地让你从未初始化的本地中读取垃圾)。相反, 这是因为这样的代码path的存在可能是一个bug,我们想把你扔进质量坑; 你应该努力工作来写这个bug。
为什么这不适用于class级领域? 那么,我认为这行必须绘制在某个地方,局部variables的初始化更容易诊断和得到正确的,而不是类字段。 编译器可以做到这一点,但想想所有可能的检查(其中一些独立于类代码本身),以评估一个类中的每个字段是否被初始化。 我不是编译器的devise者,但是我相信这肯定会比较困难,因为有很多案例需要考虑,而且必须及时完成。 对于每个function,您必须进行devise,编写,testing和部署,而实施这个function的价值与其付出的努力相比是不值得的和复杂的。
为什么局部variables需要初始化,但字段不?
简短的答案是编译器可以使用静态分析以可靠的方式检测访问未初始化的局部variables的代码。 而这不是字段的情况。 所以编译器强制执行第一种情况,而不是第二种情况。
为什么局部variables需要初始化?
这不过是Eric Lippert解释的C#语言的devise决定。 CLR和.NET环境不需要它。 例如,VB.NET将会使用未初始化的本地variables进行编译,实际上,CLR会将所有未初始化的variables初始化为默认值。
C#也可能出现这种情况,但语言devise者select不这样做。 原因是初始化的variables是错误的巨大来源,所以通过强制初始化,编译器有助于减less意外错误。
为什么字段不需要初始化?
那么为什么这个强制性的显式初始化不会在一个类中的域中发生呢? 只是因为在构build过程中可能会发生显式的初始化,通过一个对象初始值设定项调用属性,或者甚至在事件发生之后调用一个方法。 编译器不能使用静态分析来确定通过代码的每个可能path是否导致在我们之前显式初始化variables。 弄错了会很烦人,因为开发人员可能会留下无法编译的有效代码。 因此,C#根本不强制执行它,如果没有明确设置,CLR会自动将字段初始化为默认值。
收集types呢?
C#的局部variables初始化的执行是有限的,这往往使开发人员。 考虑以下四行代码:
string str; var len1 = str.Length; var array = new string[10]; var len2 = array[0].Length;
第二行代码不会编译,因为它试图读取未初始化的stringvariables。 第四行代码编译得很好,因为array
已经被初始化,但是只有默认值。 由于string的默认值为空,我们在运行时会得到一个exception。 任何在Stack Overflow上花费时间的人都会知道这个显式/隐式的初始化不一致会导致很多“为什么我会得到一个”对象引用没有设置为对象实例“的错误? 的问题。
上面的答案很好,但是我想我会发布一个更简单/更短的答案,让人们懒读一个长的(像我自己)。
类
class Foo { private string Boo; public Foo() { /** bla bla bla **/ } public string DoSomething() { return Boo; } }
属性Boo
可能已经或可能没有在构造函数中初始化。 所以当它findreturn Boo;
它并不假定它已经被初始化了。 它只是压制错误。
function
public string Foo() { string Boo; return Boo; // triggers error }
{ }
字符定义了代码块的范围。 编译器遍历这些块的分支来跟踪内容。 可以很容易地看出Boo
没有初始化。 然后触发错误。
为什么错误存在?
引入错误是为了减less为使源代码安全所需的代码行数。 没有错误,上面看起来像这样。
public string Foo() { string Boo; /* bla bla bla */ if(Boo == null) { return ""; } return Boo; }
从手册:
C#编译器不允许使用未初始化的variables。 如果编译器检测到使用了一个可能尚未初始化的variables,则会生成编译器错误CS0165。 有关更多信息,请参阅字段(C#编程指南)。 请注意,如果编译器遇到可能导致使用未分配variables的构造,即使您的特定代码不使用,也会生成此错误。 这避免了为明确分配而过分复杂的规则的必要性。
参考: https : //msdn.microsoft.com/en-us/library/4y7h161d.aspx