Улучшения для Lambda
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия зафиксированы в соответствующих заседаниях по проектированию языка (LDM).
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Вопрос чемпиона: https://github.com/dotnet/csharplang/issues/4934
Сводка
Предлагаемые изменения:
- Разрешить лямбда-выражения с атрибутами
- Разрешить лямбда-выражения с явным типом возвращаемого значения
- Вывод естественного типа делегата для лямбда-групп и групп методов
Мотивация
Поддержка атрибутов для лямбда-кодов обеспечивает паритетность с методами и локальными функциями.
Поддержка явных типов возвращаемых данных обеспечивает симметрию с лямбда-параметрами, где можно указать явные типы. Разрешение явных типов возвращаемых значений также обеспечивает контроль над производительностью компилятора в вложенных лямбда-выражениях, где разрешение перегрузки должно привязать тело лямбда-выражения для определения сигнатуры.
Естественный тип для лямбда-выражений и групп методов позволит использовать больше сценариев, где лямбда-выражения и группы методов могут использоваться без явного типа делегата, включая в качестве инициализаторов в объявлениях 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_type
G
, если параметры и возвращаемые типы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
, в свою очередь: для каждого точно привязанногоU
Xi
всех типовUj
которые не идентичныU
удаляются из набора кандидатов. Для каждой нижней границыU
Xi
всех типовUj
, к которым не не неявное преобразование изU
удаляются из набора кандидатов. Для каждой верхней границыU
Xi
всех типов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
, если
- для каждого аргумента неявное преобразование из
Ex
вPx
не является преобразованием типа функциии
Mp
является не универсальным методом илиMp
является универсальным методом с параметрами типа{X1, X2, ..., Xp}
и для каждого параметра типаXi
аргумент типа выводится из выражения или типа, отличного от function_type, и- по крайней мере для одного аргумента неявное преобразование из
Ex
вQx
является преобразованием типа функцииилиMq
является универсальным методом с параметрами типа{Y1, Y2, ..., Yq}
и по крайней мере для одного параметра типаYi
аргумент типа выводится из типа функцииили- для каждого аргумента неявное преобразование из
Ex
вQx
не лучше, чем неявное преобразование изEx
вPx
, а для хотя бы одного аргумента преобразование изEx
вPx
лучше, чем преобразование изEx
вQx
.
Более предпочтительное преобразование выражения (§12.6.4.5) обновлено, чтобы предпочитать преобразования, не подразумевающие использование выводимых типов из лямбда-выражений или групп методов.
Улучшенное преобразование из выражения
Учитывая неявное преобразование
C1
, которое преобразуется из выраженияE
в типT1
, а неявное преобразованиеC2
, которое преобразуется из выраженияE
в типT2
,C1
— это лучшее преобразование, чемC2
, если:
C1
не является преобразованием типа функции иC2
является преобразованием типа функцииилиE
является неконстантным interpolated_string_expression,C1
является implicit_string_handler_conversion,T1
является applicable_interpolated_string_handler_type, иC2
не является implicit_string_handler_conversion, илиE
точно не соответствуетT2
, и истинно хотя бы одно из следующих условий:
Синтаксис
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<>
независимо от арасти? И если ожидаемый тип отсутствует, синтезировать тип делегата в противном случае?
C# feature specifications