【问题标题】:Why do local variables require initialization, but fields do not?为什么局部变量需要初始化,而字段不需要?
【发布时间】:2015-08-29 06:08:57
【问题描述】:

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

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

【问题讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • 这个问题很模糊。 “因为规范是这样说的”是一个可以接受的答案吗?
  • 因为这就是他们复制 Java 时的方式。 :P

标签: c# language-design local-variables


【解决方案1】:

Yuval 和 David 的回答基本正确;总结:

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

David 的回答的评论者询问为什么无法通过静态分析检测未分配字段的使用;这是我想在这个答案中展开的重点。

首先,对于任何变量,无论是局部变量还是其他变量,实际上都无法准确地确定变量是被赋值还是未赋值。考虑:

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

问题“分配了 x 吗?”相当于“M() 是否返回 true?”现在,假设如果费马大定理对于所有小于 110 万亿的整数都为真,则 M() 返回真,否则返回假。为了确定 x 是否被明确赋值,编译器本质上必须产生一个费马大定理的证明。编译器没那么聪明。

因此,编译器为局部变量所做的是实现一种快速的算法,并且当局部变量未明确分配时高估。也就是说,它有一些误报,它说“我无法证明这个本地是被分配的”,即使你我都知道它是。例如:

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

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

好的,那么到目前为止我们知道什么?对于当地人来说,得到一个准确的答案是不切实际的,但我们可以廉价地高估未分配性,并得到一个相当不错的结果,即“让你修复你不清楚的程序”。那挺好的。为什么不对字段做同样的事情?也就是说,做一个明确的高估便宜的分配检查器?

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

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

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

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

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

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

  • 使常见、安全​​、常用的编程习惯成为非法。
  • 进行昂贵的整个程序分析,这使得编译需要几个小时才能找到可能不存在的错误。
  • 依靠自动初始化为默认值。

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

【讨论】:

  • 很好的答案,像往常一样。不过我有一个问题:为什么不自动为局部变量分配默认值? 换句话说,为什么不让bool x; 等同于bool x = false; 即使在方法内部 b>?
  • @durron597:因为经验表明忘记给本地赋值可能是一个错误。如果它可能是一个错误并且它便宜且易于检测,那么就有很好的动机使该行为成为非法或警告。
  • 在 Yuval 下面的回答中,它说本地变量会自动初始化为默认值。如果开发人员无论如何都必须初始化 var,为什么还要进行这种自动初始化?
【解决方案2】:

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

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

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

Eric Lippert 谈到了这个in a blog post

我们之所以要将其定为非法的原因并不像很多人那样 相信,因为局部变量将被初始化为 垃圾,我们希望保护您免受垃圾的侵害。我们实际上做 自动将局部变量初始化为其默认值。 (虽然 C 而 C++ 编程语言则不会,而且会很乐意让您 从未初始化的本地读取垃圾。)相反,这是因为 这样的代码路径的存在可能是一个错误,我们想抛出 你在质量的坑里;你应该努力写那个 错误。

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

【讨论】:

  • " 想象这是一个引用类型,并且你将这个未初始化的对象传递给一个期望初始化对象的方法" 你的意思是:"想象这是一个引用类型并且你传递了默认值 ( null) 而不是对象的引用”?
  • @Deduplicator 是的。期望非空值的方法。编辑了那部分。希望现在更清楚了。
  • 我不认为是因为画线。每个类都应该有一个构造函数,至少是默认构造函数。因此,当您坚持使用默认构造函数时,您将获得默认值(安静透明)。在定义构造函数时,您应该或应该知道自己在其中做什么以及希望以何种方式初始化哪些字段,包括了解默认值。
  • 相反:方法中的字段可以在不同的执行路径中声明和赋值。在您查看您可能使用的框架的文档或什至在您可能不维护的代码的其他部分之前,可能存在很容易监督的异常。这会引入非常复杂的执行路径。因此编译器提示。
  • @Peter 我不太明白你的第二条评论。关于第一个,不需要初始化构造函数中的任何字段。这是一种常见的做法。编译器的工作不是强制执行这种做法。您不能依赖任何运行的构造函数的实现并说“好吧,所有字段都可以使用”。 Eric 在他的回答中详细阐述了如何初始化类的字段,并展示了如何花费非常长时间来计算所有逻辑方式的初始化。
【解决方案3】:

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

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

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

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

【讨论】:

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

上面的答案很好,但我想我会发布一个更简单/更短的答案,让那些懒得读长答案的人(比如我)。

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

【讨论】:

    猜你喜欢
    • 2014-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-09
    • 2021-07-13
    • 2018-06-11
    相关资源
    最近更新 更多