A a = new A() 和 A a = null 的区别是什么?

4
在C#中,
A a = new A();
A a = null;
A a;

这三行代码如何涉及内存?

我知道第一行将在堆中创建一个内存,但是后面两行呢?

如果 A a 是一个字段和局部变量,它会如何工作?


5
你是在声明一个字段还是一个局部变量?最后一行的行为在这两种情况下会有所不同。另外,A是一个类还是结构体? - Jon Skeet
2
请注意,你得到了一些不好(在我看来)的答案,因为你在问题中没有表述清楚,从而导致了一些假设。上下文在这里起着非常重要的作用。 - Jon Skeet
2
@Jon,你能澄清一下吗?希望从你这里学到一些东西 :) - Patrick Hofman
1
@JonSkeet 我的意思是将A类声明为字段和本地变量两种情况。 - keerti_h
1
那么你应该编辑你的问题,以显示该字段是结构体还是类的字段,包括这一点。回答你的问题的人不应该阅读评论才能理解你的问题。 - Jon Skeet
显示剩余6条评论
6个回答

4
  1. 创建一个A的新实例并将其赋值给变量a
  2. 什么也没做。它只是将引用a赋为null。如果不使用a,编译器可能会将其优化掉。
  3. 同样什么也没做。它将恢复为A a = default(A);,和2一样,因为default(A)null。对于方法变量,如果没有分配,则会显示警告或错误。如果未使用,则此选项也可以进行优化。

请解释为什么在代码中,您可以轻松地返回第二行而不编辑它,但是第三行会抱怨未初始化的变量。 - Falgantil
我建议您也查看此处的信息,以更深入地了解null:https://msdn.microsoft.com/zh-cn/library/edakx9da.aspx - Denis Kralj
由于它未被初始化,因此它会显示一个警告或错误(它不知道它是什么)。由于使用2,您会分配一个值:null(它知道它是什么:它没有值)。 - Patrick Hofman
当我说“complain”时,我的意思是它不允许你编译。因此(至少在编译时),第2行和第3行不是同一件事。 - Falgantil
如果A是一个类,并且这些声明是类中的字段,那么@BjarkeSøgaard第2行和第3行是相同的,因为字段(与局部变量相反)默认初始化,这等同于将它们置为空。请参阅https://msdn.microsoft.com/en-us/library/aa645756%28v=vs.71%29.aspx。 - Peter - Reinstate Monica

1
这取决于上下文,如果 A a 是一个字段,例如:field
  class MyClass {
    ...
    // A a is field of some class/structure
    A a = new A(); // A a = null; or A a;
    ...
  }

所以
  A a = new A();

A 类型的字段 a 具有新实例化的 A 对象作为初始值; 这两行代码是 相等的 (类型为 A 的字段 a 具有初始值 null):

  A a = null; 
  A a;

自从

“无论是静态字段还是实例字段,其初始值都是默认值。”

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

对于局部变量,例如

  public void MyMethod() {
    ...
    // A a is a local variable in some method
    A a = new A(); // A a = null; or A a;
    ...
  }

编译器不会初始化局部变量。

https://msdn.microsoft.com/en-us/library/4y7h161d(v=vs.71).aspx

所以。
  A a = new A(); // "a" of type "A" declaration with new instance of A as an initial value
  A a = null;    // "a" of type "A" declaration with null initial value
  A a;           // just "a" declaration, "a" contains trash and should be initialized before using

1

假设A是一个引用类型并且这段代码在方法中:

A a = new A(); 总是创建一个新的对象在堆上,并将一个指向该新对象的引用赋值给 a

A a = null;A a; 都会将 null 赋值给 a

然而,对于 A a = null;A a; 生成的 IL 可能会有所不同。

考虑以下简单的程序:

static void Main()
{
    string s;

    if (Environment.TickCount > 0)
        s = "A";
    else
        s = "B";

    Console.WriteLine(s);
}

发布版本生成的IL代码如下所示:
.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] string s)
    L_0000: call int32 [mscorlib]System.Environment::get_TickCount()
    L_0005: ldc.i4.0 
    L_0006: ble.s L_0010
    L_0008: ldstr "A"
    L_000d: stloc.0 
    L_000e: br.s L_0016
    L_0010: ldstr "B"
    L_0015: stloc.0 
    L_0016: ldloc.0 
    L_0017: call void [mscorlib]System.Console::WriteLine(string)
    L_001c: ret 
}

现在修改代码以将引用初始化为null:
static void Main()
{
    string s = null;

    if (Environment.TickCount > 0)
        s = "A";
    else
        s = "B";

    Console.WriteLine(s);
}

而IL更改为:

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] string s)
    L_0000: ldnull         <====== Lookie here
    L_0001: stloc.0        <====== and here
    L_0002: call int32 [mscorlib]System.Environment::get_TickCount()
    L_0007: ldc.i4.0 
    L_0008: ble.s L_0012
    L_000a: ldstr "A"
    L_000f: stloc.0 
    L_0010: br.s L_0018
    L_0012: ldstr "B"
    L_0017: stloc.0 
    L_0018: ldloc.0 
    L_0019: call void [mscorlib]System.Console::WriteLine(string)
    L_001e: ret 

请注意,已生成两个新的IL指令来将变量初始化为null(尽管据我所知,.locals init ([0] string s)已将其初始化为null)。
很可能JIT编译器会对此进行优化,但从生成的IL代码来看,肯定存在差异。
(在此示例中,我使用了string以简化问题,但如果您使用自己的类,情况也是一样的。)

类型的默认引用可能在局部变量和类字段、静态字段等之间有所不同。此外,我不会以字符串作为参考点,不是说它生成了错误的结果,但它肯定是一个特定的类型,感谢例如字符串内插 :) - mikus
@mikus 我已经使用自定义类进行了测试,结果发现相同的情况也出现了,所以我决定为了简洁起见在示例中继续使用字符串。请注意我的回答中最后一句话。 - Matthew Watson

1

A a = new A(); 这实际上是实例化了一个类型为A的新对象。a是对该对象的引用。a存储在堆栈上,而实际的对象存储在堆上。

A a = null; 只是在堆栈上创建了引用 - 没有数据存储在堆上。

A a; 我认为这与A a = null;相同 - 编辑 需要从OP获取问题背景的澄清。


1
如果引用是一个本地变量或结构体中的字段,而该结构体也在堆栈上,则仅在堆栈上存储引用 - 在这种情况下,您的最终语句是不正确的。 - Jon Skeet
你是正确的,但是在第三行你说 A aA a = null 是相同的。在代码块中,你不能使用 a.Anything,因为它会在编译时显示“未初始化的变量”,但如果你将其赋值为 null 并以相同的方式使用它,则会抛出 NullReferenceException 异常。这是唯一的原因。 - Suren Srapyan

0

所有三个结构都为局部作用域变量a在堆栈上分配了一个引用。

  1. A a = new A(); 在堆上构造一个对象(假设A是一个类,因为它的引用可以被赋值为空)。

如果您的团队指南支持显式类型声明而不是var,则此结构很有用。否则,在这种情况下,您将使用var,并且类型由编译器推断:

var a = new A();
  • A a = null;将null引用分配给a,如果您要引入对引用的读取访问,则此方法非常有用。
  • 例如:

    Func<int, int> factorial = null;
    factorial = n => n < 3 ? n : n * factorial(n-1);
    

    lambda表达式的作用域继承变量声明的作用域,因此需要进行变量初始化。 如果没有= null,则会被视为编译器错误,因为它引入了对委托的读取访问,而您还没有非空值,也不想虚构一个。

    这种结构很有用,因为您无法推断出null的类型。 您可以使用var a = default(A);代替,这对于值类型也适用。

    1. A a;

    只是声明一个变量。如果需要引入对变量的读取访问,则必须在作用域中分配它。基本上您无法使用它,所以没有理由以这种方式声明它。一般规则是将变量声明更接近第一次使用。

    这种结构在您的代码中出现的有效用例仅限于自动重构(ReSharper)期间,当使用拆分声明和赋值重构时,它会将var转换为A,紧接着是一个移动到外部作用域重构。 假设您有一个类型为SomeVerylongTypeName<EvenMoreLongTypeName>的变量x,它是一个推断类型,并且想要获取类型名称。

    var x = container.GetFirst(); // some imaginary GetFirst method which returns an instance of non-keyboard friendly type.
    

    你可以使用以下按键序列: var a = x; (左)(左)(左) (Alt-Enter)...拆分声明和赋值...(Enter),然后你就可以在编辑器中得到显式声明:
    SomeVerylongTypeName<EvenMoreLongTypeName> a;
    a = x;
    

    1
    var不是必须的。事实上,我公司的编码指南明确禁止使用它。 - Liam
    1
    你可以直接要求Resharper为你指定类型,不需要在外部范围做所有的魔法来获取它 :) 几乎总是建议使用“var”关键字进行重构。 我也不同意“A a = new A()”是无用的说法,var并非必须,在左侧保留完整类型不仅在许多情况下增加了可读性,而且还可以帮助避免某些令人讨厌的错误,例如,您使用方法返回值初始化'a',然后更改其返回类型,以使使用的方法签名仍然匹配,因此代码编译,但不是您想要的类型。 - mikus
    @Liam 谢谢,已更新答案以供使用显式类型声明的人参考。 - George Polevoy
    @mikus 根据Liskov的替换原则,你不需要担心这个问题。 - George Polevoy
    @mikus,我们实际上正在谈论一种与多态性相关的概念。这种隐式类型依赖关系是现代语言中最有生产力的东西之一,在我的经验中,它超过了你所描述的问题。我不认为有理由在这里辩论var,请查看https://dev59.com/fHVD5IYBdhLWcg3wQJKT - George Polevoy
    显示剩余2条评论

    0

    第二和第三个语句是相同的,即分配一个指向“null”的空引用; 第一个语句将在托管堆中分配A类的新实例,并将其地址分配给引用变量


    1
    什么是空引用,它会如何映射? - keerti_h
    1
    它们不同,因为 A a = null; A b = a; 不会导致编译错误,但是 A a; A b = a; 会导致编译错误。 - Matthew Watson
    从内存分配的角度来看,它们是相同的。编译器会在您分配值之前阻止您使用引用,但是内存分配不会发生任何变化。 - Gian Paolo
    OP问道:“这三行代码在内存方面是如何工作的?” - Gian Paolo

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