Назначение локальных функций делегатам
В C# 7.0 вы можете объявлять локальные функции, то есть функции, живущие внутри другого метода. Эти локальные функции могут обращаться к локальным переменным окружающего метода. Поскольку локальные переменные существуют только во время вызова метода, я подумал, может ли локальная функция быть назначена делегату (который может жить дольше, чем этот вызов метода).
public static Func<int,int> AssignLocalFunctionToDelegate()
{
int factor;
// Local function
int Triple(int x) => factor * x;
factor = 3;
return Triple;
}
public static void CallTriple()
{
var func = AssignLocalFunctionToDelegate();
int result = func(10);
Console.WriteLine(result); // ==> 30
}
Это на самом деле работает!
Мой вопрос: почему это работает? Что здесь происходит?
2 ответа
Это работает, потому что компилятор создает делегат, который захватывает factor
переменная в закрытии.
Фактически, если вы используете декомпилятор, вы увидите, что генерируется следующий код:
public static Func<int, int> AssignLocalFunctionToDelegate()
{
int factor = 3;
return delegate (int x) {
return (factor * x);
};
}
Ты это видишь factor
будет захвачен в закрытии. (Вы, наверное, уже знаете, что за кулисами компилятор сгенерирует класс, содержащий поле для хранения factor
.)
На моей машине он создает следующий класс, чтобы действовать как замыкание:
[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
// Fields
public int factor;
// Methods
internal int <AssignLocalFunctionToDelegate>g__Triple0(int x)
{
return (this.factor * x);
}
}
Если я изменю AssignLocalFunctionToDelegate()
в
public static Func<int, int> AssignLocalFunctionToDelegate()
{
int factor;
int Triple(int x) => factor * x;
factor = 3;
Console.WriteLine(Triple(2));
return Triple;
}
тогда реализация становится:
public static Func<int, int> AssignLocalFunctionToDelegate()
{
<>c__DisplayClass1_0 CS$<>8__locals0;
int factor = 3;
Console.WriteLine(CS$<>8__locals0.<AssignLocalFunctionToDelegate>g__Triple0(2));
return delegate (int x) {
return (factor * x);
};
}
Вы можете видеть, что он создает экземпляр сгенерированного компилятором класса для использования с Console.WriteLine().
То, что вы не можете видеть, это то, где он на самом деле назначает 3
в factor
в декомпилированном коде. Чтобы увидеть это, вы должны взглянуть на сам IL (это может быть сбой в используемом мной декомпиляторе, который довольно старый).
IL выглядит так:
L_0009: ldc.i4.3
L_000a: stfld int32 ConsoleApp3.Program/<>c__DisplayClass1_0::factor
Это загружает постоянное значение 3 и сохраняет его в factor
поле сгенерированного компилятором класса замыкания.
Поскольку локальные переменные существуют только во время вызова метода,
Это утверждение неверно. И как только вы поверите ложному утверждению, вся ваша цепочка рассуждений перестанет звучать.
"Время жизни не дольше, чем активация метода" не является определяющей характеристикой локальных переменных. Определяющей характеристикой локальной переменной является то, что имя переменной имеет смысл только для кода в локальной области видимости переменной.
Не путайте прицел с продолжительностью жизни! Они не одно и то же. Время жизни - это концепция времени выполнения, описывающая, как восстанавливается хранилище. Scope - это концепция времени компиляции, описывающая, как имена связаны с элементами языка. Локальные переменные называются локальными из-за их локальной области видимости; их местность - все об их именах, а не их жизнях.
Время жизни локальных переменных может быть произвольно увеличено или сокращено по соображениям производительности или корректности. В C# отсутствует требование, чтобы локальные переменные имели время жизни только при активированном методе.
Но вы уже знали, что:
IEnumerable<int> Numbers(int n)
{
for (int i = 0; i < n; i += 1) yield return i;
}
...
var nums = Numbers(7);
foreach(var num in nums)
Console.WriteLine(num);
Если время жизни локальных элементов i и n ограничено методом, то как i и n могут иметь значения после Numbers
возвращается?
Task<int> FooAsync(int n)
{
int sum = 0;
for(int i = 0; i < n; i += 1)
sum += await BarAsync(i);
return sum;
}
...
var task = FooAsync(7);
FooAsync
возвращает задание после первого вызова BarAsync
, Но почему-то sum
а также n
а также i
продолжать иметь ценности, даже после FooAsync
возвращается к звонящему.
Func<int, int> MakeAdder(int n)
{
return x => x + n;
}
...
var add10 = MakeAdder(10);
Console.WriteLine(add10(20));
как-то n
слоняется даже после MakeAdder
вернулся.
Локальные переменные могут легко жить после того, как метод, который их активировал, вернется; это происходит все время в C#.
Что здесь происходит?
Локальная функция, преобразованная в делегат, логически не сильно отличается от лямбды; поскольку мы можем преобразовывать лямбда-выражения в делегаты, мы можем преобразовывать локальные методы в делегаты.
Еще один способ думать об этом: предположим, что вместо этого ваш код был:
return y=>Triple(y);
Если вы не видите никаких проблем с этим лямбда, то не должно быть никаких проблем с просто return Triple;
- опять же, эти два фрагмента кода логически являются одной и той же операцией, поэтому, если есть стратегия реализации для одного, то есть стратегия реализации для другого.
Обратите внимание, что вышеизложенное не подразумевает, что команда компилятора должна генерировать локальные методы как лямбды с именами. Команда компиляторов, как всегда, может свободно выбирать любую стратегию реализации, которая им нравится, в зависимости от того, как используется локальный метод. Подобно тому, как команда разработчиков имеет множество незначительных изменений в стратегии генерации лямбда-в-преобразование в зависимости от деталей лямбда-выражения.
Если, например, вы заботитесь о влиянии на производительность этих различных стратегий, то, как всегда, ничто не заменит попробовать реалистичные сценарии и получить эмпирические измерения.