Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
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/6051
Resumo
Para aproveitar os aprimoramentos de lambda introduzidos no C# 10 (consulte o histórico relevante), nós propomos dar suporte para valores padrão de parâmetros e arrays params em lambdas. Isso permitiria que os usuários implementassem as seguintes lambdas:
var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3
Da mesma forma, permitiremos o mesmo tipo de comportamento para grupos de métodos:
var addWithDefault = AddWithDefaultMethod;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = CountMethod;
counter(); // 0
counter(1, 2); // 2
int AddWithDefaultMethod(int addTo = 2) {
return addTo + 1;
}
int CountMethod(params int[] xs) {
return xs.Length;
}
Plano de fundo relevante
Especificação de conversão de grupo do método §10.8
Motivação
Os frameworks de aplicativos no ecossistema do .NET aproveitam muito as lambdas, permitindo aos usuários escrever rapidamente a lógica de negócios associada a um endpoint.
var app = WebApplication.Create(args);
app.MapPost("/todos/{id}", (TodoService todoService, int id, string task) => {
var todo = todoService.Create(id, task);
return Results.Created(todo);
});
No momento, o Lambdas não dá suporte à configuração de valores padrão em parâmetros, portanto, se um desenvolvedor quiser criar um aplicativo resiliente a cenários em que os usuários não forneceram dados, eles serão deixados para usar funções locais ou definir os valores padrão dentro do corpo lambda, em oposição à sintaxe proposta mais sucinta.
var app = WebApplication.Create(args);
app.MapPost("/todos/{id}", (TodoService todoService, int id, string task = "foo") => {
var todo = todoService.Create(id, task);
return Results.Created(todo);
});
A sintaxe proposta também tem o benefício de reduzir diferenças confusas entre lambdas e funções locais, tornando mais fácil raciocinar sobre construções e transformar lambdas em funções sem comprometer recursos, particularmente em outros cenários em que lambdas são usadas em APIs em que grupos de métodos também possam ser usados como referências.
Essa também é a principal motivação para dar suporte à matriz de params, que não é abordada pelo cenário de caso de uso mencionado acima.
Por exemplo:
var app = WebApplication.Create(args);
Result TodoHandler(TodoService todoService, int id, string task = "foo") {
var todo = todoService.Create(id, task);
return Results.Created(todo);
}
app.MapPost("/todos/{id}", TodoHandler);
Comportamento anterior
Antes do C# 12, quando um usuário implementa um lambda com um parâmetro opcional ou params, o compilador gera um erro.
var addWithDefault = (int addTo = 2) => addTo + 1; // error CS1065: Default values are not valid in this context.
var counter = (params int[] xs) => xs.Length; // error CS1670: params is not valid in this context
Quando um usuário tenta usar uma coleção de métodos em que o método subjacente tem um parâmetro opcional ou params, essas informações não são propagadas, logo, a chamada para o método não passa na verificação de tipos devido a uma incompatibilidade no número de argumentos esperados.
void M1(int i = 1) { }
var m1 = M1; // Infers Action<int>
m1(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int>'
void M2(params int[] xs) { }
var m2 = M2; // Infers Action<int[]>
m2(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int[]>'
Novo comportamento
Seguindo esta proposta (parte do C# 12), os valores padrão e params podem ser aplicados aos parâmetros lambda com o seguinte comportamento:
var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6
var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3
Os valores padrão e params podem ser aplicados aos parâmetros do grupo de métodos definindo especificamente esse grupo de métodos:
int AddWithDefault(int addTo = 2) {
return addTo + 1;
}
var add1 = AddWithDefault;
add1(); // ok, default parameter value will be used
int Counter(params int[] xs) {
return xs.Length;
}
var counter1 = Counter;
counter1(1, 2, 3); // ok, `params` will be used
Alterações da falha
Antes do C# 12, o tipo inferido de um grupo de métodos é Action ou Func para que o código a seguir compile:
void WriteInt(int i = 0) {
Console.Write(i);
}
var writeInt = WriteInt; // Inferred as Action<int>
DoAction(writeInt, 3); // Ok, writeInt is an Action<int>
void DoAction(Action<int> a, int p) {
a(p);
}
int Count(params int[] xs) {
return xs.Length;
}
var counter = Count; // Inferred as Func<int[], int>
DoFunction(counter, 3); // Ok, counter is a Func<int[], int>
int DoFunction(Func<int[], int> f, int p) {
return f(new[] { p });
}
Após essa alteração (parte do C# 12), o código dessa natureza deixa de ser compilado no SDK do .NET 7.0.200 ou posterior.
void WriteInt(int i = 0) {
Console.Write(i);
}
var writeInt = WriteInt; // Inferred as anonymous delegate type
DoAction(writeInt, 3); // Error, cannot convert from anonymous delegate type to Action
void DoAction(Action<int> a, int p) {
a(p);
}
int Count(params int[] xs) {
return xs.Length;
}
var counter = Count; // Inferred as anonymous delegate type
DoFunction(counter, 3); // Error, cannot convert from anonymous delegate type to Func
int DoFunction(Func<int[], int> f, int p) {
return f(new[] { p });
}
O impacto dessa alteração significativa precisa ser considerado. Felizmente, o uso de var para inferir o tipo de um grupo de métodos só tem suporte desde o C# 10, portanto, somente o código que foi escrito desde então e que depende explicitamente desse comportamento seria quebrado.
Design detalhado
Alterações de gramática e analisador
Esse aprimoramento requer as seguintes alterações na gramática para expressões lambda.
lambda_expression
: modifier* identifier '=>' (block | expression)
- | attribute_list* modifier* type? lambda_parameters '=>' (block | expression)
+ | attribute_list* modifier* type? lambda_parameter_list '=>' (block | expression)
;
+lambda_parameter_list
+ : lambda_parameters (',' parameter_array)?
+ | parameter_array
+ ;
lambda_parameter
: identifier
- | attribute_list* modifier* type? identifier
+ | attribute_list* modifier* type? identifier default_argument?
;
Observe que esta configuração permite valores de parâmetro padrão e matrizes de params somente para lambdas, não para métodos anônimos declarados com a sintaxe delegate { }.
As mesmas regras de parâmetros de método (§15.6.2) se aplicam a parâmetros lambda:
- Um parâmetro com um modificador
ref,outouthisnão pode ter um default_argument. - Um parameter_array pode ocorrer após um parâmetro opcional, mas não pode ter um valor padrão – a omissão de argumentos para um parameter_array resultaria na criação de uma matriz vazia.
Nenhuma alteração na gramática é necessária para grupos de métodos, pois essa proposta só alteraria sua semântica.
A adição a seguir (em negrito) é necessária para conversões de função anônima (§10.7):
Especificamente, uma função anônima
Fé compatível com um tipo delegadoDfornecido:
- [...]
- Se
Ftiver uma lista de parâmetros explicitamente tipada, cada parâmetro emDterá o mesmo tipo e os mesmos modificadores que o parâmetro correspondente emF, desconsiderando os modificadoresparamse os valores padrão.
Atualizações de propostas anteriores
A seguinte adição (em negrito) é necessária à especificação dos tipos de função em uma proposta anterior:
Um grupo de métodos terá um tipo natural se todos os métodos candidatos no grupo de métodos tiverem uma assinatura comum incluindo valores padrão e modificadores de
params. (Se o grupo de métodos puder incluir métodos de extensão, os candidatos incluirão o tipo de contenção e todos os escopos do método 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, valores padrão, tipos ref, modificadores
paramse tipo de retorno e tipo ref. Expressões de função anônima ou grupos de métodos com a mesma assinatura têm o mesmo tipo de função .
A seguinte adição (em negrito) é necessária para a especificação dos tipos delegados em uma proposta anterior:
O tipo de delegado para a função anônima ou grupo de métodos com tipos de parâmetro
P1, ..., Pne tipo de retornoRé:
- se qualquer parâmetro ou valor retornado não for por valor, ou qualquer parâmetro for opcional ou
params, ou houver mais de 16 parâmetros, ou qualquer um dos tipos de parâmetro ou retorno não for um argumento de tipo válido (digamos,(int* p) => { }), o delegado será um tipo de delegado anônimo sintetizadointernalcuja assinatura corresponda à da função anônima ou ao grupo de métodos, com nomes de parâmetroarg1, ..., argnouargse um único parâmetro; [...]
Alterações na encadernação
Sintetizando novos tipos de delegado
Assim como acontece com o comportamento dos delegados com parâmetros ref ou out, os tipos delegados são sintetizados para lambdas ou grupos de métodos definidos com parâmetros opcionais ou params.
Observe que, nos exemplos abaixo, a notação a', b'etc. é usada para representar esses tipos de delegado anônimos.
var addWithDefault = (int addTo = 2) => addTo + 1;
// internal delegate int a'(int arg = 2);
var printString = (string toPrint = "defaultString") => Console.WriteLine(toPrint);
// internal delegate void b'(string arg = "defaultString");
var counter = (params int[] xs) => xs.Length;
// internal delegate int c'(params int[] arg);
string PathJoin(string s1, string s2, string sep = "/") { return $"{s1}{sep}{s2}"; }
var joinFunc = PathJoin;
// internal delegate string d'(string arg1, string arg2, string arg3 = " ");
Comportamento de conversão e unificação
Delegados anônimos com parâmetros opcionais serão unificados quando o mesmo parâmetro (com base na posição) tiver o mesmo valor padrão, independentemente do nome do parâmetro.
int E(int j = 13) {
return 11;
}
int F(int k = 0) {
return 3;
}
int G(int x = 13) {
return 4;
}
var a = (int i = 13) => 1;
// internal delegate int b'(int arg = 13);
var b = (int i = 0) => 2;
// internal delegate int c'(int arg = 0);
var c = (int i = 13) => 3;
// internal delegate int b'(int arg = 13);
var d = (int c = 13) => 1;
// internal delegate int b'(int arg = 13);
var e = E;
// internal delegate int b'(int arg = 13);
var f = F;
// internal delegate int c'(int arg = 0);
var g = G;
// internal delegate int b'(int arg = 13);
a = b; // Not allowed
a = c; // Allowed
a = d; // Allowed
c = e; // Allowed
e = f; // Not Allowed
b = f; // Allowed
e = g; // Allowed
d = (int c = 10) => 2; // Warning: default parameter value is different between new lambda
// and synthesized delegate b'. We won't do implicit conversion
Delegados anônimos com uma matriz como o último parâmetro serão unificados quando o último parâmetro tiver o mesmo modificador params e tipo de array, independentemente do nome do parâmetro.
int C(int[] xs) {
return xs.Length;
}
int D(params int[] xs) {
return xs.Length;
}
var a = (int[] xs) => xs.Length;
// internal delegate int a'(int[] xs);
var b = (params int[] xs) => xs.Length;
// internal delegate int b'(params int[] xs);
var c = C;
// internal delegate int a'(int[] xs);
var d = D;
// internal delegate int b'(params int[] xs);
a = b; // Not allowed
a = c; // Allowed
b = c; // Not allowed
b = d; // Allowed
c = (params int[] xs) => xs.Length; // Warning: different delegate types; no implicit conversion
d = (int[] xs) => xs.Length; // OK. `d` is `delegate int (params int[] arg)`
Da mesma forma, naturalmente há compatibilidade com delegados nomeados que já dão suporte a parâmetros opcionais e params.
Quando valores padrão ou modificadores de params diferem em uma conversão, o de origem será não utilizado se estiver em uma expressão lambda, já que o lambda não pode ser chamado de outra forma.
Isso pode parecer contra-intuitivo para os usuários, portanto, um aviso será emitido quando o valor padrão de origem ou params modificador estiver presente e diferente do de destino.
Se a origem for um grupo de métodos, ela poderá ser chamada por conta própria, portanto, nenhum aviso será emitido.
delegate int DelegateNoDefault(int x);
delegate int DelegateWithDefault(int x = 1);
int MethodNoDefault(int x) => x;
int MethodWithDefault(int x = 2) => x;
DelegateNoDefault d1 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d2 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d3 = MethodNoDefault; // no warning: source is a method group
DelegateNoDefault d4 = (int x = 1) => x; // warning: source present, target missing
DelegateWithDefault d5 = (int x = 2) => x; // warning: source present, target different
DelegateWithDefault d6 = (int x) => x; // no warning: source missing, target present
delegate int DelegateNoParams(int[] xs);
delegate int DelegateWithParams(params int[] xs);
int MethodNoParams(int[] xs) => xs.Length;
int MethodWithParams(params int[] xs) => xs.Length;
DelegateNoParams d7 = MethodWithParams; // no warning: source is a method group
DelegateWithParams d8 = MethodNoParams; // no warning: source is a method group
DelegateNoParams d9 = (params int[] xs) => xs.Length; // warning: source present, target missing
DelegateWithParams d10 = (int[] xs) => xs.Length; // no warning: source missing, target present
Comportamento de tempo de execução do IL
Os valores de parâmetro padrão serão emitidos para metadados. O IL para esse recurso será muito semelhante por natureza ao IL emitido para lambdas com parâmetros ref e out. Uma classe que herda de System.Delegate ou semelhante será gerada e o método Invoke incluirá diretivas .param para definir valores de parâmetro padrão ou System.ParamArrayAttribute, assim como seria o caso de um delegado nomeado padrão com parâmetros opcionais ou params.
Esses tipos delegados podem ser inspecionados em runtime, normalmente.
No código, os usuários podem fazer introspecção do DefaultValue no ParameterInfo associado ao lambda ou ao grupo de métodos usando o MethodInfo associado.
var addWithDefault = (int addTo = 2) => addTo + 1;
int AddWithDefaultMethod(int addTo = 2)
{
return addTo + 1;
}
var defaultParm = addWithDefault.Method.GetParameters()[0].DefaultValue; // 2
var add1 = AddWithDefaultMethod;
defaultParm = add1.Method.GetParameters()[0].DefaultValue; // 2
Perguntas abertas
Nenhum deles foi implementado. As propostas permanecem abertas.
Pergunta aberta: como isso interage com o atributo DefaultParameterValue existente?
Resposta proposta: Para paridade, permita o atributo DefaultParameterValue em lambdas e verifique se o comportamento de geração delegada corresponde aos valores de parâmetro padrão com suporte por meio da sintaxe.
var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed
Pergunta aberta: Primeiro, observe que isso está fora do escopo da proposta atual, mas pode valer a pena discutir no futuro. Desejamos dar suporte a padrões com parâmetros lambda tipado implicitamente? Ou seja,
delegate void M1(int i = 3);
M1 m = (x = 3) => x + x; // Ok
delegate void M2(long i = 2);
M2 m = (x = 3.0) => ...; //Error: cannot convert implicitly from long to double
Essa inferência leva a alguns problemas complicados de conversão que exigiriam mais discussão.
Também há considerações de desempenho de análise aqui. Por exemplo, hoje o termo (x = nunca poderia ser o início de uma expressão lambda. Se essa sintaxe fosse permitida para padrões lambda, o analisador precisaria de um lookahead maior (verificando até um token =>) para determinar se um termo é um lambda ou não.
Reuniões de design
-
LDM 2022-10-10: decisão de adicionar suporte para
paramsda mesma forma que os valores de parâmetro padrão.
C# feature specifications