Compartir a través de


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:

  1. Permitir expresiones lambda con atributos
  2. Permitir expresiones lambda con tipo de valor devuelto explícito
  3. 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 (§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 (vea §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.

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_typeG si los parámetros y los tipos de valor devuelto de F son variaciones que se pueden convertir en parámetros y tipo de valor devuelto de G
  • A System.MulticastDelegate o las clases base o las interfaces de System.MulticastDelegate
  • A System.Linq.Expressions.Expression o System.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 y Ti es un tipo delegado o un tipo de árbol de expresión, se realiza una inferencia de tipo de parámetro explícito de Ei a Tiy se realiza una inferencia de tipo de valor devuelto explícito de de Ei a Ti.
  • De lo contrario, si Ei tiene un tipo U y xi es un parámetro de valor, la inferencia de límite inferior se crea deUaTi.
  • De lo contrario, si Ei tiene un tipo U y xi es un parámetro ref o out, se realiza una inferencia exacta deUaTi.
  • 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 Ea un tipo T de la siguiente manera:

  • Si E es una función anónima con Ur de tipo de valor devuelto explícito y T es un tipo delegado o tipo de árbol de expresión con tipo de valor devuelto Vr, se realiza una exacta de inferencia (§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 Xi con un conjunto de límites se fija de la siguiente manera:

  • El grupo de tipos candidatosUj comienza como el conjunto de todos los tipos del conjunto de límites de Xidonde 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 cada U exacto de Xi todos los tipos Uj que no son idénticos a U se quitan del conjunto candidato. En cada límite inferior U de Xi, se eliminan del conjunto de candidatos todos los tipos Uj que no sean el resultado de una conversión implícita a partir de U. En cada límite superior U de Xi, se eliminan del conjunto de candidatos todos los tipos Uj que no sean el origen de una conversión implícita que pase a U.
  • Si entre los tipos candidatos restantes Uj hay un tipo único V desde el que hay una conversión implícita a todos los demás tipos candidatos, Xi se fija en V.
  • 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 sintetizado internal con una firma que coincide con la función anónima o el grupo de métodos, y con nombres de parámetros arg1, ..., argn o arg si hay un solo parámetro;
  • si R es void, el tipo de delegado es System.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 aplicables Mp y Mq con tipos de parámetro {P1, P2, ..., Pn} y {Q1, Q2, ..., Qn}, Mp se define como un miembro de función mejor que Mq si

  1. en cada argumento, la conversión implícita de Ex a Px no es una function_type_conversion; además,
    • Mp es un método no genérico o Mp es un método genérico con parámetros de tipo {X1, X2, ..., Xp} y para cada parámetro de tipo Xi 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 a Qx es un function_type_conversion, o Mq es un método genérico con parámetros de tipo {Y1, Y2, ..., Yq} y para al menos un parámetro de tipo Yi el argumento de tipo se deduce de un function_typeo
  2. para cada argumento, la conversión implícita de Ex a Qx no es mejor que la conversión implícita de Ex a Pxy, para al menos un argumento, la conversión de Ex a Px es mejor que la conversión de Ex a Qx.

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ón E a un tipo T1y una conversión implícita C2 que convierte de una expresión E a un tipo T2, C1 es una conversión mejor que C2 si:

  1. C1 no es un function_type_conversion y C2 es un function_type_conversiono
  2. E es un interpolated_string_expression, no constante C1 es un implicit_string_handler_conversion, T1 es un applicable_interpolated_string_handler_type y C2 no es un implicit_string_handler_conversion, o bien,
  3. E no coincide exactamente con T2 y se cumple al menos una de las siguientes condiciones:
    • E coincide exactamente con T1 (§12.6.4.5)
    • T1 es un destino de conversión mejor que T2 (§12.6.4.7)

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?