Bagikan melalui


Peningkatan Struktur Tingkat Rendah

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:

  1. ref bidang dan scoped
  2. [UnscopedRef]

Fitur-fitur ini tetap membuka proposal untuk versi C yang akan datang#:

  1. ref bidang ke ref struct
  2. 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 struct untuk mendeklarasikan kolom ref.
  • Izinkan runtime untuk sepenuhnya menentukan Span<T> menggunakan sistem jenis C# dan menghapus jenis kasus khusus seperti ByReference<T>
  • Izinkan jenis struct mengembalikan ref ke kolom mereka.
  • Izinkan runtime untuk menghapus penggunaan unsafe yang disebabkan oleh batasan default seumur hidup
  • Izinkan deklarasi buffer fixed yang aman untuk tipe terkelola dan tidak terkelola dalam struct

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 metode init. 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 parameter in dapat dikonversi ulang ke bidang ref.
  • readonly ref readonly: kombinasi ref readonly dan readonly 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:

  1. Jika adalah bidang konteks ref-safe-adalah konteks aman .
  2. Jika tidak, jika adalah jenis referensi, konteks aman ref dari konteks pemanggil
  3. 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 e2 kedua hal berikut ini harus benar:

  1. e2 harus memiliki konteks ref-safe- setidaknya sebesar konteks ref-safe- dari e1
  2. harus 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 atau nilai yang merupakan dan memiliki dampak membatasi konteks aman ref atau masa pakai konteks aman, masing-masing, ke anggota fungsi . Misalnya:

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 yang memiliki aturan konteks ref-safe yang berbeda dari parameter lainnya (lihat semua referensi untuk menyertakan atau mengecualikan penerima dalam aturan konteks yang aman). Sekarang dapat dinyatakan sebagai konsep umum di seluruh aturan yang lebih menyederhanakannya.

Anotasi scoped juga dapat diterapkan ke lokasi berikut:

  • lokal: Anotasi ini menetapkan masa pakai sebagai konteks aman, atau konteks aman-referensi jika terjadi ref lokal, 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 dan yang dapat dikembalikan sebagai bidang , bahasa akan mengubah nilai konteks ref-safe-context default untuk parameter menjadi . 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 aman variabel deklarasi dari argumen () atau dekonstruksi () adalah tersempit dari yang berikut:

  • 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 out dan 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

Lihat juga Contoh ekspresi deklarasi konteks aman yang disimpulkan.

Parameter scoped secara implisit

Secara keseluruhan ada dua lokasi ref yang secara implisit dinyatakan sebagai scoped:

  • this pada metode instans struct
  • out parameter

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 argumen yang sesuai dengan parameter , argumen tersebut akan digeneralisasi sebagai argumen dalam spesifikasi. Jika argumen adalah lvalue, maka ref-safe-context adalah lvalue, jika tidak, itu anggota fungsi . Sekali lagi 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 karena dapat dikembalikan tetapi hanya dapat dikembalikan melalui pernyataan .

Detail hanya kembali adalah konteks yang lebih besar dari anggota fungsi tetapi lebih kecil darikonteks pemanggil . Ekspresi yang diberikan ke pernyataan 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 struct dalam 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 out sama ekspresifnya. Ini tidak memiliki masalah penugasan siklik yang konyol karena out secara implisit adalah scoped sehingga 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 harus memiliki konteks aman setidaknya hanya-kembali. Namun, ini bukan kasus khusus, ini hanya mengikuti dari aturan penugasan yang ada.

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:

  1. Jika pscoped ref maka expr tidak berkontribusi konteks ref-safe-context saat mempertimbangkan argumen.
  2. Jika pscoped maka expr tidak berkontribusi konteks aman saat mempertimbangkan argumen.
  3. 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 mana M() tidak mengembalikan ref-to-ref-struct, memiliki konteks aman yang diambil dari yang paling sempit dari yang berikut ini:

  1. konteks pemanggil
  2. Saat pengembalian adalah konteks aman dikontribusikan oleh semua ekspresi argumen
  3. Ketika pengembalian adalah ref struct, konteks ref-safe- yang dikontribusikan oleh semua argumen ref

Jika 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 aman yang berbeda karena argumen metode harus cocok dengan.

Aturan panggilan ref dapat disederhanakan ke:

Nilai yang dihasilkan dari pemanggilan metode ref e1.M(e2, ...), di mana M() tidak mengembalikan ref-to-ref-struct, adalah ref-safe-context dalam konteks tersipit berikut:

  1. konteks pemanggil
  2. Konteks aman berasal dari semua ekspresi argumen.
  3. konteks ref-safe-context dikontribusikan oleh semua argumen ref

Jika 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:

  1. Konteks aman dari panggilan konstruktor.
  2. Konteks aman dan ref-konteks aman dari argumen untuk pengindeks inisialisasi anggota yang bisa mengalir ke penerima.
  3. 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

  1. 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
  2. Semua argumen ref dari jenis ref struct harus dapat ditetapkan oleh nilai dengan konteks aman tersebut. Ini adalah kasus di mana ref tidak tidak menggeneralisasi untuk menyertakan in dan out

e.M(a1, a2, ... aN) untuk pemanggilan metode apa pun

  1. Hitung konteks aman tersempit dari:
    • konteks pemanggil
    • konteks aman dari semua argumen
    • konteks ref-safe dari semua argumen ref yang parameter terkaitnya tidak scoped
  2. Semua argumen out dari jenis ref struct harus 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 scoped ke parameter ref atau in
  • Menambahkan scoped ke parameter ref struct
  • Menghapus [UnscopedRef] dari parameter out
  • Menghapus [UnscopedRef] dari parameter ref dari jenis ref 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 ref atau out jenis ref struct dengan ketidakcocokan dalam menambahkan [UnscopedRef] (tidak menghapus scoped). (Dalam hal ini, penugasan siklik konyol dimungkinkan, sehingga tidak ada parameter lain yang diperlukan.)
  • Atau kedua hal ini benar:
    • Metode mengembalikan ref struct atau mengembalikan ref atau ref readonly, atau metode memiliki parameter ref atau out dari jenis ref struct.
    • Metode ini memiliki setidaknya satu parameter ref, in, atau out tambahan, atau parameter jenis ref struct.

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 out yang sudah seperti biasanya digunakan dalam tanda tangan metode TryParse), dan melaporkan ketidakcocokan cakupan hanya karena digunakan dalam versi bahasa 11 (sehingga parameter out memiliki 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 scoped dan atribut [UnscopedRef] tidak memengaruhi persembunyian
  • Kelebihan beban tidak dapat berbeda hanya pada scoped atau [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 out akan memiliki konteks aman dari anggota fungsi.

Catatan Terperinci:

  • Bidang ref hanya dapat dideklarasikan di dalam ref struct
  • Bidang ref tidak dapat dideklarasikan static, volatile, atau const
  • Bidang ref tidak boleh memiliki tipe yang ref struct
  • Proses pembuatan perakitan referensi harus mempertahankan keberadaan bidang ref di dalam ref struct
  • readonly ref struct harus mendeklarasikan bidang ref sebagai readonly ref
  • Untuk nilai by-ref, pengubah scoped harus muncul sebelum in, out, atau ref
  • 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 langversion adalah 11 atau lebih tinggi

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
    ;

12.7 Ekspresi dekonstruksi:

[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:

  • __makeref akan diperlakukan sebagai metode dengan tanda tangan static TypedReference __makeref<T>(ref T value)
  • __refvalue akan diperlakukan sebagai metode dengan signature static ref T __refvalue<T>(TypedReference tr). Ekspresi __refvalue(tr, int) akan secara efektif menggunakan argumen kedua sebagai parameter jenis.
  • __arglist sebagai 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 default ini wajar, tetapi tidak ada yang secara inheren salah dengan penghindaran melalui referensi, ini hanya default yang dipilih oleh aturan konteks aman referensi.

Untuk memperbaikinya, bahasa ini akan memberikan kebalikan dari anotasi masa hidup scoped dengan menyediakan dukungan untuk UnscopedRefAttribute. Ini dapat diterapkan pada mana pun dan akan mengubah ref-safe-context menjadi satu tingkat lebih lebar dari tingkat defaultnya. Misalnya:

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 dari this yang 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, anggota init, atau konstruktor pada struct
    • Parameter yang ditandai scoped
    • Parameter yang diteruskan berdasarkan nilai
    • Parameter yang diteruskan melalui referensi yang tidak dicakup secara implisit

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.

  1. parameter ref/in/out yang tidak terlingkup dapat lolos dari pemanggilan metode sebagai bidang refref struct di C#11, bukan di C#7.2
  2. Parameter out dilingkup secara implisit dalam C#11, dan tidak dilibatkan dalam C#7.2.
  3. ref / in parameter untuk jenis ref struct secara 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 mana version adalah 11, panggilan metode dianalisis dengan aturan C#11.
  • Jika modul yang berisi deklarasi metode berasal dari sumber, dan dikompilasi dengan -langversion:11 atau dengan corlib yang berisi bendera fitur untuk bidang ref, 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 new yang 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:

  1. konteks pemanggil
  2. Konteks aman berasal dari semua ekspresi argumen.
  3. 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:

  1. Menurut pengembalian nilai
  2. Dengan pengembalian ref
  3. Dengan bidang ref di ref struct yang dikembalikan atau diteruskan sebagai parameter ref / 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 struct pada tanda tangan
    • Di mana ref struct adalah jenis pengembalian, ref, atau parameter out
    • Memiliki parameter in atau ref tambahan yang tidak termasuk penerima

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 struct yang memiliki bidang ref tidak pernah dianggap unmanaged
  • Jenis bidang ref berdampak pada aturan ekspansi generik tak terbatas. Oleh karena itu, jika jenis bidang ref berisi 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 : $a untuk semua $a masa pakai
  • where $cm : $ro
  • where $x : $local untuk 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 ref harus dapat dikonversi ke $this
  • Masa pakai $this semua bidang non-ref harus $heap atau $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 $heap dan $local.
  • Semua jenis T yang bukan ref struct secara implisit memiliki masa pakai T<$heap>. Ini implisit, tidak perlu menulis int<$heap> di setiap sampel.
  • Untuk bidang ref yang didefinisikan sebagai ref<$l0> T<$l1, $l2, ... $ln>:
    • Semua masa pakai $l1 hingga $ln harus tetap tidak berubah.
    • Masa pakai $l0 harus dapat dikonversi ke $this
  • Untuk ref yang didefinisikan sebagai ref<$a> T<$b, ...>, $b harus dapat dikonversi ke $a
  • Masa hidup ref dari suatu variabel ditentukan oleh:
    • Untuk lokal ref, parameter, bidang, atau pengembalian dengan jenis ref<$a> T, masa hidupnya adalah $a
    • $heap untuk semua jenis referensi dan bidang jenis referensi
    • $local untuk semua yang lain
  • 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 $a untuk T<...>
    • ref<$a> (T<$b>)expr masa berlaku nilai adalah $b untuk T<...> 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 ref adalah $ro
  • parameter jenis ref struct memiliki masa pakai $cm ini
  • pengembalian ref memiliki masa pakai ref $ro
  • pengembalian jenis ref struct memiliki masa pakai nilai $ro
  • scoped pada parameter atau ref mengubah 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> atau ref struct
    • Di mana ref struct adalah jenis pengembalian, ref, atau parameter out
    • Memiliki parameter in atau ref tambahan (tidak termasuk penerima)

Untuk memahami dampaknya, sangat membantu untuk memecah API ke dalam kategori:

  1. Meminta konsumen untuk memperhitungkan bahwa ref dicatat sebagai bidang ref. Contoh utama adalah konstruktor Span(ref T value)
  2. Jangan biarkan konsumen menganggap ref sebagai bidang ref. Meskipun begitu, item-item ini terbagi menjadi dua kategori
    1. API tidak aman. Ini adalah API-API di dalam jenis Unsafe dan MemoryMarshal, di mana MemoryMarshal.CreateSpan adalah yang paling menonjol. API ini memang menangkap ref dengan cara yang tidak aman, tetapi mereka juga sudah dikenal sebagai API yang berisiko.
    2. API aman. Ini adalah API-API yang menggunakan parameter ref untuk efisiensi tetapi sebenarnya tidak terobservasi di mana pun. Contohnya berukuran kecil, tetapi salah satunya adalah AsnDecoder.ReadEnumeratedBytes

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:

  1. 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.
  2. 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 ke scoped ref
  • [DoesNotEscape] diarahkan ke scoped
  • [RefDoesEscape] diarahkan ke unscoped

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:

  1. Unsafe.AsRef<T>(in T value) dapat memperluas tujuan yang ada dengan mengubah ke scoped in T value. Ini akan memungkinkannya untuk menghapus in dan scoped dari parameter. Kemudian menjadi metode "hapus keamanan ref" universal
  2. Perkenalkan metode baru yang seluruh tujuannya adalah untuk menghapus scoped: ref T Unsafe.AsUnscoped<T>(scoped in T value). Ini mencakup juga penghapusan in, 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:

  • this adalah scoped ref
  • out adalah scoped 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:

  • this sebagai ref: struct, readonly ref struct, atau readonly member
  • this dalam bentuk scoped ref: ref struct atau readonly ref struct dengan bidang ref ke ref 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 di mana jenis adalah , #ref-field-safe-context harus sama.

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:

  1. ref bidang dan scoped
  2. [UnscopedRef]
  3. ref bidang ke ref struct
  4. Jenis terbatas matahari terbenam
  5. 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.

Masalah

Semua masalah berikut terkait dengan proposal ini:

Proposal

Proposal berikut terkait dengan proposal ini:

Sampel yang ada

Utf8JsonReader

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.

Utf8JsonWriter

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 untuk menentukan konteks aman lokal tetapi sekarang hanya dapat menggunakan .

// 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 konteks aman ke konteks pemanggil. Ini adalah default yang masuk akal karena berbaris dengan pola pengkodian .NET secara keseluruhan. Dalam kode level rendah, 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 this dalam anggota struct.
  • 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 mempromosikan konteks ref-safe-dari apa pun untuk nilai konteks pemanggil dan karenanya memungkinkan penugasan siklik dan memaksa penggunaan viral meningkatkan rantai panggilan:

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 memungkinkan penugasan siklik karena parameter memiliki konteks aman dan konteks aman ref hanya kembali.

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:

  1. Model sebagai metode di mana adalah lokal di mana konteks aman konteks pemanggil
  2. Model sebagai metode static di mana this adalah parameter out.

Selanjutnya konstruktor harus memenuhi invarian berikut:

  1. Pastikan parameter ref dapat diambil sebagai bidang ref.
  2. Pastikan bahwa ref ke bidang this tidak dapat diloloskan melalui parameter ref. 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 yang dapat ditetapkan dalam metode yang disebut, mengevaluasi konteks-nya , dan kemudian memverifikasi bahwa tidak ada input yang dapat dikembalikan ke metode memiliki konteks yang lebih kecil dari yang . Jika ada kasus seperti itu maka panggilan metode harus ilegal karena dapat melanggar keamanan 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:

  1. Lihat definisi metode mengidentifikasi semua tempat di mana status dapat dikembalikan secara tidak langsung: a. Parameter ref yang dapat diubah menunjuk ke ref struct b. Parameter ref yang dapat diubah dengan bidang ref yang dapat ditetapkan ref c. Parameter ref atau kolom ref yang dapat ditetapkan mengarah ke ref struct (pertimbangkan secara rekursif)
  2. 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:

  • a memetakan ke x yang memiliki konteks konteks pemanggil dari konteks
  • b yang dipetakan ke y dengan konteks dari anggota-fungsi

Kumpulan masukan yang dapat dikembalikan ke metode adalah

  • x dengan cakupan escape dari konteks pemanggil
  • ref x dengan cakupan escape dari konteks pemanggil
  • y dengan 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 yang lebih kecil (argumen) daripada salah satu output (argumen), panggilan metode tersebut ilegal.

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:

  • a memetakan ke x yang memiliki konteks konteks pemanggil dari konteks

Set input yang dapat dikembalikan ke metode adalah:

  • x dengan konteks dari konteks pemanggil
  • ref x dengan konteks dari konteks pemanggil
  • ref y dengan konteks dari anggota fungsi

Mengingat bahwa setidaknya ada satu input dengan cakupan escape yang lebih kecil ( argumen) daripada salah satu output (argumen) panggilan metode ilegal.

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.