Baca dalam bahasa Inggris

Bagikan melalui


Parameter opsional dan parameter array untuk lambda dan kelompok metode

Catatan

Artikel ini adalah spesifikasi fitur. Spesifikasi berfungsi sebagai dokumen desain untuk fitur tersebut. Ini termasuk perubahan spesifikasi yang diusulkan, bersama dengan informasi yang diperlukan selama desain dan pengembangan fitur. Artikel ini diterbitkan sampai perubahan spesifikasi yang diusulkan diselesaikan dan dimasukkan dalam spesifikasi ECMA saat ini.

Mungkin ada beberapa perbedaan antara spesifikasi fitur dan implementasi yang selesai. Catatan rapat desain bahasa (LDM) yang relevanmencatat perbedaan tersebut.

Anda dapat mempelajari lebih lanjut tentang proses untuk mengadopsi speklet fitur ke dalam standar bahasa C# dalam artikel tentang spesifikasi .

Masalah utama: https://github.com/dotnet/csharplang/issues/6051

Ringkasan

Untuk melanjutkan dari peningkatan lambda yang diperkenalkan dalam C# 10 (lihat latar belakang yang relevan), kami mengusulkan penambahan dukungan untuk parameter default dan array params dalam lambda. Ini akan memungkinkan pengguna untuk mengimplementasikan lambda berikut:

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

Demikian pula, kita akan mengizinkan jenis perilaku yang sama untuk grup metode:

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;
}

Latar belakang yang relevan

Peningkatan Lambda di C# 10

spesifikasi konversi grup metode §10.8

Motivasi

Kerangka kerja aplikasi dalam ekosistem .NET sangat memanfaatkan lambda untuk memungkinkan pengguna menulis logika bisnis dengan cepat yang terkait dengan titik akhir.

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 saat ini tidak mendukung pengaturan nilai default pada parameter, jadi jika pengembang ingin membangun aplikasi yang tahan terhadap skenario di mana pengguna tidak memberikan data, mereka dibiarkan menggunakan fungsi lokal atau mengatur nilai default dalam isi lambda, dibandingkan dengan sintaks yang diusulkan yang lebih succinct.

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);
});

Sintaks yang diusulkan juga memiliki manfaat mengurangi perbedaan yang membingungkan antara lambda dan fungsi lokal, sehingga lebih mudah untuk memahami konstruksi dan mentransformasi lambda menjadi fungsi tanpa mengorbankan fitur, terutama dalam skenario lain di mana lambda digunakan dalam API yang juga menyediakan kelompok metode sebagai acuan. Ini juga merupakan motivasi utama untuk mendukung array params yang tidak dicakup oleh skenario kasus penggunaan yang disebutkan di atas.

Misalnya:

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);

Perilaku sebelumnya

Sebelum C# 12, ketika pengguna mengimplementasikan lambda dengan parameter opsional atau params, pengkompilasi menimbulkan kesalahan.

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

Ketika pengguna mencoba menggunakan kelompok metode di mana metode yang mendasar memiliki parameter opsional atau params, informasi ini tidak diteruskan, sehingga pemanggilan metode tidak dapat memvalidasi tipe karena ketidakcocokan dalam jumlah argumen yang sesuai.

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[]>'

Perilaku baru

Mengikuti proposal ini (bagian dari C# 12), nilai default dan params dapat diterapkan ke parameter lambda dengan perilaku berikut:

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

Nilai default dan params dapat diterapkan ke parameter grup metode dengan secara khusus menentukan grup metode tersebut:

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

Perubahan yang mengganggu

Sebelum C# 12, jenis grup metode yang disimpulkan Action atau Func sehingga kode berikut mengkompilasi:

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 });
}

Setelah perubahan ini (bagian dari C# 12), kode sifat ini berhenti dikompilasi di .NET SDK 7.0.200 atau yang lebih baru.

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 });
}

Dampak dari perubahan besar ini perlu dipertimbangkan. Untungnya, penggunaan var untuk menyimpulkan jenis grup metode hanya didukung sejak C# 10, jadi hanya kode yang telah ditulis sejak saat itu yang secara eksplisit bergantung pada perilaku ini akan rusak.

Desain terperinci

Perubahan tata bahasa dan pengurai

Peningkatan ini memerlukan perubahan berikut pada tata bahasa untuk ekspresi 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?
   ;

Perhatikan bahwa ini memungkinkan nilai parameter default dan array params hanya untuk lambda, bukan untuk metode anonim yang dideklarasikan dengan sintaks delegate { }.

Aturan yang sama seperti untuk parameter metode (§15.6.2) berlaku untuk parameter lambda:

  • Parameter dengan pengubah ref, out, atau this tidak dapat memiliki default_argument.
  • parameter_array dapat terjadi setelah parameter opsional, tetapi tidak dapat memiliki nilai bawaan – penghilangan argumen untuk parameter_array akan mengakibatkan pembuatan array kosong.

Tidak ada perubahan pada tata bahasa yang diperlukan untuk grup metode karena proposal ini hanya akan mengubah semantik mereka.

Penambahan berikut (dalam huruf tebal) diperlukan untuk konversi fungsi anonim (§10,7):

Secara khusus, fungsi anonim F kompatibel dengan jenis delegasi D disediakan:

  • [...]
  • Jika F memiliki daftar parameter yang diketik secara eksplisit, setiap parameter dalam D memiliki jenis dan pengubah yang sama dengan parameter yang sesuai dalam Fmengabaikan pengubah params dan nilai default.

Pembaruan atas proposal sebelumnya

Penambahan berikut (dalam huruf tebal) diperlukan untuk spesifikasi jenis fungsi dalam proposal sebelumnya:

Grup metode memiliki tipe alami jika semua metode kandidat dalam grup tersebut memiliki tanda tangan bersama , termasuk nilai default dan pengubah params. (Jika grup metode dapat mencakup metode ekstensi, kandidat menyertakan jenis yang berisi dan semua cakupan metode ekstensi.)

Jenis alami ekspresi fungsi anonim atau grup metode adalah function_type. function_type mewakili spesifikasi metode: jenis parameter, nilai default, jenis referensi, pengubah params, serta jenis pengembalian dan jenis referensi. Ekspresi fungsi anonim atau grup metode dengan tanda tangan yang sama memiliki function_typeyang sama.

Penambahan berikut (dalam huruf tebal) diperlukan untuk jenis delegasi spesifikasi dalam proposal sebelumnya:

Jenis delegasi untuk fungsi anonim atau grup metode dengan jenis parameter P1, ..., Pn dan jenis pengembalian R adalah:

  • jika ada parameter atau nilai yang dikembalikan bukan berdasarkan nilai, atau parameter apa pun bersifat opsional atau params, atau ada lebih dari 16 parameter, atau salah satu jenis parameter atau pengembalian bukan argumen jenis yang valid (misalnya, (int* p) => { }), maka delegasi adalah jenis delegasi anonim yang disintesis internal dengan tanda tangan yang cocok dengan fungsi anonim atau grup metode, dan dengan nama parameter arg1, ..., argn atau arg jika satu parameter; [...]

Perubahan pengikat

Mensintesis jenis delegasi baru

Seperti halnya perilaku untuk delegasi dengan parameter ref atau out, tipe delegasi dihasilkan untuk lambda atau grup metode yang ditentukan dengan parameter opsional atau params. Perhatikan bahwa dalam contoh di bawah ini, notasi a', b', dll. digunakan untuk mewakili jenis delegasi anonim ini.

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 = " ");

Perilaku konversi dan penyatuan

Delegasi anonim dengan parameter opsional akan disatukan ketika parameter yang sama (berdasarkan posisi) memiliki nilai default yang sama, terlepas dari nama parameter.

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

Delegasi anonim dengan array sebagai parameter terakhir akan disatukan ketika parameter terakhir memiliki pengubah params dan jenis array yang sama, terlepas dari nama parameter.

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)`

Demikian pula, tentu saja ada kompatibilitas dengan delegasi bernama yang sudah mendukung parameter opsional dan params. Ketika nilai default atau pengubah params berbeda dalam konversi, sumber akan tidak digunakan jika berada dalam ekspresi lambda, karena lambda tidak dapat dipanggil dengan cara lain. Itu mungkin tampak kontra-intuitif bagi pengguna, oleh karena itu peringatan akan dipancarkan ketika nilai default sumber atau pengubah params ada dan berbeda dari target. Jika sumbernya adalah grup metode, sumber dapat dipanggil sendiri, sehingga tidak ada peringatan yang akan dipancarkan.

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

Perilaku IL/runtime

Nilai parameter default akan dipancarkan ke metadata. IL untuk fitur ini akan sangat mirip secara alami dengan IL yang dipancarkan untuk lambda dengan parameter ref dan out. Kelas yang mewarisi dari System.Delegate atau sejenisnya akan dihasilkan, dan metode Invoke akan menyertakan arahan .param untuk mengatur nilai parameter default atau System.ParamArrayAttribute - seperti halnya untuk delegasi standar bernama dengan parameter opsional atau params.

Jenis delegasi ini dapat diperiksa pada runtime, seperti biasa. Dalam kode, pengguna dapat mengintrospeksi DefaultValue pada ParameterInfo yang dihubungkan dengan lambda atau kelompok metode dengan menggunakan MethodInfoyang terkait.

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

Buka pertanyaan

Tidak satu pun dari ini telah diimplementasikan. Proposals tersebut tetap terbuka.

Buka pertanyaan: bagaimana hal ini berinteraksi dengan atribut DefaultParameterValue yang ada?

Usulan jawaban: Untuk paritas, izinkan atribut DefaultParameterValue pada lambda dan pastikan bahwa perilaku pembuatan delegasi cocok untuk nilai parameter default yang didukung melalui sintaks.

var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed

Pertanyaan terbuka: Pertama, perhatikan bahwa ini berada di luar cakupan proposal saat ini tetapi mungkin perlu dibahas di masa depan. Apakah kita ingin mendukung default dengan parameter lambda yang ditik secara implisit? Yaitu,

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

Inferensi ini menyebabkan beberapa masalah konversi yang sulit yang akan membutuhkan lebih banyak diskusi.

Terdapat juga pertimbangan performa penguraian di sini. Misalnya, saat ini istilah (x = tidak pernah bisa menjadi awal ekspresi lambda. Jika sintaks ini diizinkan untuk default lambda, maka pengurai akan membutuhkan tinjauan ke depan yang lebih besar (memindai hingga token =>) untuk menentukan apakah elemen tersebut adalah lambda atau bukan.

Rapat desain

  • LDM 2022-10-10 : keputusan untuk menambahkan dukungan untuk params dengan cara yang sama seperti nilai parameter default.