Почему локальные функции генерируют IL, отличные от анонимных методов и лямбда-выражений?

Почему компилятор С# 7 превращает локальные функции в методы в том же классе, где их родительская функция. В то время как для Анонимных методов (и Лямбда-выражений) компилятор генерирует вложенный класс для каждой родительской функции, которая будет содержать все его Анонимные методы как методы экземпляра?

Например, код С# (анонимный метод):

internal class AnonymousMethod_Example
{
    public void MyFunc(string[] args)
    {
        var x = 5;
        Action act = delegate ()
        {
            Console.WriteLine(x);
        };
        act();
    }
}

Создает IL-код (анонимный метод), похожий на:

.class private auto ansi beforefieldinit AnonymousMethod_Example
{
    .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_0'
    {
        .field public int32 x

        .method assembly hidebysig instance void '<MyFunc>b__0' () cil managed 
        {
            ...
            AnonymousMethod_Example/'<>c__DisplayClass0_0'::x
            call void [mscorlib]System.Console::WriteLine(int32)
            ...
        }
        ...
    }
...

Пока это, код С# (локальная функция):

internal class LocalFunction_Example
{
    public void MyFunc(string[] args)
    {
        var x = 5;
        void DoIt()
        {
            Console.WriteLine(x);
        };
        DoIt();
    }
}

Создает IL-код (локальная функция), похожий на:

.class private auto ansi beforefieldinit LocalFunction_Example
{
    .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_0' extends [mscorlib]System.ValueType
    {
        .field public int32 x
    }

    .method public hidebysig instance void MyFunc(string[] args) cil managed 
    {
        ...
        ldc.i4.5
        stfld int32 LocalFunction_Example/'<>c__DisplayClass1_0'::x
        ...
        call void LocalFunction_Example::'<MyFunc>g__DoIt1_0'(valuetype LocalFunction_Example/'<>c__DisplayClass1_0'&)
    }

    .method assembly hidebysig static void '<MyFunc>g__DoIt0_0'(valuetype LocalFunction_Example/'<>c__DisplayClass0_0'& '') cil managed 
    {
        ...
        LocalFunction_Example/'<>c__DisplayClass0_0'::x
        call void [mscorlib]System.Console::WriteLine(int32)
         ...
    }
}

Обратите внимание, что функция DoIt превратилась в статическую функцию в том же классе, что и ее родительская функция. Также закрытая переменная x превратилась в поле во вложенном struct (не вложенном class как в примере Анонимного метода).

Ответ 1

Анонимные методы, хранящиеся в делегатах, могут вызываться любым кодом, даже кодом, написанным на разных языках, скомпилированным за несколько лет до выхода С# 7, а CIL, сгенерированный компилятором, должен быть действительным для всех возможных применений. Это означает, что в вашем случае на уровне CIL метод не должен принимать никаких параметров.

Локальные методы могут вызываться только одним и тем же проектом С# (от более содержательного метода), поэтому тот же компилятор, который компилирует этот метод, также будет обработан для компиляции всех вызовов. Поэтому такой совместимости, как и для анонимных методов, не существует. Любой CIL, который производит те же самые эффекты, будет работать здесь, поэтому имеет смысл пойти на то, что наиболее эффективно. В этом случае повторная запись компилятором для включения использования типа значения вместо ссылочного типа предотвращает ненужные выделения.

Ответ 2

Основное использование анонимных методов (и лямбда-выражений) - это возможность передать их методу потребления, чтобы указать фильтр, предикат или что угодно, что хочет этот метод. Они не были специально предназначены для вызова из того же метода, который их определял, и эта способность рассматривалась только позже с делегатом System.Action.

С другой стороны, локальные методы - это точная противоположность - их основная цель - вызываться из того же метода, например, используя локальную переменную.

Анонимные методы могут быть вызваны из исходного метода, но они были реализованы на С# 2, и это конкретное использование не принималось во внимание.

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

Итак, посмотрим, где находится оптимизация. Наиболее важным изменением является структура вместо класса. Ну, анонимный метод требует доступа к внешним локальным переменным даже после возвращения исходного метода. Это называется закрытием, а "DisplayClass" - это то, что его реализует. Основное различие между указателями функций C и делегатами С# заключается в том, что делегат может дополнительно также переносить целевой объект, просто используемый как this (первый аргумент внутри). Метод привязан к целевому объекту, и объект передается методу каждый раз при вызове делегата (внутренне в качестве первого аргумента, а привязка фактически работает даже для статических методов).

Однако целевой объект... ну, object. Вы можете привязать метод к типу значения, но перед этим он должен быть помещен в бокс. Теперь вы можете понять, почему DisplayClass должен быть ссылочным типом в случае анонимного метода, потому что тип значения будет бременем, а не оптимизацией.

Использование локального метода устраняет необходимость привязки метода к объекту и рассмотрение передачи метода во внешний код. Мы можем выделить DisplayClass исключительно в стеке (как это должно быть для локальных данных), не предъявляя нагрузки на GC. Теперь у разработчиков было два варианта: либо сделать экземпляр LocalFunc, либо перенести его в DisplayClass, либо сделать его статическим, и сделать DisplayClass первым (ref). Нет никакой разницы в вызове метода, поэтому я считаю, что выбор был просто произвольным. Они могли бы решить иначе, без каких-либо различий.

Однако обратите внимание, как быстро эта оптимизация отбрасывается, когда она может превратиться в проблему с производительностью. Простое дополнение к вашему коду, например, Action a = DoIt;, немедленно нарушит метод LocalFunc. Затем реализация немедленно возвращается к одному из анонимных методов, потому что DisplayClass потребуется бокс и т.д.