在C#中使用带有`new`运算符的值类型的困境

dra*_*vic 10 .net c# value-type reference-type

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

class Person
{
    public int id;
    public string name;
}
Run Code Online (Sandbox Code Playgroud)

在以下代码中:

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

        Date someDate;
        someDate= new Date();
Run Code Online (Sandbox Code Playgroud)

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

然而,如果答案是:"何时new与值类型一起使用,它只会将对象的成员清零",而不是new运算符的含义不一致,因为:

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

提前谢谢,
干杯

Eri*_*ert 20

首先让我纠正你的错误.

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

作为"new"结果的引用是,而不是变量.该值指的是存储位置.

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

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

p变量在堆栈上,并且创建的Person实例(所有的memebers)都在堆上.p.id为0,p.name为null.

正确,但当然,如果抖动如此决定,p可以实现为寄存器.如果有可用的寄存器,则无需使用堆栈.

你似乎对这个堆栈正在使用的想法非常感兴趣.抖动可能有大量的寄存器,这些寄存器可能非常大.

我来自C++背景.

啊,这就解释了为什么你如此挂在这堆栈和堆上的东西.学会不要担心它.我们设计了一个托管内存环境,只要需要,它们就会存在.管理者是否选择使用堆栈,堆或寄存器来有效地管理内存取决于它.

在第一行中,someDate变量在堆栈上分配.准确地说是12个字节.

让我们假设为了论证,这个12字节结构被分配在堆栈上.似乎合理.

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

这个问题预示着错误的二分法,因此无法按照说明回答.这个问题提出了两个或两个选择,两者都不一定正确.

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

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

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

不正确."new"不保证堆已分配.相反,"new"保证在置零内存上调用构造函数.

让我们回到你的问题:

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

我们知道它不会在堆上分配空间.它是否将日期结构的成员归零?

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

someDate = new Date();    
Run Code Online (Sandbox Code Playgroud)
  • someDate的地址已确定
  • 为新对象分配空间(关闭"堆栈").它被清零了.
  • 然后调用构造函数(如果有的话),其中"this"是对新堆栈存储的引用
  • 然后将新堆栈存储的字节复制到someDate的地址.

现在,实际上会发生什么?您完全有权注意到无法判断是否已分配,初始化和复制新堆栈空间,或者是否初始化了"旧"堆栈空间.

答案是,在编译器推断用户无法注意到现有堆栈空间发生变异的情况下,现有的堆栈空间会发生变化,并且会省略额外的分配和后续复制.

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

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

http://blogs.msdn.com/b/ericlippert/archive/2010/10/11/debunking-another-myth-about-value-types.aspx


Ali*_*tad 5

好的,这是一个简单的:

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

编译为此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
Run Code Online (Sandbox Code Playgroud)

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

现实是,作为埃里克利珀解释在这里,有关于在栈上被分配值类型没有这样的一般规则.这完全取决于CLR的实现.