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 rapat desain bahasa (LDM) terkait .
Anda dapat mempelajari lebih lanjut tentang proses untuk mengadopsi speklet fitur ke dalam standar bahasa C# dalam artikel tentang spesifikasi .
Masalah juara: https://github.com/dotnet/csharplang/issues/1147, https://github.com/dotnet/csharplang/issues/6476
Ringkasan
Proposal ini adalah agregasi dari beberapa proposal yang berbeda untuk peningkatan performa struct: parameter ref dan kemampuan untuk mengubah pengaturan bawaan masa pakai. Tujuannya adalah mencapai desain yang mempertimbangkan berbagai proposal untuk menciptakan satu kumpulan fitur menyeluruh dalam konteks peningkatan struct pada tingkat rendah.
Catatan: Versi sebelumnya dari spesifikasi ini menggunakan istilah "ref-safe-to-escape" dan "safe-to-escape", yang diperkenalkan dalam spesifikasi fitur keamanan rentang. Komite standar ECMA mengubah nama menjadi "ref-safe-context" dan "safe-context". Nilai konteks aman telah disempurnakan agar menggunakan "declaration-block", "function-member", dan "caller-context" secara konsisten. Spesifikasi ringkas telah menggunakan pilihan kata yang berbeda untuk istilah-istilah ini, dan juga menggunakan "safe-to-return" sebagai sinonim untuk "caller-context". Speklet ini telah diperbarui untuk menggunakan istilah dalam standar C# 7.3.
Tidak semua fitur yang diuraikan dalam dokumen ini telah diimplementasikan di C# 11. C# 11 meliputi:
-
refbidang danscoped [UnscopedRef]
Fitur-fitur ini tetap membuka proposal untuk versi C yang akan datang#:
-
refbidang keref struct - Jenis terbatas matahari terbenam
Motivasi
Versi C# sebelumnya menambahkan sejumlah fitur performa tingkat rendah: pengembalian ref, ref struct, penunjuk fungsi, dll. Ini memungkinkan pengembang .NET untuk menulis kode yang sangat berkinerja sambil memanfaatkan aturan bahasa C# untuk keamanan tipe dan memori. Ini juga memungkinkan pembuatan jenis performa dasar di pustaka .NET seperti Span<T>.
Karena fitur-fitur ini telah mendapatkan daya tarik dalam pengembang ekosistem .NET, baik internal maupun eksternal, telah memberi kami informasi tentang titik gesekan yang tersisa dalam ekosistem. Tempat di mana mereka masih perlu menggunakan kode unsafe untuk menyelesaikan pekerjaan mereka, atau memerlukan runtime untuk menangani jenis kasus khusus seperti Span<T>.
Saat ini Span<T> dicapai dengan menggunakan jenis internalByReference<T> yang diperlakukan secara efektif oleh runtime sebagai field ref. Ini memberikan manfaat dari bidang ref tetapi dengan kelemahan bahwa bahasa tidak menyediakan verifikasi keamanan untuknya, seperti yang dilakukan untuk penggunaan lain dari ref. Selanjutnya, hanya dotnet/runtime yang dapat menggunakan jenis ini karena internal, sehingga pihak ketiga tidak dapat merancang primitif mereka sendiri berdasarkan bidang ref. Bagian dari motivasi untuk pekerjaan ini adalah menghapus ByReference<T> dan menggunakan bidang ref yang tepat di semua basis kode.
Proposal ini bertujuan untuk menangani masalah ini dengan memanfaatkan fitur tingkat rendah yang sudah ada. Secara khusus bertujuan untuk:
- Perbolehkan jenis
ref structuntuk mendeklarasikan kolomref. - Izinkan runtime untuk sepenuhnya menentukan
Span<T>menggunakan sistem jenis C# dan menghapus jenis kasus khusus sepertiByReference<T> - Izinkan jenis
structmengembalikanrefke kolom mereka. - Izinkan runtime untuk menghapus penggunaan
unsafeyang disebabkan oleh batasan default seumur hidup - Izinkan deklarasi buffer
fixedyang aman untuk tipe terkelola dan tidak terkelola dalamstruct
Desain Terperinci
Aturan untuk keamanan ref struct ditentukan dalam dokumen keamanan rentang menggunakan persyaratan sebelumnya. Aturan tersebut telah dimasukkan ke dalam standar C# 7 dalam §9,7,2 dan §16.4.12. Dokumen ini akan menjelaskan perubahan yang diperlukan pada dokumen ini sebagai hasil dari proposal ini. Setelah diterima sebagai fitur yang disetujui, perubahan ini akan dimasukkan ke dalam dokumen tersebut.
Setelah desain ini selesai, definisi Span<T> kami adalah sebagai berikut:
readonly ref struct Span<T>
{
readonly ref T _field;
readonly int _length;
// This constructor does not exist today but will be added as a part
// of changing Span<T> to have ref fields. It is a convenient, and
// safe, way to create a length one span over a stack value that today
// requires unsafe code.
public Span(ref T value)
{
_field = ref value;
_length = 1;
}
}
Menyediakan bidang referensi dan diatur ruang lingkupnya
Bahasa ini akan memungkinkan pengembang untuk mendeklarasikan bidang ref di dalam ref struct. Ini dapat berguna, misalnya, saat mengemas instans besar struct yang dapat diubah atau menentukan tipe kinerja tinggi seperti Span<T> di pustaka di luar runtime.
ref struct S
{
public ref int Value;
}
Bidang ref akan dipancarkan ke metadata menggunakan tanda tangan ELEMENT_TYPE_BYREF. Ini tidak berbeda dengan cara kami mengeluarkan local ref atau argumen ref. Misalnya ref int _field akan dipancarkan sebagai ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4. Ini akan mengharuskan kami untuk memperbarui ECMA335 untuk memungkinkan entri ini, tetapi ini seharusnya cukup sederhana.
Pengembang dapat terus menginisialisasi ref struct dengan bidang ref menggunakan ekspresi default dalam hal ini semua bidang ref yang dinyatakan akan memiliki nilai null. Setiap upaya untuk menggunakan bidang tersebut akan mengakibatkan NullReferenceException dilemparkan.
ref struct S
{
public ref int Value;
}
S local = default;
local.Value.ToString(); // throws NullReferenceException
Meskipun bahasa C# beranggapan bahwa ref tidak dapat menjadi null, hal ini legal pada level runtime dan memiliki semantik yang jelas. Pengembang yang memperkenalkan bidang ref ke dalam jenisnya perlu menyadari kemungkinan ini dan harus sangat mencegah membocorkan detail ini ke dalam menggunakan kode. Sebaliknya, ref field harus divalidasi sebagai non-null menggunakan pembantu runtime dan melontarkan ketika struct yang tidak diinisialisasi digunakan dengan salah.
ref struct S1
{
private ref int Value;
public int GetValue()
{
if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
{
throw new InvalidOperationException(...);
}
return Value;
}
}
Bidang ref dapat dikombinasikan dengan pengubah readonly dengan cara berikut:
-
readonly ref: ini adalah bidang yang tidak dapat ditetapkan ulang di luar konstruktor atau metodeinit. Ini dapat berupa nilai yang ditetapkan meskipun di luar konteks tersebut -
ref readonly: ini adalah bidang yang dapat ditetapkan ulang tetapi tidak dapat ditetapkan nilai kapan saja. Inilah caranya parameterindapat dikonversi ulang ke bidangref. -
readonly ref readonly: kombinasiref readonlydanreadonly ref.
ref struct ReadOnlyExample
{
ref readonly int Field1;
readonly ref int Field2;
readonly ref readonly int Field3;
void Uses(int[] array)
{
Field1 = ref array[0]; // Okay
Field1 = array[0]; // Error: can't assign ref readonly value (value is readonly)
Field2 = ref array[0]; // Error: can't repoint readonly ref
Field2 = array[0]; // Okay
Field3 = ref array[0]; // Error: can't repoint readonly ref
Field3 = array[0]; // Error: can't assign ref readonly value (value is readonly)
}
}
readonly ref struct akan mengharuskan bidang ref dideklarasikan readonly ref. Tidak ada persyaratan bahwa mereka dinyatakan readonly ref readonly. Ini memungkinkan readonly struct memiliki mutasi tidak langsung melalui bidang seperti itu tetapi itu tidak berbeda dari bidang readonly yang menunjuk ke jenis referensi hari ini (detail lebih lanjut)
readonly ref akan dikirimkan ke metadata menggunakan penanda initonly, sama seperti bidang lainnya. Bidang ref readonly akan dikaitkan dengan System.Runtime.CompilerServices.IsReadOnlyAttribute.
readonly ref readonly akan dikirimkan bersama kedua item.
Fitur ini memerlukan dukungan runtime dan perubahan pada spesifikasi ECMA. Oleh karena itu, fitur ini hanya akan diaktifkan ketika flag fitur yang sesuai diatur dalam corelib. Masalah terkait API yang tepat dapat ditelusuri di sini https://github.com/dotnet/runtime/issues/64165
Serangkaian perubahan pada aturan konteks aman kami yang diperlukan untuk memungkinkan bidang ref kecil dan ditargetkan. Aturan sudah memperhitungkan field ref yang ada dan diambil dari API. Perubahan hanya perlu berfokus pada dua aspek: bagaimana mereka dibuat dan bagaimana mereka ditetapkan kembali.
Pertama, aturan yang menetapkan nilai konteks ref-safe untuk bidang perlu diperbarui sebagai berikut untuk bidang ref:
Ekspresi dalam formulir
ref e.Fref-safe-context sebagai berikut:
- Jika
adalah bidang konteks ref-safe- adalah konteks aman . - Jika tidak, jika
adalah jenis referensi, konteks aman ref dari konteks pemanggil - Jika tidak, konteks ref-safe diambil dari konteks ref-safe dari
e.
Ini tidak mewakili perubahan aturan karena aturan selalu memperhitungkan status ref ada di dalam ref struct. Inilah cara status ref dalam Span<T> selalu berfungsi, dan aturan konsumsi memperhitungkan hal ini dengan benar. Perubahan di sini hanya bertujuan agar pengembang dapat mengakses bidang ref secara langsung dan memastikan mereka melakukannya sesuai dengan aturan yang sudah ada dan secara implisit diterapkan pada Span<T>.
Ini berarti bahwa bidang ref dapat dikembalikan sebagai ref dari ref struct tetapi bidang normal tidak dapat.
ref struct RS
{
ref int _refField;
int _field;
// Okay: this falls into bullet one above.
public ref int Prop1 => ref _refField;
// Error: This is bullet four above and the ref-safe-context of `this`
// in a `struct` is function-member.
public ref int Prop2 => ref _field;
}
Ini mungkin tampak seperti kesalahan pada pandangan pertama tetapi ini adalah titik desain yang disyaratkan. Sekali lagi, ini bukan aturan baru yang dibuat oleh proposal ini; sebaliknya, ini mengakui aturan yang sudah ada Span<T> yang berlaku hingga sekarang bahwa pengembang dapat mendeklarasikan status ref mereka sendiri.
Selanjutnya aturan untuk penetapan ulang ref perlu disesuaikan untuk keberadaan bidang ref. Skenario utama untuk penetapan ulang ref adalah konstruktor ref struct menyimpan parameter ref ke dalam bidang ref. Dukungan akan lebih umum tetapi ini adalah skenario inti. Untuk mendukung hal ini, aturan untuk penetapan ulang ref akan disesuaikan untuk memperhitungkan bidang ref sebagai berikut:
Aturan penetapan ulang ref
Operan kiri operator = ref harus berupa ekspresi yang mengikat ke variabel lokal ref, parameter ref (selain this), parameter keluar, atau bidang ref.
Untuk penetapan ulang ref dalam formulir
e1 = ref e2kedua hal berikut ini harus benar:
e2harus memiliki konteks ref-safe- setidaknya sebesar konteks ref-safe- darie1harus memiliki konteks aman yang sama dengan Note
Itu berarti konstruktor Span<T> yang diinginkan berfungsi tanpa anotasi tambahan:
readonly ref struct Span<T>
{
readonly ref T _field;
readonly int _length;
public Span(ref T value)
{
// Falls into the `x.e1 = ref e2` case, where `x` is the implicit `this`. The
// safe-context of `this` is *return-only* and ref-safe-context of `value` is
// *caller-context* hence this is legal.
_field = ref value;
_length = 1;
}
}
Perubahan pada aturan pengalihan referensi berarti parameter ref sekarang dapat keluar dari metode sebagai bidang ref dalam nilai ref struct. Seperti yang dibahas di bagian pertimbangan kompatibilitas , ini dapat mengubah aturan untuk API yang ada yang tidak pernah dimaksudkan agar parameter ref digunakan sebagai bidang ref. Aturan seumur hidup untuk parameter hanya didasarkan pada deklarasinya bukan pada penggunaannya. Semua parameter ref dan inmemiliki konteks aman-ref dari konteks penghubung dan karenanya sekarang dapat dikembalikan oleh ref atau bidang ref. Untuk mendukung API yang memiliki parameter ref yang dapat diabaikan atau tidak diabaikan, dan dengan demikian memulihkan semantik situs panggilan C# 10, bahasa ini akan memperkenalkan anotasi umur terbatas.
pengubah scoped
Kata kunci scoped akan digunakan untuk membatasi masa pakai nilai. Ini dapat diterapkan ke
| Parameter atau Lokal | konteks-aman-referensi | konteks aman |
|---|---|---|
Span<int> s |
anggota fungsi | konteks pemanggil |
scoped Span<int> s |
anggota fungsi | anggota fungsi |
ref Span<int> s |
konteks pemanggil | konteks pemanggil |
scoped ref Span<int> s |
anggota fungsi | konteks pemanggil |
Dalam hubungan ini, konteks ref-safe-context dari suatu nilai tidak dapat pernah lebih luas dari konteks aman .
Ini memungkinkan API di C# 11 untuk diannotasi sehingga mereka memiliki aturan yang sama dengan C# 10:
Span<int> CreateSpan(scoped ref int parameter)
{
// Just as with C# 10, the implementation of this method isn't relevant to callers.
}
Span<int> BadUseExamples(int parameter)
{
// Legal in C# 10 and legal in C# 11 due to scoped ref
return CreateSpan(ref parameter);
// Legal in C# 10 and legal in C# 11 due to scoped ref
int local = 42;
return CreateSpan(ref local);
// Legal in C# 10 and legal in C# 11 due to scoped ref
Span<int> span = stackalloc int[42];
return CreateSpan(ref span[0]);
}
Anotasi scoped juga berarti bahwa parameter this dari struct sekarang dapat didefinisikan sebagai scoped ref T. Sebelumnya harus dijadikan kasus khusus dalam aturan sebagai parameter
Anotasi scoped juga dapat diterapkan ke lokasi berikut:
- lokal: Anotasi ini menetapkan masa pakai sebagai konteks aman, atau konteks aman-referensi jika terjadi
reflokal, ke anggota fungsi terlepas dari umur awal inisialisasi.
Span<int> ScopedLocalExamples()
{
// Error: `span` has a safe-context of *function-member*. That is true even though the
// initializer has a safe-context of *caller-context*. The annotation overrides the
// initializer
scoped Span<int> span = default;
return span;
// Okay: the initializer has safe-context of *caller-context* hence so does `span2`
// and the return is legal.
Span<int> span2 = default;
return span2;
// The declarations of `span3` and `span4` are functionally identical because the
// initializer has a safe-context of *function-member* meaning the `scoped` annotation
// is effectively implied on `span3`
Span<int> span3 = stackalloc int[42];
scoped Span<int> span4 = stackalloc int[42];
}
Penggunaan lain untuk scoped pada penduduk setempat yang dibahas di bawah .
Anotasi scoped tidak dapat diterapkan ke lokasi lain termasuk nilai pengembalian, bidang, elemen array, dll ... Sementara itu, scoped berdampak ketika diterapkan ke ref, in, atau out, tetapi berdampak hanya ketika diterapkan pada nilai yang bersifat ref struct. Memiliki deklarasi seperti scoped int tidak berdampak karena non ref struct selalu aman untuk kembali. Pengkompilasi akan membuat diagnostik untuk kasus tersebut untuk menghindari kebingungan pengembang.
Mengubah perilaku parameter out
Untuk lebih membatasi dampak perubahan kompat dalam membuat parameter out secara efektif akan secara implisit scoped out di masa mendatang. Dari perspektif kompatibilitas, ini berarti mereka tidak dapat dikembalikan oleh ref:
ref int Sneaky(out int i)
{
i = 42;
// Error: ref-safe-context of out is now function-member
return ref i;
}
Ini akan meningkatkan fleksibilitas API yang mengembalikan nilai ref struct dan memiliki parameter out karena tidak perlu mempertimbangkan parameter yang ditangkap oleh referensi lagi. Ini penting karena ini adalah pola umum dalam API gaya pembaca:
Span<byte> Read(Span<byte> buffer, out int read)
{
// ..
}
Span<byte> Use()
{
var buffer = new byte[256];
// If we keep current `out` ref-safe-context this is an error. The language must consider
// the `read` parameter as returnable as a `ref` field
//
// If we change `out` ref-safe-context this is legal. The language does not consider the
// `read` parameter to be returnable hence this is safe
int read;
return Read(buffer, out read);
}
Bahasa ini juga tidak akan lagi menganggap argumen yang diteruskan ke parameter out dapat dikembalikan. Memperlakukan input ke parameter out sebagai dapat dikembalikan sangat membingungkan bagi para pengembang. Pada dasarnya ini mengubah maksud out dengan memaksa pengembang untuk mempertimbangkan nilai dari pemanggil yang hanya digunakan dalam bahasa yang tidak menghormati out. Ke depan bahasa yang mendukung ref struct harus memastikan nilai asli yang diteruskan ke parameter out tidak pernah dibaca.
C# mencapai ini melalui aturan penugasan pasti. Keduanya mencapai aturan konteks aman ref kami serta memungkinkan kode yang ada yang menetapkan dan kemudian mengembalikan nilai parameter out.
Span<int> StrangeButLegal(out Span<int> span)
{
span = default;
return span;
}
Bersama-sama, perubahan ini berarti argumen ke parameter out tidak memberikan nilai konteks aman atau konteks aman ref ke pemanggilan metode. Ini secara signifikan mengurangi dampak kompatibilitas secara keseluruhan dari bidang ref serta menyederhanakan bagaimana pengembang memikirkan tentang out. Argumen ke parameter out tidak berkontribusi pada pengembalian, itu hanyalah output.
Menyimpulkan konteks aman dari ekspresi deklarasi
- konteks pemanggil
- jika variabel 'out' ditandai
scoped, maka blok deklarasi (yaitu anggota fungsi atau yang lebih sempit). - jika tipe variabel 'out' adalah
ref struct, pertimbangkan semua argumen pada pemanggilan yang menyertakan, termasuk penerima:-
konteks aman dari argumen mana pun di mana parameter yang sesuai tidak
outdan memiliki konteks aman dalam pengembalian saja atau lebih luas. - konteks ref-safe dari argumen apa pun di mana parameter yang sesuai memiliki ref-safe-context return-only atau lebih luas
-
konteks aman dari argumen mana pun di mana parameter yang sesuai tidak
Lihat juga
Parameter scoped secara implisit
Secara keseluruhan ada dua lokasi ref yang secara implisit dinyatakan sebagai scoped:
-
thispada metode instansstruct -
outparameter
Aturan konteks aman ref akan ditulis berdasarkan scoped ref dan ref. Untuk tujuan konteks aman ref, parameter in setara dengan ref dan out setara dengan scoped ref. Baik in maupun out hanya akan dipanggil secara khusus ketika penting untuk semantik aturan. Jika tidak, mereka hanya dianggap ref dan scoped ref masing-masing.
Saat membahas konteks aman ref in hanya akan dipanggil di sini ketika penting untuk semantik aturan saat ini.
Konteks aman hanya untuk pengembalian
Desain ini juga mengharuskan pengenalan konteks aman baru: yang hanya berfungsi untuk "return". Ini mirip dengan konteks pemanggil
Detail return harus setidaknya return-only . Dengan demikian, sebagian besar aturan yang ada tidak berlaku. Misalnya, penugasan ke dalam parameter ref dari ekspresi dengan konteks aman dari hanya return akan gagal karena lebih kecil dari ref parameter yang merupakan konteks pemanggil. Kebutuhan akan konteks pelarian baru ini akan dibahas di bawah.
Ada tiga lokasi yang secara default hanya untuk pengembalian :
- Parameter
atau akan memiliki konteks aman ref denganhanya-kembali . Ini dilakukan sebagian untuk ref structdalam rangka mencegah masalah penugasan siklik yang tidak masuk akal . Ini dilakukan secara seragam meskipun demikian untuk menyederhanakan model serta meminimalkan perubahan kompatibilitas. - Parameter
untuk akan memiliki konteks aman return-only . Ini memungkinkan pengembalian dan outsama ekspresifnya. Ini tidak memiliki masalah penugasan siklik yang konyol karenaoutsecara implisit adalahscopedsehingga konteks aman-ref masih lebih kecil dari konteks aman . - Parameter
untuk konstruktor akan memiliki konteks-aman darireturn-only. Ini terjadi karena dimodelkan menggunakan parameter out.
Ekspresi atau pernyataan apa pun yang secara eksplisit menghasilkan nilai dari suatu metode atau lambda harus memiliki konteks aman , dan jika berlaku, konteks ref-aman , yang setidaknya return-only. Itu termasuk pernyataan return, anggota bertubuh ekspresi, dan ekspresi lambda.
Demikian juga, setiap penugasan ke
Catatan: Ekspresi yang jenisnya bukan ref struct selalu memiliki konteks aman dari konteks pemanggil.
Aturan untuk pemanggilan metode
Aturan konteks aman ref untuk pemanggilan metode akan diperbarui dalam beberapa cara. Yang pertama adalah dengan mengenali dampak yang scoped miliki pada argumen. Untuk argumen tertentu expr yang diteruskan ke parameter p:
- Jika
pscoped refmakaexprtidak berkontribusi konteks ref-safe-context saat mempertimbangkan argumen.- Jika
pscopedmakaexprtidak berkontribusi konteks aman saat mempertimbangkan argumen.- Jika
maka tidak berkontribusi konteks aman ref atau konteks aman detail selengkapnya
Bahasa "tidak berkontribusi" berarti argumen tidak dipertimbangkan saat menghitung nilai konteks aman ref atau nilai konteks aman dari pengembalian metode masing-masing. Itu karena nilai tidak dapat berkontribusi pada masa pakai tersebut karena anotasi scoped mencegahnya.
Sekarang aturan pemanggilan metode dapat disederhanakan. Penerima tidak perlu lagi diperlakukan secara khusus, pada kasus struct sekarang sekadar scoped ref T. Aturan nilai perlu diubah untuk memperhitungkan pengembalian bidang ref:
Pemanggilan metode
e1.M(e2, ...)menghasilkan nilai, di manaM()tidak mengembalikan ref-to-ref-struct, memiliki konteks aman yang diambil dari yang paling sempit dari yang berikut ini:
- konteks pemanggil
- Saat pengembalian adalah
konteks aman dikontribusikan oleh semua ekspresi argumen - Ketika pengembalian adalah
ref struct, konteks ref-safe- yang dikontribusikan oleh semua argumenrefJika
M()mengembalikan ref-to-ref-struct, konteks aman sama dengan konteks aman dari semua argumen yang merupakan ref-to-ref-struct. Ini adalah kesalahan jika ada beberapa argumen dengan konteks amanyang berbeda karena argumen metode harus cocok dengan .
Aturan panggilan ref dapat disederhanakan ke:
Nilai yang dihasilkan dari pemanggilan metode
ref e1.M(e2, ...), di manaM()tidak mengembalikan ref-to-ref-struct, adalah ref-safe-context dalam konteks tersipit berikut:
- konteks pemanggil
- Konteks aman berasal dari semua ekspresi argumen.
- konteks ref-safe-context dikontribusikan oleh semua argumen
refJika
mengembalikan ref-to-ref-struct, ref-safe-context adalah konteks ref-safe- tersempit yang disumbangkan oleh semua argumen yang merupakan ref-to-ref-struct.
Aturan ini sekarang memungkinkan kita menentukan dua varian metode yang diinginkan:
Span<int> CreateWithoutCapture(scoped ref int value)
{
// Error: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
// of the ref argument. That is the *function-member* for value hence this is not allowed.
return new Span<int>(ref value);
}
Span<int> CreateAndCapture(ref int value)
{
// Okay: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
// of the ref argument. That is the *caller-context* for value hence this is not allowed.
return new Span<int>(ref value);
}
Span<int> ComplexScopedRefExample(scoped ref Span<int> span)
{
// Okay: the safe-context of `span` is *caller-context* hence this is legal.
return span;
// Okay: the local `refLocal` has a ref-safe-context of *function-member* and a
// safe-context of *caller-context*. In the call below it is passed to a
// parameter that is `scoped ref` which means it does not contribute
// ref-safe-context. It only contributes its safe-context hence the returned
// rvalue ends up as safe-context of *caller-context*
Span<int> local = default;
ref Span<int> refLocal = ref local;
return ComplexScopedRefExample(ref refLocal);
// Error: similar analysis as above but the safe-context of `stackLocal` is
// *function-member* hence this is illegal
Span<int> stackLocal = stackalloc int[42];
return ComplexScopedRefExample(ref stackLocal);
}
Aturan untuk penginisialisasi objek
konteks aman dari ekspresi penginisialisasi objek adalah yang paling sempit di antara:
- Konteks aman dari panggilan konstruktor.
- Konteks aman dan ref-konteks aman dari argumen untuk pengindeks inisialisasi anggota yang bisa mengalir ke penerima.
- konteks aman dari RHS penugasan pada penginisialisasi anggota ke setter non-readonly atau konteks aman-referensi jika terjadi penugasan referensi.
Cara lain untuk memodelkan ini adalah dengan menganggap bahwa setiap argumen yang ditujukan kepada penginisialisasi anggota, yang dapat ditugaskan kepada penerima, sebagai argumen untuk konstruktor. Ini karena penginisialisasi anggota secara efektif merupakan panggilan konstruktor.
Span<int> heapSpan = default;
Span<int> stackSpan = stackalloc int[42];
var x = new S(ref heapSpan)
{
Field = stackSpan;
}
// Can be modeled as
var x = new S(ref heapSpan, stackSpan);
Pemodelan ini penting karena menunjukkan bahwa MAMM kami perlu memperhitungkan inisialisasi anggota secara khusus. Pertimbangkan bahwa kasus khusus ini harus dianggap ilegal karena memungkinkan nilai dengan konteks aman yang lebih sempit ditetapkan ke yang lebih tinggi.
Argumen metode harus cocok
Kehadiran bidang ref berarti aturan di sekitar argumen metode harus cocok perlu diperbarui karena parameter ref sekarang dapat disimpan sebagai bidang dalam argumen ref struct ke metode . Sebelumnya, aturan hanya perlu mempertimbangkan ref struct lain yang disimpan sebagai kolom. Dampaknya dibahas dalam pertimbangan kompatibilitas. Aturan baru adalah ...
e.M(a1, a2, ... aN)untuk pemanggilan metode apa pun
- Hitung konteks aman tersempit dari:
- konteks pemanggil
- konteks aman dari semua argumen
- konteks ref-safe- dari semua argumen ref yang parameter terkaitnya memiliki konteks ref-safe- dari konteks pemanggil
- Semua argumen
refdari jenisref structharus dapat ditetapkan oleh nilai dengan konteks aman tersebut. Ini adalah kasus di manareftidak tidak menggeneralisasi untuk menyertakanindanout
e.M(a1, a2, ... aN)untuk pemanggilan metode apa pun
- Hitung konteks aman tersempit dari:
- konteks pemanggil
- konteks aman dari semua argumen
- konteks ref-safe dari semua argumen ref yang parameter terkaitnya tidak
scoped- Semua argumen
outdari jenisref structharus dapat ditetapkan oleh nilai dengan konteks aman tersebut.
Kehadiran scoped memungkinkan pengembang untuk mengurangi gesekan yang dibuat aturan ini dengan menandai parameter yang tidak dikembalikan sebagai scoped. Ini menghapus argumen mereka dari (1) dalam kedua kasus di atas dan memberikan fleksibilitas yang lebih besar kepada pemanggil.
Dampak perubahan ini dibahas lebih dalam di bawah . Secara keseluruhan, ini akan memungkinkan pengembang untuk membuat lokasi pemanggilan lebih fleksibel dengan menganotasi nilai jenis ref yang tidak membocorkan konteks dengan scoped.
Variansi cakupan parameter
Pengubah scoped dan atribut [UnscopedRef] (lihat di bawah) pada parameter juga berdampak pada penimpaan objek, implementasi antarmuka, dan aturan konversi delegate kami. Tanda tangan untuk penimpaan, implementasi antarmuka, atau konversi delegate dapat:
- Menambahkan
scopedke parameterrefatauin - Menambahkan
scopedke parameterref struct - Menghapus
[UnscopedRef]dari parameterout - Menghapus
[UnscopedRef]dari parameterrefdari jenisref struct
Perbedaan lain sehubungan dengan scoped atau [UnscopedRef] dianggap tidak cocok.
Pengkompilasi akan melaporkan diagnostik untuk ketidakcocokan cakupan yang tidak aman di seluruh penimpaan, implementasi antarmuka, dan mendelegasikan konversi saat:
- Metode ini memiliki parameter
refatauoutjenisref structdengan ketidakcocokan dalam menambahkan[UnscopedRef](tidak menghapusscoped). (Dalam hal ini, penugasan siklik konyol dimungkinkan, sehingga tidak ada parameter lain yang diperlukan.) - Atau kedua hal ini benar:
- Metode mengembalikan
ref structatau mengembalikanrefatauref readonly, atau metode memiliki parameterrefatauoutdari jenisref struct. - Metode ini memiliki setidaknya satu parameter
ref,in, atauouttambahan, atau parameter jenisref struct.
- Metode mengembalikan
Diagnostik tidak dilaporkan dalam kasus lain karena:
- Metode dengan tanda tangan seperti itu tidak dapat menangkap referensi yang diteruskan, sehingga ketidakcocokan cakupan tidak menjadi masalah.
- Ini termasuk skenario yang sangat umum dan sederhana (misalnya, parameter
outyang sudah seperti biasanya digunakan dalam tanda tangan metodeTryParse), dan melaporkan ketidakcocokan cakupan hanya karena digunakan dalam versi bahasa 11 (sehingga parameteroutmemiliki cakupan yang berbeda) dapat menyebabkan kebingungan.
Diagnostik dilaporkan sebagai kesalahan jika tanda tangan yang tidak cocok keduanya menggunakan aturan konteks aman C#11 ref; jika tidak, diagnostiknya adalah peringatan .
Peringatan ketidakcocokan cakupan dapat dilaporkan pada modul yang dikompilasi dengan aturan konteks aman 'ref' C#7.2 di mana scoped tidak tersedia. Dalam beberapa kasus seperti itu, mungkin perlu untuk menekan peringatan jika tanda tangan lain yang tidak cocok tidak dapat dimodifikasi.
Pengubah scoped dan atribut [UnscopedRef] juga memiliki efek berikut pada tanda tangan metode:
- Pengubah
scopeddan atribut[UnscopedRef]tidak memengaruhi persembunyian - Kelebihan beban tidak dapat berbeda hanya pada
scopedatau[UnscopedRef]
Bagian tentang bidang ref dan scoped panjang sehingga ingin ditutup dengan ringkasan singkat tentang perubahan pemecahan yang diusulkan:
- Nilai yang memiliki konteks ref-safe-
ke konteks pemanggil dapat dikembalikan oleh bidang atau . - Parameter
outakan memiliki konteks aman dari anggota fungsi.
Catatan Terperinci:
- Bidang
refhanya dapat dideklarasikan di dalamref struct - Bidang
reftidak dapat dideklarasikanstatic,volatile, atauconst - Bidang
reftidak boleh memiliki tipe yangref struct - Proses pembuatan perakitan referensi harus mempertahankan keberadaan bidang
refdi dalamref struct -
readonly ref structharus mendeklarasikan bidangrefsebagaireadonly ref - Untuk nilai by-ref, pengubah
scopedharus muncul sebelumin,out, atauref - Dokumen aturan keamanan rentang akan diperbarui seperti yang diuraikan dalam dokumen ini
- Aturan konteks aman ref baru akan berlaku ketika salah satu dari dua kondisi terjadi.
- Perpustakaan inti berisi penanda fitur yang menunjukkan dukungan untuk bidang
ref - Nilai
langversionadalah 11 atau lebih tinggi
- Perpustakaan inti berisi penanda fitur yang menunjukkan dukungan untuk bidang
Sintaksis
13.6.2 Deklarasi variabel lokal: ditambahkan 'scoped'?.
local_variable_declaration
: 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
;
local_variable_mode_modifier
: 'ref' 'readonly'?
;
13.9.4 Pernyataan for: menambahkan 'scoped'?secara tidak langsung dari local_variable_declaration.
13.9.5 Pernyataan foreach: ditambahkan 'scoped'?.
foreach_statement
: 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
embedded_statement
;
12.6.2 Daftar argumen: menambahkan 'scoped'? untuk variabel deklarasi out.
argument_value
: expression
| 'in' variable_reference
| 'ref' variable_reference
| 'out' ('scoped'? local_variable_type)? identifier
;
[TBD]
parameter Metode 15.6.2: menambahkan 'scoped'? ke parameter_modifier.
fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;
parameter_modifier
| 'this' 'scoped'? parameter_mode_modifier?
| 'scoped' parameter_mode_modifier?
| parameter_mode_modifier
;
parameter_mode_modifier
: 'in'
| 'ref'
| 'out'
;
20.2 Deklarasi delegasi: menambahkan 'scoped'?secara tidak langsung dari fixed_parameter.
12.19 Ekspresi fungsi anonim: ditambahkan 'scoped'?.
explicit_anonymous_function_parameter
: 'scoped'? anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'in'
| 'ref'
| 'out'
;
Jenis terbatas matahari terbenam
Pengkompilasi memiliki konsep sekumpulan "jenis terbatas" yang sebagian besar tidak terdokumentasi. Jenis-jenis ini diberi status khusus karena dalam C# 1.0 tidak ada cara tujuan umum untuk mengekspresikan perilaku mereka. Fakta yang paling menonjol adalah bahwa tipe-tipe tersebut dapat mengandung referensi ke tumpukan eksekusi. Sebaliknya, kompilator memiliki pengetahuan khusus tentang mereka dan membatasi penggunaannya dengan cara yang akan selalu aman: pengembalian yang tidak diizinkan, tidak dapat digunakan sebagai elemen array, tidak dapat digunakan dalam generik, dll ...
Setelah bidang ref tersedia dan diperluas untuk mendukung ref struct jenis ini dapat ditentukan dengan benar di C# menggunakan kombinasi bidang ref struct dan ref. Oleh karena itu, ketika kompilator mendeteksi bahwa runtime mendukung bidang ref, itu tidak akan lagi memiliki gagasan tentang jenis yang terbatas. Sebagai gantinya, ini akan menggunakan jenis seperti yang ditentukan dalam kode.
Untuk mendukung ini, aturan konteks aman ref kami akan diperbarui sebagai berikut:
-
__makerefakan diperlakukan sebagai metode dengan tanda tanganstatic TypedReference __makeref<T>(ref T value) -
__refvalueakan diperlakukan sebagai metode dengan signaturestatic ref T __refvalue<T>(TypedReference tr). Ekspresi__refvalue(tr, int)akan secara efektif menggunakan argumen kedua sebagai parameter jenis. -
__arglistsebagai parameter akan memiliki konteks ref-safe dan konteks aman dari anggota fungsi . -
__arglist(...)sebagai ekspresi akan memiliki konteks ref-safe dan konteks aman dari anggota fungsi.
Runtime yang sesuai akan memastikan bahwa TypedReference, RuntimeArgumentHandle, dan ArgIterator didefinisikan sebagai ref struct. Lebih lanjut TypedReference harus dilihat memiliki bidang ref ke ref struct untuk jenis apa pun yang mungkin (dapat menyimpan nilai apa pun). Ketentuan tersebut digabungkan dengan aturan di atas akan memastikan referensi ke stapel tidak melebihi masa pakainya.
Catatan: secara teknis, ini merupakan detail implementasi pengompiler dibandingkan dengan bagian dari bahasa. Tetapi mengingat hubungan dengan bidang ref itu dimasukkan dalam proposal bahasa untuk kesederhanaan.
Sediakan tanpa batasan
Salah satu titik gesekan yang paling terkenal adalah ketidakmampuan untuk mengembalikan bidang dengan ref dalam contoh anggota struct. Ini berarti pengembang tidak dapat membuat metode/properti yang mengembalikan ref dan harus terpaksa mengekspos field secara langsung. Ini mengurangi kegunaan ref dalam konteks struct, di mana sering kali paling diinginkan.
struct S
{
int _field;
// Error: this, and hence _field, can't return by ref
public ref int Prop => ref _field;
}
alasan
Untuk memperbaikinya, bahasa ini akan memberikan kebalikan dari anotasi masa hidup scoped dengan menyediakan dukungan untuk UnscopedRefAttribute. Ini dapat diterapkan pada
| UnscopedRef diterapkan ke | Original konteks aman-referensi | Konteks aman baru |
|---|---|---|
| anggota instance | anggota fungsi | hanya pengembalian |
parameter in / ref |
hanya pengembalian | konteks pemanggil |
parameter out |
anggota fungsi | hanya pengembalian |
Saat menerapkan [UnscopedRef] ke metode instans struct itu memiliki dampak memodifikasi parameter this implisit. Ini berarti this bertindak sebagai ref tanpa anotasi dengan jenis yang sama.
struct S
{
int field;
// Error: `field` has the ref-safe-context of `this` which is *function-member* because
// it is a `scoped ref`
ref int Prop1 => ref field;
// Okay: `field` has the ref-safe-context of `this` which is *caller-context* because
// it is a `ref`
[UnscopedRef] ref int Prop1 => ref field;
}
Anotasi juga dapat ditempatkan pada parameter out untuk memulihkannya ke perilaku C# 10.
ref int SneakyOut([UnscopedRef] out int i)
{
i = 42;
return ref i;
}
Untuk tujuan aturan konteks ref yang aman, [UnscopedRef] out semacam itu dianggap hanya sebagai ref. Mirip dengan bagaimana in dianggap ref dalam konteks masa hidup.
Anotasi [UnscopedRef] tidak akan diizinkan pada anggota init dan konstruktor yang ada di dalam struct. Anggota tersebut sudah istimewa dalam kaitannya dengan semantik ref karena mereka memandang anggota readonly sebagai bisa berubah. Ini berarti membawa ref kepada anggota tersebut tampak seperti refsederhana, bukan ref readonly. Ini diizinkan dalam konteks konstruktor dan init. Mengizinkan [UnscopedRef] akan memungkinkan ref tersebut keluar secara tidak benar dari konstruktor dan memungkinkan terjadinya mutasi setelah semantik readonly berlangsung.
Jenis atribut akan memiliki definisi berikut:
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(
AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class UnscopedRefAttribute : Attribute
{
}
}
Catatan Terperinci:
- Metode instans atau properti yang diannotasikan dengan
[UnscopedRef]memiliki konteks aman ref darithisyang diatur ke konteks pemanggil . - Anggota yang dianotasi dengan
[UnscopedRef]tidak dapat menerapkan antarmuka. - Ini adalah kesalahan jika menggunakan
[UnscopedRef]pada- Anggota yang tidak dideklarasikan pada
struct - Anggota
static, anggotainit, atau konstruktor padastruct - Parameter yang ditandai
scoped - Parameter yang diteruskan berdasarkan nilai
- Parameter yang diteruskan melalui referensi yang tidak dicakup secara implisit
- Anggota yang tidak dideklarasikan pada
ScopedRefAttribute
Anotasi scoped akan dipancarkan ke dalam metadata melalui atribut jenis System.Runtime.CompilerServices.ScopedRefAttribute. Atribut akan dicocokkan dengan nama yang memenuhi syarat namespace sehingga definisi tidak perlu muncul di rakitan tertentu.
Jenis ScopedRefAttribute hanya untuk penggunaan pengkompilasi - tidak diizinkan dalam sumber. Deklarasi tipe data akan disintesis oleh kompiler jika belum disertakan dalam kompilasi.
Jenisnya akan memiliki definisi berikut:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class ScopedRefAttribute : Attribute
{
}
}
Pengkompilasi akan memancarkan atribut ini pada parameter dengan sintaks scoped. Ini hanya akan dipancarkan ketika sintaks menyebabkan nilai berbeda dari status defaultnya. Misalnya, scoped out tidak akan menyebabkan atribut dikeluarkan.
RefSafetyRulesAttribute
Ada beberapa perbedaan dalam konteks ref aman aturan aman antara C#7.2 dan C#11. Salah satu dari perbedaan ini dapat mengakibatkan perubahan yang merusak ketika mengkompilasi ulang dengan C#11 terhadap referensi yang dibuat dengan C#10 atau yang lebih lama.
- parameter
ref/in/outyang tidak terlingkup dapat lolos dari pemanggilan metode sebagai bidangrefref structdi C#11, bukan di C#7.2 - Parameter
outdilingkup secara implisit dalam C#11, dan tidak dilibatkan dalam C#7.2. -
ref/inparameter untuk jenisref structsecara implisit memiliki ruang lingkup dalam C#11, dan tidak memiliki ruang lingkup dalam C#7.2
Untuk mengurangi kemungkinan perubahan signifikan saat kompilasi ulang dengan C#11, kami akan memperbarui kompilator C#11 untuk menggunakan aturan konteks aman ref untuk pemanggilan metode yang sesuai dengan aturan yang digunakan untuk menganalisis deklarasi metode. Pada dasarnya, ketika menganalisis panggilan ke metode yang dikompilasi dengan kompilator yang lebih lama, pengkompilasi C#11 akan menggunakan aturan konteks aman ref C#7.2.
Untuk mengaktifkan ini, pengkompilasi akan memancarkan atribut [module: RefSafetyRules(11)] baru saat modul dikompilasi dengan -langversion:11 atau lebih tinggi atau dikompilasi dengan korlib yang berisi bendera fitur untuk bidang ref.
Argumen untuk atribut menunjukkan versi bahasa dari aturan ref konteks aman yang digunakan saat modul dikompilasi.
Versi saat ini tetap pada 11 terlepas dari versi bahasa aktual yang dikompilasi.
Harapannya adalah bahwa versi kompilator di masa mendatang akan memperbarui aturan konteks aman ref dan memancarkan atribut dengan versi yang berbeda.
Jika pengkompilasi memuat modul yang menyertakan [module: RefSafetyRules(version)]dengan version selain 11, pengkompilasi akan melaporkan kesalahan untuk versi yang tidak dikenali jika ada panggilan ke metode yang dideklarasikan dalam modul tersebut.
Saat pengkompilasi C#11 menganalisis panggilan metode:
- Jika modul yang berisi deklarasi metode mencakup
[module: RefSafetyRules(version)], di manaversionadalah11, panggilan metode dianalisis dengan aturan C#11. - Jika modul yang berisi deklarasi metode berasal dari sumber, dan dikompilasi dengan
-langversion:11atau dengan corlib yang berisi bendera fitur untuk bidangref, panggilan metode dianalisis dengan aturan C#11. - Jika modul yang berisi deklarasi metode mereferensikan
System.Runtime { ver: 7.0 }, panggilan metode dianalisis dengan aturan C#11. Aturan ini merupakan mitigasi sementara untuk modul yang telah dikompilasi menggunakan pratinjau awal C#11 / .NET 7 dan akan dihapus kemudian. - Jika tidak, panggilan metode dianalisis dengan aturan C#7.2.
Pengkompilasi pra-C#11 akan mengabaikan RefSafetyRulesAttribute dan menganalisis panggilan metode hanya dengan aturan C#7.2.
RefSafetyRulesAttribute akan dicocokkan dengan nama yang memenuhi syarat namespace sehingga definisi tidak perlu muncul dalam rakitan tertentu.
Jenis RefSafetyRulesAttribute hanya untuk penggunaan pengkompilasi - tidak diizinkan dalam sumber. Deklarasi tipe data akan disintesis oleh kompiler jika belum disertakan dalam kompilasi.
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public RefSafetyRulesAttribute(int version) { Version = version; }
public readonly int Version;
}
}
Buffer ukuran tetap aman
Buffer berukuran tetap yang aman tidak disertakan dalam C# 11. Fitur ini dapat diimplementasikan dalam versi C#yang akan datang.
Bahasa ini akan melonggarkan pembatasan pada array berukuran tetap sehingga dapat dinyatakan dalam kode aman dan jenis elemen dapat dikelola atau tidak dikelola. Ini akan membuat jenis seperti berikut ini legal:
internal struct CharBuffer
{
internal char Data[128];
}
Deklarasi ini, sama seperti mitra unsafe mereka, akan menentukan urutan elemen N dalam tipe yang mengandung. Anggota ini dapat diakses dengan pengindeks dan juga dapat dikonversi ke instans Span<T> dan ReadOnlySpan<T>.
Saat mengindeks buffer jenis fixedT, status readonly dari kontainer harus diperhitungkan. Jika kontainer readonly maka pengindeks mengembalikan ref readonly T jika tidak, kontainer mengembalikan ref T.
Mengakses buffer fixed tanpa pengindeks tidak memiliki jenis alami namun dapat dikonversi ke jenis Span<T>. Jika kontainer readonly buffer secara implisit dapat dikonversi ke ReadOnlySpan<T>, jika tidak, kontainer dapat secara implisit dikonversi ke Span<T> atau ReadOnlySpan<T> (konversi Span<T> dianggap lebih baik).
Instans Span<T> yang dihasilkan akan memiliki panjang yang sama dengan ukuran yang dideklarasikan pada buffer fixed.
konteks aman dari nilai yang dikembalikan akan sama dengan konteks aman dari kontainer, sama seperti jika data cadangan diakses sebagai bidang.
Untuk setiap deklarasi fixed dalam tipe di mana tipe elemennya adalah T, bahasa akan menghasilkan metode pengindeks get hanya yang sesuai dengan jenis pengembalian ref T. Pengindeks akan diannotasikan dengan atribut [UnscopedRef] karena implementasi akan mengembalikan bidang dari jenis deklarasikan. Aksesibilitas anggota akan sesuai dengan aksesibilitas pada kolom fixed.
Misalnya, tanda tangan pengindeks untuk CharBuffer.Data adalah sebagai berikut:
[UnscopedRef] internal ref char DataIndexer(int index) => ...;
Jika indeks yang disediakan berada di luar batas array fixed yang dideklarasikan, maka IndexOutOfRangeException akan dilemparkan. Dalam hal nilai konstanta disediakan maka akan diganti dengan referensi langsung ke elemen yang sesuai. Kecuali konstanta berada di luar batas yang dideklarasikan dalam hal ini kesalahan waktu kompilasi akan terjadi.
Juga akan ada aksesor bernama yang dihasilkan untuk setiap buffer fixed yang disediakan oleh get nilai dan operasi set. Memiliki ini berarti bahwa buffer fixed akan lebih mirip dengan semantik array yang ada dengan memiliki aksesor ref serta operasi byval get dan set. Ini berarti kompilator akan memiliki fleksibilitas yang sama saat memancarkan kode yang mengkonsumsi buffer fixed seperti yang mereka lakukan saat mengkonsumsi array. Ini akan membuat operasi seperti await melalui buffer fixed lebih mudah dipancarkan.
Ini juga memiliki manfaat tambahan yang akan membuat buffer fixed lebih mudah dikonsumsi dari bahasa lain. Pengindeks bernama adalah fitur yang telah ada sejak versi 1.0 dari .NET. Bahkan bahasa yang tidak dapat langsung mengimplementasikan indeks bergelar umumnya dapat menggunakannya (C# sebenarnya adalah contoh yang baik dari ini).
Penyimpanan cadangan untuk buffer akan dibuat menggunakan atribut [InlineArray]. Ini adalah mekanisme yang dibahas dalam masalah 12320 yang secara khusus memungkinkan untuk kasus mendeklarasikan sebuah urutan bidang dengan jenis yang sama secara efisien. Masalah khusus ini masih dalam diskusi aktif dan harapannya adalah bahwa implementasi fitur ini akan mengikuti tergantung pada bagaimana jalannya diskusi tersebut.
Penginisialisasi dengan nilai ref dalam ekspresi new dan with
Di bagian penginisialisasi objek 12.8.17.3, kami memperbarui tata bahasa ke:
initializer_value
: 'ref' expression // added
| expression
| object_or_collection_initializer
;
Di bagian untuk ekspresi with, kami memperbarui tata bahasa menjadi:
member_initializer
: identifier '=' 'ref' expression // added
| identifier '=' expression
;
Operand kiri dari penetapan harus berupa ekspresi yang mengikat ke field 'ref'.
Operand kanan harus berupa ekspresi yang menghasilkan lvalue yang menunjuk nilai dengan jenis yang sama dengan operand kiri.
Kami menambahkan aturan serupa ke ref local reassignment:
Jika operand kiri adalah ref yang dapat ditulis (yaitu menunjuk apa pun selain bidang ref readonly), maka operan kanan harus berupa lvalue yang dapat ditulis.
Aturan escape untuk pemanggilan konstruktor tetap:
Ekspresi
newyang memanggil konstruktor mematuhi aturan yang sama dengan pemanggilan metode yang dianggap mengembalikan jenis yang sedang dikonstruksi.
Yaitu aturan pemanggilan metode yang diperbarui sebelumnya:
Rvalue yang dihasilkan dari pemanggilan metode
e1.M(e2, ...)memiliki konteks aman yang berasal dari konteks terkecil berikut:
- konteks pemanggil
- Konteks aman berasal dari semua ekspresi argumen.
- Ketika pengembalian adalah
maka konteks ref-safe- dikontribusikan oleh semua argumen
Untuk ekspresi new dengan penginisialisasi, ekspresi penginisialisasi dihitung sebagai argumen (mereka berkontribusi pada konteks aman) dan ekspresi penginisialisasi ref dihitung sebagai argumen ref (mereka menyumbangkan konteks aman referensi mereka), secara rekursif.
Perubahan dalam konteks yang tidak aman
Jenis penunjuk (bagian 23.3) diperluas untuk memungkinkan jenis yang dikelola sebagai jenis referensi.
Jenis pointer tersebut ditulis sebagai jenis terkelola diikuti dengan token *. Mereka mengeluarkan peringatan.
Operator alamat (bagian 23.6.5) diperlonggar agar dapat menerima variabel dengan tipe terkelola sebagai operannya.
Pernyataan fixed (bagian 23.7) dilonggarkan untuk menerima fixed_pointer_initializer yang merupakan alamat dari sebuah variabel dengan jenis dikelola T atau merupakan sebuah ekspresi dari array_type dengan elemen-elemen berjenis dikelola T.
Penginisialisasi alokasi tumpukan (bagian 12.8.22) juga dilonggarkan.
Pertimbangan
Ada pertimbangan bagian lain dari tumpukan pengembangan yang harus dipertimbangkan saat mengevaluasi fitur ini.
Pertimbangan Kompatibilitas
Tantangan dalam proposal ini adalah implikasi kompatibilitas yang ditimbulkan oleh desain ini terhadap aturan keselamatan rentang yang ada, atau §9.7.2. Meskipun aturan tersebut sepenuhnya mendukung konsep ref struct yang memiliki bidang ref, aturan tersebut tidak mengizinkan API, selain stackalloc, untuk menangkap status ref yang mengacu pada tumpukan. Aturan konteks aman ref memiliki asumsi keras , atau §16.4.12.8 bahwa konstruktor dengan bentuk Span(ref T value) tidak ada. Ini berarti aturan keamanan tidak mempertimbangkan kemungkinan parameter ref dapat lolos sebagai bidang ref, sehingga memungkinkan kode seperti berikut.
Span<int> CreateSpanOfInt()
{
// This is legal according to the 7.2 span rules because they do not account
// for a constructor in the form Span(ref T value) existing.
int local = 42;
return new Span<int>(ref local);
}
Secara efektif ada tiga cara bagi parameter ref untuk melarikan diri dari pemanggilan metode:
- Menurut pengembalian nilai
- Dengan pengembalian
ref - Dengan bidang
refdiref structyang dikembalikan atau diteruskan sebagai parameterref/out
Aturan yang ada hanya memperhitungkan (1) dan (2). Mereka tidak memperhitungkan (3) sehingga celah seperti mengembalikan lokal sebagai bidang ref tidak dihitung. Desain ini harus mengubah aturan untuk mempertimbangkan (3). Ini akan berdampak kecil pada kompatibilitas untuk API yang ada. Secara khusus akan berdampak pada API yang memiliki properti berikut.
- Memiliki
ref structpada tanda tangan- Di mana
ref structadalah jenis pengembalian,ref, atau parameterout - Memiliki parameter
inataureftambahan yang tidak termasuk penerima
- Di mana
Dalam C# 10 pemanggil API tersebut tidak perlu mempertimbangkan bahwa input status ref ke API dapat ditangkap sebagai bidang ref. Hal itu memungkinkan beberapa pola untuk ada dengan aman di C# 10, yang akan menjadi tidak aman di C# 11 karena adanya kemungkinan status ref melarikan diri sebagai bidang ref. Misalnya:
Span<int> CreateSpan(ref int parameter)
{
// The implementation of this method is irrelevant when considering the lifetime of the
// returned Span<T>. The ref safe context rules only look at the method signature, not the
// implementation. In C# 10 ref fields didn't exist hence there was no way for `parameter`
// to escape by ref in this method
}
Span<int> BadUseExamples(int parameter)
{
// Legal in C# 10 but would be illegal with ref fields
return CreateSpan(ref parameter);
// Legal in C# 10 but would be illegal with ref fields
int local = 42;
return CreateSpan(ref local);
// Legal in C# 10 but would be illegal with ref fields
Span<int> span = stackalloc int[42];
return CreateSpan(ref span[0]);
}
Dampak dari jeda kompatibilitas ini diperkirakan sangat kecil. Bentuk API yang terkena dampak tidak masuk akal dengan tidak adanya bidang ref sehingga tidak mungkin pelanggan menciptakan banyak dari ini. Percobaan yang menggunakan alat untuk mendeteksi bentuk API ini pada repositori yang ada mendukung pernyataan tersebut. Satu-satunya repositori yang memiliki jumlah signifikan dari bentuk ini adalah dotnet/runtime, dan hal itu karena repositori tersebut dapat membuat bidang ref melalui jenis intrinsik ByReference<T>.
Meskipun demikian desain harus memperhitungkan API seperti itu yang ada karena mengekspresikan pola yang valid, hanya saja bukan yang umum. Oleh karena itu desain harus memberi pengembang alat untuk memulihkan aturan seumur hidup yang ada saat meningkatkan ke C# 10. Secara khusus harus menyediakan mekanisme yang memungkinkan pengembang untuk membuat anotasi pada parameter ref sebagai tidak dapat lepas dari bidang ref atau ref. Hal ini memungkinkan pelanggan untuk menentukan API di C# 11 yang memiliki aturan panggilan C# 10 yang sama.
Rakitan Referensi
Rakitan referensi untuk kompilasi menggunakan fitur yang dijelaskan dalam proposal ini harus mempertahankan elemen yang menyampaikan informasi konteks aman ref. Itu berarti semua atribut anotasi seumur hidup harus dipertahankan dalam posisi aslinya. Setiap upaya untuk mengganti atau menghilangkannya dapat menyebabkan rakitan referensi yang tidak valid.
Mewakili bidang ref lebih kompleks. Idealnya bidang ref akan muncul dalam rakitan referensi seperti halnya bidang lain. Namun bidang ref mewakili perubahan pada format metadata dan yang dapat menyebabkan masalah dengan rantai alat yang tidak diperbarui untuk memahami perubahan metadata ini. Contoh konkretnya adalah C++/CLI yang kemungkinan akan mengalami kesalahan jika menggunakan bidang ref. Oleh karena itu menguntungkan jika bidang ref dapat dihilangkan dari rakitan referensi di pustaka inti kami.
Bidang ref dengan sendirinya tidak berdampak pada aturan konteks aman ref. Sebagai contoh konkret, pertimbangkan bahwa membalik definisi Span<T> yang ada untuk menggunakan bidang ref tidak berdampak pada konsumsi. Oleh karena itu ref itu sendiri dapat dihilangkan dengan aman. Namun bidang ref memang memiliki dampak lain terhadap konsumsi yang harus dipertahankan:
-
ref structyang memiliki bidangreftidak pernah dianggapunmanaged - Jenis bidang
refberdampak pada aturan ekspansi generik tak terbatas. Oleh karena itu, jika jenis bidangrefberisi parameter jenis yang harus dipertahankan
Mengingat aturan tersebut di sini adalah transformasi rakitan referensi yang valid untuk ref struct:
// Impl assembly
ref struct S<T>
{
ref T _field;
}
// Ref assembly
ref struct S<T>
{
object _o; // force managed
T _f; // maintain generic expansion protections
}
Anotasi
Rentang waktu secara alami diekspresikan menggunakan tipe. Masa pakai program tertentu aman ketika jenis jenis masa pakai diperiksa. Meskipun sintaks C# secara implisit menambahkan masa pakai ke nilai, ada sistem jenis yang mendasarinya yang menjelaskan aturan dasar di sini. Seringkali lebih mudah untuk membahas implikasi perubahan pada desain dalam hal aturan ini sehingga mereka disertakan di sini untuk kepentingan diskusi.
Perhatikan bahwa ini tidak dimaksudkan untuk menjadi dokumentasi lengkap 100%. Mendokumen setiap perilaku bukanlah tujuan di sini. Sebaliknya, ini dimaksudkan untuk menetapkan pemahaman umum dan istilah-istilah umum di mana model serta perubahan potensial padanya dapat dibahas.
Biasanya tidak perlu secara langsung membahas tipe masa pakai. Pengecualian adalah tempat di mana masa pakai dapat bervariasi berdasarkan situs "instansiasi" tertentu. Ini adalah semacam polimorfisme dan kami menyebut masa pakai yang bervariasi ini "masa pakai generik", direpresentasikan sebagai parameter generik. C# tidak menyediakan sintaks untuk mengekspresikan konsep generik terkait masa pakai, jadi kami menentukan "terjemahan" implisit dari C# ke bahasa yang telah diturunkan dan diperluas yang mencakup parameter generik eksplisit.
Contoh di bawah ini menggunakan masa pakai bernama. Sintaks $a mengacu pada masa pakai bernama a. Ini adalah masa pakai yang tidak memiliki arti dengan sendirinya tetapi dapat diberikan hubungan dengan masa pakai lain melalui sintaks where $a : $b. Ini menetapkan bahwa $a dapat dikonversi ke $b. Ini dapat membantu memandangnya sebagai penetapan bahwa $a memiliki masa berlaku setidaknya sama dengan $b.
Ada beberapa masa pakai yang telah ditentukan sebelumnya untuk kenyamanan dan kemudahan di bawah ini:
-
$heap: ini adalah masa pakai nilai apa pun yang ada di heap memori. Ini tersedia dalam semua konteks dan tanda tangan metode. -
$local: ini adalah masa pakai nilai apa pun yang ada pada tumpukan metode. Ini secara efektif merupakan tempat penampung nama untuk anggota fungsi. Ini didefinisikan secara implisit dalam metode dan dapat muncul dalam tanda tangan metode kecuali pada posisi keluaran mana pun. -
$ro: tempat penampung nama untuk hanya mengembalikan -
$cm: tempat penampung nama untuk konteks pemanggil
Ada beberapa hubungan yang telah ditentukan sebelumnya antara masa hidup.
-
where $heap : $auntuk semua$amasa pakai where $cm : $ro-
where $x : $localuntuk semua masa pakai yang telah ditentukan sebelumnya. Masa pakai yang ditentukan pengguna tidak memiliki hubungan dengan lokal kecuali ditentukan secara eksplisit.
Variabel seumur hidup ketika didefinisikan pada jenis dapat invarian atau kovarian. Ini dinyatakan menggunakan sintaks yang sama dengan parameter generik:
// $this is covariant
// $a is invariant
ref struct S<out $this, $a>
Parameter seumur hidup $this pada definisi jenis tidak ditentukan sebelumnya tetapi memang memiliki beberapa aturan yang terkait dengannya ketika didefinisikan:
- Ini harus menjadi parameter seumur hidup pertama.
- Ini harus kovarian:
out $this. - Masa pakai bidang
refharus dapat dikonversi ke$this - Masa pakai
$thissemua bidang non-ref harus$heapatau$this.
Waktu hidup dari ref dinyatakan dengan memberikan argumen waktu hidup kepada ref. Misalnya, ref yang mengacu pada heap dinyatakan sebagai ref<$heap>.
Saat menentukan konstruktor dalam model, nama new akan digunakan untuk metode tersebut. Anda perlu memiliki daftar parameter untuk nilai yang dikembalikan serta argumen konstruktor. Ini diperlukan untuk mengekspresikan hubungan antara input konstruktor dan nilai yang dibangun. Alih-alih memiliki Span<$a><$ro> model akan menggunakan Span<$a> new<$ro> sebagai gantinya. Jenis this dalam konstruktor, termasuk masa pakai, akan menjadi nilai pengembalian yang ditentukan.
Aturan dasar untuk masa pakai didefinisikan sebagai:
- Semua masa pakai diekspresikan secara sintaksis sebagai argumen generik, muncul sebelum argumen tipe. Ini berlaku untuk masa pakai yang telah ditentukan sebelumnya kecuali
$heapdan$local. - Semua jenis
Tyang bukanref structsecara implisit memiliki masa pakaiT<$heap>. Ini implisit, tidak perlu menulisint<$heap>di setiap sampel. - Untuk bidang
refyang didefinisikan sebagairef<$l0> T<$l1, $l2, ... $ln>:- Semua masa pakai
$l1hingga$lnharus tetap tidak berubah. - Masa pakai
$l0harus dapat dikonversi ke$this
- Semua masa pakai
- Untuk
refyang didefinisikan sebagairef<$a> T<$b, ...>,$bharus dapat dikonversi ke$a - Masa hidup
refdari suatu variabel ditentukan oleh:- Untuk lokal
ref, parameter, bidang, atau pengembalian dengan jenisref<$a> T, masa hidupnya adalah$a -
$heapuntuk semua jenis referensi dan bidang jenis referensi -
$localuntuk semua yang lain
- Untuk lokal
- Penugasan atau pengembalian adalah legal ketika konversi jenis yang mendasarinya adalah legal.
- Masa pakai ekspresi dapat dibuat eksplisit dengan menggunakan anotasi cast:
-
(T<$a> expr)masa hidup nilai ditentukan secara eksplisit$auntukT<...> -
ref<$a> (T<$b>)exprmasa berlaku nilai adalah$buntukT<...>dan masa berlaku referensi adalah$a.
-
Untuk tujuan aturan seumur hidup, ref dianggap sebagai bagian dari jenis ekspresi untuk tujuan konversi. Ini secara logis diwakili dengan mengonversi ref<$a> T<...> ke ref<$a, T<...>> di mana $a kovarian dan T invarian.
Selanjutnya mari kita tentukan aturan yang memungkinkan kita memetakan sintaks C# ke model yang mendasar.
Untuk kepentingan kejelasan, jenis yang tidak memiliki parameter masa hidup eksplisit diperlakukan seolah-olah ada out $this yang didefinisikan dan diterapkan ke semua bidang dari tipe tersebut. Jenis dengan bidang ref harus menentukan parameter seumur hidup eksplisit.
Aturan ini diciptakan untuk mendukung invarian yang telah ada bahwa T dapat ditetapkan ke scoped T untuk semua tipe. Hal ini membuat T<$a, ...> dapat dipetakan ke T<$local, ...> untuk semua seumur hidup yang diketahui dapat dikonversi ke $local. Selanjutnya, ini mendukung item lain seperti kemampuan menetapkan Span<T> dari kumpulan ke tumpukan. Ini memang mengecualikan jenis di mana bidang memiliki masa pakai yang berbeda untuk nilai non-ref, tetapi itulah kenyataan C# saat ini. Perubahan tersebut akan memerlukan perubahan signifikan pada aturan C# yang perlu dipetakan.
Jenis this untuk jenis S<out $this, ...> di dalam metode instans secara implisit didefinisikan sebagai berikut:
- Untuk metode instans normal:
ref<$local> S<$cm, ...> - Untuk metode instance yang dilabeli dengan
[UnscopedRef]:ref<$ro> S<$cm, ...>
Kurangnya parameter this eksplisit memaksa aturan implisit di sini. Untuk sampel dan diskusi yang kompleks, pertimbangkan untuk menulis dalam bentuk metode static dan membuat this sebagai parameter eksplisit.
ref struct S<out $this>
{
// Implicit this can make discussion confusing
void M<$ro, $cm>(ref<$ro> S<$cm> s) { }
// Rewrite as explicit this to simplify discussion
static void M<$ro, $cm>(ref<$local> S<$cm> this, ref<$ro> S<$cm> s) { }
}
Sintaks dari metode C# memetakan ke model dengan cara berikut:
- Masa pakai ref dari parameter
refadalah$ro - parameter jenis
ref structmemiliki masa pakai$cmini - pengembalian ref memiliki masa pakai ref
$ro - pengembalian jenis
ref structmemiliki masa pakai nilai$ro -
scopedpada parameter ataurefmengubah masa pakai ref menjadi$local
Mengingat bahwa mari kita jelajahi contoh sederhana yang menunjukkan model di sini:
ref int M1(ref int i) => ...
// Maps to the following.
ref<$ro> int Identity<$ro>(ref<$ro> int i)
{
// okay: has ref lifetime $ro which is equal to $ro
return ref i;
// okay: has ref lifetime $heap which convertible $ro
int[] array = new int[42];
return ref array[0];
// error: has ref lifetime $local which has no conversion to $a hence
// it's illegal
int local = 42;
return ref local;
}
Sekarang mari kita jelajahi contoh yang sama menggunakan ref struct:
ref struct S
{
ref int Field;
S(ref int f)
{
Field = ref f;
}
}
S M2(ref int i, S span1, scoped S span2) => ...
// Maps to
ref struct S<out $this>
{
// Implicitly
ref<$this> int Field;
S<$ro> new<$ro>(ref<$ro> int f)
{
Field = ref f;
}
}
S<$ro> M2<$ro>(
ref<$ro> int i,
S<$ro> span1)
S<$local> span2)
{
// okay: types match exactly
return span1;
// error: has lifetime $local which has no conversion to $ro
return span2;
// okay: type S<$heap> has a conversion to S<$ro> because $heap has a
// conversion to $ro and the first lifetime parameter of S<> is covariant
return default(S<$heap>)
// okay: the ref lifetime of ref $i is $ro so this is just an
// identity conversion
S<$ro> local = new S<$ro>(ref $i);
return local;
int[] array = new int[42];
// okay: S<$heap> is convertible to S<$ro>
return new S<$heap>(ref<$heap> array[0]);
// okay: the parameter of the ctor is $ro ref int and the argument is $heap ref int. These
// are convertible.
return new S<$ro>(ref<$heap> array[0]);
// error: has ref lifetime $local which has no conversion to $a hence
// it's illegal
int local = 42;
return ref local;
}
Selanjutnya, mari kita lihat bagaimana ini membantu dengan masalah penugasan diri yang siklik.
ref struct S
{
int field;
ref int refField;
static void SelfAssign(ref S s)
{
s.refField = ref s.field;
}
}
// Maps to
ref struct S<out $this>
{
int field;
ref<$this> int refField;
static void SelfAssign<$ro, $cm>(ref<$ro> S<$cm> s)
{
// error: the types work out here to ref<$cm> int = ref<$ro> int and that is
// illegal as $ro has no conversion to $cm (the relationship is the other direction)
s.refField = ref<$ro> s.field;
}
}
Selanjutnya, mari kita lihat bagaimana ini membantu dengan masalah parameter tangkapan yang konyol.
ref struct S
{
ref int refField;
void Use(ref int parameter)
{
// error: this needs to be an error else every call to this.Use(ref local) would fail
// because compiler would assume the `ref` was captured by ref.
this.refField = ref parameter;
}
}
// Maps to
ref struct S<out $this>
{
ref<$this> int refField;
// Using static form of this method signature so the type of this is explicit.
static void Use<$ro, $cm>(ref<$local> S<$cm> @this, ref<$ro> int parameter)
{
// error: the types here are:
// - refField is ref<$cm> int
// - ref parameter is ref<$ro> int
// That means the RHS is not convertible to the LHS ($ro is not covertible to $cm) and
// hence this reassignment is illegal
@this.refField = ref<$ro> parameter;
}
}
Masalah Terbuka
Ubah desain untuk menghindari pemutusan kompatibilitas
Desain ini mengusulkan beberapa pemutusan kompatibilitas dengan aturan "ref-safe-context" yang sudah ada. Meskipun perubahan diyakini berdampak minimal, pertimbangan signifikan diberikan pada desain yang tidak memiliki perubahan signifikan.
Desain yang menjaga kompatibilitas, meskipun lebih kompleks secara signifikan daripada yang satu ini. Untuk mempertahankan kompatibilitas bidang ref, diperlukan masa pakai yang berbeda agar dapat mengembalikan melalui bidang ref dan ref. Pada dasarnya mengharuskan kita untuk menyediakan ref-field-safe-context pelacakan untuk semua parameter ke dalam suatu metode. Ini perlu dihitung untuk semua ekspresi dan dilacak di hampir semua nilai di setiap tempat di mana konteks aman ref dilacak hari ini.
Selanjutnya nilai ini memiliki hubungan dengan ref-safe-context. Misalnya, tidak masuk akal memiliki kemampuan untuk mengembalikan nilai sebagai bidang ref tetapi tidak secara langsung sebagai ref. Itu karena bidang ref bisa dengan mudah dikembalikan oleh ref (statusref dalam ref struct dapat dikembalikan oleh ref bahkan ketika nilai yang dikandung tidak dapat). Oleh karena itu, aturan selanjutnya membutuhkan penyesuaian konstan untuk memastikan nilai-nilai ini masuk akal sehubungan satu sama lain.
Juga berarti bahasa memerlukan sintaks untuk mewakili parameter ref yang dapat dikembalikan dengan tiga cara berbeda: dengan bidang ref, dengan ref dan berdasarkan nilai. Default dapat dikembalikan oleh ref. Meskipun ke depan pengembalian yang lebih alami, terutama saat ref struct terlibat, diantisipasi melalui bidang ref atau ref. Itu berarti API baru memerlukan anotasi sintaks tambahan untuk benar secara default. Ini tidak diinginkan.
Namun, perubahan kompat ini akan memengaruhi metode yang memiliki properti berikut:
- Memiliki
Span<T>atauref struct- Di mana
ref structadalah jenis pengembalian,ref, atau parameterout - Memiliki parameter
inataureftambahan (tidak termasuk penerima)
- Di mana
Untuk memahami dampaknya, sangat membantu untuk memecah API ke dalam kategori:
- Meminta konsumen untuk memperhitungkan bahwa
refdicatat sebagai bidangref. Contoh utama adalah konstruktorSpan(ref T value) - Jangan biarkan konsumen menganggap
refsebagai bidangref. Meskipun begitu, item-item ini terbagi menjadi dua kategori- API tidak aman. Ini adalah API-API di dalam jenis
UnsafedanMemoryMarshal, di manaMemoryMarshal.CreateSpanadalah yang paling menonjol. API ini memang menangkaprefdengan cara yang tidak aman, tetapi mereka juga sudah dikenal sebagai API yang berisiko. - API aman. Ini adalah API-API yang menggunakan parameter
refuntuk efisiensi tetapi sebenarnya tidak terobservasi di mana pun. Contohnya berukuran kecil, tetapi salah satunya adalahAsnDecoder.ReadEnumeratedBytes
- API tidak aman. Ini adalah API-API di dalam jenis
Perubahan ini terutama menguntungkan poin (1) di atas. Diharapkan bahwa ini akan menjadi sebagian besar API yang mengambil ref dan mengembalikan ref struct di masa depan. Perubahan tersebut berdampak negatif pada (2.1) dan (2.2) karena merusak semantik pemanggilan yang ada akibat perubahan dalam aturan masa berlaku.
API dalam kategori (2.1) sebagian besar diciptakan oleh Microsoft atau oleh pengembang yang paling diuntungkan dari bidang ref (seperti Tanner di seluruh dunia). Masuk akal untuk mengasumsikan bahwa kelompok pengembang tertentu ini akan bersedia menerima pajak kompatibilitas pada peningkatan ke C# 11 dalam bentuk beberapa anotasi untuk mempertahankan semantika yang ada, jika bidang ref diberikan sebagai imbalannya.
API dalam kategori (2.2) adalah masalah terbesar. Tidak diketahui berapa banyak API seperti itu yang ada dan tidak jelas apakah ini akan lebih / kurang sering dalam kode pihak ke-3. Diharapkan jumlah mereka sangat sedikit, terutama jika kita melakukan perubahan kompatibilitas pada out. Pencarian sejauh ini telah mengungkapkan jumlah yang sangat kecil dari yang ada di area permukaan public. Ini adalah pola yang sulit untuk dicari karena membutuhkan analisis semantik. Sebelum mengambil perubahan ini, pendekatan berbasis alat akan diperlukan untuk memverifikasi asumsi sekeliling ini berdampak pada sejumlah kecil kasus yang diketahui.
Untuk kedua kasus dalam kategori (2) meskipun perbaikannya mudah. Parameter ref yang tidak ingin dianggap dapat ditangkap harus menambahkan scoped ke ref. Dalam (2.1) ini kemungkinan juga akan memaksa pengembang untuk menggunakan Unsafe atau MemoryMarshal tetapi itu diharapkan untuk API gaya yang tidak aman.
Idealnya bahasa dapat mengurangi dampak perubahan pemecahan diam-diam dengan mengeluarkan peringatan ketika API secara diam-diam jatuh ke dalam perilaku yang merepotkan. Itu akan menjadi metode yang mengambil ref, mengembalikan ref struct, tetapi tidak benar-benar menangkap ref di ref struct. Pengompilasi dapat mengeluarkan diagnostik dalam hal ini yang memberi tahu pengembang ref tersebut harus diannotasikan sebagai scoped ref sebagai gantinya.
Keputusan Desain ini dapat dicapai, tetapi fitur yang dihasilkan menjadi lebih sulit digunakan, sehingga diputuskan untuk menerapkan perubahan yang mengorbankan kompatibilitas.
Keputusan Pengkompilasi akan memberikan peringatan ketika metode memenuhi kriteria tetapi tidak menangkap parameter ref sebagai bidang ref. Ini seharusnya memperingatkan pelanggan dengan tepat saat peningkatan tentang potensi masalah yang dapat mereka timbulkan.
Kata kunci vs. atribut
Desain ini mengharuskan penggunaan atribut untuk membuat anotasi aturan masa pakai baru. Ini juga bisa dilakukan semudah dengan kata kunci kontekstual. Misalnya [DoesNotEscape] dapat dipetakan ke scoped. Namun kata kunci, bahkan yang kontekstual, umumnya harus memenuhi standar yang sangat tinggi agar dapat dimasukkan. Mereka mengambil ruang dalam bahasa yang berharga dan merupakan bagian yang semakin menonjol dalam penggunaan bahasa. Fitur ini, meskipun berharga, akan melayani minoritas pengembang C#.
Di permukaan yang tampaknya mendukung tidak menggunakan kata kunci tetapi ada dua poin penting yang perlu dipertimbangkan:
- Anotasi akan mempengaruhi semantik program. Memiliki atribut yang mempengaruhi semantik program adalah langkah yang enggan ditempuh oleh C#, dan tidak jelas apakah fitur ini dapat membenarkan bahasa tersebut untuk mengambil langkah itu.
- Pengembang yang kemungkinan besar menggunakan fitur ini beririsan kuat dengan kelompok pengembang yang menggunakan pointer fungsi. Fitur itu, meskipun juga digunakan oleh sebagian kecil pengembang, memang memerlukan sintaks baru dan keputusan itu masih dipandang sebagai keputusan yang tepat.
Diambil bersama-sama ini berarti sintaksis harus dipertimbangkan.
Sketsa kasar sintaksis adalah:
-
[RefDoesNotEscape]diarahkan kescoped ref -
[DoesNotEscape]diarahkan kescoped -
[RefDoesEscape]diarahkan keunscoped
Keputusan Gunakan sintaks untuk scoped dan scoped ref; gunakan atribut untuk unscoped.
Perbolehkan lokal buffer tetap
Desain ini memungkinkan buffer fixed aman yang dapat mendukung jenis apa pun. Salah satu ekstensi yang mungkin di sini adalah memungkinkan buffer fixed tersebut dideklarasikan sebagai variabel lokal. Ini akan memungkinkan sejumlah operasi stackalloc yang ada diganti dengan buffer fixed. Ini juga akan memperluas rangkaian skenario di mana kita bisa menggunakan alokasi gaya tumpukan, karena stackalloc terbatas pada jenis elemen yang tidak dikelola, sementara buffer fixed tidak.
class FixedBufferLocals
{
void Example()
{
Span<int> span = stackalloc int[42];
int buffer[42];
}
}
Ini bersatu padu tetapi mengharuskan kita untuk memperluas sintaks untuk variabel lokal sedikit. Tidak jelas apakah ini atau tidak sepadan dengan kompleksitas ekstra. Mungkin kita dapat memutuskan untuk menolak untuk saat ini dan membawanya kembali nanti jika kebutuhan yang memadai muncul.
Contoh di mana ini akan bermanfaat: https://github.com/dotnet/runtime/pull/34149
Keputusan menunda ini untuk saat ini
Untuk menggunakan modreqs atau tidak
Keputusan harus dibuat apakah metode yang ditandai dengan atribut baru seumur hidup harus diterjemahkan ke modreq dalam emisi atau tidak. Akan ada pemetaan 1:1 secara efektif antara anotasi dan modreq jika pendekatan ini diambil.
Alasan untuk menambahkan modreq adalah karena atribut tersebut mengubah semantik dari aturan konteks aman ref. Hanya bahasa yang memahami semantik ini yang harus memanggil metode yang dimaksud. Lebih lanjut ketika diterapkan pada skenario OHI, masa pakai menjadi kontrak yang harus diterapkan oleh semua metode turunan. Keberadaan anotasi tanpa modreq dapat menyebabkan situasi di mana rantai metode virtual dengan anotasi masa pakai yang saling bertentangan terbaca (dapat terjadi jika hanya satu bagian dari rantai virtual yang dikompilasi sementara yang lain tidak).
Pekerjaan konteks aman ref awal tidak menggunakan modreq tetapi sebaliknya bergantung pada bahasa dan kerangka kerja untuk dipahami. Pada saat yang sama, meskipun semua elemen yang berkontribusi pada aturan konteks ref yang aman adalah bagian penting dari signatur metode: ref, in, ref struct, dll. Oleh karena itu, setiap perubahan pada aturan metode yang ada sudah menghasilkan perubahan biner pada signatur. Untuk memberikan dampak yang sama pada anotasi seumur hidup yang baru, mereka akan membutuhkan penegakan modreq.
Kekhawatirannya adalah apakah ini berlebihan atau tidak. Ini memang memiliki dampak negatif yang membuat tanda tangan lebih fleksibel, dengan, misalnya, menambahkan [DoesNotEscape] ke dalam parameter, yang akan mengakibatkan perubahan kompatibilitas biner. Kompromi itu berarti bahwa seiring waktu, kerangka kerja seperti BCL mungkin tidak bisa melonggarkan karakteristik tersebut. Ini dapat dimitigasi sampai tingkat tertentu dengan mengambil pendekatan yang bekerja dengan parameter in dan hanya menerapkan modreq di posisi virtual.
Keputusan Jangan gunakan modreq dalam metadata. Perbedaan antara out dan ref bukan modreq, melainkan sekarang memiliki nilai konteks aman referensi yang berbeda. Tidak ada manfaat nyata untuk menegakkan aturan dengan setengah-setengah dengan modreq di sini.
Izinkan buffer tetap yang multi-dimensi
Haruskah desain untuk buffer fixed diperluas untuk menyertakan array bergaya multi-dimensi? Pada dasarnya mengizinkan deklarasi seperti berikut:
struct Dimensions
{
int array[42, 13];
}
Keputusan Jangan izinkan untuk saat ini
Melanggar ruang lingkup
Repositori runtime memiliki beberapa API non-publik yang menangkap parameter ref sebagai bidang ref. Ini tidak aman karena masa pakai nilai yang dihasilkan tidak dilacak. Misalnya konstruktor Span<T>(ref T value, int length).
Sebagian besar API ini kemungkinan akan memilih untuk memiliki pelacakan masa pakai yang tepat pada pengembaliannya yang dapat dicapai dengan hanya memperbarui ke C# 11. Namun, beberapa akan ingin menjaga semantik mereka saat ini untuk tidak melacak nilai kembalian karena seluruh maksud mereka memang untuk tidak aman. Contoh yang paling mencolok adalah MemoryMarshal.CreateSpan dan MemoryMarshal.CreateReadOnlySpan. Ini akan dicapai dengan menandai parameter sebagai scoped.
Itu berarti runtime membutuhkan pola yang mapan untuk menghapus scoped secara tidak aman dari parameter:
-
Unsafe.AsRef<T>(in T value)dapat memperluas tujuan yang ada dengan mengubah kescoped in T value. Ini akan memungkinkannya untuk menghapusindanscopeddari parameter. Kemudian menjadi metode "hapus keamanan ref" universal - Perkenalkan metode baru yang seluruh tujuannya adalah untuk menghapus
scoped:ref T Unsafe.AsUnscoped<T>(scoped in T value). Ini mencakup juga penghapusanin, karena jika tidak, maka pemanggil masih memerlukan kombinasi panggilan metode untuk "menghapus keamanan ref," sehingga solusi yang ada kemungkinan sudah cukup.
Batalkan cakupan ini secara default?
Desain hanya memiliki dua lokasi yang scoped secara bawaan:
-
thisadalahscoped ref -
outadalahscoped ref
Keputusan tentang out adalah secara signifikan mengurangi beban kompatibilitas bidang ref dan pada saat yang sama merupakan pengaturan awal yang lebih alami. Ini memungkinkan pengembang menganggap out sebagai data yang hanya mengalir keluar, sedangkan dengan ref, aturan harus mempertimbangkan aliran data di kedua arah. Hal ini menyebabkan kebingungan pengembang yang signifikan.
Keputusan tentang this tidak diinginkan karena itu berarti struct tidak dapat mengembalikan bidang dengan ref. Ini adalah skenario penting untuk pengembang perf tinggi dan atribut [UnscopedRef] ditambahkan pada dasarnya untuk skenario yang satu ini.
Kata kunci memiliki standar tinggi dan menambahkannya untuk satu skenario adalah hal yang meragukan. Oleh karena itu, dipertimbangkan apakah kita dapat menghindari kata kunci ini sama sekali dengan menjadikan this sebagai ref secara default dan menghindari scoped ref. Semua anggota yang membutuhkan this menjadi scoped ref dapat melakukannya dengan menandai metode scoped (karena metode dapat ditandai readonly untuk membuat readonly ref saat ini).
Pada struct normal ini umumnya merupakan perubahan yang positif karena hanya memperkenalkan masalah kompatibilitas ketika anggota memiliki pengembalian ref. Ada sangat sedikit dari metode-metode ini dan sebuah alat dapat mendeteksi ini dan dengan cepat mengonversinya menjadi anggota scoped.
Pada ref struct perubahan ini memperkenalkan masalah kompatibilitas yang jauh lebih besar. Pertimbangkan hal berikut:
ref struct Sneaky
{
int Field;
ref int RefField;
public void SelfAssign()
{
// This pattern of ref reassign to fields on this inside instance methods would now
// completely legal.
RefField = ref Field;
}
static Sneaky UseExample()
{
Sneaky local = default;
// Error: this is illegal, and must be illegal, by our existing rules as the
// ref-safe-context of local is now an input into method arguments must match.
local.SelfAssign();
// This would be dangerous as local now has a dangerous `ref` but the above
// prevents us from getting here.
return local;
}
}
Berarti semua pemanggilan metode instans pada mutabelref struct lokal akan tidak sah kecuali jika lokal tersebut ditandai lebih lanjut sebagai scoped. Peraturan harus mempertimbangkan kasus di mana bidang ref di-assign ulang ke bidang lain di this.
readonly ref struct tidak memiliki masalah ini karena sifat readonly mencegah penetapan ulang ref. Namun ini akan menjadi perubahan signifikan yang merusak kompatibilitas balik karena akan berdampak pada hampir semua ref structyang dapat diubah yang sudah ada.
Namun, readonly ref struct masih bermasalah ketika kami memperluas ke bidang ref ke ref struct. Ini memungkinkan masalah dasar yang sama hanya dengan memindahkan pengambilan ke nilai bidang ref:
readonly ref struct ReadOnlySneaky
{
readonly int Field;
readonly ref ReadOnlySpan<int> Span;
public void SelfAssign()
{
// Instance method captures a ref to itself
Span = new ReadOnlySpan<int>(ref Field, 1);
}
}
Beberapa pemikiran diberikan pada ide menjadikan this memiliki default yang berbeda berdasarkan jenis dari struct atau anggota. Misalnya:
-
thissebagairef:struct,readonly ref struct, ataureadonly member -
thisdalam bentukscoped ref:ref structataureadonly ref structdengan bidangrefkeref struct
Ini meminimalkan pemutusan kompatibilitas dan memaksimalkan fleksibilitas, tetapi dengan konsekuensi memperumit informasi bagi pelanggan. Ini juga tidak sepenuhnya menyelesaikan masalah karena fitur di masa depan, seperti buffer fixed yang aman, mengharuskan agar ref struct yang dapat diubah memiliki pengembalian ref untuk bidang yang tidak dapat berfungsi hanya dengan desain ini, karena akan termasuk dalam kategori scoped ref.
Keputusan untuk tetap sebagai thisscoped ref. Itu berarti contoh licik sebelumnya menghasilkan kesalahan kompilator.
bidang referensi untuk struktur referensi
Fitur ini membuka serangkaian aturan konteks aman ref baru karena memungkinkan bidang ref untuk merujuk ke ref struct. Sifat generik ByReference<T> ini berarti bahwa hingga saat ini runtime tidak dapat memiliki konstruksi seperti itu. Akibatnya semua aturan kami ditulis dengan asumsi ini tidak mungkin. Fitur bidang ref sebagian besar bukan tentang membuat aturan baru tetapi mengkodifikasi aturan yang ada dalam sistem kami. Mengizinkan bidang ref ke ref struct mengharuskan kami mengkodifikasi aturan baru karena ada beberapa skenario baru yang perlu dipertimbangkan.
Yang pertama adalah bahwa readonly ref sekarang mampu menyimpan status ref. Misalnya:
readonly ref struct Container
{
readonly ref Span<int> Span;
void Store(Span<int> span)
{
Span = span;
}
}
Ini berarti kita harus mempertimbangkan agar argumen metode sesuai dengan aturan, bahwa readonly ref T adalah output metode potensial ketika T berpotensi memiliki bidang ref ke ref struct.
Masalah kedua adalah bahasa harus mempertimbangkan jenis konteks aman baru: ref-field-safe-context. Semua ref struct yang secara transitif berisi bidang ref memiliki cakupan escape lain yang mewakili nilai di bidang ref. Dalam kasus beberapa bidang ref, bidang tersebut dapat dilacak secara kolektif sebagai nilai tunggal. Nilai default untuk parameter ini adalah penelepon-konteks.
ref struct Nested
{
ref Span<int> Span;
}
Span<int> M(ref Nested nested) => nested.Span;
Nilai ini tidak terkait dengan konteks aman dari kontainer; artinya, ketika konteks kontainer semakin kecil, hal tersebut tidak berdampak pada konteks aman ref-field dari nilai bidang ref. Selanjutnya, ref-field-safe-context tidak pernah lebih kecil dari konteks aman kontainer.
ref struct Nested
{
ref Span<int> Span;
}
void M(ref Nested nested)
{
scoped ref Nested refLocal = ref nested;
// the ref-field-safe-context of local is still *caller-context* which means the following
// is illegal
refLocal.Span = stackalloc int[42];
scoped Nested valLocal = nested;
// the ref-field-safe-context of local is still *caller-context* which means the following
// is still illegal
valLocal.Span = stackalloc int[42];
}
Konteks ref-field-safe-context pada dasarnya telah ada sejak dahulu. Hingga saat ini bidang ref hanya dapat menunjuk ke struct normal sehingga secara sepele diciutkan ke konteks pemanggil . Untuk mendukung bidang ref ke ref struct, aturan yang ada perlu diperbarui untuk memperhitungkan konteks ref-aman baruini.
Ketiga, aturan untuk penetapan ulang ref perlu diperbarui untuk memastikan bahwa kami tidak melanggar konteks bidang ref untuk nilai. Pada dasarnya untuk
Masalah-masalah ini sangat dapat diselesaikan. Tim kompilator telah membuat sketsa beberapa versi aturan ini dan mereka sebagian besar keluar dari analisis kami yang ada. Masalahnya adalah tidak ada kode konsumsi untuk aturan seperti itu yang membantu membuktikan kebenaran dan kegunaan di luar sana. Ini membuat kami sangat ragu untuk menambahkan dukungan karena takut memilih default yang salah dan akhirnya membatasi kegunaan runtime ketika memanfaatkannya. Kekhawatiran ini sangat kuat karena .NET 8 kemungkinan mendorong kita ke arah ini dengan allow T: ref struct dan Span<Span<T>>. Aturan akan lebih baik ditulis jika dilakukan bersama dengan kode konsumsi.
Penundaan Keputusan yang memungkinkan bidang refref struct hingga .NET 8 di mana kami memiliki skenario yang akan membantu mendorong aturan di sekitar skenario ini. Ini belum diimplementasikan pada .NET 9
Apa yang akan membuat C# 11.0?
Fitur yang diuraikan dalam dokumen ini tidak perlu diimplementasikan dalam satu tahap. Sebaliknya, mereka dapat diimplementasikan dalam beberapa fase di sejumlah rilis bahasa dalam kelompok berikut:
-
refbidang danscoped [UnscopedRef]-
refbidang keref struct - Jenis terbatas matahari terbenam
- buffer berukuran tetap
Apa yang diimplementasikan dalam rilis mana hanyalah sebuah latihan cakupan.
Keputusan Hanya (1) dan (2) menghasilkan C# 11.0. Sisanya akan dipertimbangkan dalam versi C#yang akan datang.
Pertimbangan Masa Depan
Anotasi masa pakai lanjutan
Anotasi seumur hidup dalam proposal ini terbatas karena memungkinkan pengembang untuk mengubah perilaku escape / jangan lolos dari nilai default. Ini memang menambah fleksibilitas yang kuat pada model kami tetapi tidak secara radikal mengubah serangkaian hubungan yang dapat diekspresikan. Pada inti model C# masih biner secara efektif: dapatkah nilai dikembalikan atau tidak?
Itu memungkinkan hubungan seumur hidup terbatas untuk dipahami. Misalnya nilai yang tidak dapat dikembalikan dari metode memiliki masa pakai yang lebih kecil daripada nilai yang dapat dikembalikan dari metode . Tidak ada cara untuk menggambarkan hubungan seumur hidup antara nilai yang dapat dikembalikan dari metode sekalipun. Secara khusus, tidak ada cara untuk mengatakan bahwa satu nilai memiliki umur yang lebih panjang daripada yang lain setelah dipastikan keduanya dapat dikembalikan dari sebuah metode. Langkah selanjutnya dalam evolusi seumur hidup kita adalah memungkinkan hubungan tersebut dijelaskan.
Metode lain seperti Rust memungkinkan jenis hubungan ini diekspresikan dan karenanya dapat menerapkan operasi gaya scoped yang lebih kompleks. Bahasa kami juga dapat memperoleh manfaat jika fitur seperti itu disertakan. Saat ini tidak ada dorongan yang memotivasi untuk melakukan ini, tetapi jika ada di masa depan, model scoped kita dapat diperluas untuk memasukkannya dengan cara yang cukup sederhana.
Setiap scoped dapat diberikan masa hidup yang dinamai dengan menambahkan argumen jenis generik ke sintaks. Misalnya scoped<'a> adalah nilai yang memiliki masa hidup 'a. Batasan seperti where kemudian dapat digunakan untuk menggambarkan hubungan antara masa pakai ini.
void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
where 'b >= 'a
{
s.Span = span;
}
Metode ini mendefinisikan dua masa pakai 'a dan 'b dan hubungan mereka, khususnya bahwa 'b lebih besar dari 'a. Ini memungkinkan callsite memiliki aturan yang lebih terperinci tentang bagaimana nilai dapat diteruskan dengan aman ke metode vs. aturan yang lebih kasar yang ada saat ini.
Informasi Terkait
Masalah
Semua masalah berikut terkait dengan proposal ini:
- https://github.com/dotnet/csharplang/issues/1130
- https://github.com/dotnet/csharplang/issues/1147
- https://github.com/dotnet/csharplang/issues/992
- https://github.com/dotnet/csharplang/issues/1314
- https://github.com/dotnet/csharplang/issues/2208
- https://github.com/dotnet/runtime/issues/32060
- https://github.com/dotnet/runtime/issues/61135
- https://github.com/dotnet/csharplang/discussions/78
Proposal
Proposal berikut terkait dengan proposal ini:
Sampel yang ada
Cuplikan khusus ini memerlukan 'unsafe' karena mengalami masalah dengan melewati Span<T> yang dapat dialokasikan di stack ke metode instance pada objek ref struct. Meskipun parameter ini tidak ditangkap, bahasa harus mengasumsikannya dan karenanya tidak perlu menyebabkan gesekan di sini.
Cuplikan ini ingin bermutasi parameter dengan melarikan diri dari elemen data. Data yang dieksekusi dapat dialokasikan di tumpukan untuk efisiensi. Meskipun parameter tidak lolos, pengkompilasi menetapkannya konteks aman dari luar metode penutup karena itu adalah parameter. Ini berarti untuk menggunakan alokasi tumpukan, implementasi harus menggunakan unsafe untuk menetapkan kembali ke parameter setelah melarikan diri dari data.
Sampel Menyenangkan
ReadOnlySpan<T>
public readonly ref struct ReadOnlySpan<T>
{
readonly ref readonly T _value;
readonly int _length;
public ReadOnlySpan(in T value)
{
_value = ref value;
_length = 1;
}
}
Daftar hemat
struct FrugalList<T>
{
private T _item0;
private T _item1;
private T _item2;
public int Count = 3;
public FrugalList(){}
public ref T this[int index]
{
[UnscopedRef] get
{
switch (index)
{
case 0: return ref _item0;
case 1: return ref _item1;
case 2: return ref _item2;
default: throw null;
}
}
}
}
Contoh dan Catatan
Di bawah ini adalah serangkaian contoh yang menunjukkan bagaimana dan mengapa aturan bekerja seperti yang mereka lakukan. Disertakan adalah beberapa contoh yang menunjukkan perilaku berbahaya dan bagaimana aturan mencegahnya terjadi. Penting untuk mengingat hal ini saat membuat penyesuaian pada proposal.
Penetapan ulang ref dan lokasi pemanggilan
Menunjukkan bagaimana penetapan ulang dan pemanggilan metode bekerja sama.
ref struct RS
{
ref int _refField;
public ref int Prop => ref _refField;
public RS(int[] array)
{
_refField = ref array[0];
}
public RS(ref int i)
{
_refField = ref i;
}
public RS CreateRS() => ...;
public ref int M1(RS rs)
{
// The call site arguments for Prop contribute here:
// - `rs` contributes no ref-safe-context as the corresponding parameter,
// which is `this`, is `scoped ref`
// - `rs` contribute safe-context of *caller-context*
//
// This is an lvalue invocation and the arguments contribute only safe-context
// values of *caller-context*. That means `local1` has ref-safe-context of
// *caller-context*
ref int local1 = ref rs.Prop;
// Okay: this is legal because `local` has ref-safe-context of *caller-context*
return ref local1;
// The arguments contribute here:
// - `this` contributes no ref-safe-context as the corresponding parameter
// is `scoped ref`
// - `this` contributes safe-context of *caller-context*
//
// This is an rvalue invocation and following those rules the safe-context of
// `local2` will be *caller-context*
RS local2 = CreateRS();
// Okay: this follows the same analysis as `ref rs.Prop` above
return ref local2.Prop;
// The arguments contribute here:
// - `local3` contributes ref-safe-context of *function-member*
// - `local3` contributes safe-context of *caller-context*
//
// This is an rvalue invocation which returns a `ref struct` and following those
// rules the safe-context of `local4` will be *function-member*
int local3 = 42;
var local4 = new RS(ref local3);
// Error:
// The arguments contribute here:
// - `local4` contributes no ref-safe-context as the corresponding parameter
// is `scoped ref`
// - `local4` contributes safe-context of *function-member*
//
// This is an lvalue invocation and following those rules the ref-safe-context
// of the return is *function-member*
return ref local4.Prop;
}
}
Penugasan ulang ref dan pelarian yang tidak aman
Alasan untuk baris berikut dalam aturan penetapan ulang ref mungkin tidak jelas pada pandangan pertama:
harus memiliki konteks aman yang sama dengan
Ini karena masa pakai nilai yang ditunjukkan oleh lokasi ref invariant. Indireksi mencegah kita untuk memungkinkan adanya jenis varians apa pun di sini, bahkan untuk mempersempit masa hidup. Jika penyempitan diizinkan, kode tidak aman berikut akan terbuka:
void Example(ref Span<int> p)
{
Span<int> local = stackalloc int[42];
ref Span<int> refLocal = ref local;
// Error:
// The safe-context of refLocal is narrower than p. For a non-ref reassignment
// this would be allowed as its safe to assign wider lifetimes to narrower ones.
// In the case of ref reassignment though this rule prevents it as the
// safe-context values are different.
refLocal = ref p;
// If it were allowed this would be legal as the safe-context of refLocal
// is *caller-context* and that is satisfied by stackalloc. At the same time
// it would be assigning through p and escaping the stackalloc to the calling
// method
//
// This is equivalent of saying p = stackalloc int[13]!!!
refLocal = stackalloc int[13];
}
Untuk ref ke non ref struct, aturan ini dengan mudah terpenuhi karena semua nilai memiliki konteks aman yang sama. Aturan ini benar-benar hanya dimainkan ketika nilainya adalah ref struct.
Perilaku ref ini juga akan menjadi penting di masa depan ketika kami mengizinkan bidang ref untuk ref struct.
lokal terlingkup
Penggunaan scoped pada variabel lokal akan sangat membantu pola kode yang secara kondisional menetapkan nilai dengan konteks aman yang berbeda kepada variabel lokal. Ini berarti kode tidak perlu lagi mengandalkan trik inisialisasi seperti
// Old way
// Span<byte> span = stackalloc byte[0];
// New way
scoped Span<byte> span;
int len = ...;
if (len < MaxStackLen)
{
span = stackalloc byte[len];
}
else
{
span = new byte[len];
}
Pola ini sering muncul dalam kode tingkat rendah. Ketika ref struct yang terlibat adalah Span<T>, trik di atas dapat digunakan. Ini tidak berlaku untuk jenis ref struct lain dan dapat mengakibatkan kode tingkat rendah yang perlu menggunakan unsafe untuk mengatasi ketidakmampuan untuk menentukan masa pakai dengan benar.
nilai parameter terlingkup
Salah satu sumber konflik berulang dalam kode tingkat rendah adalah mekanisme pelolosan default untuk parameter yang permisif. Mereka ref struct lebih sering digunakan, dan padanan default ini dapat menyebabkan konflik dengan bagian lain dari aturan konteks aman ref.
Titik gesekan utama terjadi karena argumen metode harus cocok dengan aturan. Aturan ini umumnya digunakan dengan metode instans pada ref struct di mana setidaknya satu parameter juga merupakan ref struct. Ini adalah pola umum dalam kode tingkat rendah di mana jenis ref struct umumnya memanfaatkan parameter Span<T> dalam metodenya. Misalnya itu akan terjadi pada gaya penulis apa pun ref struct yang menggunakan Span<T> untuk melewati buffer.
Aturan ini ada untuk mencegah skenario seperti berikut ini:
ref struct RS
{
Span<int> _field;
void Set(Span<int> p)
{
_field = p;
}
static void DangerousCode(ref RS p)
{
Span<int> span = stackalloc int[] { 42 };
// Error: if allowed this would let the method return a reference to
// the stack
p.Set(span);
}
}
Pada dasarnya aturan ini ada karena bahasa harus mengasumsikan bahwa semua input ke metode lolos ke maksimum yang diizinkan konteks aman. Ketika ada parameter ref atau out, termasuk penerima, input dapat lolos sebagai bidang dari nilai ref tersebut (seperti yang terjadi pada RS.Set di atas).
Dalam praktiknya, ada banyak metode seperti itu yang meneruskan ref struct sebagai parameter tanpa bermaksud untuk menggunakannya dalam hasil. Ini hanyalah nilai yang digunakan dalam metode saat ini. Misalnya:
ref struct JsonReader
{
Span<char> _buffer;
int _position;
internal bool TextEquals(ReadOnlySpan<char> text)
{
var current = _buffer.Slice(_position, text.Length);
return current == text;
}
}
class C
{
static void M(ref JsonReader reader)
{
Span<char> span = stackalloc char[4];
span[0] = 'd';
span[1] = 'o';
span[2] = 'g';
// Error: The safe-context of `span` is function-member
// while `reader` is outside function-member hence this fails
// by the above rule.
if (reader.TextEquals(span))
{
...
}
}
}
Untuk mengatasi ini, kode tingkat rendah akan menggunakan trik unsafe untuk menyesatkan kompilator tentang masa pakai ref structmereka. Ini secara signifikan mengurangi proposisi nilai ref struct karena dimaksudkan untuk menjadi sarana untuk menghindari unsafe sambil terus menulis kode performa tinggi.
Di sinilah scoped adalah alat yang efektif pada parameter ref struct karena menghapusnya dari pertimbangan sebagai dikembalikan dari metode sesuai dengan argumen metode yang diperbarui harus cocok dengan aturan. Parameter ref struct yang digunakan, tetapi tidak pernah dikembalikan, dapat diberi label sebagai scoped untuk membuat situs panggilan lebih fleksibel.
ref struct JsonReader
{
Span<char> _buffer;
int _position;
internal bool TextEquals(scoped ReadOnlySpan<char> text)
{
var current = _buffer.Slice(_position, text.Length);
return current == text;
}
}
class C
{
static void M(ref JsonReader reader)
{
Span<char> span = stackalloc char[4];
span[0] = 'd';
span[1] = 'o';
span[2] = 'g';
// Okay: the compiler never considers `span` as capturable here hence it doesn't
// contribute to the method arguments must match rule
if (reader.TextEquals(span))
{
...
}
}
}
Mencegah penugasan referensi yang rumit dari mutasi yang hanya bisa dibaca
Ketika ref dibawa ke bidang readonly di dalam konstruktor atau anggota init, tipenya adalah ref, bukan ref readonly. Ini adalah perilaku jangka panjang yang memungkinkan kode seperti berikut:
struct S
{
readonly int i;
public S(string s)
{
M(ref i);
}
static void M(ref int i) { }
}
Itu memang menimbulkan masalah potensial, walaupun jika ref seperti itu dapat dimasukkan ke dalam bidang ref pada jenis yang sama. Hal ini akan memungkinkan perubahan langsung pada readonly struct dari anggota instance:
readonly ref struct S
{
readonly int i;
readonly ref int r;
public S()
{
i = 0;
// Error: `i` has a narrower scope than `r`
r = ref i;
}
public void Oops()
{
r++;
}
}
Usulan ini mencegah hal tersebut karena melanggar aturan konteks aman referensi. Pertimbangkan hal berikut:
- konteks aman
dari adalah anggota fungsi dan konteks aman adalah konteks pemanggil . Keduanya adalah standar untuk thisdalam anggotastruct. - konteks
ref-safe dari adalah anggota fungsi . Ini berasal dari aturan masa pakai bidang . Khususnya aturan 4.
Pada saat itu, baris r = ref i dianggap ilegal menurut aturan penugasan ulang referensi .
Aturan ini tidak dimaksudkan untuk mencegah perilaku ini tetapi melakukannya sebagai efek samping. Penting untuk mengingat hal ini untuk setiap pembaruan aturan di masa mendatang untuk mengevaluasi dampak terhadap skenario seperti ini.
Penugasan berulang yang tidak masuk akal
Salah satu aspek yang menjadi tantangan desain ini adalah seberapa bebas ref dapat dikembalikan melalui suatu metode. Memungkinkan semua ref untuk dikembalikan dengan kebebasan seperti nilai normal adalah apa yang sebagian besar pengembang harapkan secara intuitif. Namun, hal ini memungkinkan skenario patologis yang harus dipertimbangkan oleh pengkompilasi saat menghitung keamanan referensi. Pertimbangkan hal berikut:
ref struct S
{
int field;
ref int refField;
static void SelfAssign(ref S s)
{
// Error: s.field can only escape the current method through a return statement
s.refField = ref s.field;
}
}
Ini bukan pola kode yang kami harapkan digunakan pengembang apa pun. Namun, ketika ref dapat dikembalikan dengan masa pakai yang sama seperti nilai, maka itu sah menurut aturan. Pengkompilasi harus mempertimbangkan semua kasus hukum saat mengevaluasi panggilan metode dan ini menyebabkan API tersebut tidak dapat digunakan secara efektif.
void M(ref S s)
{
...
}
void Usage()
{
// safe-context to caller-context
S local = default;
// Error: compiler is forced to assume the worst and concludes a self assignment
// is possible here and must issue an error.
M(ref local);
}
Untuk membuat API ini dapat digunakan, pengkompilasi memastikan bahwa masa pakai ref untuk parameter ref lebih kecil dari masa pakai referensi apa pun dalam nilai parameter terkait. Ini adalah alasan untuk memiliki konteks ref-safe sebagai bagi ref hingga ref struct menjadi return-only dan out menjadi konteks penelepon . Itu mencegah penugasan siklik karena perbedaan masa pakai.
Perhatikan bahwa
S F()
{
S local = new();
// Error: self assignment possible inside `S.M`.
S.M(ref local);
return local;
}
ref struct S
{
int field;
ref int refField;
public static void M([UnscopedRef] ref S s)
{
// Allowed: s has both safe-context and ref-safe-context of caller-context
s.refField = ref s.field;
}
}
Demikian pula
Mempromosikan [UnscopedRef] ref ke konteks pemanggil berguna ketika tipe bukanref struct (perhatikan bahwa kami ingin menjaga aturan tetap sederhana agar tidak membedakan antara referensi ke struktur ref versus non-ref):
int x = 1;
F(ref x).RefField = 2;
Console.WriteLine(x); // prints 2
static S F([UnscopedRef] ref int x)
{
S local = new();
local.M(ref x);
return local;
}
ref struct S
{
public ref int RefField;
public void M([UnscopedRef] ref int data)
{
RefField = ref data;
}
}
Dalam hal anotasi lanjutan, desain [UnscopedRef] membuat yang berikut:
ref struct S { }
// C# code
S Create1(ref S p)
S Create2([UnscopedRef] ref S p)
// Annotation equivalent
scoped<'b> S Create1(scoped<'a> ref scoped<'b> S)
scoped<'a> S Create2(scoped<'a> ref scoped<'b> S)
where 'b >= 'a
readonly tidak dapat diterapkan secara mendalam melalui bidang ref
Pertimbangkan sampel kode di bawah ini:
ref struct S
{
ref int Field;
readonly void Method()
{
// Legal or illegal?
Field = 42;
}
}
Saat merancang aturan untuk bidang ref pada instans readonly secara terisolasi, aturan dapat dirancang dengan sah sehingga yang disebutkan di atas menjadi legal atau ilegal. Pada dasarnya readonly dapat secara valid terletak lebih dalam melalui bidang ref atau hanya dapat berlaku untuk ref. Menerapkan hanya ke ref mencegah penetapan ulang ref tetapi memungkinkan penetapan normal yang mengubah nilai yang dirujuk.
Desain ini tidak berdiri sendiri, melainkan merancang aturan untuk jenis yang sudah secara efektif memiliki bidang ref. Yang paling menonjol, Span<T>, sudah memiliki ketergantungan kuat pada readonly yang tidak mendalam di sini. Skenario utamanya adalah kemampuan untuk menetapkan bidang ref melalui instans readonly.
readonly ref struct SpanOfOne
{
readonly ref int Field;
public ref int this[int index]
{
get
{
if (index != 1)
throw new Exception();
return ref Field;
}
}
}
Ini berarti kita harus memilih interpretasi dangkal readonly.
Pemodelan konstruktor
Salah satu pertanyaan desain yang halus adalah: Bagaimana badan konstruktor dimodelkan untuk keamanan ref? Pada dasarnya bagaimana konstruktor berikut dianalisis?
ref struct S
{
ref int field;
public S(ref int f)
{
field = ref f;
}
}
Ada sekitar dua pendekatan:
- Model sebagai metode
di mana adalah lokal di mana konteks aman konteks pemanggil - Model sebagai metode
staticdi manathisadalah parameterout.
Selanjutnya konstruktor harus memenuhi invarian berikut:
- Pastikan parameter
refdapat diambil sebagai bidangref. - Pastikan bahwa
refke bidangthistidak dapat diloloskan melalui parameterref. Itu akan melanggar tugas ref rumit.
Tujuannya adalah untuk memilih formulir yang memenuhi invarian kami tanpa pengenalan aturan khusus untuk konstruktor. Mengingat bahwa model terbaik untuk konstruktor adalah melihat this sebagai parameter out.
kembali hanya sifat out memungkinkan kita untuk memenuhi semua invarian di atas tanpa casing khusus:
public static void ctor(out S @this, ref int f)
{
// The ref-safe-context of `ref f` is *return-only* which is also the
// safe-context of `this.field` hence this assignment is allowed
@this.field = ref f;
}
Argumen metode harus cocok
Argumen metode harus cocok dengan aturan adalah sumber kebingungan umum bagi pengembang. Ini adalah aturan yang memiliki sejumlah kasus khusus yang sulit dipahami kecuali Anda terbiasa dengan penalaran di balik aturan. Demi pemahaman yang lebih baik tentang alasan aturan, kami akan menyederhanakan konteks aman ref dan konteks aman menjadi hanya konteks.
Metode dapat secara bebas mengembalikan status yang diteruskan ke mereka sebagai parameter. Pada dasarnya setiap status yang dapat dijangkau yang tidak tercakup dapat dikembalikan (termasuk dikembalikan oleh ref). Ini dapat dikembalikan langsung melalui pernyataan return atau secara tidak langsung dengan menetapkan ke dalam nilai ref.
Pengembalian langsung tidak menimbulkan masalah yang signifikan untuk keamanan ref. Pengkompilasi hanya perlu melihat semua input yang dapat dikembalikan ke metode dan kemudian secara efektif membatasi nilai pengembalian menjadi konteks minimum input. Nilai pengembalian tersebut kemudian melalui pemrosesan normal.
Pengembalian tidak langsung menimbulkan masalah yang signifikan karena semua ref merupakan input dan output untuk metode ini. Output ini sudah memiliki konteks yang diketahui. Kompilator tidak dapat menyimpulkan yang baru, ia harus mempertimbangkannya pada tingkat saat ini. Itu berarti pengkompilasi harus melihat setiap ref.
Argumen metode harus cocok adalah proses di mana pengkompilasi menegaskan pemeriksaan keamanan ini.
Cara berbeda untuk mengevaluasi hal ini yang sering lebih mudah dipertimbangkan pengembang adalah dengan melakukan latihan berikut:
- Lihat definisi metode mengidentifikasi semua tempat di mana status dapat dikembalikan secara tidak langsung: a. Parameter
refyang dapat diubah menunjuk keref structb. Parameterrefyang dapat diubah dengan bidangrefyang dapat ditetapkan ref c. Parameterrefatau kolomrefyang dapat ditetapkan mengarah keref struct(pertimbangkan secara rekursif) - Lihat situs panggilan a. Identifikasi konteks yang sesuai dengan lokasi yang diidentifikasi di atas b. Identifikasi konteks semua input ke metode yang dapat dikembalikan (jangan sejajar dengan parameter
scoped)
Jika ada nilai dalam 2.b yang lebih kecil dari 2.a, maka panggilan metode harus ilegal. Mari kita lihat beberapa contoh untuk mengilustrasikan aturan:
ref struct R { }
class Program
{
static void F0(ref R a, scoped ref R b) => throw null;
static void F1(ref R x, scoped R y)
{
F0(ref x, ref y);
}
}
Melihat kepada panggilan ke F0, mari kita bahas (1) dan (2). Parameter dengan potensi pengembalian tidak langsung a dan b karena keduanya dapat langsung ditetapkan. Argumen yang sesuai dengan parameter tersebut adalah:
-
amemetakan kexyang memiliki konteks konteks pemanggil dari konteks -
byang dipetakan keydengan konteks dari anggota-fungsi
Kumpulan masukan yang dapat dikembalikan ke metode adalah
-
xdengan cakupan escape dari konteks pemanggil -
ref xdengan cakupan escape dari konteks pemanggil -
ydengan cakupan escape dari anggota fungsi
Nilai ref y tidak dapat dikembalikan karena dipetakan ke scoped ref sehingga tidak dianggap sebagai input. Tetapi mengingat bahwa setidaknya ada satu input dengan cakupan escape
Variasi yang berbeda adalah sebagai berikut:
ref struct R { }
class Program
{
static void F0(ref R a, ref int b) => throw null;
static void F1(ref R x)
{
int y = 42;
F0(ref x, ref y);
}
}
Sekali lagi parameter dengan potensi pengembalian tidak langsung a dan b karena keduanya dapat langsung ditetapkan. Tetapi b dapat dikecualikan karena tidak menunjuk ke ref struct sehingga tidak dapat digunakan untuk menyimpan status ref. Dengan demikian kita memiliki:
-
amemetakan kexyang memiliki konteks konteks pemanggil dari konteks
Set input yang dapat dikembalikan ke metode adalah:
-
xdengan konteks dari konteks pemanggil -
ref xdengan konteks dari konteks pemanggil -
ref ydengan konteks dari anggota fungsi
Mengingat bahwa setidaknya ada satu input dengan cakupan escape
Ini adalah logika bahwa argumen metode harus sesuai dengan aturan yang berupaya mencakup hal ini. Ini melangkah lebih jauh karena menganggap kedua scoped sebagai cara untuk menghapus input dari pertimbangan dan readonly sebagai cara untuk menghapus ref sebagai output (tidak dapat ditetapkan ke dalam readonly ref sehingga tidak dapat menjadi sumber output). Kasus khusus ini memang menambah kompleksitas pada aturan tetapi dilakukan untuk kepentingan pengembang. Pengkompilasi berusaha menghapus semua input dan output yang diketahuinya tidak dapat berkontribusi pada hasilnya untuk memberi pengembang fleksibilitas maksimum saat memanggil anggota. Sama seperti resolusi kelebihan beban, ada baiknya upaya untuk membuat aturan kami lebih kompleks ketika menciptakan lebih banyak fleksibilitas bagi konsumen.
Contoh konteks aman dari ekspresi deklarasi yang disimpulkan
Terkait dengan Menyimpulkan konteks aman dari ekspresi deklarasi.
ref struct RS
{
public RS(ref int x) { } // assumed to be able to capture 'x'
static void M0(RS input, out RS output) => output = input;
static void M1()
{
var i = 0;
var rs1 = new RS(ref i); // safe-context of 'rs1' is function-member
M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
}
static void M2(RS rs1)
{
M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
}
static void M3(RS rs1)
{
M0(rs1, out scoped var rs2); // 'scoped' modifier forces safe-context of 'rs2' to the current local context (function-member or narrower).
}
}
Perhatikan bahwa konteks lokal yang dihasilkan dari pengubah scoped adalah yang paling sempit yang mungkin dapat digunakan untuk variabel --menjadi lebih sempit berarti ekspresi mengacu pada variabel yang hanya dideklarasikan dalam konteks yang lebih sempit daripada ekspresi.
C# feature specifications