当前位置: 首页 > 知识库问答 >
问题:

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

金钧
2023-03-14

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

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

共有3个答案

白高逸
2023-03-14

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

简而言之,编译器可以使用静态分析以可靠的方式检测访问未初始化局部变量的代码。然而,字段的情况并非如此。因此编译器执行第一种情况,但不执行第二种情况。

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

正如Eric Lippert所解释的,这只不过是C语言的设计决策。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;

第二行代码无法编译,因为它试图读取未初始化的字符串变量。第四行代码编译得很好,因为数组已经初始化,但只使用默认值。由于字符串的默认值为null,因此在运行时会出现异常。在这里花时间研究堆栈溢出的任何人都会知道,这种显式/隐式初始化不一致会导致许多“为什么我得到的对象引用没有设置为对象实例”错误问题。

逄皓轩
2023-03-14

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

因为编译器试图防止您出错。

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

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

我们之所以要将此设置为非法,并不像许多人认为的那样,因为局部变量将被初始化为垃圾,我们希望保护您免受垃圾的影响。事实上,我们会自动将局部变量初始化为其默认值。(虽然C和C编程语言没有,但它将允许您从未初始化的本地读取垃圾。)相反,这是因为这种代码路径的存在可能是一个bug,我们想让您陷入质量的深渊;你应该努力写那个bug。

为什么这不适用于类字段?好吧,我假设这条线必须画在某个地方,与类字段相比,局部变量初始化更容易诊断和得到正确的结果。编译器可以做到这一点,但需要考虑所有可能的检查(其中一些检查独立于类代码本身),以便评估类中的每个字段是否已初始化。我不是编译器设计师,但我相信这肯定会更加困难,因为有很多情况需要考虑,而且必须及时完成。对于您必须设计、编写、测试和部署的每一项功能,实现这项功能的价值与投入的努力相比,都是毫无价值和复杂的。

堵雅健
2023-03-14

尤瓦尔和大卫的答案基本正确;总结:

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

David回答的一位评论者问道,为什么不可能通过静态分析来检测未分配字段的使用;这就是我想在这个答案中进一步阐述的一点。

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

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

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

因此,编译器对局部变量所做的是实现一个快速的算法,并在局部变量没有明确赋值时高估。也就是说,它有一些误报,它说“我不能证明这个局部是指定的”,即使你和我都知道是。例如:

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指定默认值?但编译器已经做到了这一点。

假设这个图书馆是合法的。如果福特公司写

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

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

那么,为什么不做一个更便宜的分析,而不必分析整个程序,只是更严重地高估?好吧,提出一种算法,它的工作不会使编写一个实际编译的正确程序变得太困难,设计团队可以考虑它。我不知道有这样的算法。

现在,评论员建议“要求构造函数初始化所有字段”。这主意不错。事实上,C#已经为结构提供了这个特性,这是一个不错的主意。结构构造函数需要在ctor正常返回时明确分配所有字段;默认构造函数将所有字段初始化为其默认值。

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

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

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

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

 类似资料:
  • 下面的示例类无法编译: 此代码的编译错误消息是: 但是,对于包含以下方法的类,Java不会生成任何错误消息: 关于初始化及其要求,为什么Java对最终实例变量和最终局部变量的处理不同?谢谢

  • print语句会导致以下编译时错误, 局部变量f可能尚未初始化 如果Java中的原语已经有一个默认值(float=0.0f),为什么我需要定义一个呢? 所以,这是有效的 谢谢大家!

  • 问题内容: print语句导致以下编译时错误, 局部变量f可能尚未初始化 如果Java中的原语已经具有默认值(float = 0.0f) ,为什么需要定义一个? 所以这有效 感谢大家! 问题答案: 因为它是一个局部变量。这就是为什么什么都没有分配的原因: 局部变量略有不同。编译器永远不会为未初始化的局部变量分配默认值。如果您无法在声明它的地方初始化本地变量,请确保在尝试使用它之前为其分配一个值。访

  • 问题内容: Java的设计者是否有任何理由认为不应为局部变量提供默认值?认真地讲,如果实例变量可以被赋予默认值,那为什么我们不能对局部变量做同样的事情呢? 问题答案: 声明局部变量主要是为了进行一些计算。因此,程序员决定设置变量的值,并且不应采用默认值。如果程序员错误地没有初始化局部变量并且使用默认值,则输出可能是一些意外值。因此,在使用局部变量的情况下,编译器将要求程序员在访问变量之前使用一些值

  • 问题内容: 在lambda中,局部变量需要是最终变量,而实例变量则不需要。为什么这样? 问题答案: 字段和局部变量之间的根本区别在于,当JVM创建lambda实例时,将复制局部变量。另一方面,字段可以自由更改,因为对它们的更改也将传播到外部类实例(它们的范围是整个外部类,如Boris所指出的)。 考虑到匿名类,闭包和Labmdas的最简单方法是从可变范围的角度来看。想象一个为传递给闭包的所有局部变

  • 问题内容: 当我只是尝试使用 Java 编写一些程序时,我尝试使用变量,我知道变量必须在声明时进行初始化,但是在main方法内部它接受了没有初始化的变量。我不知道是什么原因。任何人都可以告诉我原因。 谢谢 码: 问题答案: 对于 实例变量水平 最终变量只能初始化一次。 必须 在构造函数结束之前 初始化类级别的最终变量。 对于 本地(方法)级别 方法级别的最终变量只能初始化一次。 使用前* 必须先初