Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Nota
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. Perbedaan tersebut dicatat dalam catatan terkait rapat desain bahasa (LDM) .
Anda dapat mempelajari lebih lanjut tentang proses untuk mengadopsi speklet fitur ke dalam standar bahasa C# dalam artikel tentang spesifikasi .
Edisi juara: https://github.com/dotnet/csharplang/issues/2691
Ringkasan
Kelas dan struktur dapat memiliki daftar parameter, dan spesifikasi kelas dasarnya dapat memiliki daftar argumen. Parameter konstruktor utama berada dalam cakupan di seluruh kelas atau deklarasi struktur, dan jika ditangkap oleh anggota fungsi atau fungsi anonim, parameter tersebut disimpan dengan tepat (misalnya sebagai bidang privat yang tidak dapat dipecahkan dari kelas atau struktur yang dideklarasikan).
Proposal "retcons" konstruktor utama yang sudah tersedia pada record sebagai bagian dari fitur yang lebih umum ini dengan beberapa anggota tambahan yang disintesis.
Motivasi
Kemampuan kelas atau struktur dalam C# untuk memiliki lebih dari satu konstruktor menyediakan kegeneralan, tetapi dengan mengorbankan beberapa tedium dalam sintaks deklarasi, karena input konstruktor dan status kelas perlu dipisahkan dengan bersih.
Konstruktor utama menempatkan parameter satu konstruktor dalam cakupan untuk seluruh kelas atau struktur yang akan digunakan untuk inisialisasi atau langsung sebagai status objek. Konsekuensinya adalah bahwa konstruktor lainnya harus memanggil melalui konstruktor utama.
public class B(bool b) { } // base class
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(S));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
Desain terperinci
Ini menjelaskan desain umum di seluruh rekaman dan non-rekaman, lalu merinci bagaimana konstruktor utama yang ada untuk rekaman ditentukan dengan menambahkan sekumpulan anggota yang disintesis di hadapan konstruktor utama.
Sintaksis
Deklarasi kelas dan struktur ditambahkan untuk memungkinkan daftar parameter pada nama jenis, daftar argumen pada kelas dasar, dan isi yang hanya terdiri dari ;:
class_declaration
: attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
parameter_list? class_base? type_parameter_constraints_clause* class_body
;
class_designator
: 'record' 'class'?
| 'class'
class_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? ',' interface_type_list
;
class_body
: '{' class_member_declaration* '}' ';'?
| ';'
;
struct_declaration
: attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
;
struct_body
: '{' struct_member_declaration* '}' ';'?
| ';'
;
interface_declaration
: attributes? interface_modifier* 'partial'? 'interface'
identifier variant_type_parameter_list? interface_base?
type_parameter_constraints_clause* interface_body
;
interface_body
: '{' interface_member_declaration* '}' ';'?
| ';'
;
enum_declaration
: attributes? enum_modifier* 'enum' identifier enum_base? enum_body
;
enum_body
: '{' enum_member_declarations? '}' ';'?
| '{' enum_member_declarations ',' '}' ';'?
| ';'
;
Catatan: Produksi ini menggantikan record_declaration dalam rekaman dan record_struct_declaration dalam struktur Rekaman, yang keduanya menjadi usang.
Ini adalah kesalahan bagi class_base untuk memiliki argument_list jika class_declaration yang diapit tidak berisi parameter_list. Paling banyak satu deklarasi jenis parsial dari kelas atau struktur parsial dapat menyediakan parameter_list. Parameter dalam parameter_list deklarasi record harus berupa parameter nilai.
Perhatikan, menurut proposal ini class_body, struct_body, interface_body dan enum_body diizinkan untuk hanya terdiri dari ;.
Kelas atau struktur dengan parameter_list memiliki konstruktor publik implisit yang tanda tangannya sesuai dengan parameter nilai deklarasi jenis. Ini disebut konstruktor utama untuk jenis tersebut, dan menyebabkan konstruktor tanpa parameter yang dideklarasikan secara implisit, jika ada, dinonaktifkan. Ini adalah kesalahan jika terdapat konstruktor utama dan konstruktor dengan tanda tangan yang sama yang sudah ada dalam deklarasi tipe.
Lookup
Pencarian nama sederhana dipasang untuk menangani parameter konstruktor utama. Perubahan disorot dalam tebal dalam kutipan berikut:
- Jika tidak, untuk setiap tipe instans
T(§15.3.2), dimulai dengan tipe instans dari deklarasi tipe yang paling dekat mengapit dan dilanjutkan dengan tipe instans dari setiap deklarasi kelas atau struktur pembungkus (jika ada):
- Jika deklarasi
Tmencakup parameter konstruktor utamaIdan referensi terjadi dalamargument_listTclass_baseatau dalam inisialisasi bidang, properti, atau peristiwaT, hasilnya adalah parameter konstruktor utamaI- Sebaliknya, jika
enol dan deklarasiTmenyertakan parameter jenis dengan namaI, maka simple_name mengacu pada parameter jenis tersebut.- Jika pencarian anggota (§12,5)
IdalamTdengan argumen jenisemenghasilkan kecocokan, maka:
- Jika
Tadalah tipe instans dari kelas atau tipe struct yang melingkupi secara langsung dan pencarian mengidentifikasi satu atau beberapa metode, hasilnya adalah grup metode dengan ekspresi instans terkait darithis. Jika daftar argumen jenis ditentukan, itu digunakan dalam memanggil metode generik (§12.8.10.2).- Jika tidak, jika
Tadalah jenis instans dari kelas atau jenis struct yang segera mengelilingi, jika pencarian berhasil mengidentifikasi anggota instans, dan jika referensi terjadi di blok konstruktor, metode, atau pengakses instans (§12.2.1), hasilnya sama dengan akses anggota (§12.8.7) dari formulirthis.I. Ini hanya dapat terjadi ketikaeadalah nol.- Selain itu, hasilnya sama dengan akses anggota (§12.8.7) dari formulir
T.IatauT.I<A₁, ..., Aₑ>.- Sebaliknya, jika deklarasi
Tmenyertakan parameter konstruktor utamaI, hasilnya adalah parameter konstruktor utamaI.
Penambahan pertama sesuai dengan perubahan yang dikeluarkan oleh konstruktor utama pada rekaman, dan memastikan bahwa parameter konstruktor utama ditemukan sebelum bidang yang sesuai dalam penginisialisasi dan argumen kelas dasar. Ini memperluas aturan ini ke penginisialisasi statis juga. Namun, karena rekaman selalu memiliki anggota instans dengan nama yang sama dengan parameter, ekstensi hanya dapat menyebabkan perubahan pesan kesalahan. Akses ilegal ke parameter vs. akses ilegal ke anggota instans.
Penambahan kedua memungkinkan parameter konstruktor utama ditemukan di tempat lain dalam badan tipe, tapi hanya jika tidak dibayangi oleh anggota lain.
Ini adalah kesalahan untuk mereferensikan parameter konstruktor utama jika referensi tidak terjadi dalam salah satu hal berikut:
- sebuah argumen
nameof - inisialisasi bidang instans, properti, atau peristiwa dari jenis deklarasikan (jenis yang mendeklarasikan konstruktor utama dengan parameter ).
-
argument_listclass_basedari jenis yang mendeklarasikan. - isi metode instance (perhatikan bahwa konstruktor instance dikecualikan) dari tipe deklarasi.
- isi aksesor instans dari tipe deklarasi.
Dengan kata lain, parameter dari konstruktor utama berada dalam lingkup di seluruh badan jenis yang dideklarasikan. Mereka membayangi anggota dari jenis yang mendeklarasikan dalam penginisialisasi bidang, properti, atau peristiwa dari jenis yang mendeklarasikan, atau dalam argument_list dari class_base dari jenis yang mendeklarasikan. Mereka dibayangi oleh anggota dari jenis yang mendeklarasikan di tempat lain.
Dengan demikian, dalam deklarasi berikut:
class C(int i)
{
protected int i = i; // references parameter
public int I => i; // references field
}
Penginisialisasi untuk bidang i mereferensikan parameter i, sedangkan isi properti I mereferensikan bidang i.
Peringatan tentang bayangan oleh anggota dari dasar
Compiler akan menghasilkan peringatan tentang penggunaan pengidentifikasi ketika anggota dasar membayangi parameter konstruktor utama jika parameter konstruktor utama tersebut tidak diteruskan ke jenis dasar melalui konstruktornya.
Parameter konstruktor utama dianggap diteruskan ke jenis dasar melalui konstruktornya ketika semua kondisi berikut berlaku untuk argumen di class_base:
- Argumen mewakili konversi identitas implisit atau eksplisit dari parameter konstruktor utama;
- Argumen bukan bagian dari argumen
paramsyang diperluas;
Semantik
Konstruktor utama mengarah pada pembuatan konstruktor instans pada jenis penutup dengan parameter yang diberikan. Jika class_base memiliki daftar argumen, konstruktor instans yang dihasilkan akan memiliki penginisialisasi base dengan daftar argumen yang sama.
Parameter konstruktor utama dalam deklarasi kelas/struktur dapat dideklarasikan ref, in atau out. Menyatakan parameter ref atau out tetap ilegal dalam konstruktor utama deklarasi rekaman.
Semua penginisialisasi anggota instans dalam isi kelas akan menjadi penugasan dalam konstruktor yang dihasilkan.
Jika parameter konstruktor utama direferensikan dari dalam anggota instans, dan referensi tidak berada di dalam argumen nameof, parameter tersebut ditangkap ke dalam status jenis penutup, sehingga tetap dapat diakses setelah penghentian konstruktor. Strategi implementasi yang mungkin adalah melalui field privat menggunakan nama yang diacak. Dalam struct readonly, field capture akan bersifat readonly. Oleh karena itu, akses ke parameter yang diambil dari struktur baca-saja akan memiliki batasan serupa sebagai akses ke bidang readonly. Akses ke parameter yang ditangkap dalam anggota readonly akan memiliki batasan serupa dengan akses ke field instance dalam konteks yang sama.
Pengambilan tidak diperbolehkan untuk parameter yang memiliki jenis seperti ref, dan pengambilan tidak diperbolehkan untuk parameter ref, in, atau out. Ini serupa dengan pembatasan dalam penggunaan lambda.
Jika parameter konstruktor utama hanya dirujuk dari dalam penginisialisasi anggota instans, parameter tersebut dapat langsung mereferensikan parameter konstruktor yang dihasilkan, karena dijalankan sebagai bagian darinya.
Konstruktor Utama akan melakukan urutan operasi berikut:
- Nilai parameter disimpan dalam bidang tangkapan, jika ada.
- Penginisialisasi contoh dijalankan
- Penginisialisasi konstruktor dasar dipanggil
Referensi parameter dalam kode pengguna apa pun diganti dengan referensi bidang tangkapan yang sesuai.
Misalnya deklarasi ini:
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
Menghasilkan kode yang mirip dengan yang berikut:
public class C : B
{
public int I { get; set; }
public string S
{
get => __s;
set => __s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(0, s) { ... } // must call this(...)
// generated members
private string __s; // for capture of s
public C(bool b, int i, string s)
{
__s = s; // capture s
I = i; // run I's initializer
B(b) // run B's constructor
}
}
Merupakan kesalahan bagi deklarasi konstruktor non-primer yang memiliki daftar parameter yang sama dengan konstruktor primer. Semua deklarasi konstruktor non-primer harus menggunakan penginisialisasi this, sehingga konstruktor utama pada akhirnya dipanggil.
Catatan akan menghasilkan peringatan jika parameter konstruktor utama tidak dibaca dalam penginisialisasi instans yang mungkin dihasilkan atau penginisialisasi dasar. Peringatan serupa akan dilaporkan untuk parameter konstruktor utama di kelas dan struktur:
- untuk parameter berdasarkan nilai, jika parameter tidak diambil dan tidak dibaca dalam penginisialisasi instans atau penginisialisasi dasar apa pun.
- untuk parameter
in, jika parameter tidak dibaca di dalam penginisialisasi instans atau penginisialisasi dasar mana pun. - untuk parameter
ref, jika parameter tidak dibaca atau ditulis ke dalam penginisialisasi instans atau penginisialisasi dasar apa pun.
Nama-nama sederhana dan nama jenis yang identik
Ada aturan bahasa khusus untuk skenario yang sering disebut sebagai skenario "Warna Warna" - Nama sederhana dan nama jenis yang identik.
Dalam akses anggota formulir
E.I, jikaEadalah pengidentifikasi tunggal, dan jika artiEsebagai simple_name (§12.8.4) adalah konstanta, bidang, properti, variabel lokal, atau parameter dengan jenis yang sama dengan artiEsebagai type_name (§§7.8.1), maka kedua kemungkinan artiEdiizinkan. Pencarian anggotaE.Itidak pernah ambigu, karenaIharus menjadi anggota jenisEdalam kedua kasus. Dengan kata lain, aturan tersebut hanya memungkinkan akses ke anggota statis dan jenis bersarang dariEdi mana sebaliknya akan terjadi kesalahan waktu kompilasi.
Sehubungan dengan konstruktor utama, aturan memengaruhi apakah pengidentifikasi anggota instans harus dianggap sebagai referensi jenis, atau sebagai referensi parameter konstruktor utama, yang pada akhirnya menangkap parameter ke dalam keadaan tipe yang melingkupi. Meskipun "pencarian anggota E.I tidak pernah ambigu", ketika pencarian menghasilkan grup anggota, dalam beberapa kasus tidak mungkin untuk menentukan apakah akses anggota mengacu pada anggota statis atau anggota instans tanpa sepenuhnya menyelesaikan (mengikat) akses anggota. Pada saat yang sama, menangkap parameter dari konstruktor utama mengubah properti tipe pembungkus dengan cara yang memengaruhi analisis semantik. Misalnya, tipe mungkin menjadi tidak terkelola dan gagal memenuhi batasan tertentu karena hal tersebut.
Bahkan ada skenario di mana pengikatan dapat berhasil, tergantung pada apakah parameter dianggap ditangkap atau tidak. Misalnya:
struct S1(Color Color)
{
public void Test()
{
Color.M1(this); // Error: ambiguity between parameter and typename
}
}
class Color
{
public void M1<T>(T x, int y = 0)
{
System.Console.WriteLine("instance");
}
public static void M1<T>(T x) where T : unmanaged
{
System.Console.WriteLine("static");
}
}
Jika kita memperlakukan penerima Color sebagai nilai, kita memperoleh parameter dan 'S1' akan diatur. Kemudian metode statis menjadi tidak dapat diaplikasikan karena kendala dan kami akan memanggil metode instans. Namun, jika kami memperlakukan penerima sebagai jenis, kami tidak menangkap parameter dan 'S1' tetap tidak terkelola, kedua metode tersebut berlaku, tetapi metode statisnya "lebih baik" karena tidak memiliki parameter opsional. Tidak ada pilihan yang mengarah pada kesalahan, tetapi masing-masing akan mengakibatkan perilaku yang berbeda.
Mengingat ini, compiler akan menghasilkan kesalahan ambiguitas untuk akses anggota E.I ketika semua kondisi berikut terpenuhi:
- Pencarian anggota
E.Imenghasilkan grup anggota yang berisi instans dan anggota statis secara bersamaan. Metode ekstensi yang berlaku untuk jenis penerima diperlakukan sebagai metode instans untuk tujuan pemeriksaan ini. - Jika
Ediperlakukan sebagai nama biasa, bukan nama tipe, itu akan merujuk ke parameter konstruktor utama dan akan menangkap parameter ke dalam status jenis pembungkus.
Peringatan penyimpanan ganda
Jika parameter konstruktor utama diteruskan ke basis dan juga ditangkap, ada risiko tinggi bahwa parameter tersebut secara tidak sengaja disimpan dua kali di objek.
Compiler akan menghasilkan peringatan untuk argumen in atau menurut nilai dalam class_baseargument_list ketika semua kondisi berikut ini benar:
- Argumen mewakili konversi identitas implisit atau eksplisit dari parameter konstruktor utama;
- Argumen bukan bagian dari argumen
paramsyang diperluas; - Parameter konstruktor utama ditangkap ke dalam status jenis penutup.
Compiler akan menghasilkan peringatan untuk variable_initializer ketika semua kondisi berikut ini benar:
- Penginisialisasi variabel mewakili konversi identitas implisit atau eksplisit dari parameter konstruktor utama;
- Parameter konstruktor utama ditangkap ke dalam status jenis penutup.
Misalnya:
public class Person(string name)
{
public string Name { get; set; } = name; // warning: initialization
public override string ToString() => name; // capture
}
Atribut yang menargetkan konstruktor utama
Pada https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md kami memutuskan untuk merangkul proposal https://github.com/dotnet/csharplang/issues/7047.
Target atribut "metode" diizinkan pada class_declaration/struct_declaration dengan parameter_list dan menghasilkan konstruktor utama yang sesuai yang memiliki atribut tersebut.
Atribut dengan target method pada class_declaration/struct_declaration tanpa parameter_list diabaikan dengan peringatan.
[method: FooAttr] // Good
public partial record Rec(
[property: Foo] int X,
[field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
public void Frobnicate()
{
...
}
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;
Konstruktor utama pada rekaman
Dengan proposal ini, rekaman tidak perlu lagi menentukan mekanisme konstruktor utama secara terpisah. Sebagai gantinya, rekam (kelas dan struktur) deklarasi yang memiliki konstruktor utama akan mengikuti aturan umum, dengan penambahan sederhana ini:
- Untuk setiap parameter konstruktor utama, jika anggota dengan nama yang sama sudah ada, parameter tersebut harus berupa properti atau bidang instans. Jika tidak, properti otomatis khusus init publik dengan nama yang sama disintesis dengan penginisialisasi properti yang ditetapkan dari parameter .
- Dekonstruktor disintesis dengan parameter out untuk mencocokkan parameter konstruktor utama.
- Jika deklarasi konstruktor eksplisit adalah "konstruktor salinan" - yaitu konstruktor yang menerima satu parameter dari tipe terlampir - maka tidak perlu memanggil penginisialisasi
this, dan inisialisasi anggota yang ada dalam deklarasi rekod tidak akan dijalankan.
Kekurangan
- Ukuran alokasi objek yang dibangun kurang jelas, karena pengkompilasi menentukan apakah akan mengalokasikan bidang untuk parameter konstruktor utama berdasarkan teks lengkap kelas. Risiko ini mirip dengan tangkapan variabel implisit oleh ekspresi lambda.
- Godaan umum (atau pola yang terjadi secara tidak sengaja) mungkin untuk menangkap parameter "sama" pada beberapa tingkat pewarisan seperti saat diteruskan dalam rantai konstruktor alih-alih secara eksplisit menyediakan sebuah bidang yang bersifat proteksi di kelas dasar, yang mengarah ke duplikasi alokasi untuk data yang sama dalam objek. Ini sangat mirip dengan risiko saat ini dalam menimpa properti otomatis dengan properti otomatis lainnya.
- Seperti yang diusulkan di sini, tidak ada tempat untuk logika tambahan yang biasanya diekspresikan dalam badan konstruktor. Ekstensi "badan konstruktor utama" di bawah ini membahasnya.
- Seperti yang diusulkan, semantik urutan eksekusi sangat berbeda dari dalam konstruktor biasa, menunda penginisialisasi anggota menjadi setelah panggilan dasar. Ini mungkin dapat diperbaiki, tetapi dengan mengorbankan beberapa usulan ekstensi (terutama "badan konstruktor utama").
- Proposal hanya berfungsi untuk skenario di mana satu konstruktor dapat ditunjuk sebagai primer.
- Tidak ada cara untuk menyatakan aksesibilitas yang terpisah antara kelas dan konstruktor utama. Contohnya adalah ketika semua konstruktor publik mendelegasikan tugasnya ke satu konstruktor privat "build-it-all". Jika perlu, sintaksis dapat diusulkan untuk itu nanti.
Alternatif
Tidak ada tangkapan
Versi fitur yang jauh lebih sederhana akan melarang parameter konstruktor utama terjadi di badan anggota. Mereferensikannya akan menjadi kesalahan. Bidang harus dideklarasikan secara eksplisit jika penyimpanan diinginkan di luar kode inisialisasi.
public class C(string s)
{
public string S1 => s; // Nope!
public string S2 { get; } = s; // Still allowed
}
Ini masih dapat berkembang menjadi proposal penuh di waktu mendatang, dan akan menghindari sejumlah keputusan dan kompleksitas, dengan konsekuensi mengurangi lebih sedikit elemen standar pada awalnya, dan mungkin juga terkesan tidak intuitif.
Bidang eksplisit yang terbentuk
Pendekatan alternatif adalah bahwa parameter konstruktor utama selalu dan secara jelas menghasilkan field dengan nama yang sama. Alih-alih menutup parameter dengan cara yang sama seperti fungsi lokal dan anonim, secara eksplisit akan ada deklarasi anggota yang dihasilkan, mirip dengan properti publik yang dihasilkan untuk parameter konstrukor utama dalam rekaman. Sama seperti untuk rekaman, jika sudah ada anggota yang cocok, maka tidak akan dibuat yang baru.
Jika bidang yang dihasilkan bersifat privat, bidang tersebut masih dapat diabaikan ketika tidak digunakan sebagai bidang dalam bagian dari anggota. Namun, dalam kelas, bidang privat sering kali tidak akan menjadi pilihan yang tepat, karena dapat menyebabkan duplikasi status di kelas turunan. Opsi lainnya di sini adalah dengan menghasilkan bidang yang dilindungi dalam kelas, yang mendorong penggunaan kembali penyimpanan di seluruh lapisan pewarisan. Namun, jika demikian, kami tidak akan dapat mengabaikan deklarasi tersebut, dan akan dikenakan biaya alokasi untuk setiap parameter konstruktor utama.
Ini akan menyelaraskan konstruktor utama non-catatan lebih dekat dengan konstruktor catatan, di mana anggota selalu (setidaknya secara konseptual) dihasilkan, meskipun anggota yang berbeda jenis dengan tingkat aksesibilitas yang berbeda. Tetapi itu juga akan menyebabkan perbedaan mengejutkan dibandingkan dengan bagaimana parameter dan variabel lokal ditangkap di tempat lain di C#. Jika kita diizinkan menggunakan kelas lokal, misalnya, mereka akan menangkap parameter dan variabel lokal yang melingkupi secara implisit. Membuat bidang bayangan yang terlihat bagi mereka tampaknya bukanlah perilaku yang wajar.
Masalah lain yang sering muncul dengan pendekatan ini adalah bahwa banyak pengembang memiliki konvensi penamaan yang berbeda untuk parameter dan bidang. Mana yang harus digunakan untuk parameter konstruktor utama? Salah satu pilihan akan menyebabkan ketidakkonsistensian dengan sisa kode.
Akhirnya, deklarasi anggota yang terlihat benar-benar merupakan inti dari rekor, tetapi jauh lebih mengejutkan dan tidak lazim untuk kelas dan struktur non-rekor. Secara keseluruhan, itulah alasan mengapa proposal utama memilih penangkapan implisit, dengan perilaku yang masuk akal (konsisten dengan rekaman) untuk deklarasi anggota eksplisit saat diinginkan.
Menghapus anggota instans dari cakupan penginisialisasi
Aturan pencarian di atas dimaksudkan untuk memungkinkan perilaku parameter konstruktor utama saat ini dalam rekaman ketika anggota yang sesuai dideklarasikan secara manual, dan untuk menjelaskan perilaku anggota yang dihasilkan ketika tidak. Ini mengharuskan pencarian untuk membedakan antara "cakupan inisialisasi" (inisialisasi ini/dasar, penginisialisasi anggota) dan "cakupan tubuh" (isi anggota), yang dicapai oleh usulan di atas dengan mengubah ketika parameter konstruktor utama dicari, tergantung di mana referensi terjadi.
Pengamatan adalah bahwa mereferensikan anggota instans dengan nama sederhana dalam cakupan inisialisasi selalu menyebabkan kesalahan. Alih-alih hanya menyembunyikan anggota instans di tempat-tempat itu, bisakah kita mengeluarkan mereka dari ruang lingkup? Dengan begitu, tidak akan ada pengurutan lingkup bersyarat yang aneh ini.
Alternatif ini kemungkinan besar dapat dilakukan, namun akan memiliki beberapa konsekuensi yang berjangkauan luas dan berpotensi tidak diinginkan. Pertama-tama, jika kita menghapus anggota instans dari cakupan inisialisasi, maka nama sederhana yang sesuai dengan anggota instans dan tidak ke parameter konstruktor utama dapat secara tidak sengaja mengikat sesuatu di luar deklarasi jenis! Tampaknya hal ini jarang disengaja, dan kesalahan bisa menjadi alternatif yang lebih baik.
Selain itu, anggota statis dapat direferensikan dalam cakupan inisialisasi. Jadi kita harus membedakan antara anggota statis dan instans dalam pencarian, sesuatu yang tidak kita lakukan hari ini. (Kami membedakan dalam resolusi overload tetapi itu tidak relevan di sini). Jadi itu juga harus diubah, yang mengarah ke situasi yang lebih di mana misalnya dalam konteks statis sesuatu akan mengikat "lebih jauh" daripada kesalahan karena menemukan anggota instans.
Secara keseluruhan, "penyederhanaan" ini akan menyebabkan komplikasi lanjutan yang tidak diharapkan oleh siapapun.
Ekstensi yang mungkin
Ini adalah variasi atau penambahan pada proposal inti yang dapat dipertimbangkan bersama dengannya, atau pada tahap selanjutnya jika dianggap berguna.
Akses parameter konstruktor utama di dalam konstruktor
Aturan di atas menganggapnya sebagai kesalahan untuk merujuk parameter konstruktor utama dalam konstruktor lain. Karena konstruktor utama berjalan terlebih dahulu, ini dapat diizinkan dalam tubuh dari konstruktor lain. Namun perlu tetap dilarang dalam daftar argumen penginisialisasi this.
public class C(bool b, int i, string s) : B(b)
{
public C(string s) : this(b, s) // b still disallowed
{
i++; // could be allowed
}
}
Akses semacam itu tetap akan menyebabkan penangkapan, karena itu adalah satu-satunya cara agar badan konstruktor dapat mengakses variabel setelah konstruktor utama selesai berjalan.
Larangan pada parameter konstruktor utama dalam argumen penginisialisasi ini dapat dilonggarkan untuk mengizinkannya, meskipun itu berarti parameter tersebut tidak pasti akan diinisialisasi; namun, hal tersebut tampaknya tidak berguna.
Izinkan konstruktor tanpa inisialisasi this
Konstruktor tanpa penginisialisasi this (yaitu dengan penginisialisasi base implisit atau eksplisit) dapat diizinkan. Konstruktor seperti itu akan tidak menjalankan bidang instans, properti, dan penginisialisasi peristiwa, karena konstruktor tersebut hanya akan dianggap sebagai bagian dari konstruktor utama.
Dengan adanya konstruktor pemanggilan dasar seperti itu, terdapat beberapa opsi tentang bagaimana pengambilan parameter dari konstruktor utama ditangani. Yang paling sederhana adalah melarang penangkapan sepenuhnya dalam situasi ini. Parameter konstruktor utama akan digunakan untuk inisialisasi saja ketika konstruktor tersebut ada.
Atau, jika dikombinasikan dengan opsi yang dijelaskan sebelumnya untuk memungkinkan akses ke parameter konstruktor utama dalam konstruktor, parameter dapat memasuki isi konstruktor sebagai tidak ditugaskan secara pasti, dan parameter yang ditangkap harus ditugaskan secara pasti pada akhir isi konstruktor. Mereka pada dasarnya akan menjadi parameter keluaran yang implisit. Dengan begitu, parameter konstruktor utama yang diambil akan selalu memiliki nilai yang masuk akal (yaitu ditetapkan secara eksplisit) pada saat mereka dikonsumsi oleh anggota fungsi lain.
Daya tarik ekstensi ini (dalam kedua bentuk) adalah bahwa ekstensi ini sepenuhnya menggeneralisasi pengecualian saat ini untuk "konstruktor salinan" dalam rekaman, tanpa mengarah ke situasi di mana parameter konstruktor utama yang tidak diinisialisasi diamati. Pada dasarnya, konstruktor yang menginisialisasi objek dengan cara alternatif baik-baik saja. Pembatasan terkait penangkapan tidak akan menjadi perubahan yang mengganggu untuk konstruktor salinan yang ditentukan secara manual yang ada dalam rekor, karena rekor tidak pernah menangkap parameter konstruktor utama mereka (mereka menghasilkan bidang sebagai gantinya).
public class C(bool b, int i, string s) : B(b)
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s2) : base(true) // cannot use `string s` because it would shadow
{
s = s2; // must initialize s because it is captured by S
}
protected C(C original) : base(original) // copy constructor
{
this.s = original.s; // assignment to b and i not required because not captured
}
}
Badan konstruktor utama
Konstruktor sendiri sering berisi logika validasi parameter atau kode inisialisasi nontrivial lainnya yang tidak dapat dinyatakan sebagai penginisialisasi.
Konstruktor utama dapat diperluas untuk memungkinkan blok pernyataan muncul langsung di isi kelas. Pernyataan tersebut akan dimasukkan ke dalam konstruktor yang dihasilkan pada titik di mana pernyataan tersebut muncul di antara penugasan inisialisasi, dan dengan demikian akan dijalankan berselingan dengan penginisialisasi. Misalnya:
public class C(int i, string s) : B(s)
{
{
if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
}
int[] a = new int[i];
public int S => s;
}
Banyak skenario ini mungkin cukup tercakup jika kita memperkenalkan "penginisialisasi akhir" yang berjalan setelah konstruktor dan penginisialisasi objek/koleksi apa pun telah selesai. Namun, validasi argumen adalah satu hal yang idealnya akan terjadi sedini mungkin.
Badan konstruktor utama juga dapat menyediakan tempat untuk mengizinkan pengubah akses untuk konstruktor utama, memungkinkannya menyimpang dari aksesibilitas jenis penutup.
Parameter gabungan dan deklarasi anggota
Penambahan yang mungkin dan sering disebutkan bisa untuk memungkinkan parameter konstruktor utama dianotasikan sehingga mereka akan juga mendeklarasikan anggota pada jenis tersebut. Pada umumnya diusulkan untuk mengizinkan spesifikator akses pada parameter untuk mengaktifkan pembuatan anggota.
public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
void M()
{
... i ... // refers to the field i
... s ... // closes over the parameter s
}
}
Ada beberapa masalah:
- Bagaimana jika properti diinginkan, bukan bidang? Memiliki sintaks
{ get; set; }dalam satu baris di dalam daftar parameter tampaknya tidak menarik. - Bagaimana jika konvensi penamaan yang berbeda digunakan untuk parameter dan bidang? Maka fitur ini tidak akan berguna.
Ini adalah potensi penambahan di masa depan yang dapat diadopsi atau tidak. Proposal saat ini membuat kemungkinan terbuka.
Buka pertanyaan
Urutan pencarian untuk parameter tipe
Bagian https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup menentukan bahwa parameter tipe dari tipe yang mendeklarasikan harus datang sebelum parameter konstruktor utama dalam setiap konteks di mana parameter-parameter tersebut berada dalam cakupan. Namun, kami sudah memiliki perilaku yang ada dengan catatan - parameter dari konstruktor utama muncul sebelum parameter jenis di dalam inisialisasi dasar serta inisialisasi bidang.
Apa yang harus kita lakukan tentang perbedaan ini?
- Sesuaikan aturan agar sesuai dengan perilaku.
- Sesuaikan perilaku (kemungkinan perubahan yang dapat mengganggu).
- Larang parameter konstruktor primirius untuk menggunakan nama parameter jenis (kemungkinan perubahan yang melanggar).
- Jangan lakukan apa-apa, terima inkonsistensi antara spesifikasi dan implementasi.
Kesimpulan:
Sesuaikan aturan agar sesuai dengan perilaku (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors).
Atribut penargetan bidang untuk parameter konstruktor utama yang diambil
Haruskah kita mengizinkan atribut penargetan bidang untuk parameter konstruktor utama yang diambil?
class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = x;
}
Saat ini atribut diabaikan dengan pemberitahuan peringatan, tanpa memandang apakah parameter tersebut ditangkap atau tidak.
Perhatikan bahwa untuk catatan, atribut yang ditujukan untuk bidang diizinkan ketika properti disintesis untuknya. Atribut ditempatkan pada bidang penyimpanan.
record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = X;
}
Kesimpulan:
Tidak diperbolehkan (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters).
Peringatan tentang bayangan oleh anggota dari dasar
Haruskah kita melaporkan peringatan ketika anggota dari kelas dasar menutupi parameter konstruktor utama di dalam anggota (lihat https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621)?
Kesimpulan:
Desain alternatif disetujui - https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
Menangkap instans jenis penutup dalam penutupan
Ketika parameter yang ditangkap ke dalam status jenis penutup juga direferensikan dalam lambda di dalam penginisialisasi instans atau penginisialisasi dasar, lambda dan status jenis penutup harus merujuk ke lokasi yang sama untuk parameter . Misalnya:
partial class C1
{
public System.Func<int> F1 = Execute1(() => p1++);
}
partial class C1 (int p1)
{
public int M1() { return p1++; }
static System.Func<int> Execute1(System.Func<int> f)
{
_ = f();
return f;
}
}
Karena pada implementasi naif, menangkap parameter ke dalam keadaan tipe umumnya hanya menangkap parameter dalam sebuah bidang instans privat, lambda perlu merujuk ke bidang yang sama. Akibatnya, diperlukan akses ke instans dari jenis tersebut. Ini memerlukan penangkapan this ke dalam closure sebelum konstruktor dasar dipanggil. Itu, pada gilirannya, menghasilkan kondisi yang aman, tetapi IL yang tidak dapat diverifikasi. Apakah ini dapat diterima?
Atau kita bisa:
- Melarang penggunaan lambda seperti itu
- Atau, sebagai gantinya, ambil parameter seperti itu dalam instans kelas terpisah (penutupan lain), dan bagikan instans tersebut antara penutupan dan instans tipe yang melingkupinya. Dengan demikian menghilangkan kebutuhan untuk menangkap
thisdalam konteks tertutup.
Kesimpulan:
Kami nyaman dengan menangkap this menjadi penutup sebelum konstruktor dasar dipanggil (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).
Tim runtime juga tidak menemukan pola IL bermasalah.
Menetapkan this dalam sebuah struktur
C# memungkinkan menetapkan nilai kepada this di dalam struct. Jika struct menangkap parameter konstruktor utama, penugasan akan menimpa nilainya, yang mungkin tidak jelas bagi pengguna. Apakah kita ingin melaporkan peringatan untuk penugasan seperti ini?
struct S(int x)
{
int X => x;
void M(S s)
{
this = s; // 'x' is overwritten
}
}
Kesimpulan:
Diizinkan, tidak ada peringatan (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).
Peringatan penyimpanan ganda untuk inisialisasi dan pengambilan data
Kami memiliki peringatan jika parameter konstruktor utama diteruskan ke basis dan juga ditangkap, karena ada risiko tinggi bahwa parameter tersebut secara tidak sengaja disimpan dua kali di objek.
Tampaknya ada risiko serupa jika suatu parameter digunakan untuk menginisialisasi anggota, dan parameter tersebut juga ditangkap. Berikut adalah contoh kecil:
public class Person(string name)
{
public string Name { get; set; } = name; // initialization
public override string ToString() => name; // capture
}
Untuk instans Persontertentu, perubahan pada Name tidak akan terlihat pada hasil ToString, yang mungkin tidak diinginkan dari pihak pengembang.
Haruskah kita memperkenalkan peringatan penyimpanan ganda untuk situasi ini?
Ini adalah cara kerjanya:
Pengkompilasi akan menghasilkan peringatan untuk variable_initializer ketika semua kondisi berikut benar:
- Penginisialisasi variabel mewakili konversi identitas implisit atau eksplisit dari parameter konstruktor utama;
- Parameter konstruktor utama ditangkap ke dalam status jenis penutup.
Kesimpulan:
Disetujui, lihat https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
Rapat LDM
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-10-17.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-01-18.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-22.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors
C# feature specifications