Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
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)
-
varinicializadores
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
Gse os parâmetros e tipos de retorno deFforem convertíveis em variância para os parâmetros e tipo de retorno deG - Para
System.MulticastDelegateou classes base ou interfaces deSystem.MulticastDelegate - Para
System.Linq.Expressions.ExpressionouSystem.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
Eifor uma função anônima eTifor um tipo delegado ou um tipo de árvore de expressão, uma inferência de tipo de parâmetro explícita é feita deEiparaTie uma inferência de tipo de retorno explícita é feita deEiparaTi.- Caso contrário, se
Eitem um tipoUexié um parâmetro de valor, então uma de inferência de limite inferior é feita deUparaTi.- Caso contrário, se
Eitem um tipoUexié um parâmetrorefouout, 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 tipoTda seguinte maneira:
- Se
Eé uma função anônima com tipo de retorno explícitoUreTé um tipo delegado ou tipo de árvore de expressão com tipo de retornoVrentã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 fixa
Xicom um conjunto de limites é fixa da seguinte maneira:
- O conjunto de tipos candidatos
Ujcomeça como o conjunto de todos os tipos no conjunto de limites paraXionde 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
Xipor vez: Para cada limite exatoUdeXi, todos os tiposUjque não são idênticos aUsão removidos do conjunto de candidatos. Para cada limite inferiorUdeXi, todos os tiposUjpara os quais não há uma conversão implícita deUsão removidos do conjunto de candidatos. Para cada limite superiorUdeXi, todos os tiposUjdos quais não existe uma conversão implícita paraUsão retirados do conjunto de candidatos.- Se entre os tipos candidatos restantes
Ujhouver um tipo únicoVa 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 sintetizadointernalcom assinatura que corresponde à função anónima ou grupo de métodos, e com nomes de parâmetrosarg1, ..., argnouargno caso de haver um único parâmetro; - se
Rfor 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
Acom um conjunto de expressões de argumento{E1, E2, ..., En}e dois membros de função aplicáveisMpeMqcom tipos de parâmetros{P1, P2, ..., Pn}e{Q1, Q2, ..., Qn},Mpé definido como um membro de função melhor do queMqse
- para cada argumento, a conversão implícita de
ExparaPxnã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 tipoXio 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
ExparaQxé 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 tipoYio argumento de tipo é inferido a partir de um function_type, ou- Para cada argumento, a conversão implícita de
ExparaQxnão é melhor do que a conversão implícita deExparaPx, e para pelo menos um argumento, a conversão deExparaPxé melhor do que a conversão deExparaQx.
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
C1de conversão implícito que converte de uma expressãoEpara um tipoT1, e uma conversão implícitaC2que converte de uma expressãoEpara um tipoT2,C1é umde conversãomelhor do queC2se:
C1nã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, eC2não é um implicit_string_handler_conversion, ouEnão corresponde exatamente aT2e 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