Ответ 1
Хорошо, посмотрим, смогу ли я сделать это более ясным.
Во-первых, Эш прав: вопрос не в том, где выделяются переменные типа значения. Это другой вопрос - и тот, на который ответ не просто "на стек". Это сложнее, чем это (и еще более усложнило С# 2). У меня есть статья по теме и будет расширяться по ней, если потребуется, но разрешите работать только с оператором new
.
Во-вторых, все это действительно зависит от уровня, о котором вы говорите. Я смотрю, что делает компилятор с исходным кодом, с точки зрения ИЛ, который он создает. Это более чем возможно, что JIT-компилятор будет делать умные вещи с точки зрения оптимизации довольно много "логического" распределения.
В-третьих, я игнорирую дженерики, главным образом потому, что на самом деле я не знаю ответа, а отчасти потому, что это слишком усложняет ситуацию.
Наконец, все это только с текущей реализацией. Спецификация С# не указывает многое из этого - это эффективно деталь реализации. Есть те, кто считает, что разработчикам управляемого кода действительно не стоит. Я не уверен, что я зашел так далеко, но стоит вообразить мир, где на самом деле все локальные переменные живут на куче, которые по-прежнему будут соответствовать спецификации.
Существует две ситуации с оператором new
для типов значений: вы можете вызвать конструктор без параметров (например, new Guid()
) или конструктор с параметрами (например, new Guid(someString)
). Они генерируют значительно разные ИЛ. Чтобы понять, почему вам нужно сравнить спецификации С# и CLI: согласно С#, все типы значений имеют конструктор без параметров. Согласно спецификации CLI, никакие типы значений не имеют конструкторов без параметров. (Выбираем конструкторы типа значения с отражением некоторое время - вы не найдете без параметров.)
Для С# имеет смысл рассматривать "инициализировать значение с нулями" как конструктор, потому что он сохраняет язык согласованным - вы можете думать о new(...)
, как всегда вызывающем конструктор. Для CLI имеет смысл думать об этом по-другому, так как нет никакого реального кода для вызова - и, конечно, никакого кода, специфичного для конкретного типа.
Также имеет значение то, что вы собираетесь делать со значением после того, как вы его инициализировали. ИЛ, используемый для
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 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
, но на самом деле (для этого конкретного кода) стек распределяется только один раз, а затем одно и то же хранилище местоположение повторно используется.
EDIT: просто, чтобы быть ясным, это верно только в некоторых случаях... в частности, значение guid
не будет видно, если конструктор guid
генерирует исключение, поэтому компилятор С# может повторно использовать один и тот же слот стека. См. Eric Lippert сообщение в блоге о построении типа значения для получения более подробной информации и случая, когда оно не применяется.
Я многому научился в написании этого ответа - просьба уточнить, если кто-либо из них неясен!