Start Debugging

C# 14: упрощённые параметры с модификаторами в лямбдах

В C# 14 модификаторы ref, out, in, scoped и ref readonly можно применять к лямбда-параметрам с неявно выводимыми типами, что избавляет от необходимости явно указывать типы параметров.

Лямбда-выражения уже много лет являются одной из ключевых возможностей C#: они позволяют лаконично описывать встроенные функции и обратные вызовы. В C# у лямбды могут быть параметры с явно указанными типами (когда вы задаёте тип каждого параметра) или параметры с неявно выводимыми типами (когда типы определяются по контексту). До C# 14, чтобы использовать в лямбде определённые модификаторы параметров (например, передачу по ссылке или out-параметры), приходилось явно объявлять типы параметров. Это часто приводило к более многословной форме лямбды в тех случаях, когда эти модификаторы были нужны.

В C# 14 появилась новая возможность, устраняющая это ограничение: простые параметры лямбды с модификаторами. Эта возможность позволяет использовать модификаторы параметров, такие как ref, in, out, scoped и ref readonly, в лямбда-выражении без явного указания типов параметров. Проще говоря, теперь эти модификаторы можно добавлять к “нетипизированным” параметрам лямбды (параметрам, типы которых выводятся), и лямбды со специальными способами передачи параметров становятся проще и для написания, и для чтения.

Лямбды в C# 13 и более ранних версиях

В C# 13 и всех предыдущих версиях параметры лямбды могли быть либо с явно указанными, либо с неявно выводимыми типами, но при использовании модификаторов параметров был один нюанс. Если хотя бы одному параметру лямбды требовался модификатор (например, параметр out или ref), компилятор C# требовал, чтобы у всех параметров этой лямбды были явно указаны типы. Применить ref, in, out, scoped или ref readonly к параметру лямбды было невозможно без указания его типа.

Например, представьте тип делегата с параметром out:

// A delegate that tries to parse a string into T, returning true on success.
delegate bool TryParse<T>(string text, out T result);

Если в C# 13 вы хотели присвоить этому делегату лямбду, нужно было явно указать типы обоих параметров, потому что один из них использует модификатор out. Корректное присваивание лямбды в C# 13 выглядело бы так:

// C# 13 and earlier: must explicitly specify types when using 'out'
TryParse<int> parseOld = (string text, out int result) => Int32.TryParse(text, out result);

Здесь мы явно написали string для параметра text и int для параметра result. Если попытаться опустить типы, код просто не скомпилируется. Иными словами, что-то вроде (text, out result) => ... в C# 13 было запрещено, поскольку наличие out у result требовало явного указания типа result (int в данном случае). Это требование распространялось на любой из модификаторов ref, in, out, ref readonly и scoped в списках параметров лямбды.

Модификаторы параметров лямбды в C# 14

В C# 14 это ограничение снято, и лямбды стали более гибкими. Теперь к параметрам лямбды можно добавлять модификаторы без явного указания их типов. Компилятор будет выводить типы из контекста (например, из типа делегата или дерева выражений, к которому приводится лямбда), при этом по-прежнему позволяя использовать модификаторы. Это улучшение означает меньше шаблонного кода и более читаемый код при работе с делегатами или выражениями, в которых задействованы параметры по ссылке или scoped-параметры.

Поддерживаемые модификаторы. Начиная с C# 14, к параметрам лямбды с неявно выводимыми типами можно применять следующие модификаторы:

Раньше эти модификаторы можно было использовать только при явной типизации параметров лямбды. Теперь их можно записывать в списке параметров лямбды без указания типов.

Важная оговорка: модификатор params в эту новую возможность не входит. Если у лямбды есть параметр params (для переменного числа аргументов), его тип по-прежнему нужно указывать явно. Короче говоря, params всё так же требует явно типизированного списка параметров в лямбдах.

Вернёмся к раннему примеру с делегатом TryParse<T>, чтобы увидеть, как C# 14 упрощает синтаксис. Теперь можно опустить имена типов и продолжать использовать модификатор out:

// C# 14: type inference with 'out' parameter
TryParse<int> parseNew = (text, out result) => Int32.TryParse(text, out result);

Эта лямбда присваивается TryParse<int>, поэтому из определения делегата компилятор знает, что text — это string, а resultint. Мы смогли написать (text, out result) => ... без явного указания типов, и код корректно компилируется и работает. Модификатор out применяется к result, хотя мы не написали int. C# 14 выводит это за нас, благодаря чему объявление лямбды короче и не повторяет информацию, которую компилятор и так знает.

То же самое работает и для других модификаторов. Рассмотрим делегат с параметром по ссылке:

// A delegate that doubles an integer in place.
delegate void Doubler(ref int number);

В C# 13, чтобы создать лямбду, соответствующую этому делегату, тип нужно было указывать вместе с модификатором ref:

// C# 13: explicit type needed for 'ref' parameter
Doubler makeDoubleOld = (ref int number) => number *= 2;

В C# 14 можно опустить тип и оставить только модификатор и имя параметра:

// C# 14: implicit type with 'ref' parameter
Doubler makeDoubleNew = (ref number) => number *= 2;

Здесь контекст (делегат Doubler, который принимает ref int и возвращает void) сообщает компилятору, что number — это int, поэтому повторно указывать тип не требуется. В списке параметров лямбды мы просто пишем ref number.

Можно использовать сразу несколько модификаторов или другие формы этих модификаторов точно так же. Например, если у вас есть делегат с параметром ref readonly или scoped-параметром, в C# 14 их также можно записывать без явных типов. Например:

// A delegate with an 'in' (readonly ref) parameter
delegate void PrintReadOnly(in DateTime value);

// C# 14: using 'in' without explicit type
PrintReadOnly printDate = (in value) => Console.WriteLine(value);

Аналогично для делегата со scoped-параметром:

// A delegate that takes a scoped Span<int>
delegate int SumElements(scoped Span<int> data);

// C# 14: using 'scoped' without explicit type
SumElements sum = (scoped data) =>
{
    int total = 0;
    foreach (int x in data)
        total += x;
    return total;
};

Здесь из делегата известно, что data — это Span<int> (тип, существующий только на стеке), и мы помечаем его как scoped, не указывая имя типа. Это гарантирует, что data не сможет быть захвачен за пределами лямбды (в соответствии с семантикой scoped), как если бы мы написали (scoped Span<int> data).

Какие выгоды это даёт

Простые параметры лямбды с модификаторами делают код чище и уменьшают количество повторов. В предыдущих версиях C# использование параметров по ссылке или scoped-параметров в лямбдах означало, что приходилось писать типы, которые компилятор и так умеет выводить. Теперь можно доверить выведение типов компилятору и при этом по-прежнему выражать намерение (например, что параметр передаётся по ссылке или является выходным). Это даёт более лаконичные и легко читаемые лямбды, особенно когда сигнатуры делегатов сложны или используют обобщённые типы.

Стоит отметить, что эта возможность не меняет поведение лямбд во время выполнения и не меняет работу самих модификаторов; меняется лишь синтаксис, которым вы объявляете параметры лямбды. Лямбда продолжит следовать тем же правилам для ref, out, in и так далее, как если бы вы написали их с явными типами. Модификатор scoped по-прежнему гарантирует, что значение не будет захвачено за пределами выполнения лямбды. Главное улучшение состоит просто в том, что исходный код меньше засорён именами типов.

Эта возможность в C# 14 приводит синтаксис лямбд к тому же удобству вывода типов, что уже давно есть в других местах языка. Теперь лямбды с ref и другими модификаторами можно писать более естественно, аналогично тому, как уже много лет можно опускать типы в лямбдах, когда модификаторов нет. Только не забывайте: если в лямбде нужен params-массив, тип по-прежнему придётся писать явно.

Ссылки

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад