Хорошо, давайте посмотрим, смогу ли я сделать это более понятным.
Во-первых, Эш прав: вопрос не о том, где тип значения переменные . Это другой вопрос, на который ответ не просто «в стеке». Это сложнее, чем это (и стало еще сложнее в C # 2). У меня есть статья на тему , и я буду расширять ее, если потребуется, но давайте разберемся только с оператором new
.
Во-вторых, все это действительно зависит от того, на каком уровне вы говорите. Я смотрю на то, что компилятор делает с исходным кодом, с точки зрения IL, который он создает. Более чем возможно, что JIT-компилятор будет делать умные вещи с точки зрения оптимизации большого количества «логического» распределения.
В-третьих, я игнорирую генерики, в основном потому, что я на самом деле не знаю ответа, а отчасти потому, что это слишком усложнит ситуацию.
Наконец, все это только с текущей реализацией. Спецификация C # не определяет многое из этого - это фактически деталь реализации. Есть те, кто считает, что разработчикам управляемого кода на самом деле все равно. Я не уверен, что зашел бы так далеко, но стоит представить мир, в котором фактически все локальные переменные живут в куче - что все равно будет соответствовать спецификации.
<Ч />
Существуют две различные ситуации с оператором new
для типов значений: вы можете вызвать конструктор без параметров (например, new Guid()
) или конструктор с параметрами (например, new Guid(someString)
). Они генерируют существенно разные IL. Чтобы понять почему, вам нужно сравнить спецификации C # и CLI: в соответствии с C # все типы значений имеют конструктор без параметров. Согласно спецификации CLI, типы значений no имеют конструкторы без параметров. (Получить конструкторы типа значения с отражением некоторое время - вы не найдете один без параметров.)
В 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
выдает исключение, поэтому компилятор C # может использовать один и тот же слот стека. См. Блог Эрика Липперта о конструкции типа значения для получения более подробной информации и случая, когда не не применимо.
Я многому научился писать этот ответ - пожалуйста, попросите разъяснений, если что-то неясно!