Parâmetros opcionais e de matriz de parâmetros para lambdas e grupos de métodos
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 .
Resumo
Para tirar partido das melhorias de lambda introduzidas no C# 10 (consulte para informações antecedentes relevantes), propomos adicionar suporte para valores de parâmetros padrão e matrizes em lambdas params
. 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;
}
Contexto relevante
Especificação de conversão de grupo de métodos §10.8
Motivação
Os frameworks de aplicações no ecossistema .NET aproveitam fortemente os lambdas para permitir que os utilizadores escrevam rapidamente a lógica de negócio 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);
});
Atualmente, o Lambdas não oferece suporte à definiçã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, ele terá que 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 as 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 funcionalidades, especialmente noutros cenários onde lambdas são usados em APIs onde grupos de métodos também podem ser usados como referências.
Esta é também a principal motivação para apoiar o array params
, que não é abrangido pelo cenário de caso de uso acima mencionado.
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 um grupo de métodos em que o método subjacente tem um parâmetro opcional ou params
, essas informações não são propagadas, portanto, a chamada para o método não verifica o tipo 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 a 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 e params
padrão 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
Quebrando a mudança
Antes do C# 12, o tipo inferido de um grupo de métodos é Action
ou Func
portanto, o código a seguir é compilado:
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 .NET SDK 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 desta mudança revolucionária deve ser considerado. Felizmente, o uso de var
para inferir o tipo de um grupo de métodos só tem sido suportado desde C# 10, portanto, apenas o código que foi escrito desde então que se baseia explicitamente nesse comportamento seria quebrado.
Projeto detalhado
Alterações na gramática e no analisador de sintaxe
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 isso permite valores de parâmetros padrão e matrizes params
somente para lambdas, não para métodos anônimos declarados com sintaxe delegate { }
.
Aplicam-se aos parâmetros lambda as mesmas regras que para os parâmetros do método (§15.6.2):
- Um parâmetro com um modificador
ref
,out
outhis
não pode ter um default_argument. - Uma 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, em vez disso, na criação de uma matriz vazia.
Não são necessárias alterações gramaticais para grupos de métodos, uma vez que esta proposta apenas alteraria a sua semântica.
É necessário o seguinte aditamento (a negrito) às conversões de funções anónimas (§10.7):
Especificamente, uma função anónima
F
é compatível com um tipo de delegadoD
desde que:
- [...]
- Se
F
tiver uma lista de parâmetros explicitamente tipada, cada parâmetro noD
terá o mesmo tipo e modificadores que o parâmetro correspondente emF
, ignorando os modificadores e valores padrão deparams
.
Atualizações de propostas anteriores
É necessária a seguinte adição (a negrito) à especificação dos tipos de função numa proposta anterior:
Um grupo de métodos tem um tipo natural se todos os métodos candidatos no grupo de métodos tiverem uma assinatura comum , incluindo valores padrão e modificadores
params
. (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 dos 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, valores padrão, tipos ref, modificadores
params
e tipo de retorno e tipo ref. Expressões de função anónimas ou grupos de métodos com a mesma assinatura têm o mesmo function_type.
É necessária a seguinte adição (a negrito) à especificação dos tipos de delegados na proposta anterior:
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 retornoR
é:
- se qualquer parâmetro ou valor de retorno não for por valor, ou qualquer parâmetro for opcional ou
params
, ou se houver mais de 16 parâmetros, ou qualquer um dos tipos de parâmetro ou retorno não forem argumentos de tipo válidos (digamos,(int* p) => { }
), então o delegado é um tipo de delegado sintetizadointernal
anônimo com assinatura que corresponde à função anônima ou grupo de métodos, e com nomes de parâmetrosarg1, ..., argn
ouarg
se um único parâmetro; [...]
Alterações no fichário
Sintetizando novos tipos de delegados
Assim como acontece com o comportamento de delegados com parâmetros ref
ou out
, os tipos de delegados são sintetizados para lambdas ou grupos de métodos definidos com parâmetros opcionais ou com parâmetros params
.
Observe que, nos exemplos abaixo, a notação a'
, b'
, etc. é usada para representar esses tipos de delegados 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
Os 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
Os delegados anônimos com uma matriz como o último parâmetro serão unificados quando o último parâmetro tiver o mesmo modificador de params
e tipo de matriz, 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, é claro que há compatibilidade com delegados nomeados que já suportam parâmetros opcionais e params
.
Quando os valores padrão ou modificadores de params
diferem em uma conversão, o de origem não será usado se estiver em uma expressão lambda, já que o lambda não pode ser chamado de outra maneira.
Isso pode parecer contraintuitivo 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 fonte for um grupo de métodos, ele pode ser chamado 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 da IL
Os valores dos parâmetros padrão serão emitidos para metadados. A IL para esta característica será de natureza muito semelhante à IL emitida para lambdas com parâmetros ref
e out
. Uma classe que herda de System.Delegate
ou similar será gerada, e o método Invoke
incluirá diretivas .param
para definir valores de parâmetros padrão ou System.ParamArrayAttribute
– assim como seria o caso de um delegado nomeado padrão com parâmetros opcionais ou params
.
Esses tipos de delegados podem ser inspecionados em tempo de execução, normalmente.
No código, os usuários podem introspeccionar o DefaultValue
no ParameterInfo
associado ao lambda ou grupo de método 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
Nenhuma delas foi implementada. Continuam a ser propostas em aberto.
Pergunta aberta: como isso interage com o atributo DefaultParameterValue
existente?
Resposta proposta: Para paridade, permita o atributo DefaultParameterValue
em lambdas e certifique-se de que o comportamento de geração de delegados corresponda aos valores de parâmetros padrão suportados por meio da sintaxe.
var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed
Pergunta em aberto: Em primeiro lugar, note-se que isto está fora do âmbito da atual proposta, mas que poderá valer a pena discuti-lo no futuro. Queremos oferecer suporte a padrões com parâmetros lambda digitados 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
Esta inferência leva a algumas questões de conversão complicadas que exigiriam mais discussão.
Existem também considerações sobre o desempenho da 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 mais amplo (varrendo 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
params
de maneira semelhante aos valores padrão dos parâmetros.
C# feature specifications