【问题标题】:Difference between A a =new A() and A a=nullA a =new A() 和 A a=null 之间的区别
【发布时间】:2016-02-17 09:28:00
【问题描述】:

在 C# 中,

A a = new A();
A a = null;
A a;

这 3 行在内存方面是如何工作的?

我知道第一行会在堆中创建一个内存,但是剩下的两行呢?

如果,它是如何工作的, 一个; 是一个字段和局部变量。

【问题讨论】:

  • 你是声明一个字段还是一个局部变量?最后一行在这两种情况下的行为会有所不同。另外,A 是类还是结构?
  • 请注意,由于您的问题不够清楚,因此您做出了假设的错误(IMO)答案。上下文在这里产生了很大的不同。
  • @Jon 你能澄清一下吗?希望能在这里向你学习:)
  • @JonSkeet 我的意思是将 A 类声明为字段和本地两种情况。
  • 那么您应该编辑您的问题以显示这一点,包括该字段是结构的字段还是类的字段。回答您问题的人不必阅读 cmets 即可理解您的问题。

标签: c# heap-memory


【解决方案1】:

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

【讨论】:

  • 什么是空引用,如何映射??
  • 它们不一样,因为A a = null; A b = a;不会导致编译错误,但A a; A b = a;会导致编译错误。
  • 从内存分配的角度来看,它们是相同的。编译器在您为其赋值之前阻止您使用引用,但内存分配没有任何变化
  • OP 询问“这 3 行在内存方面是如何工作的?”
【解决方案2】:
  1. 创建A 的新实例并将其分配给变量a
  2. 什么都不做。它只是将null 分配给引用a。如果不使用a,编译器可能会将其优化掉。
  3. 什么也不做。它将恢复为A a = default(A);,这与 2 相同,因为default(A)null。对于方法变量,如果您不分配它,它将向您显示警告或错误。如果不使用,这个也可以被优化掉。

【讨论】:

  • 请解释为什么,在代码中,你可以很容易地返回第 2 行而不用编辑它,但是第 3 行它会抱怨一个未初始化的变量。
  • 我建议您也查看这里的信息以了解有关 null 的更多信息:msdn.microsoft.com/en-us/library/edakx9da.aspx
  • 由于它没有初始化,它会显示一个警告或错误(它不知道它是什么)。因为使用 2 你确实分配了一个值:null(它知道它是什么:它没有值)。
  • 当我说“抱怨”时,我的意思是它不允许你编译。所以(至少在编译时),第 2 行和第 3 行不是一回事。
  • @BjarkeSøgaard 如果 A 是一个类并且声明是类中的字段,则第 2 行和第 3 行是相同的,因为字段(与局部变量相反)是默认初始化的,这相当于将它们清空。 cfmsdn.microsoft.com/en-us/library/aa645756%28v=vs.71%29.aspx.
【解决方案3】:

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

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

A a; 我相信这与A a = null; 相同 - EDIT OP 需要根据问题的上下文进行澄清。

【讨论】:

  • 只有当它是一个局部变量或结构体中的一个字段时,该引用才会存储在堆栈中——在这种情况下,您的最终语句是不正确的。
  • 你是对的,但在第三行你说A aA a = null 相同。在块中的某处你不能使用a.Anything,因为它会在编译时说 UnInstantiated Variable , 但是如果你将它分配为 null 并以同样的方式使用它会抛出异常 NullReferenceException。唯一的原因是这个
【解决方案4】:

这取决于上下文,如果A a 是一个字段,例如

  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

【讨论】:

    【解决方案5】:

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

    A a = new A(); 将始终在堆上创建一个新对象,并为a 分配对该新对象的引用。

    A a = null;A a; 都会将 null 分配给 a

    但是,与 A a; 相比,为 A a = null; 生成的 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 我使用自定义类对此进行了测试,结果同样如此,因此为了简洁起见,我决定在示例中继续使用字符串。请注意我回答中的最后一句话。
    【解决方案6】:

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

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

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

    var a = new A();
    
    1. A a = 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) 期间,当使用 Split declaration and assignment 重构时,代码中才会出现这种构造的有效用例,这会将 var 转换为 A,紧接着是Move to outer scope 重构。 假设您有一个类型为 SomeVerylongTypeName&lt;EvenMoreLongTypeName&gt; 的变量 x,它属于推断类型,并且想要获取类型名称。

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

    您可以只使用以下键序列: 变量 a = x; (Left)(Left)(Left) (Alt-Enter)...拆分声明和赋值...(Enter),然后您在编辑器中得到显式声明:

    SomeVerylongTypeName<EvenMoreLongTypeName> a;
    a = x;
    

    【讨论】:

    • var 不是强制性的。事实上,我公司的编码指南明确禁止它
    • 你可以只要求 resharper 为你指定类型,不需要用外部范围做所有的魔法来得到它:) 几乎总是建议重构 'var' 关键字。我也不同意 A a = new A() 没用的说法,var 不是强制性的,并且在左侧保留完整类型不仅在许多情况下增加了可读性,而且还有助于避免一些令人讨厌的错误,其中 ex .您使用方法返回值初始化“a”,然后更改其返回类型,使用的方法签名仍然匹配,因此代码编译,但它不是您想要的类型
    • @Liam 谢谢,为那些使用显式类型声明的人更新了答案。
    • @mikus 根据 Liskov 的替换原则,您不必担心。
    • @mikus 至于'Specify Type Explicitly'重构,这无助于回答原始问题的第3点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-11-23
    • 2016-03-15
    • 2016-05-04
    • 1970-01-01
    • 2016-04-14
    • 2021-10-22
    • 1970-01-01
    相关资源
    最近更新 更多