Jenis Rentang Kelas Utama

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 tertuangkan dalam catatan rapat terkait desain bahasa (LDM) .

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

Edisi unggulan: https://github.com/dotnet/csharplang/issues/8714

Ringkasan

Kami memperkenalkan dukungan kelas satu untuk Span<T> dan ReadOnlySpan<T> dalam bahasa, termasuk jenis konversi implisit baru dan mempertimbangkannya di lebih banyak tempat, memungkinkan pemrograman yang lebih alami dengan jenis integral ini.

Motivasi

Sejak diperkenalkan di C# 7.2, Span<T> dan ReadOnlySpan<T> telah diintegrasikan ke dalam bahasa dan pustaka kelas dasar (BCL) dalam banyak cara penting. Ini bagus untuk pengembang, karena pengenalan mereka meningkatkan performa tanpa membebani keselamatan pengembang. Namun, bahasa ini telah menjaga jenis-jenis ini pada jarak tertentu dalam beberapa cara utama, yang membuatnya sulit untuk mengekspresikan maksud API dan menyebabkan terjadinya banyak duplikasi pada API baru. Misalnya, BCL telah menambahkan sejumlah API primitif tensor baru di .NET 9, tetapi API ini semuanya ditawarkan pada ReadOnlySpan<T>. C# tidak mengenali hubungan antara ReadOnlySpan<T>, Span<T>, dan T[], jadi meskipun ada konversi yang ditentukan pengguna antara jenis ini, mereka tidak dapat digunakan untuk penerima metode ekstensi, tidak dapat menyusun dengan konversi lain yang ditentukan pengguna, dan tidak membantu dengan semua skenario inferensi jenis generik. Pengguna perlu menggunakan konversi eksplisit atau argumen tipe, ini berarti bahwa alat IDE tidak memandu pengguna untuk menggunakan API-API ini, karena tidak ada yang menunjukkan kepada IDE bahwa valid untuk meneruskan tipe-tipe ini setelah dikonversi. Untuk memberikan kegunaan maksimum untuk gaya API ini, BCL harus menentukan seluruh set Span<T> dan T[] overload, yang membuat banyak area permukaan duplikat yang harus dipertahankan tanpa keuntungan nyata. Proposal ini berusaha mengatasi masalah dengan meminta bahasa lebih langsung mengenali jenis dan konversi ini.

Misalnya, BCL hanya dapat menambahkan satu kelebihan beban pembantu MemoryExtensions seperti:

int[] arr = [1, 2, 3];
Console.WriteLine(
    arr.StartsWith(1) // CS8773 in C# 13, permitted with this proposal
    );

public static class MemoryExtensions
{
    public static bool StartsWith<T>(this ReadOnlySpan<T> span, T value) where T : IEquatable<T> => span.Length != 0 && EqualityComparer<T>.Default.Equals(span[0], value);
}

Sebelumnya, overload untuk Span dan array diperlukan untuk membuat metode ekstensi dapat digunakan pada variabel bertipe Span/array karena konversi yang ditentukan oleh pengguna (yang ada antara Span/array/ReadOnlySpan) tidak dipertimbangkan untuk penerima ekstensi.

Desain Terperinci

Perubahan dalam proposal ini akan terkait dengan LangVersion >= 14.

Konversi jangkauan

Kami menambahkan jenis konversi implisit baru ke daftar dalam §10.2.1, konversi rentang implisit . Konversi ini adalah konversi dari jenis dan didefinisikan sebagai berikut:


Konversi rentang implisit memungkinkan array_types, System.Span<T>, System.ReadOnlySpan<T>, dan string untuk dikonversi satu sama lain sebagai berikut:

  • Dari dimensi tunggal mana pun array_type berjenis elemen Ei hingga System.Span<Ei>
  • Dari array_type berdimensi tunggal mana pun dengan jenis elemen Ei ke System.ReadOnlySpan<Ui>, asalkan Ei dapat dikonversi secara kovarians (§18.2.3.3) ke Ui.
  • Dari System.Span<Ti> ke System.ReadOnlySpan<Ui>, dengan syarat kovarians Ti dapat dikonversi (§18.2.3.3) ke Ui
  • Dari System.ReadOnlySpan<Ti> ke System.ReadOnlySpan<Ui>, dengan syarat kovarians Ti dapat dikonversi (§18.2.3.3) ke Ui
  • Dari string ke System.ReadOnlySpan<char>

Setiap jenis Span/ReadOnlySpan dianggap berlaku untuk konversi jika mereka adalah ref structdan mereka cocok dengan nama lengkap (LDM 2024-06-24).

Kami juga menambahkan konversi rentang implisit ke daftar konversi implisit standar (§10,4,2). Ini memungkinkan resolusi kelebihan beban untuk mempertimbangkannya saat melakukan resolusi argumen, seperti dalam proposal API yang ditautkan sebelumnya.

Konversi rentang eksplisit adalah sebagai berikut:

  • Semua konversi rentang implisit .
  • Dari array_type dengan tipe elemen Ti ke System.Span<Ui> atau System.ReadOnlySpan<Ui> dengan syarat terdapat konversi referensi eksplisit dari Ti ke Ui.

Tidak ada konversi rentang eksplisit standar, tidak seperti konversi eksplisit standar lain (§10.4.3) yang selalu ada dengan adanya konversi implisit standar yang berlawanan.

Konversi yang ditentukan pengguna

Konversi yang ditentukan pengguna tidak dipertimbangkan saat mengonversi antara tipe yang memiliki konversi span implisit atau eksplisit.

Konversi rentang implisit dikecualikan dari aturan bahwa tidak dimungkinkan untuk menentukan operator yang didefinisikan oleh pengguna di antara jenis di mana konversi yang bukan didefinisikan oleh pengguna ada (§10.5.2 Konversi yang didefinisikan oleh pengguna yang diizinkan). Ini diperlukan agar BCL dapat terus mendefinisikan operator konversi Span yang ada bahkan ketika mereka beralih ke C# 14 (mereka masih diperlukan untuk LangVersions yang lebih rendah dan juga karena operator ini digunakan dalam codegen dari konversi rentang standar baru). Namun, ini dapat dilihat sebagai detail implementasi (codegen dan LangVersions yang lebih rendah bukan bagian dari spesifikasi), dan Roslyn melanggar bagian dari spesifikasi ini, karena aturan yang spesifik ini tentang konversi yang ditentukan pengguna tidak diberlakukan.

Penerima ekstensi

Kami juga menambahkan konversi rentang implisit ke daftar konversi implisit yang dapat diterima pada parameter pertama metode ekstensi saat menentukan penerapan (12.8.9.3) (perubahan tebal):

Metode ekstensi Cᵢ.Mₑmemenuhi syarat jika:

  • Cᵢ adalah kelas non-generik dan tidak bersarang
  • Nama Mₑ adalah pengidentifikasi
  • Mₑ dapat diakses dan berlaku saat diterapkan ke argumen sebagai metode statis seperti yang ditunjukkan di atas
  • Identitas implisit, referensi atau tinju, tinju, atau konversi rentang ada dari expr ke tipe parameter pertama dari Mₑ. Konversi rentang tidak dipertimbangkan pada saat resolusi kelebihan beban dilakukan untuk sebuah konversi grup metode.

Perhatikan bahwa konversi rentang implisit tidak dipertimbangkan untuk penerima ekstensi dalam konversi grup metode (LDM 2024-07-15) yang membuat kode berikut terus berfungsi dibandingkan dengan mengakibatkan kesalahan waktu kompilasi CS1113: Extension method 'E.M<int>(Span<int>, int)' defined on value type 'Span<int>' cannot be used to create delegates:

using System;
using System.Collections.Generic;
Action<int> a = new int[0].M; // binds to M<int>(IEnumerable<int>, int)
static class E
{
    public static void M<T>(this Span<T> s, T x) => Console.Write(1);
    public static void M<T>(this IEnumerable<T> e, T x) => Console.Write(2);
}

Sebagai kemungkinan pekerjaan di masa depan, kita dapat mempertimbangkan untuk menghapus kondisi ini, yaitu bahwa konversi rentang tidak dipertimbangkan untuk penerima ekstensi dalam konversi kelompok metode, dan sebaliknya menerapkan perubahan sehingga skenario seperti di atas akan berhasil memanggil kelebihan beban Span sebagai hasilnya.

  • Kompiler dapat menghasilkan sebuah thunk yang akan mengambil array sebagai penerima dan melakukan konversi rentang di dalamnya (mirip dengan pengguna yang membuat delegasi secara manual seperti x => new int[0].M(x)).
  • Delegasi nilai apabila diimplementasikan bisa menerima Span secara langsung sebagai penerima.

Variansi

Tujuan dari bagian varians dalam konversi rentang implisit adalah untuk mereplikasi sejumlah kovariansi untuk System.ReadOnlySpan<T>. Perubahan runtime akan diperlukan untuk mengimplementasikan varians sepenuhnya melalui generik di sini (lihat .. /csharp-13.0/ref-struct-interfaces.md untuk menggunakan ref struct jenis dalam generik), tetapi kita dapat mengizinkan kovariansi dalam jumlah terbatas melalui penggunaan API .NET 9 yang diusulkan: https://github.com/dotnet/runtime/issues/96952. Ini akan memungkinkan bahasa memperlakukan System.ReadOnlySpan<T> seolah-olah T dinyatakan sebagai out T dalam beberapa skenario. Namun, kami tidak menelusuri konversi varian ini melalui semua skenario varians, dan tidak menambahkannya ke definisi varians-konversibel dalam §18.2.3.3. Jika di masa depan, kami mengubah runtime untuk lebih memahami variansi di sini, kita dapat mengambil perubahan kecil untuk sepenuhnya mengenalinya dalam bahasa.

Pola

Perhatikan bahwa ketika ref structdigunakan sebagai jenis dalam pola apa pun, hanya konversi identitas yang diizinkan:

class C<T> where T : allows ref struct
{
    void M1(T t) { if (t is T x) { } } // ok (T is T)
    void M2(R r) { if (r is R x) { } } // ok (R is R)
    void M3(T t) { if (t is R x) { } } // error (T is R)
    void M4(R r) { if (r is T x) { } } // error (R is T)
}
ref struct R { }

Dari spesifikasi , operator tipe (§12.12.12.1):

Hasil operasi E is T [...] adalah nilai Boolean yang menunjukkan apakah E non-null dan dapat berhasil dikonversi ke jenis T melalui konversi referensi, konversi pembungkusan, konversi pembukaan kotak, konversi pembungkusan, atau konversi pembukaan bungkus.

[...]

Jika T adalah jenis nilai yang tidak dapat diubah ke null, hasilnya true jika D dan T adalah jenis yang sama.

Perilaku ini tidak berubah dengan fitur ini, oleh karena itu tidak akan mungkin untuk menulis pola untuk Span/ReadOnlySpan, meskipun pola serupa dimungkinkan untuk array (termasuk varians):

using System;

M1<object[]>(["0"]); // prints
M1<string[]>(["1"]); // prints

void M1<T>(T t)
{
    if (t is object[] r) Console.WriteLine(r[0]); // ok
}

void M2<T>(T t) where T : allows ref struct
{
    if (t is ReadOnlySpan<object> r) Console.WriteLine(r[0]); // error
}

Pembuatan kode

Konversi akan selalu ada, terlepas dari apakah ada pembantu runtime yang digunakan untuk mengimplementasikannya ada (LDM 2024-05-13). Jika penolong tidak ada, upaya menggunakan konversi akan menghasilkan kesalahan saat kompilasi karena adanya anggota yang dibutuhkan kompilator yang tidak ditemukan.

Pengkompilasi mengharapkan untuk menggunakan pembantu atau setara berikut untuk mengimplementasikan konversi:

Konversi Pembantu
array menjadi rentang static implicit operator Span<T>(T[]) (ditentukan dalam Span<T>)
array ke ReadOnlySpan static implicit operator ReadOnlySpan<T>(T[]) (ditentukan dalam ReadOnlySpan<T>)
Rentang ke ReadOnlySpan static implicit operator ReadOnlySpan<T>(Span<T>) (didefinisikan dalam Span<T>) dan static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
ReadOnlySpan ke ReadOnlySpan static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
string ke ReadOnlySpan static ReadOnlySpan<char> MemoryExtensions.AsSpan(string)

Perhatikan bahwa MemoryExtensions.AsSpan digunakan alih-alih operator implisit yang setara yang ditentukan pada string. Ini berarti codegen berbeda antara LangVersions (operator implisit digunakan dalam C# 13; metode statis AsSpan digunakan dalam C# 14). Di sisi lain, konversi bisa dilakukan pada .NET Framework (metode AsSpan ada sedangkan operator string tidak).

Konversi array eksplisit ke (ReadOnly)Span pertama-tama secara eksplisit mengonversi dari array sumber ke array dengan jenis elemen tujuan, lalu ke (ReadOnly)Span menggunakan pembantu yang sama dengan yang digunakan pada konversi implisit, yaitu, op_Implicit(T[])yang sesuai.

Konversi yang lebih baik dari ekspresi

Konversi dari ekspresi (§12.6.4.5) diperbarui untuk lebih mengutamakan konversi implisit rentang. Ini didasarkan pada ekspresi koleksi perubahan resolusi kelebihan beban.

Mengingat C₁ konversi implisit yang mengonversi dari ekspresi E menjadi jenis T₁, dan C₂ konversi implisit yang mengonversi dari ekspresi E menjadi jenis T₂, C₁ adalah konversi yang lebih baik daripada C₂ jika salah satu penangguhan berikut:

  • E adalah ekspresi koleksi, dan C₁ adalah konversi koleksi lebih baik dari ekspresi daripada C₂
  • E bukan ekspresi koleksi dan salah satu kondisi berikut:
    • E sama persis dengan T₁ dan E tidak sama persis dengan T₂
    • E tidak persis sama dengan T₁ dan T₂, dan C₁ adalah konversi rentang implisit sedangkan C₂ bukan konversi rentang implisit.
    • E tepat cocok dengan keduanya atau tidak satupun dari T₁ dan T₂, keduanya atau tidak satupun dari C₁ dan C₂ adalah konversi rentang implisit, dan T₁ lebih baik sebagai target konversi daripadaT₂
  • E adalah grup metode, T₁ kompatibel dengan metode terbaik tunggal dari grup metode untuk konversi C₁, dan T₂ tidak kompatibel dengan metode terbaik tunggal dari grup metode untuk konversi C₂

Target konversi yang lebih baik

Sasaran konversi yang lebih baik (§12.6.4.7) diperbarui agar lebih memilih ReadOnlySpan<T> daripada Span<T>.

Mengingat dua jenis T₁ dan T₂, T₁ adalah target konversi yang lebih baik daripada T₂ jika salah satu dari kondisi berikut berlaku:

  • T₁ System.ReadOnlySpan<E₁>, T₂System.Span<E₂>, dan terdapat konversi identik dari E₁ ke E₂
  • T₁ System.ReadOnlySpan<E₁>, T₂System.ReadOnlySpan<E₂>, dan konversi implisit dari T₁ ke T₂ ada dan tidak ada konversi implisit dari T₂ ke T₁ ada
  • Setidaknya salah satu T₁ atau T₂ tidak System.ReadOnlySpan<Eᵢ> dan tidak System.Span<Eᵢ>, dan konversi implisit dari T₁ ke T₂ ada dan tidak ada konversi implisit dari T₂ ke T₁ ada
  • ...

Rapat desain:

Komentar perbaikan

konversi aturan yang lebih baik dari ekspresi harus memastikan bahwa setiap kali kelebihan beban berlaku karena konversi rentang baru, potensi ambiguitas dengan kelebihan beban lainnya dapat dihindari karena kelebihan beban yang baru berlaku lebih diprioritaskan.

Tanpa aturan ini, kode berikut yang berhasil dikompilasi dalam C# 13 akan mengakibatkan kesalahan ambiguitas di C# 14 karena konversi implisit standar baru dari array ke ReadOnlySpan yang berlaku untuk penerima metode ekstensi:

using System;
using System.Collections.Generic;

var a = new int[] { 1, 2, 3 };
a.M();

static class E
{
    public static void M(this IEnumerable<int> x) { }
    public static void M(this ReadOnlySpan<int> x) { }
}

Aturan ini juga memungkinkan pengenalan API baru yang sebelumnya akan menghasilkan ambiguitas, misalnya:

using System;
using System.Collections.Generic;

C.M(new int[] { 1, 2, 3 }); // would be ambiguous before

static class C
{
    public static void M(IEnumerable<int> x) { }
    public static void M(ReadOnlySpan<int> x) { } // can be added now
}

Peringatan

Karena aturan kesempurnaan didefinisikan untuk konversi rentang yang hanya ada di LangVersion >= 14, penulis API tidak dapat menambahkan kelebihan beban baru tersebut jika mereka ingin tetap mendukung pengguna di LangVersion <= 13. Misalnya, jika .NET 9 BCL memperkenalkan kelebihan beban seperti itu, pengguna yang meningkatkan ke net9.0 TFM tetapi tetap berada di LangVersion yang lebih rendah akan mendapatkan kesalahan ambiguitas untuk kode yang ada. Lihat juga pertanyaan terbuka di bawah ini.

Inferensi jenis

Kami memperbarui bagian penarikan jenis dari spesifikasi sebagai berikut (perubahan dalam bold).

12.6.3.9 Inferensi yang tepat

Inferensi yang tepat dari tipe ke tipe U dibuat sebagai berikut:

  • Jika V adalah salah satu dari Xᵢ yang belum diperbaiki, maka U ditambahkan ke dalam kumpulan batas yang tepat untuk Xᵢ.
  • Jika tidak, set V₁...Vₑ dan U₁...Uₑ ditentukan dengan memeriksa apakah salah satu kasus berikut berlaku:
    • V adalah jenis array V₁[...] dan U adalah jenis array U₁[...] dengan peringkat yang sama
    • V adalah Span<V₁> dan U adalah jenis array U₁[] atau Span<U₁>
    • V adalah ReadOnlySpan<V₁> dan U adalah jenis array U₁[] atau Span<U₁> atau ReadOnlySpan<U₁>
    • V adalah jenis V₁? dan U adalah jenis U₁
    • V adalah jenis yang dibentuk C<V₁...Vₑ> dan U adalah jenis yang dibentuk C<U₁...Uₑ>
      Jika salah satu kasus ini berlaku, maka inferensi persis dapat dibuat dari setiap Uᵢ ke Vᵢyang sesuai.
  • Jika tidak, tidak ada inferensi yang dibuat.

12.6.3.10 Inferensi batas bawah

Inferensi batas bawah dari jenis Uke jenis V dibuat sebagai berikut:

  • Jika V adalah salah satu Xᵢ yang belum diperbaiki maka U ditambahkan ke set batas bawah untuk Xᵢ.
  • Jika tidak, jika V adalah jenis V₁? dan U adalah jenis U₁? maka inferensi terikat yang lebih rendah dibuat dari U₁ ke V₁.
  • Jika tidak, set U₁...Uₑ dan V₁...Vₑ ditentukan dengan memeriksa apakah salah satu kasus berikut berlaku:
    • V adalah jenis array V₁[...] dan U adalah jenis array U₁[...]dari peringkat yang sama
    • V adalah Span<V₁> dan U adalah jenis array U₁[] atau Span<U₁>
    • V adalah ReadOnlySpan<V₁> dan U adalah jenis array U₁[] atau Span<U₁> atau ReadOnlySpan<U₁>
    • V adalah salah satu dari IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁>, atau IList<V₁>, dan U adalah jenis array satu dimensi U₁[]
    • V adalah class jenis struct, interface, delegate, atau C<V₁...Vₑ> yang dibangun dan ada jenis unik C<U₁...Uₑ> yang membuat U (atau, jika U adalah jenis parameter, kelas dasar efektifnya atau anggota set antarmuka efektifnya) identik dengan, inherits dari (secara langsung atau tidak langsung), atau mengimplementasikan (secara langsung atau tidak langsung) C<U₁...Uₑ>.
    • (Pembatasan "keunikan" berarti bahwa dalam kasus antarmuka C<T>{} class U: C<X>, C<Y>{}, maka tidak ada inferensi yang dibuat ketika menyimpulkan dari U ke C<T> karena U₁ bisa X atau Y.)
      Jika salah satu kasus ini berlaku, inferensi dibuat dari setiap Uᵢ ke Vᵢ yang sesuai sebagai berikut:
    • Jika Uᵢ tidak diketahui sebagai jenis referensi, maka dibuat inferensi yang tepat
    • Jika tidak, jika U adalah tipe array, maka inferensi batas bawah dibuatinferensi tergantung pada jenis V:
      • Jika V adalah Span<Vᵢ>, maka inferensi yang tepat dibuat
      • Jika V adalah jenis array atau ReadOnlySpan<Vᵢ>, maka inferensi batas bawah dilakukan
    • Jika tidak, jika U adalah Span<Uᵢ> maka inferensi tergantung pada jenis V:
      • Jika V adalah Span<Vᵢ>, maka inferensi yang tepat dibuat
      • Jika V adalah ReadOnlySpan<Vᵢ>, maka inferensi batas bawah dibuat
    • Jika tidak, jika U adalah ReadOnlySpan<Uᵢ> dan V adalah ReadOnlySpan<Vᵢ>inferensi yang terikat lebih rendah dibuat:
    • Jika tidak, jika V adalah C<V₁...Vₑ> maka inferensi bergantung pada parameter tipe i-th dari C:
      • Jika itu kovarian maka dibuat inferensi terikat bawah .
      • Jika kontravarian maka inferensi batas atas dibuat.
      • Jika bersifat invarian maka inferensi yang tepat dibuat.
  • Jika tidak, tidak ada inferensi yang dibuat.

Tidak ada aturan untuk inferensi batas atas karena tidak mungkin untuk mencapainya. Inferensi jenis tidak pernah dimulai dari batas atas, itu harus melalui inferensi batas bawah dan parameter jenis kontravarian. Karena aturan "jika Uᵢ tidak diketahui sebagai tipe referensi maka sebuah inferensi yang tepat dibuat," argumen tipe sumber tidak dapat Span/ReadOnlySpan (tidak dapat menjadi tipe referensi). Namun, inferensi rentang batas atas hanya akan berlaku jika jenis sumber adalah Span/ReadOnlySpan, karena akan memiliki aturan seperti:

  • U adalah Span<U₁> dan V adalah jenis array V₁[] atau Span<V₁>
  • U adalah ReadOnlySpan<U₁> dan V adalah jenis array V₁[] atau Span<V₁> atau ReadOnlySpan<V₁>

Perubahan mendasar

Seperti halnya proposal lain yang mengubah konversi skenario yang ada, proposal ini memang memperkenalkan beberapa perubahan signifikan baru. Berikut adalah beberapa contoh:

Memanggil Reverse pada array

Memanggil x.Reverse() di mana x adalah instans jenis T[] sebelumnya akan mengikat ke IEnumerable<T> Enumerable.Reverse<T>(this IEnumerable<T>), sedangkan sekarang mengikat ke void MemoryExtensions.Reverse<T>(this Span<T>). Sayangnya API-API ini tidak kompatibel (API yang terakhir melakukan pembalikan di tempat dan mengembalikan void).

.NET 10 mengurangi ini dengan menambahkan kelebihan beban khusus array IEnumerable<T> Reverse<T>(this T[]), lihat https://github.com/dotnet/runtime/issues/107723.

void M(int[] a)
{
    foreach (var x in a.Reverse()) { } // fine previously, an error now (`Reverse` returns `void`)
    foreach (var x in Enumerable.Reverse(a)) { } // workaround
}

Lihat juga:

Rapat desain: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#reverse

Ambiguitas

Contoh berikut sebelumnya gagal penyimpulan tipe untuk overload Span, tetapi sekarang penyimpulan tipe dari array ke Span berhasil, sehingga hasilnya menjadi ambigu. Untuk mengatasi hal ini, pengguna dapat menggunakan .AsSpan() atau penulis API dapat menggunakan OverloadResolutionPriorityAttribute.

var x = new long[] { 1 };
Assert.Equal([2], x); // previously Assert.Equal<T>(T[], T[]), now ambiguous with Assert.Equal<T>(ReadOnlySpan<T>, Span<T>)
Assert.Equal([2], x.AsSpan()); // workaround
var x = new int[] { 1, 2 };
var s = new ArraySegment<int>(x, 1, 1);
Assert.Equal(x, s); // previously Assert.Equal<T>(T, T), now ambiguous with Assert.Equal<T>(Span<T>, Span<T>)
Assert.Equal(x.AsSpan(), s); // workaround

xUnit menambahkan lebih banyak kelebihan beban untuk mengurangi hal ini: https://github.com/xunit/xunit/discussions/3021.

Rapat desain: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#new-ambiguities

Array kovarian

Overload yang menggunakan IEnumerable<T> berfungsi pada array kovarian, tetapi overload yang menggunakan Span<T> (yang sekarang lebih kita sukai) tidak, karena konversi rentang menghasilkan ArrayTypeMismatchException untuk array kovarian. Bisa dibilang, pemakaian yang berlebihan dari Span<T> sebaiknya tidak ada, sebaiknya gunakan ReadOnlySpan<T> sebagai gantinya. Untuk mengatasi hal ini, pengguna dapat menggunakan .AsEnumerable(), atau penulis API dapat menggunakan OverloadResolutionPriorityAttribute atau menambahkan kelebihan beban ReadOnlySpan<T> yang lebih disukai karena aturan kesempurnaan.

string[] s = new[] { "a" };
object[] o = s;

C.R(o); // wrote 1 previously, now crashes in Span<T> constructor with ArrayTypeMismatchException
C.R(o.AsEnumerable()); // workaround

static class C
{
    public static void R<T>(IEnumerable<T> e) => Console.Write(1);
    public static void R<T>(Span<T> s) => Console.Write(2);
    // another workaround:
    public static void R<T>(ReadOnlySpan<T> s) => Console.Write(3);
}

Rapat desain: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#covariant-arrays

Memilih ReadOnlySpan daripada Span

Aturan keunggulan menyebabkan preferensi terhadap overload ReadOnlySpan dibandingkan Span untuk menghindari ArrayTypeMismatchExceptiondalam skenario array kovarian seperti . Hal ini dapat menyebabkan kompilasi terputus dalam beberapa skenario, misalnya ketika kelebihan beban berbeda dengan jenis pengembaliannya:

double[] x = new double[0];
Span<ulong> y = MemoryMarshal.Cast<double, ulong>(x); // previously worked, now a compilation error (returns ReadOnlySpan, not Span)
Span<ulong> z = MemoryMarshal.Cast<double, ulong>(x.AsSpan()); // workaround

static class MemoryMarshal
{
    public static ReadOnlySpan<TTo> Cast<TFrom, TTo>(ReadOnlySpan<TFrom> span) => default;
    public static Span<TTo> Cast<TFrom, TTo>(Span<TFrom> span) => default;
}

Lihat https://github.com/dotnet/roslyn/issues/76443.

Pohon Ekspresi

Overload yang menggunakan rentang seperti MemoryExtensions.Contains lebih disukai daripada overload klasik seperti Enumerable.Contains, bahkan di dalam pohon ekspresi - tetapi struct ref tidak didukung oleh mesin interpreter.

Expression<Func<int[], int, bool>> exp = (array, num) => array.Contains(num);
exp.Compile(preferInterpretation: true); // fails at runtime in C# 14

Expression<Func<int[], int, bool>> exp2 = (array, num) => Enumerable.Contains(array, num); // workaround
exp2.Compile(preferInterpretation: true); // ok

Demikian pula, mesin terjemahan seperti LINQ-ke-SQL perlu bereaksi terhadap ini jika pengunjung pohon mereka mengharapkan Enumerable.Contains karena mereka akan bertemu MemoryExtensions.Contains sebagai gantinya.

Lihat juga:

Rapat desain:

Konversi yang ditentukan pengguna melalui pewarisan

Dengan menambahkan konversi rentang implisit ke daftar konversi implisit standar, kita berpotensi mengubah perilaku saat konversi yang ditentukan pengguna terlibat dalam hierarki jenis. Contoh ini menunjukkan bagaimana perubahan dibandingkan dengan skenario angka integer yang sudah berperilaku seperti yang akan dilakukan oleh perilaku baru C# 14.

Span<string> span = [];
var d = new Derived();
d.M(span); // Base today, Derived tomorrow
int i = 1;
d.M(i); // Derived today, demonstrates new behavior

class Base
{
    public void M(Span<string> s)
    {
        Console.WriteLine("Base");
    }

    public void M(int i)
    {
        Console.WriteLine("Base");
    }
}

class Derived : Base
{
    public static implicit operator Derived(ReadOnlySpan<string> r) => new Derived();
    public static implicit operator Derived(long l) => new Derived();

    public void M(Derived s)
    {
        Console.WriteLine("Derived");
    }
}

Lihat juga: https://github.com/dotnet/roslyn/issues/78314

Metode pencarian ekstensi

Dengan mengizinkan konversi rentang implisit dalam pencarian metode ekstensi, kita berpotensi mengubah metode ekstensi apa yang diselesaikan dengan resolusi kelebihan beban.

namespace N1
{
    using N2;

    public class C
    {
        public static void M()
        {
            Span<string> span = new string[0];
            span.Test(); // Prints N2 today, N1 tomorrow
        }
    }

    public static class N1Ext
    {
        public static void Test(this ReadOnlySpan<string> span)
        {
            Console.WriteLine("N1");
        }
    }
}

namespace N2
{
    public static class N2Ext
    {
        public static void Test(this Span<string> span)
        {
            Console.WriteLine("N2");
        }
    }
}

Buka pertanyaan

Aturan kesempurnaan yang tidak dibatasi

Haruskah kita membuat aturan peningkatan tidak bersyarat terhadap LangVersion? Itu akan memungkinkan penulis API untuk menambahkan API Span baru di mana ada padanan IEnumerable tanpa mengganggu pengguna pada versi bahasa yang lebih lama atau kompilator atau bahasa lain (misalnya, VB). Namun, itu berarti pengguna bisa mendapatkan perilaku yang berbeda setelah memperbarui toolset (tanpa mengubah LangVersion atau TargetFramework):

  • Pengompilasi dapat memilih overload yang berbeda (secara teknis merupakan perubahan yang dapat menyebabkan kesalahan, tetapi diharapkan overload tersebut mempunyai perilaku yang setara).
  • Jeda lainnya dapat muncul, belum diketahui saat ini.

Perhatikan bahwa OverloadResolutionPriorityAttribute tidak dapat sepenuhnya menyelesaikan ini karena juga diabaikan pada LangVersions yang lebih lama. Namun, harus dimungkinkan untuk menggunakannya untuk menghindari ambiguitas dari VB di mana atribut harus dikenali.

Mengabaikan lebih banyak lagi konversi yang didefinisikan oleh pengguna

Kami mendefinisikan sekumpulan pasangan tipe untuk mana konversi rentang implisit dan eksplisit didefinisikan oleh bahasa. Setiap kali konversi rentang yang ditentukan bahasa ada dari T1 ke T2, konversi yang ditentukan pengguna dari T1 ke T2diabaikan (terlepas dari rentang dan konversi yang ditentukan pengguna yang implisit atau eksplisit).

Perhatikan bahwa ini mencakup semua kondisi, jadi misalnya tidak ada konversi rentang dari Span<object> ke ReadOnlySpan<string> (ada konversi rentang dari Span<T> ke ReadOnlySpan<U> tetapi harus menahan T : U), oleh karena itu konversi yang ditentukan pengguna akan dipertimbangkan antara jenis tersebut jika ada (yang harus menjadi konversi khusus seperti Span<T> ke ReadOnlySpan<string> karena operator konversi tidak dapat memiliki parameter generik).

Haruskah kita mengabaikan konversi yang ditentukan oleh pengguna juga untuk kombinasi lain dari jenis array/Span/ReadOnlySpan/string di mana tidak ada konversi span yang ditentukan oleh bahasa yang tersedia? Misalnya, jika ada konversi yang ditentukan pengguna dari ReadOnlySpan<T> ke Span<T>, haruskah kita mengabaikannya?

Opsi spesifikasi yang perlu dipertimbangkan

  1. Setiap kali terdapat konversi rentang dari T1 ke T2, abaikan konversi yang didefinisikan oleh pengguna dari T1 ke T2atau dari T2 ke T1.

  2. Konversi yang ditentukan pengguna tidak dipertimbangkan saat mengonversi antara

    • dimensi tunggal array_type dan setiap System.Span<T>/System.ReadOnlySpan<T>,
    • kombinasi dari System.Span<T>/System.ReadOnlySpan<T>,
    • string dan System.ReadOnlySpan<char>.
  3. Seperti di atas tetapi mengganti poin terakhir dengan:
    • string dan System.Span<char>/System.ReadOnlySpan<char>.
  4. Seperti di atas tetapi mengganti poin terakhir dengan:
    • string dan System.Span<T>/System.ReadOnlySpan<T>.

Secara teknis, spesifikasi melarang beberapa konversi yang ditentukan pengguna ini untuk didefinisikan: tidak mungkin untuk menentukan operator yang ditentukan pengguna di antara jenis di mana konversi yang tidak ditentukan pengguna ada (§10.5.2). Tapi Roslyn sengaja melanggar bagian spesifikasi ini. Dan beberapa konversi seperti antara Span dan string tetap diizinkan (tidak ada konversi yang ditentukan bahasa antara jenis ini).

Namun demikian, daripada hanya mengabaikan konversi, kita dapat melarang pendefinisian mereka sama sekali dan mungkin keluar dari pelanggaran spesifikasi, setidaknya untuk konversi rentang baru ini. Yaitu, mengubah Roslyn untuk benar-benar melaporkan kesalahan pada waktu kompilasi jika konversi ini didefinisikan (kemungkinan kecuali yang sudah ditentukan oleh BCL).

Alternatif

Pertahankan hal-hal apa adanya.