Delen via


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

Lambda-verbeteringen in C# 10

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 of this 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-type D mits:

  • [...]
  • Als F een expliciet getypte parameterlijst heeft, heeft elke parameter in D hetzelfde type en dezelfde wijzigingsfunctie als de bijbehorende parameter in Fhet negeren van params 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 retourtype R is:

  • als een parameter of retourwaarde niet op waarde is, of een parameter optioneel of paramsis, 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 gesynthetiseerde internal anonieme gemachtigde met handtekening die overeenkomt met de anonieme functie of methodegroep, en met parameternamen arg1, ..., argn of arg 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.