Поделиться через


Улучшения для Lambda

Заметка

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

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия зафиксированы в соответствующих заседаниях по проектированию языка (LDM).

Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .

Вопрос чемпиона: https://github.com/dotnet/csharplang/issues/4934

Сводка

Предлагаемые изменения:

  1. Разрешить лямбда-выражения с атрибутами
  2. Разрешить лямбда-выражения с явным типом возвращаемого значения
  3. Вывод естественного типа делегата для лямбда-групп и групп методов

Мотивация

Поддержка атрибутов для лямбда-кодов обеспечивает паритетность с методами и локальными функциями.

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

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

Требование явных типов делегатов для лямбда-выражений и групп методов стало камнем преткновения для клиентов и препятствием для прогресса в ASP.NET в связи с недавней работой над MapAction.

ASP.NET MapAction без предлагаемых изменений (MapAction() принимает аргумент System.Delegate):

[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction((Func<Todo>)GetTodo);

[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo;
app.MapAction((Func<Todo, Todo>)PostTodo);

ASP.NET MapAction с естественными типами для групп методов:

[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction(GetTodo);

[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo;
app.MapAction(PostTodo);

ASP.NET MapAction с атрибутами и естественными типами для лямбда-выражений.

app.MapAction([HttpGet("/")] () => new Todo(Id: 0, Name: "Name"));
app.MapAction([HttpPost("/")] ([FromBody] Todo todo) => todo);

Атрибуты

Атрибуты могут быть добавлены в лямбда-выражения и лямбда-параметры. Чтобы избежать неоднозначности атрибутов метода и атрибутов параметров, лямбда-выражение с атрибутами должно использовать список параметров с скобками. Типы параметров не требуются.

f = [A] () => { };        // [A] lambda
f = [return:A] x => x;    // syntax error at '=>'
f = [return:A] (x) => x;  // [A] lambda
f = [A] static x => x;    // syntax error at '=>'

f = ([A] x) => x;         // [A] x
f = ([A] ref int x) => x; // [A] x

Можно указать несколько атрибутов, разделенных запятыми в одном списке атрибутов или в виде отдельных списков атрибутов.

var f = [A1, A2][A3] () => { };    // ok
var g = ([A1][A2, A3] int x) => x; // ok

Атрибуты не поддерживаются для анонимных методов объявленных с помощью синтаксиса delegate { }.

f = [A] delegate { return 1; };         // syntax error at 'delegate'
f = delegate ([A] int x) { return x; }; // syntax error at '['

Парсер будет изучать инициализатор коллекции с назначением элемента, чтобы отличить его от инициализатора коллекции с лямбда-выражением.

var y = new C { [A] = x };    // ok: y[A] = x
var z = new C { [A] x => x }; // ok: z[0] = [A] x => x

Средство синтаксического анализа будет рассматривать ?[ как начало условного доступа к элементу.

x = b ? [A];               // ok
y = b ? [A] () => { } : z; // syntax error at '('

Атрибуты лямбда-выражения или лямбда-параметров будут заноситься в метаданные метода, который сопоставлен с лямбда-выражением.

Как правило, клиентам не следует полагаться на то, как лямбда-выражения и локальные функции отображаются из исходного кода в метаданные. Как лямбда-коды и локальные функции могут выдаваться и изменяются между версиями компилятора.

Предлагаемые здесь изменения предназначены для сценария, управляемого Delegate. Чтобы подтвердить правильность проверки MethodInfo, связанной с экземпляром Delegate, следует определить подпись лямбда-выражения или локальной функции, включая любые явные атрибуты и дополнительные метаданные, сгенерированные компилятором, такие как параметры по умолчанию. Это позволяет таким командам, как ASP.NET сделать доступными те же действия для лямбда-кодов и локальных функций, что и обычные методы.

Явный тип возвращаемого значения

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

f = T () => default;                    // ok
f = short x => 1;                       // syntax error at '=>'
f = ref int (ref int x) => ref x;       // ok
f = static void (_) => { };             // ok
f = async async (async async) => async; // ok?

Средство синтаксического анализа будет смотреть вперед, чтобы отличить вызов метода T() от лямбда-выражения T () => e.

Явные типы возвращаемых данных не поддерживаются для анонимных методов, объявленных с помощью синтаксиса delegate { }.

f = delegate int { return 1; };         // syntax error
f = delegate int (int x) { return x; }; // syntax error

Вывод типа метода должен сделать точное вывод из явного лямбда-возвращаемого типа.

static void F<T>(Func<T, T> f) { ... }
F(int (i) => i); // Func<int, int>

Преобразования ковариантности не допускаются из типа возвращаемого значения лямбда-выражения к типу возвращаемого значения делегата (аналогично текущему поведению для типов параметров).

Func<object> f1 = string () => null; // error
Func<object?> f2 = object () => x;   // warning

Средство синтаксического анализа позволяет лямбда-выражениям с ref возвращать типы в выражениях без дополнительных скобок.

d = ref int () => x; // d = (ref int () => x)
F(ref int () => x);  // F((ref int () => x))

var нельзя использовать в качестве явного возвращаемого типа для лямбда-выражений.

class var { }

d = var (var v) => v;              // error: contextual keyword 'var' cannot be used as explicit lambda return type
d = @var (var v) => v;             // ok
d = ref var (ref var v) => ref v;  // error: contextual keyword 'var' cannot be used as explicit lambda return type
d = ref @var (ref var v) => ref v; // ok

Тип естественной функции

Выражение анонимной функции (§12.19) (выражение лямбда-выражения или анонимного метода ) имеет естественный тип, если типы параметров являются явными, а возвращаемый тип — явным или может быть выведен (см. §12.6.3.13).

Группа методов имеет естественный тип, если все методы-кандидаты имеют общую сигнатуру. (Если группа методов может включать методы расширения, кандидаты включают содержащий тип и все области методов расширения.)

Естественный тип анонимного выражения функции или группы методов — это function_type. function_type представляет сигнатуру метода: типы параметров и виды ссылок, а также тип возврата и вид ссылки. Анонимные выражения функций или группы методов с той же сигнатурой имеют одинаковые function_type.

Function_types используются только в нескольких конкретных контекстах:

  • неявные и явные преобразования
  • Вывод типа метода (§12.6.3) и лучший распространенный тип (§12.6.3.15)
  • инициализаторы var

function_type существует только во время компиляции: function_types не отображаются в исходном коде или метаданных.

Преобразования

Из function_typeF есть неявные преобразования function_type:

  • Для function_typeG, если параметры и возвращаемые типы F можно преобразовать по ковариантности в параметры и тип возвращаемого значения G
  • К System.MulticastDelegate или к базовым классам или интерфейсам System.MulticastDelegate
  • System.Linq.Expressions.Expression или System.Linq.Expressions.LambdaExpression

Анонимные выражения функций и группы методов уже имеют преобразования из выражений в типы делегатов и типы деревьев выражений (см. анонимные преобразования функций §10.7 и преобразования групп методов §10.8). Эти преобразования достаточно для преобразования в строго типизированные типы делегатов и типы дерева выражений. Приведенные выше преобразования function_type добавляют преобразования из типа только в базовые типы: System.MulticastDelegate, System.Linq.Expressions.Expressionи т. д.

Нет преобразований в function_type из типа, отличного от function_type. Отсутствуют явные преобразования для function_types, поскольку нельзя ссылаться на function_types в исходном коде.

Преобразование в System.MulticastDelegate или базовый тип или интерфейс реализует анонимную функцию или группу методов в качестве экземпляра соответствующего типа делегата. Преобразование в System.Linq.Expressions.Expression<TDelegate> или базовый тип реализует лямбда-выражение как дерево выражений с соответствующим типом делегата.

Delegate d = delegate (object obj) { }; // Action<object>
Expression e = () => "";                // Expression<Func<string>>
object o = "".Clone;                    // Func<object>

Function_type преобразования не являются неявными или явными стандартными преобразованиями §10.4 и не учитываются при определении того, применяется ли определяемый пользователем оператор преобразования к анонимной функции или группе методов. Из оценки определяемых пользователем преобразований §10.5.3:

Для применимого оператора преобразования необходимо выполнить стандартное преобразование (§10.4) из исходного типа в тип операнда оператора, и необходимо выполнить стандартное преобразование из типа результата оператора в целевой тип.

class C
{
    public static implicit operator C(Delegate d) { ... }
}

C c;
c = () => 1;      // error: cannot convert lambda expression to type 'C'
c = (C)(() => 2); // error: cannot convert lambda expression to type 'C'

Сообщается предупреждение о неявном преобразовании группы методов в object, так как преобразование является допустимым, но, возможно, непреднамеренным.

Random r = new Random();
object obj;
obj = r.NextDouble;         // warning: Converting method group to 'object'. Did you intend to invoke the method?
obj = (object)r.NextDouble; // ok

Вывод типов

Существующие правила вывода типов в основном не изменяются (см. §12.6.3). Однако существует несколько изменений, ниже к конкретным этапам вывода типов.

Первый этап

Первый этап (§12.6.3.2) позволяет анонимной функции привязаться к Ti, даже если Ti не является делегатом или типом дерева выражений (возможно, параметр типа ограничен System.Delegate, например).

Для каждого аргумента метода Ei:

  • Если Ei является анонимной функцией и Ti является типом делегата или типом дерева выражений, явного вывода типа параметра производится из Ei в Ti, а явный вывод типа возврата производится из Ei на Ti.
  • В противном случае, если Ei имеет тип U и xi является параметром значения, то вывод нижней границы выполняется изUкTi.
  • В противном случае, если Ei имеет тип U и xi является параметром ref или out, то точное вывод выполняется отUдоTi.
  • В противном случае для этого аргумента никакого логического вывода не делается.

Явное выведение типа возврата

вывод типа явного вывода типа выполняется из выражения E типа T следующим образом:

  • Если E является анонимной функцией с явным типом возврата Ur и T является типом делегата или деревом выражений с типом возврата Vr, то точный вывод (§12.6.3.9) выполняется изUrнаVr.

Закрепление

Исправление (§12.6.3.12) гарантирует, что другие преобразования предпочтительнее преобразований типа функции . (Лямбда-выражения и выражения групп методов способствуют только нижним границам, поэтому обработка function_types необходима только для нижних границ.)

Переменная типа Xi без с набором границ исправлена следующим образом:

  • Набор типов кандидатов Uj начинается как набор всех типов в наборе границ для Xi, где типы функций игнорируются в нижних границах, если нет типов функций.
  • Затем мы рассмотрим каждый привязанный Xi, в свою очередь: для каждого точно привязанного UXi всех типов Uj которые не идентичны U удаляются из набора кандидатов. Для каждой нижней границы UXi всех типов Uj, к которым не не неявное преобразование из U удаляются из набора кандидатов. Для каждой верхней границы UXi всех типов Uj, из которых не неявное преобразование в U удаляются из набора кандидатов.
  • Если среди оставшихся типов кандидатов Uj существует уникальный тип V, из которого возможно неявное преобразование во все остальные типы кандидатов, то Xi закреплён за V.
  • В противном случае вывод типа завершается ошибкой.

Лучший распространенный тип

Лучший распространенный тип (§12.6.3.15) определяется с точки зрения вывода типа, поэтому приведенные выше изменения вывода типов применяются к лучшему общему типу.

var fs = new[] { (string s) => s.Length, (string s) => int.Parse(s) }; // Func<string, int>[]

var

Анонимные функции и группы методов с типами функций можно использовать в качестве инициализаторов в объявлениях var.

var f1 = () => default;           // error: cannot infer type
var f2 = x => x;                  // error: cannot infer type
var f3 = () => 1;                 // System.Func<int>
var f4 = string () => null;       // System.Func<string>
var f5 = delegate (object o) { }; // System.Action<object>

static void F1() { }
static void F1<T>(this T t) { }
static void F2(this string s) { }

var f6 = F1;    // error: multiple methods
var f7 = "".F1; // error: the delegate type could not be inferred
var f8 = F2;    // System.Action<string> 

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

d = () => 0; // ok
_ = () => 1; // error

Типы делегатов

Тип делегата для анонимной функции или группы методов с типами параметров P1, ..., Pn и типом возврата R:

  • Если какой-либо параметр или возвращаемое значение не передаются по значению, или если более 16 параметров, или если любой из типов параметров или возвращаемое значение недопустимого типа аргумента (например, (int* p) => { }), тогда делегат — это синтезированный internal анонимный тип делегата с подписью, которая соответствует анонимной функции или группе методов, с именами параметров arg1, ..., argn или arg, если параметр один.
  • Если R равно void, то тип делегата — System.Action<P1, ..., Pn>;
  • В противном случае тип делегата System.Func<P1, ..., Pn, R>.

Компилятор может позволить большему количеству сигнатур привязываться к типам System.Action<> и System.Func<> в будущем (если типы ref struct разрешены в качестве аргументов типов, например).

modopt() или modreq() в сигнатуре группы методов игнорируются в соответствующем типе делегата.

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

Разрешение перегрузки

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

Более подходящий член функции

... Учитывая список аргументов A с набором выражений аргументов {E1, E2, ..., En} и двумя применимыми элементами функции Mp и Mq с типами параметров {P1, P2, ..., Pn} и {Q1, Q2, ..., Qn}, Mp определяется как более подходящий элемент функции, чем Mq, если

  1. для каждого аргумента неявное преобразование из Ex в Px не является преобразованием типа функциии
    • Mp является не универсальным методом или Mp является универсальным методом с параметрами типа {X1, X2, ..., Xp} и для каждого параметра типа Xi аргумент типа выводится из выражения или типа, отличного от function_type, и
    • по крайней мере для одного аргумента неявное преобразование из Ex в Qx является преобразованием типа функцииили Mq является универсальным методом с параметрами типа {Y1, Y2, ..., Yq} и по крайней мере для одного параметра типа Yi аргумент типа выводится из типа функцииили
  2. для каждого аргумента неявное преобразование из Ex в Qx не лучше, чем неявное преобразование из Ex в Px, а для хотя бы одного аргумента преобразование из Ex в Px лучше, чем преобразование из Ex в Qx.

Более предпочтительное преобразование выражения (§12.6.4.5) обновлено, чтобы предпочитать преобразования, не подразумевающие использование выводимых типов из лямбда-выражений или групп методов.

Улучшенное преобразование из выражения

Учитывая неявное преобразование C1, которое преобразуется из выражения E в тип T1, а неявное преобразование C2, которое преобразуется из выражения E в тип T2, C1 — это лучшее преобразование, чем C2, если:

  1. C1 не является преобразованием типа функции и C2 является преобразованием типа функцииили
  2. E является неконстантным interpolated_string_expression, C1 является implicit_string_handler_conversion, T1 является applicable_interpolated_string_handler_type, и C2 не является implicit_string_handler_conversion, или
  3. E точно не соответствует T2, и истинно хотя бы одно из следующих условий:
    • E точно соответствует T1 (§12.6.4.5)
    • T1 является лучшей целью преобразования, чем T2 (§12.6.4.7)

Синтаксис

lambda_expression
  : modifier* identifier '=>' (block | expression)
  | attribute_list* modifier* type? lambda_parameters '=>' (block | expression)
  ;

lambda_parameters
  : lambda_parameter
  | '(' (lambda_parameter (',' lambda_parameter)*)? ')'
  ;

lambda_parameter
  : identifier
  | attribute_list* modifier* type? identifier equals_value_clause?
  ;

Открытые проблемы

Следует ли поддерживать значения по умолчанию для лямбда-параметров выражения для полноты?

Следует ли System.Diagnostics.ConditionalAttribute быть запрещён для лямбда-выражений, так как есть несколько сценариев, где лямбда-выражение может использоваться условно?

([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?

Должен ли function_type быть доступен из API компилятора, помимо результирующего типа делегата?

В настоящее время используемый тип делегата предполагает использование System.Action<> или System.Func<>, если параметры и возвращаемые типы являются допустимыми аргументами типа и, если параметров не более 16, и если ожидаемый тип Action<> или Func<> отсутствует, то сообщается об ошибке. Вместо этого следует ли компилятору использовать System.Action<> или System.Func<> независимо от арасти? И если ожидаемый тип отсутствует, синтезировать тип делегата в противном случае?