Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия зафиксированы в соответствующих заметках собрания по проектированию языка (LDM).
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Проблема чемпиона: https://github.com/dotnet/csharplang/issues/6051
Сводка
Чтобы расширить улучшения для лямбда-выражений, представленные в 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