Optionele en parametermatrixparameters voor lambdas en methodegroepen
Notitie
Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.
Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante LDM (Language Design Meeting)-notities.
Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.
Probleem met kampioen: https://github.com/dotnet/csharplang/issues/6051
Samenvatting
Om voort te bouwen op de lambda-verbeteringen die in C# 10 zijn geïntroduceerd (zie relevante achtergrond), stellen we voor om ondersteuning toe te voegen voor standaard waarden voor parameters en params
arrays in lambdas. Hierdoor kunnen gebruikers de volgende lambdas implementeren:
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
Op dezelfde manier kunnen we hetzelfde gedrag voor methodegroepen toestaan:
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;
}
Relevante achtergrond
Methodegroepconversiespecificatie §10.8
Motivatie
App-frameworks in het .NET-ecosysteem maken intensief gebruik van lambdas, zodat gebruikers snel bedrijfslogica kunnen schrijven die is gekoppeld aan een eindpunt.
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);
});
Lambdas biedt momenteel geen ondersteuning voor het instellen van standaardwaarden voor parameters, dus als een ontwikkelaar een toepassing wilde bouwen die bestand was tegen scenario's waarin gebruikers geen gegevens opgeven, blijven ze over om lokale functies te gebruiken of de standaardwaarden in de lambda-hoofdtekst in te stellen, in plaats van de meer beknopte voorgestelde syntaxis.
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);
});
De voorgestelde syntaxis heeft ook het voordeel dat verwarrende verschillen tussen lambdas en lokale functies worden verminderd, waardoor het gemakkelijker wordt om na te denken over constructies en lambdas naar functies te transformeren zonder in te boeten op functionaliteit, vooral in andere scenario's waarbij lambdas worden gebruikt in API's waar methodengroepen ook als referenties kunnen worden aangeboden.
Dit is ook de belangrijkste motivatie voor het ondersteunen van de params
matrix die niet wordt gedekt door het bovengenoemde use-casescenario.
Bijvoorbeeld:
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);
Vorig gedrag
Voor C# 12, wanneer een gebruiker een lambda implementeert met een optionele of params
parameter, genereert de compiler een fout.
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
Wanneer een gebruiker probeert een methodegroep te gebruiken waarbij de onderliggende methode een optionele of params
parameter heeft, wordt deze informatie niet doorgegeven, zodat de aanroep naar de methode geen typecontrole veroorzaakt vanwege een onjuiste overeenkomst in het aantal verwachte argumenten.
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[]>'
Nieuw gedrag
Na dit voorstel (onderdeel van C# 12) kunnen standaardwaarden en params
worden toegepast op lambda-parameters met het volgende gedrag:
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
Standaardwaarden en params
kunnen worden toegepast op methodegroepparameters door specifiek een dergelijke methodegroep te definiëren:
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
Incompatibele wijziging
Vóór C# 12 wordt het uitgestelde type van een methodegroep Action
of Func
, zodat de volgende code wordt gecompileerd:
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 });
}
Na deze wijziging (onderdeel van C# 12) wordt de code van deze aard niet meer gecompileerd in .NET SDK 7.0.200 of hoger.
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 });
}
De impact van deze belangrijke wijziging moet worden overwogen. Gelukkig is het gebruik van var
om het type methodegroep af te stellen, alleen ondersteund sinds C# 10, dus alleen code die sindsdien is geschreven, wat expliciet afhankelijk is van dit gedrag, zou breken.
Gedetailleerd ontwerp
Wijzigingen in grammatica en parser
Deze verbetering vereist de volgende wijzigingen in de grammatica voor lambda-expressies.
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?
;
Houd er rekening mee dat hiermee standaardparameterwaarden en params
matrices alleen zijn toegestaan voor lambdas, niet voor anonieme methoden die zijn gedeclareerd met delegate { }
syntaxis.
Dezelfde regels als voor methodeparameters (§15.6.2) zijn van toepassing op lambda-parameters:
- Een parameter met een
ref
,out
ofthis
modifier kan geen default_argumenthebben. - Een parameter_array kan optreden na een optionele parameter, maar kan geen standaardwaarde hebben. Het weglaten van argumenten voor een parameter_array zou in plaats daarvan resulteren in het maken van een lege matrix.
Er zijn geen wijzigingen in de grammatica nodig voor methodegroepen, omdat dit voorstel alleen de semantiek zou wijzigen.
De volgende toevoeging (vetgedrukt) is vereist voor anonieme functieconversies (§10,7):
Specifiek, een anonieme functie
F
is compatibel met een delegaat-typeD
mits:
- [...]
- Als
F
een expliciet getypte parameterlijst heeft, heeft elke parameter inD
hetzelfde type en dezelfde wijzigingsfunctie als de bijbehorende parameter inF
het negeren vanparams
modifiers en standaardwaarden.
Updates van eerdere voorstellen
De volgende toevoeging (vetgedrukt) is vereist voor de functietypen specificatie in een eerder voorstel:
Een methodegroep heeft een natuurlijk type als alle kandidaatmethoden in de methodegroep een gemeenschappelijke signatuur hebben inclusief standaardwaarden en
params
modifiers. (Als de methodegroep uitbreidingsmethoden kan bevatten, bevatten de kandidaten het bijbehorende type en alle uitbreidingsmethodebereiken.)
Het natuurlijke type van een anonieme functie-expressie of methodegroep is een function_type. Een function_type vertegenwoordigt een methodehandtekening: de parametertypen, standaardwaarden, ref-soorten,
params
modifiersen retourtype en ref-type. Anonieme functie-expressies of methodegroepen met dezelfde handtekening hebben dezelfde function_type.
De volgende toevoeging (vetgedrukt) is vereist voor de gedelegeerde typen specificatie in een eerder voorstel:
Het gemachtigde type voor de anonieme functie of methodegroep met parametertypen
P1, ..., Pn
en retourtypeR
is:
- als een parameter of retourwaarde niet op waarde is, of een parameter optioneel of
params
is, of als er meer dan 16 parameters zijn, of een van de parametertypen of retour geen geldige typeargumenten zijn (bijvoorbeeld(int* p) => { }
), is de gedelegeerde een gesynthetiseerdeinternal
anonieme gemachtigde met handtekening die overeenkomt met de anonieme functie of methodegroep, en met parameternamenarg1, ..., argn
ofarg
als één parameter; [...]
Binderwijzigingen
Nieuwe gedelegeerdentypen synthetiseren
Net als bij het gedrag voor gemachtigden met ref
of out
parameters, worden gedelegeerde typen gesynthetiseerd voor lambdas- of methodegroepen die zijn gedefinieerd met optionele of params
parameters.
Houd er rekening mee dat in de onderstaande voorbeelden de notatie a'
, b'
, enzovoort wordt gebruikt om deze anonieme gemachtigdentypen weer te geven.
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 = " ");
Gedrag van conversie en eenwording
Anonieme gemachtigden met optionele parameters worden samengevoegd wanneer dezelfde parameter (op basis van positie) dezelfde standaardwaarde heeft, ongeacht de parameternaam.
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
Anonieme gemachtigden met een matrix als laatste parameter worden samengevoegd wanneer de laatste parameter hetzelfde params
wijzigings- en matrixtype heeft, ongeacht de parameternaam.
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)`
Op dezelfde manier is er natuurlijk compatibiliteit met benoemde gemachtigden die al optionele en params
parameters ondersteunen.
Wanneer standaardwaarden of params
wijzigingsaanpassingen verschillen in een conversie, wordt de bron niet gebruikt als deze zich in een lambda-expressie bevindt, omdat de lambda niet op een andere manier kan worden aangeroepen.
Dat lijkt misschien contra-intuïtief voor gebruikers, waardoor er een waarschuwing wordt verzonden wanneer de standaardwaarde van de bron of params
wijziging aanwezig is en verschilt van het doel.
Als de bron een methodegroep is, kan deze zelf worden aangeroepen, waardoor er geen waarschuwing wordt verzonden.
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
IL/runtime-gedrag
De standaardparameterwaarden worden verzonden naar metagegevens. De IL voor deze functie is erg vergelijkbaar met de IL die wordt verzonden voor lambdas met ref
en out
parameters. Er wordt een klasse gegenereerd die overneemt van System.Delegate
of vergelijkbaar, en de Invoke
methode bevat .param
instructies voor het instellen van standaardparameterwaarden of System.ParamArrayAttribute
, net zoals het geval is bij een standaard benoemde gemachtigde met optionele of params
parameters.
Deze typen gemachtigden kunnen tijdens runtime worden geïnspecteerd, zoals normaal.
In code kunnen gebruikers de DefaultValue
introspecteren in de ParameterInfo
die zijn gekoppeld aan de lambda- of methodegroep met behulp van de bijbehorende MethodInfo
.
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
Open vragen
Geen van deze zijn geïmplementeerd. De voorstellen blijven openstaan.
Vraag openen: hoe werkt dit met het bestaande kenmerk van DefaultParameterValue
?
Voorgesteld antwoord: Voor pariteit staat u het kenmerk DefaultParameterValue
op lambdas toe en zorgt u ervoor dat het gedrag van de gedelegeerdengeneratie overeenkomt met de standaardparameterwaarden die worden ondersteund via de syntaxis.
var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed
Open vraag: Eerste moet u er rekening mee houden dat dit buiten het bereik van het huidige voorstel valt, maar het kan de moeite waard zijn in de toekomst te bespreken. Willen we standaardinstellingen ondersteunen met impliciet getypte lambda-parameters? Dwz.
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
Deze deductie leidt tot enkele lastige conversieproblemen die meer discussie vereisen.
Er zijn hier ook overwegingen met betrekking tot de prestaties van het parseren. Vandaag kan de term (x =
bijvoorbeeld nooit het begin zijn van een lambda-expressie. Als deze syntaxis is toegestaan voor lambda-standaardwaarden, heeft de parser een grotere lookahead nodig (helemaal scannen tot een =>
token) om te bepalen of een term een lambda is of niet.
Ontwerpvergaderingen
-
LDM 2022-10-10: beslissing om ondersteuning voor
params
op dezelfde manier toe te voegen als standaardparameterwaarden.
C# feature specifications