Необязательные параметры и параметры массивов для лямбда-выражений и групп методов
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия зафиксированы в соответствующих заметках собрания по проектированию языка (LDM).
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Сводка
Чтобы расширить улучшения для лямбда-выражений, представленные в C# 10 (см. соответствующий контекст), мы предлагаем добавить поддержку значений параметров по умолчанию и params
массивов в лямбда-выражениях. Это позволит пользователям реализовать следующие лямбда-коды:
var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3
Аналогичным образом мы разрешим такое же поведение для групп методов:
var addWithDefault = AddWithDefaultMethod;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = CountMethod;
counter(); // 0
counter(1, 2); // 2
int AddWithDefaultMethod(int addTo = 2) {
return addTo + 1;
}
int CountMethod(params int[] xs) {
return xs.Length;
}
Соответствующий фон
спецификация преобразования группы методов §10.8
Мотивация
Платформы приложений в экосистеме .NET используют лямбда-коды, чтобы пользователи могли быстро писать бизнес-логику, связанную с конечной точкой.
var app = WebApplication.Create(args);
app.MapPost("/todos/{id}", (TodoService todoService, int id, string task) => {
var todo = todoService.Create(id, task);
return Results.Created(todo);
});
Лямбда-функции в настоящее время не поддерживают установку значений по умолчанию для параметров, поэтому, если разработчик хочет создать приложение, устойчивое к ситуациям, когда пользователи не предоставляют данные, ему нужно либо использовать локальные функции, либо задавать значения по умолчанию в теле лямбда-функции, вместо более сжатого предложенного синтаксиса.
var app = WebApplication.Create(args);
app.MapPost("/todos/{id}", (TodoService todoService, int id, string task = "foo") => {
var todo = todoService.Create(id, task);
return Results.Created(todo);
});
Предлагаемый синтаксис также имеет преимущество, уменьшая запутанные различия между лямбда-выражениями и локальными функциями, что упрощает понимание конструкций и преобразование лямбда-выражений в функции без потери функциональных возможностей, особенно в тех сценариях, где лямбда-выражения используются в API, где группы методов также могут быть использованы в качестве ссылок.
Это также основная мотивация для поддержки массива params
, который не охватывается вышеупомянутым сценарием использования.
Например:
var app = WebApplication.Create(args);
Result TodoHandler(TodoService todoService, int id, string task = "foo") {
var todo = todoService.Create(id, task);
return Results.Created(todo);
}
app.MapPost("/todos/{id}", TodoHandler);
Предыдущее поведение
До C# 12, когда пользователь реализует лямбда-код с необязательным или params
параметром, компилятор вызывает ошибку.
var addWithDefault = (int addTo = 2) => addTo + 1; // error CS1065: Default values are not valid in this context.
var counter = (params int[] xs) => xs.Length; // error CS1670: params is not valid in this context
Когда пользователь пытается использовать группу методов, в которой базовый метод имеет необязательный или params
параметр, эта информация не распространяется, поэтому вызов метода не выполняет проверку типа из-за несоответствия в количестве ожидаемых аргументов.
void M1(int i = 1) { }
var m1 = M1; // Infers Action<int>
m1(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int>'
void M2(params int[] xs) { }
var m2 = M2; // Infers Action<int[]>
m2(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int[]>'
Новое поведение
После этого предложения (часть C# 12) значения по умолчанию и params
можно применить к лямбда-параметрам со следующим поведением:
var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3
Значения по умолчанию и params
можно применить к параметрам группы методов, определив в частности такую группу методов:
int AddWithDefault(int addTo = 2) {
return addTo + 1;
}
var add1 = AddWithDefault;
add1(); // ok, default parameter value will be used
int Counter(params int[] xs) {
return xs.Length;
}
var counter1 = Counter;
counter1(1, 2, 3); // ok, `params` will be used
Критическое изменение
До C# 12 выводимым типом группы методов является Action
или Func
, поэтому следующий код компилируется:
void WriteInt(int i = 0) {
Console.Write(i);
}
var writeInt = WriteInt; // Inferred as Action<int>
DoAction(writeInt, 3); // Ok, writeInt is an Action<int>
void DoAction(Action<int> a, int p) {
a(p);
}
int Count(params int[] xs) {
return xs.Length;
}
var counter = Count; // Inferred as Func<int[], int>
DoFunction(counter, 3); // Ok, counter is a Func<int[], int>
int DoFunction(Func<int[], int> f, int p) {
return f(new[] { p });
}
После этого изменения (часть C# 12) код этой природы перестает компилироваться в пакете SDK для .NET 7.0.200 или более поздней версии.
void WriteInt(int i = 0) {
Console.Write(i);
}
var writeInt = WriteInt; // Inferred as anonymous delegate type
DoAction(writeInt, 3); // Error, cannot convert from anonymous delegate type to Action
void DoAction(Action<int> a, int p) {
a(p);
}
int Count(params int[] xs) {
return xs.Length;
}
var counter = Count; // Inferred as anonymous delegate type
DoFunction(counter, 3); // Error, cannot convert from anonymous delegate type to Func
int DoFunction(Func<int[], int> f, int p) {
return f(new[] { p });
}
Влияние этого критического изменения необходимо учитывать. К счастью, использование var
для определения типа группы методов поддерживается только с C# 10, поэтому ломается только тот код, который был написан с тех пор и явно зависит от этого поведения.
Подробный дизайн
Изменения грамматики и синтаксического анализа
Для этого улучшения требуются следующие изменения грамматики для лямбда-выражений.
lambda_expression
: modifier* identifier '=>' (block | expression)
- | attribute_list* modifier* type? lambda_parameters '=>' (block | expression)
+ | attribute_list* modifier* type? lambda_parameter_list '=>' (block | expression)
;
+lambda_parameter_list
+ : lambda_parameters (',' parameter_array)?
+ | parameter_array
+ ;
lambda_parameter
: identifier
- | attribute_list* modifier* type? identifier
+ | attribute_list* modifier* type? identifier default_argument?
;
Обратите внимание, что это позволяет использовать значения параметров по умолчанию и params
массивы только для лямбда-кодов, а не для анонимных методов, объявленных с помощью синтаксиса delegate { }
.
Те же правила, что и параметры метода (§15.6.2) применяются к лямбда-параметрам:
- Параметр с модификатором
ref
,out
илиthis
не может иметь default_argument. - parameter_array может возникать после необязательного параметра, но не может иметь значение по умолчанию — пропуск аргументов для parameter_array вместо этого приведет к созданию пустого массива.
Никаких изменений в грамматике для групп методов не требуется, так как это предложение только изменит их семантику.
Для анонимных преобразований функций требуется следующее дополнение (в полужирном шрифте) (§10.7):
В частности, анонимная функция
F
совместима с типом делегатаD
предоставленным:
- [...]
- Если
F
имеет явно типизированный список параметров, каждый параметр вD
имеет одинаковый тип и модификаторы, что и соответствующий параметр вF
игнорируя модификаторыparams
и значения по умолчанию.
Обновления предыдущих предложений
Следующее добавление (полужирное) требуется для типов функций спецификации в предыдущем предложении:
Группа методов
обладает естественным типом, если все кандидаты методов в группе имеют общую подпись , включая значения по умолчанию и модификаторы . (Если группа методов может включать методы расширения, кандидаты включают содержащий тип и все области методов расширения.)
Естественный тип анонимного выражения функции или группы методов — это function_type. function_type представляет сигнатуру метода: типы параметров, значения по умолчанию, виды ссылок,
params
модификаторы, а также тип и вид ссылки возвращаемого значения. Анонимные выражения функций или группы методов с одинаковой сигнатурой имеют одинаковый функциональный тип.
Следующее добавление (полужирным) требуется в спецификацию типов делегатов и в предыдущем предложении.
Тип делегата для анонимной функции или группы методов с типами параметров
P1, ..., Pn
и типом возвратаR
:
- Если какой-либо параметр или возвращаемое значение не передается по значению, или любой параметр является необязательным, или
params
, или имеется более 16 параметров, или какие-либо типы параметров или возвращаемое значение являются недопустимыми типовыми аргументами (например,(int* p) => { }
), то делегат — это синтезированныйinternal
анонимный тип делегата с сигнатурой, которая соответствует анонимной функции или группе методов, с именами параметровarg1, ..., argn
илиarg
, если параметр один; [...]
Изменения привязки
Синтез новых типов делегатов
Как и в случае с поведением делегатов с параметрами ref
или out
, типы делегатов синтезируются для лямбда-групп или групп методов, определенных с необязательными или params
параметрами.
Обратите внимание, что в приведенных ниже примерах нотация a'
, b'
и т. д. используется для представления этих анонимных типов делегатов.
var addWithDefault = (int addTo = 2) => addTo + 1;
// internal delegate int a'(int arg = 2);
var printString = (string toPrint = "defaultString") => Console.WriteLine(toPrint);
// internal delegate void b'(string arg = "defaultString");
var counter = (params int[] xs) => xs.Length;
// internal delegate int c'(params int[] arg);
string PathJoin(string s1, string s2, string sep = "/") { return $"{s1}{sep}{s2}"; }
var joinFunc = PathJoin;
// internal delegate string d'(string arg1, string arg2, string arg3 = " ");
Поведение преобразования и объединения
Анонимные делегаты с необязательными параметрами будут унифицированы, если один и тот же параметр (на основе позиции) имеет то же значение по умолчанию независимо от имени параметра.
int E(int j = 13) {
return 11;
}
int F(int k = 0) {
return 3;
}
int G(int x = 13) {
return 4;
}
var a = (int i = 13) => 1;
// internal delegate int b'(int arg = 13);
var b = (int i = 0) => 2;
// internal delegate int c'(int arg = 0);
var c = (int i = 13) => 3;
// internal delegate int b'(int arg = 13);
var d = (int c = 13) => 1;
// internal delegate int b'(int arg = 13);
var e = E;
// internal delegate int b'(int arg = 13);
var f = F;
// internal delegate int c'(int arg = 0);
var g = G;
// internal delegate int b'(int arg = 13);
a = b; // Not allowed
a = c; // Allowed
a = d; // Allowed
c = e; // Allowed
e = f; // Not Allowed
b = f; // Allowed
e = g; // Allowed
d = (int c = 10) => 2; // Warning: default parameter value is different between new lambda
// and synthesized delegate b'. We won't do implicit conversion
Анонимные делегаты с массивом в качестве последнего параметра будут унифицированы, если последний параметр имеет тот же params
модификатор и тип массива, независимо от имени параметра.
int C(int[] xs) {
return xs.Length;
}
int D(params int[] xs) {
return xs.Length;
}
var a = (int[] xs) => xs.Length;
// internal delegate int a'(int[] xs);
var b = (params int[] xs) => xs.Length;
// internal delegate int b'(params int[] xs);
var c = C;
// internal delegate int a'(int[] xs);
var d = D;
// internal delegate int b'(params int[] xs);
a = b; // Not allowed
a = c; // Allowed
b = c; // Not allowed
b = d; // Allowed
c = (params int[] xs) => xs.Length; // Warning: different delegate types; no implicit conversion
d = (int[] xs) => xs.Length; // OK. `d` is `delegate int (params int[] arg)`
Также, конечно, имеется совместимость с именованными делегатами, которые уже поддерживают необязательные параметры params
.
Если значения по умолчанию или модификаторы params
отличаются в преобразовании, исходное значение не будет использоваться, если оно находится в лямбда-выражении, так как лямбда-выражение не может быть вызвано другим способом.
Это может показаться неинтуитивным для пользователей, поэтому предупреждение будет выдаваться, когда исходное значение по умолчанию или модификатор params
присутствует и отличается от целевого.
Если источник является группой методов, он может вызываться самостоятельно, поэтому предупреждение не будет выдаваться.
delegate int DelegateNoDefault(int x);
delegate int DelegateWithDefault(int x = 1);
int MethodNoDefault(int x) => x;
int MethodWithDefault(int x = 2) => x;
DelegateNoDefault d1 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d2 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d3 = MethodNoDefault; // no warning: source is a method group
DelegateNoDefault d4 = (int x = 1) => x; // warning: source present, target missing
DelegateWithDefault d5 = (int x = 2) => x; // warning: source present, target different
DelegateWithDefault d6 = (int x) => x; // no warning: source missing, target present
delegate int DelegateNoParams(int[] xs);
delegate int DelegateWithParams(params int[] xs);
int MethodNoParams(int[] xs) => xs.Length;
int MethodWithParams(params int[] xs) => xs.Length;
DelegateNoParams d7 = MethodWithParams; // no warning: source is a method group
DelegateWithParams d8 = MethodNoParams; // no warning: source is a method group
DelegateNoParams d9 = (params int[] xs) => xs.Length; // warning: source present, target missing
DelegateWithParams d10 = (int[] xs) => xs.Length; // no warning: source missing, target present
Поведение IL/время выполнения
Значения параметров по умолчанию будут выдаваться метаданным. Il для этой функции будет очень похож на IL, создаваемый для лямбда-кодов с параметрами ref
и out
. Класс, наследуемый от System.Delegate
или аналогичного, будет создан, и метод Invoke
будет включать директивы .param
для задания значений параметров по умолчанию или System.ParamArrayAttribute
, как и для стандартного именованного делегата с необязательными или params
параметрами.
Эти типы делегатов можно проверять в рабочем процессе, как обычно.
В коде пользователи могут интроспектировать DefaultValue
в ParameterInfo
, связанной с лямбда-группой или группой методов, с помощью связанного MethodInfo
.
var addWithDefault = (int addTo = 2) => addTo + 1;
int AddWithDefaultMethod(int addTo = 2)
{
return addTo + 1;
}
var defaultParm = addWithDefault.Method.GetParameters()[0].DefaultValue; // 2
var add1 = AddWithDefaultMethod;
defaultParm = add1.Method.GetParameters()[0].DefaultValue; // 2
Открытые вопросы
Ни из них не были реализованы. Предложения остаются открытыми.
Открытый вопрос: как это взаимодействует с существующим атрибутом DefaultParameterValue
?
предлагаемый ответ: для равенства, разрешите атрибут DefaultParameterValue
в лямбда-выражениях и убедитесь, что поведение генерации делегатов соответствует значениями параметров по умолчанию, которые поддерживаются посредством синтаксиса.
var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed
Открытый вопрос: Во-первых, обратите внимание, что это находится за рамками текущего предложения, но, возможно, стоит обсудить это в будущем. Нужно ли поддерживать значения по умолчанию с неявными типизированными лямбда-параметрами? То есть.
delegate void M1(int i = 3);
M1 m = (x = 3) => x + x; // Ok
delegate void M2(long i = 2);
M2 m = (x = 3.0) => ...; //Error: cannot convert implicitly from long to double
Это вывод приводит к некоторым сложным проблемам преобразования, которые потребуют больше обсуждения.
Здесь также рассматриваются аспекты производительности парсинга. Например, сегодня термин (x =
никогда не может быть началом лямбда-выражения. Если этот синтаксис был бы разрешён для дефолтных значений лямбд, то парсеру потребовался бы больший предсмотр (сканирование до токена =>
), чтобы определить, является ли выражение лямбдой или нет.
Встречи по проектированию
-
LDM 2022-10-10: решение добавить поддержку
params
таким же образом, как и значения параметров по умолчанию.
C# feature specifications