Подтвердить что ты не робот

При использовании инициализаторов объектов, почему компилятор генерирует дополнительную локальную переменную?

Отвечая на вопрос о SO вчера, я заметил, что если объект инициализируется с помощью Object Initializer, компилятор создает дополнительную локальную переменную.

Рассмотрим следующий код С# 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.

Почему это происходит?

Далее следует, что при использовании Object Initializers (даже если он очень мал) возникают служебные издержки производительности?

4b9b3361

Ответ 1

Безопасность потоков и атомарность.

Сначала рассмотрим эту строку кода:

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 никогда не существует в полуинициализированном состоянии.

Ответ 2

Ответ Луки - правильный и превосходный, так хорош на вас. Это, однако, не завершено. Есть еще более веские причины, по которым мы это делаем.

В спецификации очень ясно, что это правильный код; в спецификации говорится, что инициализатор объекта создает временный невидимый локальный объект, в котором хранится результат выражения. Но почему мы так выразились? Именно поэтому

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 обращалось дважды, мы хотим, чтобы каждый из них обращался один раз.

Теперь, в вашем конкретном случае, мы могли бы написать оптимизирующий пропуск, который обнаруживает, что дополнительный локальный сервер не нужен и оптимизирует его. Но у нас есть другие приоритеты, и джиттер, вероятно, хорошо справляется с оптимизацией локальных жителей.

Хороший вопрос. Я имел в виду, чтобы это было какое-то время.

Ответ 3

Для чего: может быть, это сделано для обеспечения того, чтобы не существовала "известная" ссылка на не полностью (полностью) инициализированный объект (с точки зрения языка)? Что-то вроде (псевдо)) семантики конструктора для инициализатора объекта? Но это просто идея.. и я не могу себе представить способ использования ссылки и доступа к не инициализированному объекту, кроме того, в многопоточной среде.

РЕДАКТИРОВАТЬ: слишком медленно..