Partilhar via


Melhorias no Lambda

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da Language Design Meeting (LDM).

Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .

Questão campeã: https://github.com/dotnet/csharplang/issues/4934

Resumo

Alterações propostas:

  1. Permitir lambdas com atributos
  2. Permitir lambdas com tipo de retorno explícito
  3. Inferir um tipo de delegado padrão para lambdas e agrupamentos de métodos

Motivação

O suporte para atributos em lambdas forneceria paridade com métodos e funções locais.

O suporte para tipos de retorno explícitos forneceria simetria com parâmetros lambda onde tipos explícitos podem ser especificados. Permitir tipos de retorno explícitos também forneceria controle sobre o desempenho do compilador em lambdas aninhados, onde a resolução de sobrecarga deve vincular o corpo do lambda atualmente para determinar a assinatura.

Um tipo natural para expressões lambda e grupos de métodos permitirá mais cenários em que lambdas e grupos de métodos podem ser usados sem um tipo de delegado explícito, inclusive como inicializadores em declarações var.

A exigência de tipos de delegados explícitos para lambdas e grupos de métodos tem representado um ponto de atrito para os clientes e tornou-se um obstáculo ao progresso do ASP.NET com o trabalho recente em MapAction.

ASP.NET MapAction sem alterações propostas (MapAction() usa um 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 com tipos naturais 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 com atributos e tipos naturais para expressões lambda:

app.MapAction([HttpGet("/")] () => new Todo(Id: 0, Name: "Name"));
app.MapAction([HttpPost("/")] ([FromBody] Todo todo) => todo);

Atributos

Atributos podem ser adicionados a expressões lambda e parâmetros lambda. Para evitar ambiguidade entre atributos de método e atributos de parâmetro, uma expressão lambda com atributos deve usar uma lista de parâmetros entre parênteses. Os tipos de parâmetros não são necessários.

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

Vários atributos podem ser especificados, separados por vírgulas dentro da mesma lista de atributos ou como listas de atributos separadas.

var f = [A1, A2][A3] () => { };    // ok
var g = ([A1][A2, A3] int x) => x; // ok

Os atributos não são suportados para métodos anónimos declarados com a sintaxe delegate { }.

f = [A] delegate { return 1; };         // syntax error at 'delegate'
f = delegate ([A] int x) { return x; }; // syntax error at '['

O analisador analisará adiante para diferenciar um inicializador de coleção com uma atribuição de elemento de um inicializador de coleção com uma expressão lambda.

var y = new C { [A] = x };    // ok: y[A] = x
var z = new C { [A] x => x }; // ok: z[0] = [A] x => x

O analisador tratará ?[ como o início de um acesso a um elemento condicional.

x = b ? [A];               // ok
y = b ? [A] () => { } : z; // syntax error at '('

Os atributos na expressão lambda ou nos parâmetros lambda serão emitidos nos metadados do método que mapeia o lambda.

Em geral, os clientes não devem depender de como as expressões lambda e as funções locais são mapeadas da origem para os metadados. A forma como lambdas e funções locais são emitidas pode, e tem, mudado entre as versões do compilador.

As alterações aqui propostas visam o cenário orientado para o Delegate. Deve ser válido inspecionar o MethodInfo associado a uma instância Delegate para determinar a assinatura da expressão lambda ou função local, incluindo quaisquer atributos explícitos e metadados adicionais emitidos pelo compilador, como parâmetros padrão. Isso permite que equipes como ASP.NET disponibilizem os mesmos comportamentos para lambdas e funções locais que os métodos comuns.

Tipo de retorno explícito

Um tipo de retorno explícito pode ser especificado antes da lista de parâmetros entre parênteses.

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?

O analisador analisará o futuro para diferenciar uma chamada de método T() de uma expressão lambda T () => e.

Não há suporte para tipos de retorno explícitos para métodos anônimos declarados com sintaxe delegate { }.

f = delegate int { return 1; };         // syntax error
f = delegate int (int x) { return x; }; // syntax error

A inferência do tipo de método deve fazer uma inferência exata de um tipo de retorno lambda explícito.

static void F<T>(Func<T, T> f) { ... }
F(int (i) => i); // Func<int, int>

Conversões de variância não são permitidas do tipo de retorno lambda para o tipo de retorno delegado (comportamento semelhante correspondente para tipos de parâmetro).

Func<object> f1 = string () => null; // error
Func<object?> f2 = object () => x;   // warning

O analisador permite expressões lambda com tipos de retorno ref dentro de expressões sem a necessidade de parênteses adicionais.

d = ref int () => x; // d = (ref int () => x)
F(ref int () => x);  // F((ref int () => x))

var não pode ser usado como um tipo de retorno explícito para expressões 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 (função)

Uma função anónima expressão (§12.19) (uma expressão lambda ou um método anónimo ) tem um tipo natural se os tipos de parâmetros forem explícitos e o tipo de retorno for explícito ou puder ser inferido (ver §12.6.3.13).

Um grupo de métodos tem um tipo natural se todos os métodos candidatos no grupo de métodos tiverem uma assinatura comum. (Se o grupo de métodos puder incluir métodos de extensão, os candidatos incluem o tipo que os contém e todos os âmbitos de métodos de extensão.)

O tipo natural de uma expressão de função anônima ou grupo de método é um function_type. Um function_type representa uma assinatura de método: os tipos de parâmetro e de referência, e o tipo de retorno e de referência. Expressões de funções anónimas ou grupos de métodos com a mesma assinatura têm o mesmo function_type.

Function_types são usados apenas em alguns contextos específicos:

  • Conversões implícitas e explícitas
  • Inferência do tipo de método (§12.6.3) e melhor tipo comum (§12.6.3.15)
  • var inicializadores

Existe apenas um function_type no momento da compilação: function_types não surgem no código-fonte ou nos metadados.

Conversões

Há conversões implícitas de um function_typeF em function_type:

  • Para uma função do tipo G se os parâmetros e tipos de retorno de F forem convertíveis em variância para os parâmetros e tipo de retorno de G
  • Para System.MulticastDelegate ou classes base ou interfaces de System.MulticastDelegate
  • Para System.Linq.Expressions.Expression ou System.Linq.Expressions.LambdaExpression

Expressões de função anônimas e grupos de métodos já têm conversões de de expressão para tipos delegados e tipos de árvore de expressões (consulte conversões de função anônimas §10.7 e conversões de grupo de método §10.8). Essas conversões são suficientes para converter para tipos delegados fortemente tipados e tipos de árvore de expressão. As function_type conversões acima adicionam conversões do tipo apenas para os tipos base: System.MulticastDelegate, System.Linq.Expressions.Expression, etc.

Não existem conversões para uma function_type a partir de um tipo diferente de um function_type. Não há conversões explícitas para function_types uma vez que function_types não podem ser referenciadas na fonte.

Uma conversão para System.MulticastDelegate ou tipo base ou interface realiza a função anônima ou grupo de métodos como uma instância de um tipo delegado apropriado. Uma conversão para System.Linq.Expressions.Expression<TDelegate> ou tipo base realiza a expressão lambda como uma árvore de expressão com um tipo de delegado apropriado.

Delegate d = delegate (object obj) { }; // Action<object>
Expression e = () => "";                // Expression<Func<string>>
object o = "".Clone;                    // Func<object>

Function_type conversões não são conversões padrão implícitas ou explícitas §10.4 e não são consideradas ao determinar se um operador de conversão definido pelo usuário é aplicável a uma função anônima ou grupo de métodos. A partir da avaliação das conversões definidas pelo usuário §10.5.3:

Para que um operador de conversão seja aplicável, deve ser possível efetuar uma conversão normalizada (§10.4) do tipo de origem para o tipo de operando do operador, e deve ser possível efetuar uma conversão normalizada do tipo de resultado do operador para o tipo alvo.

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'

É relatado um aviso para uma conversão implícita de um grupo de métodos para object, embora a conversão seja válida, mas talvez não intencional.

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

Inferência de tipo

As regras existentes para a inferência de tipo mantêm-se, na sua maioria, inalteradas (ver §12.6.3). Há, no entanto, um par de alterações abaixo para fases específicas de inferência de tipo.

Primeira fase

A primeira fase (§12.6.3.2) permite que uma função anónima se ligue a Ti mesmo que Ti não seja um tipo de árvore de delegado ou expressão (talvez um parâmetro de tipo limitado a System.Delegate por exemplo).

Para cada argumento do método, Ei:

  • Se Ei for uma função anônima e Ti for um tipo delegado ou um tipo de árvore de expressão, uma inferência de tipo de parâmetro explícita é feita de Ei para Tie uma inferência de tipo de retorno explícita é feita de Ei para Ti.
  • Caso contrário, se Ei tem um tipo U e xi é um parâmetro de valor, então uma de inferência de limite inferior é feita deUparaTi.
  • Caso contrário, se Ei tem um tipo U e xi é um parâmetro ref ou out, então uma inferência exata é feita deUparaTi.
  • Caso contrário, não é feita qualquer inferência para este argumento.

Inferência explícita de tipo de retorno

Uma de inferência de tipo de retorno explícito é feita de uma expressão Epara um tipo T da seguinte maneira:

  • Se E é uma função anônima com tipo de retorno explícito Ur e T é um tipo delegado ou tipo de árvore de expressão com tipo de retorno Vr então uma inferência exata (§12.6.3.9) é feita deUrparaVr.

Reparação

Corrigir (§12.6.3.12) assegura que outras conversões tenham preferência sobre as conversões de tipo de função . (As expressões lambda e as expressões de grupo de método só contribuem para limites inferiores, portanto, o tratamento de function_types é necessário apenas para limites inferiores.)

Uma variável de tipo não fixaXi com um conjunto de limites é fixa da seguinte maneira:

  • O conjunto de tipos candidatos Uj começa como o conjunto de todos os tipos no conjunto de limites para Xionde os tipos de função são ignorados em limites inferiores se houver tipos que não sejam tipos de função.
  • Em seguida, examinamos cada limite para Xi por vez: Para cada limite exato U de Xi, todos os tipos Uj que não são idênticos a U são removidos do conjunto de candidatos. Para cada limite inferior U de Xi, todos os tipos Uj para os quais não há uma conversão implícita de U são removidos do conjunto de candidatos. Para cada limite superior U de Xi, todos os tipos Uj dos quais não existe uma conversão implícita para U são retirados do conjunto de candidatos.
  • Se entre os tipos candidatos restantes Uj houver um tipo único V a partir do qual há uma conversão implícita para todos os outros tipos candidatos, então Xi é fixado para V.
  • Caso contrário, a inferência de tipo falhará.

Melhor tipo comum

O melhor tipo comum (§12.6.3.15) é definido em termos de inferência de tipo, de modo que as alterações de inferência de tipo acima se aplicam também ao melhor tipo comum.

var fs = new[] { (string s) => s.Length, (string s) => int.Parse(s) }; // Func<string, int>[]

var

Funções anônimas e grupos de métodos com tipos de função podem ser usados como inicializadores em declarações 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> 

Os tipos de função não são usados em atribuições para descartar.

d = () => 0; // ok
_ = () => 1; // error

Tipos de delegados

O tipo de delegado para a função anônima ou grupo de métodos com tipos de parâmetro P1, ..., Pn e tipo de retorno R é:

  • se algum parâmetro ou valor de retorno não for passado por valor, ou houver mais de 16 parâmetros, ou se qualquer tipo de parâmetro ou retorno não for um argumento de tipo válido (por exemplo, (int* p) => { }), então o delegado é um tipo de delegado anónimo sintetizado internal com assinatura que corresponde à função anónima ou grupo de métodos, e com nomes de parâmetros arg1, ..., argn ou arg no caso de haver um único parâmetro;
  • se R for igual a void, então o tipo de delegado é System.Action<P1, ..., Pn>;
  • caso contrário, o tipo de delegado é System.Func<P1, ..., Pn, R>.

O compilador pode permitir que mais assinaturas se associem aos tipos System.Action<> e System.Func<> no futuro (caso os tipos ref struct sejam permitidos como argumentos de tipo, por exemplo).

modopt() ou modreq() na assinatura do grupo de métodos são ignorados no tipo de delegado correspondente.

Se duas funções anônimas ou grupos de métodos na mesma compilação exigirem tipos delegados sintetizados com os mesmos tipos de parâmetros e modificadores e o mesmo tipo de retorno e modificadores, o compilador usará o mesmo tipo de delegado sintetizado.

Resolução de sobrecarga

A melhor função membro (§12.6.4.3) é atualizada para preferir membros onde nenhuma das conversões e nenhum dos argumentos de tipo envolvidos têm tipos inferidos de expressões lambda ou grupos de métodos.

Melhor membro de função

... Dada uma lista de argumentos A com um conjunto de expressões de argumento {E1, E2, ..., En} e dois membros de função aplicáveis Mp e Mq com tipos de parâmetros {P1, P2, ..., Pn} e {Q1, Q2, ..., Qn}, Mp é definido como um membro de função melhor do que Mq se

  1. para cada argumento, a conversão implícita de Ex para Px não é uma conversão de tipo de função , e
    • Mp é um método não genérico ou Mp é um método genérico com parâmetros de tipo {X1, X2, ..., Xp} e para cada parâmetro de tipo Xi o argumento de tipo é inferido de uma expressão ou de um tipo diferente de um tipo_de_funçãoe
    • para pelo menos um argumento, a conversão implícita de Ex para Qx é um function_type_conversion, ou Mq é um método genérico com parâmetros de tipo {Y1, Y2, ..., Yq} e para pelo menos um parâmetro de tipo Yi o argumento de tipo é inferido a partir de um function_type, ou
  2. Para cada argumento, a conversão implícita de Ex para Qx não é melhor do que a conversão implícita de Ex para Px, e para pelo menos um argumento, a conversão de Ex para Px é melhor do que a conversão de Ex para Qx.

Uma melhor conversão da expressão (§12.6.4.5) é atualizada para preferir conversões que não envolvam tipos inferidos de expressões lambda ou grupos de métodos.

Melhor conversão de uma expressão

Dado um C1 de conversão implícito que converte de uma expressão E para um tipo T1, e uma conversão implícita C2 que converte de uma expressão E para um tipo T2, C1 é umde conversãomelhor do que C2 se:

  1. C1 não é um function_type_conversion e C2 é um function_type_conversion, ou
  2. E é um interpolated_string_expressionnão-constante, C1 é um implicit_string_handler_conversion, T1 é um applicable_interpolated_string_handler_type, e C2 não é um implicit_string_handler_conversion, ou
  3. E não corresponde exatamente a T2 e pelo menos uma das seguintes condições é verdadeira:
    • E corresponde exatamente ao T1 (§12.6.4.5)
    • T1 é uma meta de conversão melhor do que T2 (§12.6.4.7)

Sintaxe

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?
  ;

Questões em aberto

Os valores padrão devem ser suportados para parâmetros de expressão lambda por uma questão de completude?

O System.Diagnostics.ConditionalAttribute deve ser desautorizado em expressões lambda, uma vez que há poucos cenários em que uma expressão lambda pode ser usada condicionalmente?

([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?

O function_type deve estar disponível na API do compilador, além do tipo de delegado resultante?

Atualmente, o tipo de delegado inferido usa System.Action<> ou System.Func<> quando os tipos de parâmetro e retorno são argumentos de tipo válidos e, não houver mais de 16 parâmetros, e se o tipo esperado Action<> ou Func<> estiver faltando, um erro será relatado. Em vez disso, o compilador deve usar System.Action<> ou System.Func<> independentemente da aridade? E se o tipo esperado estiver faltando, sintetizar um tipo de delegado de outra forma?