Mejoras de lambda
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 .
Resumen
Cambios propuestos:
- Permitir expresiones lambda con atributos
- Permitir expresiones lambda con tipo de valor devuelto explícito
- Inferir un tipo de delegado natural para expresiones lambda y grupos de métodos
Motivación
La compatibilidad con atributos en lambdas proporcionaría paridad con métodos y funciones locales.
La compatibilidad con tipos de valor devuelto explícito proporcionaría simetría con parámetros lambda en los que se pueden especificar tipos explícitos. Si se permiten tipos de retorno explícitos, también se pondría control sobre el rendimiento del compilador en expresiones lambda anidadas, donde actualmente la resolución de sobrecarga debe vincular el cuerpo de la expresión lambda para determinar la firma.
Un tipo natural para expresiones lambda y grupos de métodos permitirá más escenarios en los que se pueden usar expresiones lambda y grupos de métodos sin un tipo delegado explícito, incluidos como inicializadores en declaraciones de var
.
Requerir tipos delegados explícitos para expresiones lambda y grupos de métodos ha sido un punto de fricción para los clientes y se ha convertido en un obstáculo para el progreso en ASP.NET con el reciente trabajo en MapAction.
ASP.NET MapAction sin cambios propuestos (MapAction()
toma un argumento 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 con tipos naturales para grupos de métodos:
[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction(GetTodo);
[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo;
app.MapAction(PostTodo);
ASP.NET MapAction con atributos y tipos naturales para expresiones lambda:
app.MapAction([HttpGet("/")] () => new Todo(Id: 0, Name: "Name"));
app.MapAction([HttpPost("/")] ([FromBody] Todo todo) => todo);
Atributos
Los atributos se pueden agregar a expresiones lambda y parámetros lambda. Para evitar ambigüedad entre atributos de método y atributos de parámetro, una expresión lambda con atributos debe usar una lista de parámetros entre paréntesis. Los tipos de parámetro no son necesarios.
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
Se pueden especificar varios atributos, separados por comas dentro de la misma lista de atributos o como listas de atributos independientes.
var f = [A1, A2][A3] () => { }; // ok
var g = ([A1][A2, A3] int x) => x; // ok
No se admiten atributos para métodos anónimos declarados con sintaxis de delegate { }
.
f = [A] delegate { return 1; }; // syntax error at 'delegate'
f = delegate ([A] int x) { return x; }; // syntax error at '['
El analizador hará un pronóstico para diferenciar un inicializador de colección con una asignación de elementos de un inicializador de colección con una expresión lambda.
var y = new C { [A] = x }; // ok: y[A] = x
var z = new C { [A] x => x }; // ok: z[0] = [A] x => x
El analizador tratará ?[
como el inicio de un acceso de elemento condicional.
x = b ? [A]; // ok
y = b ? [A] () => { } : z; // syntax error at '('
Los atributos de la expresión lambda o de los parámetros lambda se incorporarán en los metadatos del método que corresponde a la expresión lambda.
En general, los clientes no deben depender de cómo las expresiones lambda y las funciones locales se asignan desde el origen a los metadatos. Cómo se emiten las expresiones lambda y las funciones locales puede, y ha, cambiado entre diferentes versiones del compilador.
Los cambios propuestos aquí están orientado al escenario controlado por Delegate
.
Debe ser válido inspeccionar el MethodInfo
asociado a una instancia de Delegate
para determinar la firma de la expresión lambda o la función local, incluidos los atributos explícitos y los metadatos adicionales emitidos por el compilador, como los parámetros predeterminados.
Esto permite a los equipos como ASP.NET poner a disposición los mismos comportamientos para las expresiones lambda y las funciones locales que los métodos normales.
Tipo de valor devuelto explícito
Se puede especificar un tipo de valor devuelto explícito antes de la lista de parámetros entre paréntesis.
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?
El analizador mirará hacia adelante para diferenciar una llamada de método T()
de una expresión lambda T () => e
.
No se admiten tipos de valor devuelto explícitos para métodos anónimos declarados con sintaxis delegate { }
.
f = delegate int { return 1; }; // syntax error
f = delegate int (int x) { return x; }; // syntax error
La inferencia de tipos de método debe realizar una inferencia exacta a partir de un tipo de retorno lambda explícito.
static void F<T>(Func<T, T> f) { ... }
F(int (i) => i); // Func<int, int>
No se permiten conversiones de variación de un tipo de valor devuelto de la expresión lambda a un tipo de delegación (lo que daría lugar al mismo resultado que en los tipos de parámetros).
Func<object> f1 = string () => null; // error
Func<object?> f2 = object () => x; // warning
El analizador permite expresiones lambda con tipos de retorno ref
sin paréntesis adicionales dentro de expresiones.
d = ref int () => x; // d = (ref int () => x)
F(ref int () => x); // F((ref int () => x))
var
no se puede usar como un tipo de valor devuelto explícito para expresiones lambda.
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
Tipo natural (función)
Una expresión función anónima
Un grupo de métodos tiene un tipo natural si todos los métodos candidatos de este grupo tienen una firma común. (Si el grupo de métodos puede incluir métodos de extensión, los candidatos incluyen tanto el tipo contenedor como todos los ámbitos de los métodos 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ámetros y referencias, así como el tipo de valor devuelto y tipo de referencia. Las expresiones de funciones anónimas o los grupos de métodos con la misma signatura tienen el mismo function_type.
Function_types solo se usan en algunos contextos específicos:
- conversiones implícitas y explícitas
- inferencia de tipo de método (§12.6.3) y el mejor tipo común (§12.6.3.15)
- Inicializadores
var
Solo existe un function_type en tiempo de compilación: function_types no aparecen en el código fuente ni en los metadatos.
Conversiones
En un function_typeF
se dan conversiones implícitas de function_type:
- A un function_type
G
si los parámetros y los tipos de valor devuelto deF
son variaciones que se pueden convertir en parámetros y tipo de valor devuelto deG
- A
System.MulticastDelegate
o las clases base o las interfaces deSystem.MulticastDelegate
- A
System.Linq.Expressions.Expression
oSystem.Linq.Expressions.LambdaExpression
Las expresiones de función anónima y los grupos de métodos ya tienen conversiones de expresiones a tipos delegados y tipos de árbol de expresiones (consulte las conversiones de funciones anónimas §10.7 y las conversiones de grupos de métodos §10.8). Estas conversiones son suficientes para convertir a tipos delegados con tipado fuerte y tipos de árbol de expresión. Las conversiones de function_type mencionadas anteriormente incluyen conversiones de tipo exclusivamente a los tipos base: System.MulticastDelegate
, System.Linq.Expressions.Expression
, etc.
No hay conversiones a un function_type desde un tipo que sea distinto a un function_type. No hay conversiones explícitas para function_types, ya que no se puede hacer referencia a function_types en el origen.
Una conversión a System.MulticastDelegate
o tipo base o interfaz realiza la función anónima o el grupo de métodos como una instancia de un tipo delegado adecuado.
Una conversión a System.Linq.Expressions.Expression<TDelegate>
o tipo base realiza la expresión lambda como un árbol de expresiones con el tipo de delegado adecuado.
Delegate d = delegate (object obj) { }; // Action<object>
Expression e = () => ""; // Expression<Func<string>>
object o = "".Clone; // Func<object>
Function_type conversiones no son conversiones estándar implícitas o explícitas §10.4 y no se tienen en cuenta al determinar si un operador de conversión definido por el usuario es aplicable a una función anónima o grupo de métodos. A partir de la evaluación de conversiones definidas por el usuario §10.5.3:
Para que un operador de conversión sea aplicable, debe ser posible realizar una conversión estándar (§10.4) del tipo de origen al tipo de operando del operador y debe ser posible realizar una conversión estándar del tipo de resultado del operador al tipo de destino.
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'
Se genera una advertencia de la conversión implícita de un grupo de métodos a object
, ya que la conversión es válida, aunque quizás sea involuntaria.
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
Inferencia de tipos
Las reglas existentes para la inferencia de tipos no se modifican principalmente (consulte §12.6.3). Sin embargo, hay un par de par de cambios que se indican a continuación en fases específicas de inferencia de tipos.
Primera fase
La primera fase (§12.6.3.2) permite que una función anónima se enlace a Ti
incluso si Ti
no es un tipo de árbol de expresión o delegado (quizás un parámetro de tipo restringido a System.Delegate
por ejemplo).
Para cada uno de los argumentos del método
Ei
:
- Si
Ei
es una función anónima yTi
es un tipo delegado o un tipo de árbol de expresión, se realiza una inferencia de tipo de parámetro explícito deEi
aTi
y se realiza una inferencia de tipo de valor devuelto explícito de deEi
aTi
.- De lo contrario, si
Ei
tiene un tipoU
yxi
es un parámetro de valor, la inferencia de límite inferior se crea deU
aTi
.- De lo contrario, si
Ei
tiene un tipoU
yxi
es un parámetroref
oout
, se realiza una inferencia exacta deU
aTi
.- De lo contrario, no se realiza ninguna inferencia para este argumento.
Inferencia de tipo de valor devuelto explícito
Una inferencia de tipo de valor devuelto explícito se crea de una expresión
E
a un tipoT
de la siguiente manera:
- Si
E
es una función anónima conUr
de tipo de valor devuelto explícito yT
es un tipo delegado o tipo de árbol de expresión con tipo de valor devueltoVr
, se realiza una exacta de inferencia (§12.6.3.9) deUr
aVr
.
Reparación
Si se fija, (§12.6.3.12) se garantiza que otras conversiones sean prioritarias sobre otras conversiones de function_type. (Las expresiones lambda y las expresiones de grupo de métodos solo contribuyen a límites inferiores, por lo que solo se necesita el control de function_types para límites inferiores).
Una variable de tipo no fijada
Xi
con un conjunto de límites se fija de la siguiente manera:
- El grupo de tipos candidatos
Uj
comienza como el conjunto de todos los tipos del conjunto de límites deXi
donde se ignoran los tipos de función en los límites inferiores en caso de que existan tipos que no sean tipos de función.- A continuación, examinamos cada límite para
Xi
a su vez: para cadaU
exacto deXi
todos los tiposUj
que no son idénticos aU
se quitan del conjunto candidato. En cada límite inferiorU
deXi
, se eliminan del conjunto de candidatos todos los tiposUj
que no sean el resultado de una conversión implícita a partir deU
. En cada límite superiorU
deXi
, se eliminan del conjunto de candidatos todos los tiposUj
que no sean el origen de una conversión implícita que pase aU
.- Si entre los tipos candidatos restantes
Uj
hay un tipo únicoV
desde el que hay una conversión implícita a todos los demás tipos candidatos,Xi
se fija enV
.- De lo contrario, se produce un error en la inferencia de tipos.
Tipo más común
El mejor tipo común (§12.6.3.15) se define en términos de inferencia de tipo, por lo que los cambios de inferencia de tipos anteriores también se aplican al mejor tipo común.
var fs = new[] { (string s) => s.Length, (string s) => int.Parse(s) }; // Func<string, int>[]
var
Las funciones anónimas y los grupos de métodos con tipos de función se pueden usar como inicializadores de declaraciones 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>
Los tipos de función no se usan en asignaciones para descartar.
d = () => 0; // ok
_ = () => 1; // error
Tipos delegados
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 devuelto R
es:
- si cualquier parámetro o valor de retorno no es por valor, o hay más de 16 parámetros, o cualquiera de los tipos de parámetros o el valor de retorno no son argumentos de tipo válidos (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 hay un solo parámetro; - si
R
esvoid
, el tipo de delegado esSystem.Action<P1, ..., Pn>
; - de lo contrario, el tipo de delegado es
System.Func<P1, ..., Pn, R>
.
El compilador puede permitir que más firmas se enlacen a los tipos System.Action<>
y System.Func<>
en el futuro (por ejemplo, si los tipos ref struct
están permitidos como argumentos de tipo).
modopt()
o modreq()
en la firma del grupo de métodos se omiten en el tipo de delegado correspondiente.
Si dos funciones anónimas o grupos de métodos de la misma compilación requieren tipos delegados sintetizados con los mismos tipos de parámetros y modificadores y el mismo tipo de valor devuelto y modificadores, el compilador usará el mismo tipo delegado sintetizado.
Resolución de sobrecargas
El mejor miembro de función (§12.6.4.3) se actualiza para elegir los miembros preferidos donde ni las conversiones ni los argumentos de tipo que intervienen infieren tipos a partir de expresiones lambda o grupos de métodos.
Mejor miembro de función
... Dada una lista de argumentos
A
con un conjunto de expresiones de argumento{E1, E2, ..., En}
y dos miembros de función aplicablesMp
yMq
con tipos de parámetro{P1, P2, ..., Pn}
y{Q1, Q2, ..., Qn}
,Mp
se define como un miembro de función mejor queMq
si
- en cada argumento, la conversión implícita de
Ex
aPx
no es una function_type_conversion; además,
Mp
es un método no genérico oMp
es un método genérico con parámetros de tipo{X1, X2, ..., Xp}
y para cada parámetro de tipoXi
el argumento de tipo se deduce de una expresión o de un tipo distinto de un function_typey- para al menos un argumento, la conversión implícita de
Ex
aQx
es un function_type_conversion, oMq
es un método genérico con parámetros de tipo{Y1, Y2, ..., Yq}
y para al menos un parámetro de tipoYi
el argumento de tipo se deduce de un function_typeo- para cada argumento, la conversión implícita de
Ex
aQx
no es mejor que la conversión implícita deEx
aPx
y, para al menos un argumento, la conversión deEx
aPx
es mejor que la conversión deEx
aQx
.
Se actualiza una mejor conversión de la expresión (§12.6.4.5) para preferir conversiones que no implicaban tipos inferidos de expresiones lambda o grupos de métodos.
Mejor conversión a partir de expresión
Dada una conversión implícita
C1
que convierte de una expresiónE
a un tipoT1
y una conversión implícitaC2
que convierte de una expresiónE
a un tipoT2
,C1
es una conversión mejor queC2
si:
C1
no es un function_type_conversion yC2
es un function_type_conversionoE
es un interpolated_string_expression, no constanteC1
es un implicit_string_handler_conversion,T1
es un applicable_interpolated_string_handler_type yC2
no es un implicit_string_handler_conversion, o bien,E
no coincide exactamente conT2
y se cumple al menos una de las siguientes condiciones:
Sintaxis
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?
;
Problemas abiertos
¿Se deben admitir valores predeterminados para los parámetros de expresión lambda para la integridad?
¿Se debe denegar System.Diagnostics.ConditionalAttribute
en expresiones lambda, ya que hay pocos escenarios en los que se podría usar una expresión lambda condicionalmente?
([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?
¿Debe estar disponible el function_type en la API del compilador, además del tipo de delegado resultante?
Actualmente, el tipo de delegado inferido usa System.Action<>
o System.Func<>
cuando los tipos de parámetro y valor devuelto son argumentos de tipo válidos y no hay más de 16 parámetros y, si falta el tipo de Action<>
o Func<>
esperado, se notifica un error. En su lugar, ¿debería el compilador utilizar System.Action<>
o System.Func<>
independientemente de la aridad? Y si no está el tipo que se espera, ¿se debería sintetizar un tipo delegado en su lugar?
C# feature specifications