C# 未初始化的变量是否危险?

45

我熟悉C#规范,第 5.3 节指出在使用变量之前必须对其进行赋值。

在 C 和非托管 C++ 中,这是有道理的,因为堆栈不会被清除,用于指针的内存位置可以是任何地方(导致难以跟踪的错误)。

但我认为运行时不允许真正“未分配”的值。特别是,未初始化的引用类型将始终具有 null 值,而不是上一次调用该方法留下的值或随机值。

这是正确的吗?多年来,我一直错误地认为检查 null 就足够了吗?在 C# 中,您是否可以拥有真正未初始化的变量,还是 CLR 会处理这个问题,总是有一些值被设置?

6个回答

70
我认为运行时不允许存在真正的“未分配”值。特别是,未初始化的引用类型将始终具有null值,而不是上一次调用该方法留下的值或随机值。这是否正确?
我注意到还没有人回答您的问题。
实际问您所问问题的答案是“有点儿”。
正如其他人所指出的那样,某些变量(例如数组元素、字段等)被归类为自动“初始分配”到它们的默认值(对于引用类型为空、对于数字类型为零、对于布尔类型为false、对于用户定义结构的自然递归)。
某些变量不被分类为最初被赋值;局部变量尤其没有被最初赋值。编译器必须将它们在使用它们的值的所有点上分类为“确定被赋值”。
因此,您的问题实际上是“分类为不确定被分配的本地变量是否实际上与字段一样被最初分配?”该问题的答案是,在实践中,运行时最初会分配所有本地变量。
这有几个良好的特性。首先,在第一次分配之前,您可以观察它们处于其默认状态。其次,没有机会让垃圾收集器被欺骗成解除引用坏指针,仅因为堆栈上留下了垃圾,现在正在将其视为受管理的引用。等等。
如果运行时可以安全地保留本地变量的初始状态为任何垃圾,则允许这样做。但作为一个实现细节,它从不选择这样做。它积极地清空本地变量的内存。

在使用本地变量之前必须明确赋值的规则,其原因并不是为了防止您观察本地变量未初始化的垃圾状态。这已经无法观察到,因为CLR会将本地变量积极清除为默认值,就像它对字段和数组元素所做的那样。之所以在C#中这样做是不合法的,是因为使用未赋值的本地变量具有很高的可能性成为错误。我们只是使其非法化,然后编译器会防止您出现此类错误。


我想象中还有一个原因(而且这是一个很好的理由,你曾经见过不是漏洞或至少脆弱和不清楚的CS0165吗?)是C#在未定义但允许的行为上较少,这使得你可以更自由地考虑这样的实现细节,而不必担心编写使用“它是未定义的,但我们都知道它做...”的行为的代码会破坏它。 - Jon Hanna
@JonHanna:当然,"声明、条件赋值、测试数值"的模式在字段中经常使用。它在本地变量中使用时并不比在字段中更易受损或有错误。当然,如果运行时不强制清除本地变量,那么编译器应该这样做,或者以其他方式尝试防止出错。在这种情况下,显然运行时会清除它们,但不必这样做... - jmoreno
@jmoreno 从概念上讲,运行时不会清除它们。Eric在上面的回答告诉我们它确实会清除,但是虽然这很有趣,但对调用者来说并不是实用信息(我甚至想不出如何在优化中使用它,你呢?)。条件赋值不脆弱的原因是我们通常没有完全条件赋值。例如,任何out调用必须将值设置为某些内容,除非它抛出异常。这个规则严格来说是浪费的——如果我们收到false,那么我们不应该使用这个值——但是它消除了否则存在的脆弱性。 - Jon Hanna
@JonHanna:鉴于编译器无法依赖它,我会犹豫地将其用于优化,但如果它是可靠的(虽然这两个产品在概念上是不同的,但它们来自同一家公司,因此可能被视为可靠),那么至少有一个明显的小优化:忽略对变量的第一次赋值。至于 out 的情况不同,因为你不需要在返回 true 或 false 的函数中使用它。它是糖,但不是浪费,它对于确保所有代码路径都设置了一个值非常有用。如果你不想要它,你就不会使用它。 - jmoreno
@jmoreno 我的意思是我想不出任何可以将这个转化为任何有用的优化、恶意悲观化或其他任何东西的方法。我提到的返回布尔值和输出模式只是因为我能想到的最接近我们遇到缺乏有意义赋值的地方,但当然,还是会有赋值,所以没有脆弱性。 - Jon Hanna
显示剩余3条评论

9
据我所知,每种类型都有指定的默认值。
根据这份文档,类的字段被赋予默认值。

http://msdn.microsoft.com/en-us/library/aa645756(v=vs.71).aspx

这份文档指出以下内容都会自动分配默认值。
  • 静态变量。
  • 类实例的实例变量。
  • 最初分配的结构变量的实例变量。
  • 数组元素。
  • 值参数。
  • 引用参数。
  • 在catch子句或foreach语句中声明的变量。

http://msdn.microsoft.com/en-us/library/aa691173(v=vs.71).aspx

关于实际默认值的更多信息请参见: C#类型的默认值(C#参考)


前两个链接已经失效,指向的是“Visual Studio 2003已退役技术文档”。 - Peter Mortensen

5

这取决于变量声明的位置。在类中声明的变量会使用默认值自动初始化。

object o;
void Method()
{
    if (o == null)
    {
        // This will execute
    }
}

在一个方法中声明的变量未被初始化,但当这个变量第一次被使用时,编译器会检查是否已经初始化,因此代码将无法编译。

void Method()
{
    object o;
    if (o == null) // Compile error on this line
    {
    }
}

3
特别提醒:未初始化的引用类型始终具有 null 值。 我认为您混淆了局部变量和成员变量。第 5.3 节专门讨论局部变量。与会默认的成员变量不同,局部变量从不默认为 null 值或其他任何值:它们必须在首次读取之前分配。第 5.3 节解释了编译器用于确定局部变量是否已经被分配的规则。

1

有三种方法可以给变量赋初始值:

  1. 默认情况--如果您声明一个类变量而没有分配初始值,则会发生这种情况,因此初始值得到的是default(type),其中type是您声明变量的任何类型。

  2. 使用初始化器--当您声明具有初始值的变量时,例如int i = 12;

  3. 在检索其值之前的任何时间点--如果您有一个没有初始值的局部变量,则会发生这种情况。编译器确保您没有可达的代码路径将在分配变量值之前读取变量的值。

在任何时候,编译器都不允许您读取未初始化的变量的值,因此您永远不必担心尝试读取未初始化变量时会发生什么。


-1

所有原始数据类型都有默认值,因此不需要担心它们。

所有引用类型都初始化为null值,因此如果您将引用类型未初始化,然后调用该null引用类型的某些方法或属性,则会出现运行时异常,需要优雅地处理。

同样,如果未初始化所有可空类型,则需要检查其是否为null或默认值,如下所示:

    int? num = null;
    if (num.HasValue == true)
    {
        System.Console.WriteLine("num = " + num.Value);
    }
    else
    {
        System.Console.WriteLine("num = Null");
    }

    //y is set to zero
    int y = num.GetValueOrDefault();

    // num.Value throws an InvalidOperationException if num.HasValue is false
    try
    {
        y = num.Value;
    }
    catch (System.InvalidOperationException e)
    {
        System.Console.WriteLine(e.Message);
    }

但是,如果您将所有变量未初始化,则不会收到任何编译错误,因为编译器不会抱怨。只有运行时需要担心。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接