【问题标题】:Dilemma with using value types with `new` operator in C#在 C# 中使用带有 `new` 运算符的值类型的困境
【发布时间】:2011-07-30 15:27:35
【问题描述】:

operator new() 与引用类型一起使用时,实例的空间分配在堆上,引用变量本身放在堆栈上。除此之外,在堆上分配的引用类型实例中的所有内容都被清零。
例如这里是一个类:

class Person
{
    public int id;
    public string name;
}

在以下代码中:

class PersonDemo
{
    static void Main()
    {
        Person p = new Person();
        Console.WriteLine("id: {0}  name: {1}", p.id, p.name);
    }
}

p 变量在堆栈上,Person 的创建实例(它的所有成员)在堆上。 p.id 将是 0p.name 将是 null。 之所以会出现这种情况,是因为在堆上分配的所有内容都被清零了。

现在我感到困惑的是,如果我使用带有 new 运算符的值类型。例如,考虑以下结构:

struct Date
{
    public int year;
    public int month;
    public int day;
}

class DateDemo
{
    static void Main()
    {
        Date someDate;
        someDate= new Date();

        Console.WriteLine("someDate is: {0}/{1}/{2}", 
            someDate.month, someDate.day, someDate.year);
    }
}

现在我想知道 main 中的以下几行是做什么的:

        Date someDate;
        someDate= new Date();

在第一行someDate 变量在堆栈上分配。正好是 12 个字节。
我的问题是第二行发生了什么? operator new() 是做什么的?它只是将Date 结构的成员清零还是在堆上分配空间?一方面,我不希望new 在堆上分配空间,当然因为在第一行中,内存已经在堆栈上分配给结构实例。另一方面,我希望new 在堆上分配空间并返回该空间的地址,因为这是new 应该做的。 也许这是因为我来自 C++ 背景。

尽管如此,如果答案是:“当new 与值类型一起使用时,它只会将对象的成员清零”,那么new 运算符的含义有点不一致,因为:

  1. new 与值类型一起使用时,它只会将堆栈上的对象成员清零
  2. new 与引用类型一起使用时,它会在堆上为实例分配内存并将其成员清零

提前致谢,
干杯

【问题讨论】:

标签: c# .net value-type reference-type


【解决方案1】:

好的,这是一个简单的:

class Program
{
    static void Main(string[] args)
    {
        DateTime dateTime = new DateTime();
        dateTime = new DateTime();
        Console.Read();
    }
}

编译成这个 IL 代码:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       24 (0x18)
  .maxstack  1
  .locals init ([0] valuetype [mscorlib]System.DateTime dateTime)
  IL_0000:  nop
  IL_0001:  ldloca.s   dateTime
  IL_0003:  initobj    [mscorlib]System.DateTime
  IL_0009:  ldloca.s   dateTime
  IL_000b:  initobj    [mscorlib]System.DateTime
  IL_0011:  call       int32 [mscorlib]System.Console::Read()
  IL_0016:  pop
  IL_0017:  ret
} // end of method Program::Main

正如您所见,CLR 将使用相同的局部变量来存储新的值类型,尽管它会再次运行构造函数 - 这很可能只是将内存归零。我们看不到initobj 是什么,这是一个CLR 实现

现实是,正如 Eric Lippert 解释的 here没有关于在堆栈上分配值类型的一般规则。这完全取决于 CLR 的实现。

【讨论】:

    【解决方案2】:

    从开发人员的角度来看,您不知道分配的位置。例如,具有 CLR 的奇异设备不知道堆栈 -> 一切都在堆上。即使您考虑桌面 CLR,在某些情况下 JITer 也可以将变量从堆栈移动到堆。

    More info.

    【讨论】:

      【解决方案3】:

      关于结构的归零。

      无参数构造函数将成员归零。

      如果不使用new(),则无法访问结构成员,除非您先自行初始化它们。否则你会得到“使用可能未分配的字段”。

      【讨论】:

      • 要挑剔的是,无参数构造函数并不是将内存归零的东西。 内存管理器将内存归零。
      【解决方案4】:

      结构的默认构造函数返回一个所有内存都清零的结构。即new SomeStruct()default(SomeStruct) 相同。

      然后您的代码将该默认结构分配给您的变量。

      你肯定知道的就这些。

      编译器如何实现这一点完全是编译器的事情。

      但是,如果您对幕后感兴趣,编译器很可能会直接清除该变量的堆栈位置:假设该变量存储在堆栈中。有很多事情可以防止这种情况发生 - 一个例子是匿名函数访问它,即:

      Func<Person> PersonFactory()
      {
        Person p = new Person();
        return () => p;
      }
      

      这里 p 需要存储在堆上,以便在函数返回等后能够存在,因此new Person() 将清除该堆位置。

      无论如何。与 C/C++ 不同,对于 C#,最好忘记“堆栈”、“堆”等。AFAIK,语言规范没有这些概念——它们都是特定于实现的。谁知道,在逃逸分析允许的情况下,未来的某些实现可能会将一些堆值放在堆栈上以节省 GC 的一些工作量。最好不要针对 C# 规范的给定实现做出特定的设计决策。

      【讨论】:

        【解决方案5】:

        首先让我纠正你的错误。

        当 operator new() 与引用类型一起使用时,实例的空间分配在堆上,引用变量本身放在堆栈上。

        作为“new”结果的引用是一个,而不是一个变量。值指的是一个存储位置。

        引用当然是在一个 CPU 寄存器中返回的。该 CPU 寄存器的内容是否被复制到调用堆栈是由抖动的优化器决定的。它不需要永远存在于堆栈中;它可以永远存在于寄存器中,也可以直接从寄存器复制到托管堆,或者在不安全的代码中,它可以直接复制到非托管内存。

        堆栈是一个实现细节。除非您查看 jit 代码,否则您不知道堆栈何时被使用。

        p 变量在堆栈上,而创建的 Person 实例(它的所有成员)在堆上。 p.id 为 0,p.name 为空。

        正确,当然如果抖动决定了 p 也可以作为寄存器实现。如果有可用的寄存器,则不需要使用堆栈。

        您似乎对正在使用堆栈的想法很感兴趣。抖动可能有大量的寄存器可供使用,而且这些寄存器可能非常大。

        我来自 C++ 背景。

        啊,这就解释了为什么你如此沉迷于堆栈与堆的事情。学会停止担心它。我们设计了一个托管内存环境,让事物可以根据需要生存。管理器是否选择使用堆栈、堆或寄存器来有效地管理内存取决于它。

        在第一行中 someDate 变量被分配在堆栈上。正好是 12 个字节。

        我们假设这个 12 字节的结构是在堆栈上分配的。看起来很合理。

        我的问题是第二行会发生什么?运算符 new() 是做什么的?它是仅将 Date 结构的成员清零还是也在堆上分配空间?

        这个问题预设了一个错误的二分法,因此不可能按照所述回答。该问题提出了两种非此即彼的选择,但都不一定正确。

        一方面,我不希望 new 在堆上分配空间,当然因为在第一行中,内存已经在堆栈上分配给结构实例。

        正确的结论,似是而非的推理。 不执行堆分配,因为编译器知道此操作的任何部分都不需要长期存储。这就是堆的用途;当编译器确定给定变量的寿命可能比当前方法激活的时间长时,它会生成代码,该代码在长期“堆”存储上为该变量分配存储空间。如果它确定变量的生命周期肯定很短,那么它使用堆栈(或寄存器)作为优化。

        另一方面,我希望 new 在堆上分配空间并返回该空间的地址,因为这是 new 应该做的。

        不正确。 “新”不保证堆已分配。相反,“new”保证在清零内存上调用构造函数。

        让我们回到你的问题:

        它是仅将 Date 结构的成员清零还是也在堆上分配空间?

        我们知道它不会在堆上分配空间。它会将日期结构的成员清零吗?

        这是一个复杂的问题。规范说当你说时会发生什么

        someDate = new Date();    
        
        • someDate 的地址已确定
        • 为新对象分配空间(离开“堆栈”)。它被归零。
        • 然后调用构造函数(如果有),其中“this”是对新堆栈存储的引用
        • 然后将新堆栈存储的字节复制到 someDate 的地址。

        现在,真的会发生这种情况吗?您完全有权注意到,不可能告诉新堆栈空间是否已分配、初始化和复制,或者“旧”堆栈空间是否已初始化。

        答案是在编译器推断用户不可能注意到现有堆栈空间正在发生变化的情况下,现有堆栈空间会发生变化,并且会省略额外的分配和后续副本。

        如果编译器无法推断出这一点,则会创建一个临时堆栈槽,将其初始化为零,由构造函数构造、变异,然后将结果值复制到变量中。 这样可以确保在构造函数抛出异常时,您不会在变量中观察到不一致的状态。

        有关此问题的更多详细信息以及编译器对其的分析,请参阅我关于该主题的文章。

        https://ericlippert.com/2010/10/11/debunking-another-myth-about-value-types/

        【讨论】:

        • 我喜欢这个答案!详细、合乎逻辑,但我认为它忽略了一个重要点:我们在 C# 中讨论值类型。假设值类型不是某种魔法,那么:(1) 它不能有无参数构造函数 (2) 它不能有字段初始化器。 AFAIK,这意味着在此值类型的堆栈上清零设置空间的操作之后,不再执行写入(未定义初始化程序)并且没有调用“用户定义”构造函数(未定义)。因此,实际上所有类型的值的成员字段在存储意义上都真正“归零”,而不是逻辑意义。
        • 当然,如果编译器/平台在相关对象的内存空间中保留了一些额外的魔法数据,那么“新”将调用一些内部平台特定的东西,可能被称为“低-级别构造函数'将初始化它们,即非零。但是,它不是程序员认为的构造函数,并且这些魔法字段对他来说都是不可见的,而且它们都不会被编组到任何非托管意见。因此,我相信从程序员的角度来看,完全可以说:一切都归零,没有构造函数调用..
        • 至少在 C# 语言变化足够多之前,或者如果我们切换语言 :)
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-07-29
        • 1970-01-01
        • 2013-06-05
        • 1970-01-01
        • 1970-01-01
        • 2014-05-24
        相关资源
        最近更新 更多