В этой статье мы рассмотрим статические анонимные функции, которые появились в C# 9. Чтобы понять, как они могут улучшить наш код, необходимо сначала рассмотреть нестатические анонимные методы и лямбда-выражения. Посмотрим, что происходит с точки зрения выделения памяти при вызове одного из них.
Скачать исходный код этой статьи можно в нашем репозитории GitHub.
Давайте же погрузимся в работу.
Нестатическая анонимная функция
Анонимные методы и лямбда-выражения требуют больших затрат. Естественно, одиночный вызов не оказывает заметного влияния на производительность. Однако в нашей программе может быть несколько вызовов, например, в цикле. В этом случае эти незначительные потери производительности начинают нарастать.
Давайте рассмотрим один вызов и посмотрим, что происходит при вызове анонимного метода. Выделение кучи может быть нулевым, одним или двумя, в зависимости от того, что метод берет из окружающего состояния. Если он захватывает состояние окружающего экземпляра, то происходит только выделение делегата. Если метод захватывает локальную переменную или аргумент, то происходит два выделения кучи — одно для закрытия и одно для делегата. Выделения из кучи не происходит только в том случае, если метод ничего не захватывает или захватывает статическое состояние.
Рассмотрим, как анонимный метод получает переменную из закрытой области видимости:
1 2 3 4 5 6 7 8 9 10 11 |
private double _numberInEnclosingScope = 4; void Calculate(Func<double, double> func) { Console.WriteLine(func(6)); } public void Display() { Calculate(num => Math.Pow(_numberInEnclosingScope, num)); } |
Лямбда-выражение захватывает переменную _numberInEnclosingScope
из закрытой области видимости, что приводит к непреднамеренному выделению памяти. Исправить это можно, превратив анонимный метод в статический.
Static Anonymous Function
Чтобы преобразовать нестатический анонимный метод или лямбда-выражение в статическую анонимную функцию, необходимо использовать модификатор static. Статические анонимные функции не захватывают переменные из объемлющей области видимости, но могут ссылаться на них при выполнении определенных условий.
К чему имеет доступ функция
Внутри статической анонимной функции мы имеем доступ к переменным, определенным в приватной области видимости, только если помечаем их как const
или static
. Если мы не хотим, чтобы переменная _numberInEnclosingScope
в нашем примере была доступной, но при этом хотим использовать ее внутри лямбда-выражения, мы должны сделать ее константной:
1 2 3 4 5 6 |
private const double _numberInEnclosingScope = 4; public void Display() { Calculate(static num => Math.Pow(_numberInEnclosingScope, num)); } |
Как и раньше, мы должны увидеть результат увеличения одного числа в большую сторону. Естественно, никакого прироста производительности мы не заметим. Но код действительно стал более производительным, чем раньше. Это связано с тем, что теперь лямбда-выражение не блокирует переменную. Поверьте нам на слово. Мы собираемся продемонстрировать, как выбор статической функции вместо ее нестатического аналога влияет на распределение памяти. Для этого далее в статье мы проведем бенчмаркинг наших функций.
Теперь давайте пометим переменную как статическую и посмотрим, доступна ли она по-прежнему:
Все работает так же хорошо, и мы не получаем никаких ошибок. Итак, мы только что доказали, что при выполнении определенных условий можно обращаться к переменным из объемлющей области видимости. Однако существуют некоторые ограничения на то, на какие еще переменные мы можем ссылаться внутри статической анонимной функции.
К чему у функции нет доступа
Есть некоторые переменные, к которым статическая анонимная функция не имеет доступа. Она не имеет доступа к переменным, определенным в том же классе, на которые мы обычно ссылаемся с помощью ключевого слова this. В эту группу также входят переменные, определенные в базовом классе, на которые мы ссылаемся с помощью ключевого слова base. Наконец, в ней нельзя ссылаться на локали и параметры. Для демонстрации этого создадим два класса DemoStaticBase и производный от него класс DemoStaticDerivative:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class DemoStaticBase { public double numberInBase = 3; } public class DemoStaticDerivative : DemoStaticBase { private double _numberInThis = 4; void Calculate(Func<double, double> func) { Console.WriteLine(func(6)); } public void Display(double numberInParameter) { double numberInLocal = 2; // Error CS8821 Calculate(static num => Math.Pow(this._numberInThis, num)); } } |
Здесь мы видим все четыре вышеупомянутых типа переменных с довольно понятными именами: numberInBase, _numberInThis, numberInParameter и numberInLocal. Мы пытаемся использовать их в статической анонимной функции, но Visual Studio сразу же выдает ошибку:
Error CS8821: A static anonymous function cannot contain a reference to ‘this’ or ‘base’.
Кстати, мы также получаем сообщение о том, что имя может быть упрощено. Это связано с тем, что ключевое слово this в лямбда-выражении является избыточным. Мы использовали его только для того, чтобы подчеркнуть его наличие.
Далее, если мы попытаемся использовать переменную numberInBase
, определенную в базовом классе, это не сработает. Мы получили ту же ошибку во время компиляции.
Если мы попытаемся использовать numberInLocal
или numberInParameter
в лямбда-выражении:
Calculate(static num => Math.Pow(numberInLocal, num));
Мы получаем немного другую ошибку:
Error CS8820: A static anonymous function cannot contain a reference to ‘numberInLocal’.
В наших примерах статическая анонимная функция была довольно простой, но это не обязательно так. Например, ничто не мешает нам определить в нем локальные переменные и методы. Вопрос в том, как они себя ведут? Имеют ли локальные методы доступ к состоянию во включающей статической анонимной функции? Давайте посмотрим на это в действии.
Non-static Local Methods
Давайте определим нестатическую локальную функцию и посмотрим, может ли она перехватывать состояние из приватной статической функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public void Display() { Calculate(static num => { double numberInStatic = 5; double AddNumbers() { return num + numberInStatic; } return Math.Pow(AddNumbers(), 2); }); } |
Здесь мы определяем нестатический локальный метод внутри нашей статической анонимной функции. Получается, что мы имеем доступ к параметру num и локальной переменной numberInStatic, определенной во вложенной функции. Однако мы по-прежнему не можем ссылаться ни на какие другие переменные, недоступные самой статической анонимной функции.
Все это выглядит просто великолепно, но мы по-прежнему не видим никакого прироста производительности. Давайте протестируем наш код, чтобы увидеть, есть ли он вообще.
Прирост производительности
Поскольку статические анонимные функции полностью зависят от производительности, мы фактически сравним производительность с помощью библиотеки BenchmarkDotNet
.
Во-первых, давайте создадим три метода, которые будут подвергнуты сравнительному анализу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
private int _numNonConst = 10; private const int _numConst = 10; public int Calculate(Func<int, int> func) { return func(6); } [Benchmark] public int MultiplyNonStatic() { return Calculate(num => _numNonConst * num); } [Benchmark] public int MultiplyNonStaticWithConst() { return Calculate(num => _numConst * num); } [Benchmark] public int MultiplyStatic() { return Calculate(static num => _numConst * num); } |
Это очень простые методы, которые просто умножают два целых числа и возвращают результат. Первый использует нестатическое лямбда-выражение с непостоянной переменной. Второй использует нестатическое лямбда-выражение с постоянной переменной. Третий метод использует статическое лямбда-выражение с постоянной переменной.
Давайте взглянем на результаты:
1 2 3 4 5 |
| Method | Mean | Error | StdDev | Median | Gen0 | Allocated | |--------------------------- |---------:|----------:|----------:|---------:|-------:|----------:| | MultiplyNonStaticWithConst | 1.302 ns | 0.0162 ns | 0.0144 ns | 1.299 ns | - | - | | MultiplyStatic | 1.551 ns | 0.0160 ns | 0.0125 ns | 1.555 ns | - | - | | MultiplyNonStatic | 6.552 ns | 0.1619 ns | 0.3157 ns | 6.401 ns | 0.0153 | 64 B | |
Итак, мы можем ясно видеть, что два из этих методов работают быстрее и не выделяют никакой памяти. Это методы со статической анонимной функцией и метод с нестатической анонимной функцией с постоянной переменной. Похоже, что статические анонимные функции обязаны своим повышением производительности самой природе констант и статики в C#.
Заключение
Статические анонимные функции не могут захватывать состояние из приватной области видимости и могут ссылаться только на постоянные и статические переменные. Таким образом, они могут повысить производительность за счет уменьшения непредвиденного выделения памяти. Мы сравнили их, чтобы увидеть, есть ли какой-либо прирост производительности, и оказалось, что они действительно быстрее и производительнее. Их использование имеет наибольший смысл при наличии многочисленных вызовов анонимных методов или лямбда-выражений. Они определенно являются полезной функцией C#, которую мы можем использовать.
https://code-maze.com/csharp-static-anonymous-functions/