Compartilhar via


Melhorias do Lambda

Nota

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação 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 reunião de design de idioma (LDM).

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

Problema do especialista: 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. Infira um tipo natural de delegado para lambdas e grupos 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 em que tipos explícitos podem ser especificados. Ao permitir tipos de retorno explícitos, você também proporcionaria controle sobre o desempenho do compilador em lambdas aninhadas, onde a resolução de sobrecarga precisa vincular no momento o corpo da lambda 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, incluindo como inicializadores em declarações de var.

A exigência de tipos de delegado explícitos para lambdas e grupos de métodos tem sido um ponto problemático para os clientes e tornou-se um impedimento para o progresso em ASP.NET com trabalhos recentes no 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 parênteses. Tipos de parâmetro 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 na 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

Não há suporte para atributos 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 olhará para frente 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 de elemento condicional.

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

Os atributos da expressão lambda ou dos parâmetros lambda serão incorporados aos metadados no método que mapeia para 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 mudar e já mudou entre versões do compilador.

As alterações propostas aqui são direcionadas ao cenário baseado em Delegate. Deve ser válido inspecionar a MethodInfo associada 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 métodos comuns.

Tipo de retorno explícito

Um tipo de retorno explícito pode ser especificado antes da lista de parâmetros 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 olhará para frente 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 de 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 variação não são permitidas do tipo de retorno lambda para o tipo de retorno delegado (que corresponde a um comportamento semelhante para tipos de parâmetro).

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

O analisador permite expressões lambda com ref tipos de retorno dentro de expressões sem 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 expressão de função anônima (§12.19) (uma expressão lambda ou um método anônimo ) tiver um tipo natural se os tipos de parâmetros forem explícitos e o tipo de retorno for explícito ou puder ser inferido (consulte §12.6.3.13).

Um grupo de métodos terá 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 incluirão o tipo contido e todos os escopos dos métodos de extensão.)

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

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

  • conversões implícitas e explícitas
  • inferência de tipo de método (§12.6.3) e tipo comum mais adequado (§12.6.3.15)
  • Inicializadores var

Uma function_type existe apenas em tempo de compilação: function_types não aparecem no código-fonte ou nos metadados.

Conversões

De um function_typeF há conversões de function_type implícitas:

  • Para um function_typeG se os parâmetros e os tipos de retorno de F forem conversíveis por variância para os parâmetros e tipo de retorno de G
  • Para System.MulticastDelegate ou classes básicas ou interfaces de System.MulticastDelegate
  • Para System.Linq.Expressions.Expression ou System.Linq.Expressions.LambdaExpression

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

Não há conversões para um function_type de um tipo que não seja um function_type. Não há conversões explícitas para function_types, pois function_types não pode ser referenciado na origem.

Uma conversão para System.MulticastDelegate ou tipo ou interface base concretiza a função anônima ou o grupo de métodos como uma instância de um tipo delegado apropriado. Uma conversão em System.Linq.Expressions.Expression<TDelegate> ou tipo base realiza a expressão lambda como uma árvore de expressão com um tipo 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. Durante a avaliação de conversões definidas pelo usuário §10.5.3:

Para que um operador de conversão seja aplicável, deve ser possível executar uma conversão padrão (§10.4) do tipo de origem para o tipo de operando do operador e deve ser possível executar uma conversão padrão do tipo de resultado do operador para o 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'

Um alerta é emitido para uma conversão implícita de um grupo de métodos para object, já que a conversão é 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 inferência de tipo são praticamente inalteradas (consulte §12.6.3). No entanto, há algumas alterações a seguir em 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 associe a Ti mesmo que Ti não seja um tipo de árvore de expressão ou delegado (talvez um parâmetro de tipo restrito a System.Delegate por exemplo).

Para cada um dos argumentos do método Ei:

  • Se Ei for uma função anônima e Ti for um tipo de árvore de tipo delegado ou 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 tiver um tipo U e xi for um parâmetro de valor, uma inferência de associação inferior será feita deUparaTi.
  • Caso contrário, se Ei tiver um tipo U e xi for um parâmetro ref ou out, uma inferência exata será feita deUparaTi.
  • Caso contrário, nenhuma inferência será feita para esse argumento.

Inferência do tipo de retorno explícita

Uma inferência de tipo de parâmetro explícita é feita de uma expressão Epara um tipo T da seguinte forma:

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

Resolvendo

A correção de (§12.6.3.12) garante que outras conversões tenham preferência em relação às conversões function_type. (Expressões Lambda e expressões de grupo de métodos contribuem apenas para limites inferiores, portanto, o tratamento de function_types é necessário apenas para limites inferiores.)

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

  • O conjunto de tipos candidatosUj começa como o conjunto de todos os tipos no conjunto de limites para Xionde tipos de função são ignorados nos limites inferiores, caso existam tipos que não sejam de função.
  • Em seguida, examinamos cada limite para Xi por sua 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 existe 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 de onde não existe uma conversão implícita para U são removidas do conjunto candidato.
  • Se entre os tipos de candidatos restantes Uj houver um tipo único V do qual se possa fazer uma conversão implícita para todos os outros tipos de 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 para que as alterações de inferência de tipo acima também se apliquem 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 de 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 descartes.

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

Tipos de delegado

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 qualquer parâmetro ou valor de retorno não for por valor, e houver mais de 16 parâmetros, ou qualquer um dos tipos de parâmetro ou de retorno não forem argumentos de tipo válidos (por exemplo, (int* p) => { }), então o delegado será um tipo de delegado anônimo sintetizado internal, com assinatura que corresponda à função anônima ou ao grupo de métodos, e com nomes de parâmetro arg1, ..., argn ou arg se houver apenas um único parâmetro;
  • se R é void, então o tipo de delegado é System.Action<P1, ..., Pn>;
  • caso contrário, o tipo delegado é System.Func<P1, ..., Pn, R>.

O compilador pode permitir que mais assinaturas se liguem aos tipos System.Action<> e System.Func<> no futuro (se os tipos ref struct tiverem argumentos de tipo permitidos, 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 de delegados sintetizados com os mesmos tipos de parâmetro e modificadores e o mesmo tipo de retorno e modificadores, o compilador usará o mesmo tipo de delegado sintetizado.

Resolução de sobrecarga

O melhor membro de função (§12.6.4.3) é atualizado para dar preferência a membros nos quais nem as conversões nem os argumentos de tipo envolvam tipos inferidos de expressões lambda ou grupos de métodos.

Membro de função Better

... 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âmetro {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 em Px não é um function_type_conversion, 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 que não seja um function_type, e.
    • para pelo menos um argumento, a conversão implícita de Ex em 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 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 Pxe, para pelo menos um argumento, a conversão de Ex para Px é melhor do que a conversão de Ex para Qx.

A melhor conversão da expressão (§12.6.4.5) foi atualizada para passar a preferir conversões que não envolvem tipos inferidos de expressões lambda ou grupos de métodos.

Melhor conversão da expressão

Considerando uma conversão implícita C1 que converte de uma expressão E em um tipo T1e uma conversão implícita C2 que converte de uma expressão E em um tipo T2, C1 é uma conversão melhor do que C2 se:

  1. C1 não é um function_type_conversion e C2 é um function_type_conversion ou
  2. E é um interpolated_string_expression nã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 um dos seguintes se aplica:
    • E corresponde exatamente T1 (§12.6.4.5)
    • T1 é um destino 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?
  ;

Problemas abertos

Os valores padrão devem ter suporte para parâmetros de expressão lambda para integridade?

Deve System.Diagnostics.ConditionalAttribute ser não permitido em expressões lambda, pois 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?

No momento, 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 há mais de 16 parâmetros e, se o tipo de Action<> ou Func<> esperado não for encontrado, 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 ausente, sintetize um tipo de delegado caso contrário?