Parámetros opcionales y de matriz de parámetros para lambdas y grupos de métodos
Nota
Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos e se incorporan en la especificación ECMA actual.
Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de lenguaje (LDM) correspondientes.
Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones de .
Problema planteado por el experto: https://github.com/dotnet/csharplang/issues/6051
Resumen
Para aprovechar las mejoras de lambda introducidas en C# 10 (consulte antecedentes relevantes), se propone agregar compatibilidad con los valores de parámetro predeterminados y matrices params
en lambdas. Esto permitiría a los usuarios implementar las expresiones lambda siguientes:
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
Del mismo modo, permitiremos el mismo tipo de comportamiento para los grupos de métodos:
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;
}
Antecedentes relevantes
Especificación de conversión de grupos de métodos §10.8
Motivación
Las plataformas de aplicaciones del ecosistema de .NET aprovechan las expresiones lambda en gran medida para permitir a los usuarios escribir rápidamente lógica de negocios asociada a un punto de conexión.
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);
});
Las expresiones lambda no admiten actualmente la configuración de valores predeterminados en parámetros, por lo que si un desarrollador quería compilar una aplicación resistente a escenarios en los que los usuarios no proporcionaban datos, se les deja usar funciones locales o establecer los valores predeterminados dentro del cuerpo lambda, en lugar de la sintaxis propuesta más concisa.
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);
});
La sintaxis propuesta también tiene la ventaja de reducir las diferencias confusas entre las expresiones lambda y las funciones locales, permitiendo razonar más fácilmente sobre las construcciones y transformar las lambdas en funciones sin comprometer las características, especialmente en otros escenarios en los que las expresiones lambda se usan en APIs y donde los grupos de métodos también se pueden proporcionar como referencias.
Esta es también la motivación principal para admitir la matriz de params
que no está cubierta por el escenario de caso de uso mencionado anteriormente.
Por ejemplo:
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);
Comportamiento anterior
Antes de C# 12, cuando un usuario implementa una expresión lambda con un parámetro opcional o params
, el compilador genera un error.
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
Cuando un usuario intenta usar un grupo de métodos donde el método subyacente tiene un parámetro opcional o params
, esta información no se propaga, y por ello la llamada al método no pasa la comprobación de tipos debido a un desajuste en el número de argumentos esperados.
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[]>'
Nuevo comportamiento
Después de esta propuesta (parte de C# 12), los valores predeterminados y params
se pueden aplicar a los parámetros lambda con el siguiente comportamiento:
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
Los valores predeterminados y params
se pueden aplicar a los parámetros de grupo de métodos mediante la definición específica de este grupo de métodos:
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
Cambio disruptivo
Antes de C# 12, el tipo inferido de un grupo de métodos se Action
o Func
para que el código siguiente se compile:
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 });
}
Después de este cambio (parte de C# 12), el código de esta naturaleza deja de compilarse en el SDK de .NET 7.0.200 o posterior.
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 });
}
Es necesario tener en cuenta el impacto de este cambio importante. Afortunadamente, el uso de var
para deducir el tipo de un grupo de métodos solo se ha admitido desde C# 10, por lo que solo se rompería el código escrito desde entonces, que depende explícitamente de este comportamiento.
Diseño detallado
Cambios de gramática y analizador sintáctico
Esta mejora requiere los siguientes cambios en la gramática de las expresiones lambda.
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?
;
Tenga en cuenta que esto permite solo valores predeterminados de parámetro y matrices params
para lambdas, no para métodos anónimos declarados con la sintaxis delegate { }
.
Se aplican las mismas reglas que para los parámetros de método (§15.6.2) para los parámetros lambda:
- Un parámetro con un modificador
ref
,out
othis
no puede tener un default_argument. - Una parameter_array puede producirse después de un parámetro opcional, pero no puede tener un valor predeterminado: la omisión de argumentos de un parameter_array daría lugar a la creación de una matriz vacía.
No se necesitan cambios en la gramática para los grupos de métodos, ya que esta propuesta solo cambiaría su semántica.
La siguiente adición (en negrita) es necesaria para las conversiones de funciones anónimas (§10.7):
En concreto, una función anónima
F
es compatible con un tipo de delegadoD
proporcionado:
- [...]
- Si
F
tiene una lista de parámetros con tipo explícito, cada parámetro deD
tiene el mismo tipo y modificadores que el parámetro correspondiente enF
omitir los modificadores deparams
y los valores predeterminados.
Actualizaciones de propuestas anteriores
La siguiente adición (en negrita) es necesaria para la especificación de los tipos de función en una propuesta anterior:
Un grupo de métodos tiene un tipo natural si todos los métodos candidatos del grupo de métodos tienen una firma común , incluidos los valores predeterminados y los modificadores de
params
. (Si el grupo de métodos puede incluir métodos de extensión, los candidatos incluyen el tipo contenedor y todos los ámbitos del método de extensión).
El tipo natural de una expresión de función anónima o un grupo de métodos es un function_type. Un function_type representa una firma de método: los tipos de parámetro, valores predeterminados, clases de referencia, modificadores
params
y el tipo de devolución y clase de referencia. Las expresiones de función anónimas o los grupos de métodos con la misma firma tienen el mismo tipo_de_función.
Se requiere la siguiente adición (en negrita) a la especificación de tipos de delegado en una propuesta anterior:
El tipo de delegado para la función anónima o el grupo de métodos con tipos de parámetros
P1, ..., Pn
y el tipo de valor devueltoR
es:
- si algún parámetro o valor devuelto no es por valor, o algún parámetro es opcional o
params
, o hay más de 16 parámetros, o cualquiera de los tipos de parámetros o el tipo de retorno no es un argumento de tipo válido (por ejemplo,(int* p) => { }
), entonces el delegado es un tipo de delegado anónimo sintetizadointernal
con una firma que coincide con la función anónima o el grupo de métodos, y con nombres de parámetrosarg1, ..., argn
oarg
si el parámetro es único; [...]
Cambios del enlazador
Sintetizando nuevos tipos de delegados
Al igual que con el comportamiento de los delegados con parámetros ref
o out
, los tipos delegados se sintetizan para lambdas o grupos de métodos definidos con parámetros opcionales o params
.
Tenga en cuenta que en los ejemplos siguientes, la notación a'
, b'
, etc. se usa para representar estos tipos de delegado anónimos.
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 = " ");
Comportamiento de conversión y unificación
Los delegados anónimos con parámetros opcionales se unificarán cuando el mismo parámetro (basado en la posición) tenga el mismo valor predeterminado, independientemente del nombre del parámetro.
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
Los delegados anónimos con una matriz como último parámetro se unificarán cuando el último parámetro tenga el mismo params
tipo de modificador y matriz, independientemente del nombre del parámetro.
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)`
Del mismo modo, hay compatibilidad con delegados con nombre que ya admiten parámetros opcionales y params
.
Cuando los valores predeterminados o los modificadores params
difieren en una conversión, el origen no se usará si está en una expresión lambda, ya que no se puede llamar a lambda de ninguna otra manera.
Esto podría parecer contrario a los usuarios, por lo que se emitirá una advertencia cuando el valor predeterminado de origen o params
modificador esté presente y diferente del de destino.
Si la fuente es un grupo de métodos, puede llamarse por sí misma, por lo que no se generará ninguna advertencia.
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
Comportamiento de IL/tiempo de ejecución
Los valores de parámetro predeterminados se emitirán a los metadatos. El IL de esta característica será muy similar al IL generado para lambdas con parámetros ref
y out
. Se generará una clase que herede de System.Delegate
o similar, y el método Invoke
incluirá directivas .param
para establecer valores de parámetro predeterminados o System.ParamArrayAttribute
, como sería el caso de un delegado con nombre estándar con parámetros opcionales o params
.
Estos tipos de delegado se pueden inspeccionar en tiempo de ejecución, como es normal.
En código, los usuarios pueden introspeccionar el DefaultValue
en la ParameterInfo
asociada con un grupo de lambda o de métodos usando la MethodInfo
asociada.
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
Preguntas abiertas
Ninguno de ellos se ha implementado. Siguen siendo propuestas abiertas.
Pregunta abierta: ¿cómo interactúa esto con el atributo DefaultParameterValue
existente?
Respuesta propuesta: Para lograr equivalencia, permita el atributo DefaultParameterValue
en las funciones lambda y asegúrese de que el comportamiento de generación de delegados coincida con los valores predeterminados de los parámetros admitidos a través de la sintaxis.
var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed
Pregunta abierta: En primer lugar, tenga en cuenta que esto está fuera del ámbito de la propuesta actual, pero puede que valga la pena discutir en el futuro. ¿Deseamos admitir valores predeterminados con parámetros lambda tipados implícitamente? Es decir,
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
Esta inferencia conduce a algunos problemas de conversión complicados que requerirían más discusión.
También hay que tener en cuenta el rendimiento del análisis. Por ejemplo, hoy el término (x =
nunca podría ser el inicio de una expresión lambda. Si se permitiera esta sintaxis para los valores por defecto de lambda, entonces el analizador necesitaría una lectura anticipada más grande (examinando hasta un token de =>
) para determinar si un término es un lambda o no.
Reuniones de diseño
- LDM 2022-10-10: decisión de agregar compatibilidad con
params
de la misma manera que los valores de parámetro predeterminados.
C# feature specifications