Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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 y 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 especificaciones.
Problema planteado por el experto: https://github.com/dotnet/csharplang/issues/4934
Resumen
Cambios propuestos:
- Permitir lambdas con atributos
- Permitir lambdas 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 lambdas 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 .
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 la MethodInfo asociada 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 lambdas 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 la 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 de función anónima (§12.19) (una expresión lambda o un método anónimo) tiene un tipo natural si los tipos de parámetros son explícitos y el tipo de valor devuelto es explícito o se puede deducir consulte §12.6.3.13).
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.
Los 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
Gsi los parámetros y los tipos de valor devuelto deFson variaciones que se pueden convertir en parámetros y tipo de valor devuelto deG - A
System.MulticastDelegateo las clases base o las interfaces deSystem.MulticastDelegate - A
System.Linq.Expressions.ExpressionoSystem.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>
Las conversiones de Function_type 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 tipo
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
Eies una función anónima yTies un tipo delegado o un tipo de árbol de expresión, se realiza una inferencia de tipo de parámetro explícito deEiaTiy se realiza una inferencia de tipo de valor devuelto explícito deEiaTi.- De lo contrario, si
Eitiene un tipoUyxies un parámetro de valor, la inferencia de límite inferior se crea deUaTi.- De lo contrario, si
Eitiene un tipoUyxies un parámetrorefoout, se realiza una inferencia exacta deUaTi.- En caso 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
Ea un tipoTde la siguiente manera:
- Si
Ees una función anónima con tipo de valor devuelto explícitoUryTes un tipo delegado o tipo de árbol de expresión con tipo de valor devueltoVr, se realiza una inferencia exacta (§12.6.3.9) deUraVr.
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
Xicon un conjunto de límites se fija de la siguiente manera:
- El grupo de tipos candidatos
Ujcomienza como el conjunto de todos los tipos del conjunto de límites deXidonde 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
Xia su vez: para cada límite exactoUdeXitodos los tiposUjque no son idénticos aUse quitan del conjunto candidato. En cada límite inferiorUdeXi, se eliminan del conjunto de candidatos todos los tiposUjque no sean el resultado de una conversión implícita a partir deU. En cada límite superiorUdeXi, se eliminan del conjunto de candidatos todos los tiposUjque no sean el origen de una conversión implícita que pase aU.- Si entre los tipos candidatos restantes
Ujhay un único tipoVdesde el que existe una conversión implícita a todos los demás tipos candidatos,Xise fija aV.- En caso contrario, la inferencia de tipo falla.
Mejor tipo 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 sintetizadointernalcon una firma que coincide con la función anónima o el grupo de métodos, y con nombres de parámetrosarg1, ..., argnoargsi hay un solo parámetro; - si
Resvoid, 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 sobrecarga
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
Acon un conjunto de expresiones de argumentos{E1, E2, ..., En}y dos miembros de función aplicablesMpyMqcon tipos de parámetros{P1, P2, ..., Pn}y{Q1, Q2, ..., Qn},Mpse define que es un miembro de función mejor queMqsi
- en cada argumento, la conversión implícita de
ExaPxno es una function_type_conversion; además,
Mpes un método no genérico oMpes un método genérico con parámetros de tipo{X1, X2, ..., Xp}y para cada parámetro de tipoXiel argumento de tipo se deduce de una expresión o de un tipo distinto de un function_type, y- para al menos un argumento, la conversión implícita de
ExaQxes una function_type_conversion, o bienMqes un método genérico con parámetros de tipo{Y1, Y2, ..., Yq}y para al menos un parámetro de tipoYiel argumento de tipo se deduce de un function_type o- para cada argumento, la conversión implícita de
ExaQxno es mejor que la conversión implícita deExaPxy, para al menos un argumento, la conversión deExaPxes mejor que la conversión deExaQx.
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
C1que convierte de una expresiónEa un tipoT1y una conversión implícitaC2que convierte de una expresiónEa un tipoT2,C1es una conversión mejor queC2si:
C1no es una function_type_conversion yC2es una function_type_conversion oEes un interpolated_string_expression, no constanteC1es un implicit_string_handler_conversion,T1es un applicable_interpolated_string_handler_type yC2no es un implicit_string_handler_conversion, o bien,Eno coincide exactamente conT2y 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?
;
Abrir propuestas
¿Deberían admitirse valores predeterminados para los parámetros de las expresiones lambda en aras de la exhaustividad?
¿System.Diagnostics.ConditionalAttribute debería estar prohibido en expresiones lambda, ya que existen pocos escenarios en los que una expresión lambda pueda utilizarse de forma condicional?
([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?
¿Debería estar disponible el function_type en la API del compilador, además del tipo de delegado resultante?
Actualmente, el tipo 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