Answers:
好吧,让我们看看是否可以更清楚地说明这一点。
首先,Ash是正确的:问题不在于值类型变量的分配位置。那是一个不同的问题-答案不只是“在堆栈上”。比这要复杂的多(并且在C#2中更加复杂)。我有一篇关于该主题的文章,如果需要,将对其进行扩展,但让我们只讨论一下new
运算符。
其次,所有这些实际上取决于您所谈论的级别。我正在根据其创建的IL来查看编译器对源代码的处理方式。JIT编译器很有可能会在优化大量“逻辑”分配方面做一些聪明的事情。
第三,我忽略了泛型,主要是因为我实际上并不知道答案,部分是因为它会使事情变得过于复杂。
最后,所有这些仅与当前的实现有关。C#规范并未对此进行详细说明-实际上是实现细节。有些人认为托管代码开发人员确实不应该在乎。我不确定我会走的那么远,但是值得想象一个世界,实际上所有局部变量都存在于堆中-仍然符合规范。
new
值类型的运算符有两种不同的情况:您可以调用无参数构造函数(例如new Guid()
)或有参数构造函数(例如new Guid(someString)
)。这些产生明显不同的IL。要了解原因,您需要比较C#和CLI规范:根据C#,所有值类型都具有无参数构造函数。根据CLI规范,没有值类型具有无参数构造函数。(一段时间后,通过反射来获取值类型的构造函数-您将找不到无参数的值。)
对于C#来说,将“用零初始化值”视为构造函数是有意义的,因为它使语言保持一致-您可以将其new(...)
视为始终调用构造函数。对于CLI,以不同的方式考虑它是有意义的,因为没有要调用的实际代码-当然也没有特定于类型的代码。
初始化值后,对值的处理方式也会有所不同。用于的IL
Guid localVariable = new Guid(someString);
与用于以下方面的IL不同:
myInstanceOrStaticVariable = new Guid(someString);
另外,如果将该值用作中间值(例如,方法调用的参数),则情况会稍有不同。为了显示所有这些差异,这是一个简短的测试程序。它没有显示出静态变量和实例变量之间的区别:IL stfld
和和之间会有所不同stsfld
,仅此而已。
using System;
public class Test
{
static Guid field;
static void Main() {}
static void MethodTakingGuid(Guid guid) {}
static void ParameterisedCtorAssignToField()
{
field = new Guid("");
}
static void ParameterisedCtorAssignToLocal()
{
Guid local = new Guid("");
// Force the value to be used
local.ToString();
}
static void ParameterisedCtorCallMethod()
{
MethodTakingGuid(new Guid(""));
}
static void ParameterlessCtorAssignToField()
{
field = new Guid();
}
static void ParameterlessCtorAssignToLocal()
{
Guid local = new Guid();
// Force the value to be used
local.ToString();
}
static void ParameterlessCtorCallMethod()
{
MethodTakingGuid(new Guid());
}
}
这是该类的IL,不包括无关位(例如nops):
.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object
{
// Removed Test's constructor, Main, and MethodTakingGuid.
.method private hidebysig static void ParameterisedCtorAssignToField() cil managed
{
.maxstack 8
L_0001: ldstr ""
L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
L_0010: ret
}
.method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
{
.maxstack 2
.locals init ([0] valuetype [mscorlib]System.Guid guid)
L_0001: ldloca.s guid
L_0003: ldstr ""
L_0008: call instance void [mscorlib]System.Guid::.ctor(string)
// Removed ToString() call
L_001c: ret
}
.method private hidebysig static void ParameterisedCtorCallMethod() cil managed
{
.maxstack 8
L_0001: ldstr ""
L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
L_0011: ret
}
.method private hidebysig static void ParameterlessCtorAssignToField() cil managed
{
.maxstack 8
L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
L_0006: initobj [mscorlib]System.Guid
L_000c: ret
}
.method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
{
.maxstack 1
.locals init ([0] valuetype [mscorlib]System.Guid guid)
L_0001: ldloca.s guid
L_0003: initobj [mscorlib]System.Guid
// Removed ToString() call
L_0017: ret
}
.method private hidebysig static void ParameterlessCtorCallMethod() cil managed
{
.maxstack 1
.locals init ([0] valuetype [mscorlib]System.Guid guid)
L_0001: ldloca.s guid
L_0003: initobj [mscorlib]System.Guid
L_0009: ldloc.0
L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
L_0010: ret
}
.field private static valuetype [mscorlib]System.Guid field
}
如您所见,有很多不同的指令用于调用构造函数:
newobj
:在堆栈上分配值,调用参数化构造函数。用于中间值,例如用于分配给字段或用作方法参数。call instance
:使用已分配的存储位置(无论是否在堆栈上)。这在上面的代码中用于分配给局部变量。如果使用多个new
调用多次给同一个局部变量分配一个值,则它只会在旧值的顶部初始化数据- 不会每次分配更多的堆栈空间。initobj
:使用已分配的存储位置,仅擦除数据。这用于我们所有的无参数构造函数调用,包括那些分配给局部变量的调用。对于方法调用,有效地引入了一个中间局部变量,其值被擦除了initobj
。我希望这可以显示该主题有多复杂,同时又可以给它带来一些启发。从某些概念上讲,每次调用都会new
在堆栈上分配空间-但正如我们所看到的,即使在IL级别,这也不是真正发生的情况。我想强调一例。采取这种方法:
void HowManyStackAllocations()
{
Guid guid = new Guid();
// [...] Use guid
guid = new Guid(someBytes);
// [...] Use guid
guid = new Guid(someString);
// [...] Use guid
}
“逻辑上”有4个堆栈分配-一个用于变量,一个用于三个new
调用中的每个-但实际上(对于该特定代码)该堆栈仅分配一次,然后重用同一存储位置。
编辑:请清楚一点,这仅在某些情况下是正确的...特别是,guid
如果Guid
构造函数抛出异常,则w 的值将不可见,这就是C#编译器能够重用同一堆栈槽的原因。请参阅Eric Lippert 关于值类型构造的博客文章,以了解更多详细信息和不适用的情况。
我在编写此答案时学到了很多知识-如果不清楚,请要求澄清!
List<Guid>
并添加这3个元素时会发生什么?那将是3个分配(相同的IL)?但是他们一直
guid
仅被一半覆盖并不重要,因为无论如何它都不可见。
一个class
或struct
声明就像是被用来在运行时创建实例或对象的蓝图。如果定义一个class
或struct
称为Person,则Person是类型的名称。如果声明并初始化类型为Person的变量p,则称p是Person的对象或实例。可以创建同一Person类型的多个实例,并且每个实例在properties
和中可以具有不同的值fields
。
A class
是引用类型。class
创建的对象时,为其分配对象的变量仅保留对该内存的引用。当对象引用分配给新变量时,新变量引用原始对象。通过一个变量进行的更改会反映在另一个变量中,因为它们都引用相同的数据。
A struct
是值类型。当struct
被创建,以该变量struct
被分配保持结构体的实际数据。将struct
分配给新变量后,将对其进行复制。因此,新变量和原始变量包含相同数据的两个单独副本。对一个副本所做的更改不会影响另一副本。
通常,classes
用于建模更复杂的行为,或用于在class
创建对象后修改的数据。Structs
最适合于主要包含在struct
创建之后不打算修改的数据的小型数据结构。
几乎所有被认为是值类型的结构都分配在堆栈上,而对象则分配在堆上,而对象引用(指针)则分配在堆栈上。