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:
- Permitir lambdas com atributos
- Permitir lambdas com tipo de retorno explícito
- 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 deF
forem convertíveis em variância para os parâmetros e tipo de retorno deG
- Para
System.MulticastDelegate
ou classes base ou interfaces deSystem.MulticastDelegate
- Para
System.Linq.Expressions.Expression
ouSystem.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 eTi
for um tipo delegado ou um tipo de árvore de expressão, uma inferência de tipo de parâmetro explícita é feita deEi
paraTi
e uma inferência de tipo de retorno explícita é feita deEi
paraTi
.- Caso contrário, se
Ei
tem um tipoU
exi
é um parâmetro de valor, então uma de inferência de limite inferior é feita deU
paraTi
.- Caso contrário, se
Ei
tem um tipoU
exi
é um parâmetroref
ouout
, então uma inferência exata é feita deU
paraTi
.- 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
E
para um tipoT
da seguinte maneira:
- Se
E
é uma função anônima com tipo de retorno explícitoUr
eT
é um tipo delegado ou tipo de árvore de expressão com tipo de retornoVr
então uma inferência exata (§12.6.3.9) é feita deUr
paraVr
.
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 fixa
Xi
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 paraXi
onde 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 exatoU
deXi
, todos os tiposUj
que não são idênticos aU
são removidos do conjunto de candidatos. Para cada limite inferiorU
deXi
, todos os tiposUj
para os quais não há uma conversão implícita deU
são removidos do conjunto de candidatos. Para cada limite superiorU
deXi
, todos os tiposUj
dos quais não existe uma conversão implícita paraU
são retirados do conjunto de candidatos.- Se entre os tipos candidatos restantes
Uj
houver um tipo únicoV
a partir do qual há uma conversão implícita para todos os outros tipos candidatos, entãoXi
é fixado paraV
.- 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 sintetizadointernal
com assinatura que corresponde à função anónima ou grupo de métodos, e com nomes de parâmetrosarg1, ..., argn
ouarg
no caso de haver um único parâmetro; - se
R
for igual avoid
, 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áveisMp
eMq
com tipos de parâmetros{P1, P2, ..., Pn}
e{Q1, Q2, ..., Qn}
,Mp
é definido como um membro de função melhor do queMq
se
- para cada argumento, a conversão implícita de
Ex
paraPx
não é uma conversão de tipo de função , e
Mp
é um método não genérico ouMp
é um método genérico com parâmetros de tipo{X1, X2, ..., Xp}
e para cada parâmetro de tipoXi
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
paraQx
é um function_type_conversion, ouMq
é um método genérico com parâmetros de tipo{Y1, Y2, ..., Yq}
e para pelo menos um parâmetro de tipoYi
o argumento de tipo é inferido a partir de um function_type, ou- Para cada argumento, a conversão implícita de
Ex
paraQx
não é melhor do que a conversão implícita deEx
paraPx
, e para pelo menos um argumento, a conversão deEx
paraPx
é melhor do que a conversão deEx
paraQx
.
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ãoE
para um tipoT1
, e uma conversão implícitaC2
que converte de uma expressãoE
para um tipoT2
,C1
é umde conversãomelhor do queC2
se:
C1
não é um function_type_conversion eC2
é um function_type_conversion, ouE
é um interpolated_string_expressionnão-constante,C1
é um implicit_string_handler_conversion,T1
é um applicable_interpolated_string_handler_type, eC2
não é um implicit_string_handler_conversion, ouE
não corresponde exatamente aT2
e pelo menos uma das seguintes condições é verdadeira:
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?
C# feature specifications