Почему при использовании инициализаторов объектов компилятор генерирует дополнительную локальную переменную?
Отвечая вчера на вопрос о SO, я заметил, что если объект инициализируется с помощью Object Initializer, компилятор создает дополнительную локальную переменную.
Рассмотрим следующий код C# 3.0, скомпилированный в режиме выпуска в VS2008:
public class Class1
{
public string Foo { get; set; }
}
public class Class2
{
public string Foo { get; set; }
}
public class TestHarness
{
static void Main(string[] args)
{
Class1 class1 = new Class1();
class1.Foo = "fooBar";
Class2 class2 =
new Class2
{
Foo = "fooBar2"
};
Console.WriteLine(class1.Foo);
Console.WriteLine(class2.Foo);
}
}
Используя Reflector, мы можем проверить код для метода Main:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 2
.locals init (
[0] class ClassLibrary1.Class1 class1,
[1] class ClassLibrary1.Class2 class2,
[2] class ClassLibrary1.Class2 <>g__initLocal0)
L_0000: newobj instance void ClassLibrary1.Class1::.ctor()
L_0005: stloc.0
L_0006: ldloc.0
L_0007: ldstr "fooBar"
L_000c: callvirt instance void ClassLibrary1.Class1::set_Foo(string)
L_0011: newobj instance void ClassLibrary1.Class2::.ctor()
L_0016: stloc.2
L_0017: ldloc.2
L_0018: ldstr "fooBar2"
L_001d: callvirt instance void ClassLibrary1.Class2::set_Foo(string)
L_0022: ldloc.2
L_0023: stloc.1
L_0024: ldloc.0
L_0025: callvirt instance string ClassLibrary1.Class1::get_Foo()
L_002a: call void [mscorlib]System.Console::WriteLine(string)
L_002f: ldloc.1
L_0030: callvirt instance string ClassLibrary1.Class2::get_Foo()
L_0035: call void [mscorlib]System.Console::WriteLine(string)
L_003a: ret
}
Здесь мы видим, что компилятор сгенерировал две ссылки на экземпляр Class2
(class2
а также <>g__initLocal0
), но только одна ссылка на экземпляр Class1
(class1
).
Я не очень знаком с IL, но похоже, что он <>g__initLocal0
перед настройкой class2 = <>g__initLocal0
,
Почему это происходит?
Следует ли из этого, что при использовании инициализаторов объектов происходит снижение производительности (даже если оно очень незначительное)?
3 ответа
Потокобезопасность и атомарность.
Сначала рассмотрим следующую строку кода:
MyObject foo = new MyObject { Name = "foo", Value = 42 };
Любой, кто читает это утверждение, может разумно предположить, что конструкция foo
объект будет атомным. До назначения объект вообще не существует. Как только назначение завершено, объект существует и находится в ожидаемом состоянии.
Теперь рассмотрим два возможных способа перевода этого кода:
// #1
MyObject foo = new MyObject();
foo.Name = "foo";
foo.Value = 42;
// #2
MyObject temp = new MyObject(); // temp will be a compiler-generated name
temp.Name = "foo";
temp.Value = 42;
MyObject foo = temp;
В первом случае foo
Объект создается в первой строке, но он не будет в ожидаемом состоянии, пока не завершится выполнение последней строки. Что произойдет, если другой поток попытается получить доступ к объекту до выполнения последней строки? Объект будет в полуинициализированном состоянии.
Во втором случае foo
объект не существует до последней строки, когда он назначен из temp
, Поскольку ссылочное присваивание является атомарной операцией, это дает точно такую же семантику, что и исходный однострочный оператор присваивания. то есть foo
объект никогда не существует в полуинициализированном состоянии.
Ответ Люка правильный и отличный, так хорошо для тебя. Это, однако, не завершено. Есть еще больше веских причин, почему мы делаем это.
Спецификация предельно ясна, что это правильный codegen; спецификация говорит, что инициализатор объекта создает временную невидимую локальную область, в которой хранится результат выражения. Но почему мы так уточнили? То есть почему
Foo foo = new Foo() { Bar = bar };
средства
Foo foo;
Foo temp = new Foo();
temp.Bar = bar;
foo = temp;
и не более простой
Foo foo = new Foo();
foo.Bar = bar;
Ну, с чисто практической точки зрения, всегда проще указать поведение выражения на основе его содержимого, а не его контекста. Тем не менее, для этого конкретного случая, предположим, что мы указали, что это был желаемый коден для присвоения локальному или полю. В этом случае foo будет определенно назначен после (), и, следовательно, может использоваться в инициализаторе. Вы действительно хотите
Foo foo = new Foo() { Bar = M(foo) };
быть законным? Надеюсь нет. foo определенно не назначается до тех пор, пока не будет выполнена инициализация.
Или рассмотрим свойства.
Frob().MyFoo = new Foo() { Bar = bar };
Это должно быть
Foo temp = new Foo();
temp.Bar = bar;
Frob().MyFoo = temp;
и не
Frob().MyFoo = new Foo();
Frob().MyFoo.Bar = bar;
потому что мы не хотим, чтобы Frob() вызывался дважды, и мы не хотим, чтобы свойство MyFoo обращалось дважды, мы хотим, чтобы к ним каждый обращался по одному.
Теперь, в вашем конкретном случае, мы могли бы написать проход оптимизации, который обнаружит, что лишний локальный ненужен, и оптимизировать его. Но у нас есть другие приоритеты, и джиттер, вероятно, делает хорошую работу по оптимизации местных жителей.
Хороший вопрос. Я давно хотел написать это в блоге.
Почему: может быть, это сделано для того, чтобы гарантировать, что "известной" ссылки на не (полностью) инициализированный объект (с точки зрения языка) не существует? Что-то вроде (псевдо) семантики конструктора для инициализатора объекта? Но это всего лишь идея... и я не могу представить способ использования ссылки и доступа к неинициализированному объекту, кроме как в многопоточной среде.
РЕДАКТИРОВАТЬ: слишком медленно..