Perubahan perilaku saat membandingkan string pada .NET 5+

.NET 5 memperkenalkan perubahan perilaku runtime di mana API globalisasi menggunakan ICU secara default pada semua platform yang didukung. Ini adalah permulaan dari versi .NET Core sebelumnya dan dari .NET Framework, yang menggunakan fungsionalitas dukungan bahasa nasional (NLS) sistem operasi saat berjalan di Windows. Untuk informasi selengkapnya tentang perubahan ini, termasuk penelusuran kompatibilitas yang dapat mengembalikan perubahan perilaku, lihat globalisasi .NET dan ICU.

Alasan untuk berubah

Perubahan ini diperkenalkan untuk mempersatukan perilaku globalisasi .NET di semua sistem operasi yang didukung. Hal ini juga menyediakan kemampuan bagi aplikasi untuk membundel pustaka globalisasi mereka sendiri daripada bergantung pada pustaka bawaan OS. Untuk informasi selengkapnya, lihat pemberitahuan untuk menghentikan perubahan.

Perbedaan perilaku

Jika Anda menggunakan fungsi seperti string.IndexOf(string) tanpa menyebut berlebihan yang mengambil argumen StringComparison, Anda mungkin berniat untuk melakukan pencarian ordinal, tetapi sebaliknya Anda secara tidak sengaja mengambil dependensi pada perilaku spesifik budaya. Sejak NLS dan ICU menerapkan logika yang berbeda dalam pembanding linguistik mereka, hasil metode seperti string.IndexOf(string) dapat mengembalikan nilai yang tidak terduga.

Hal ini dapat memanifestasikan dirinya sendiri bahkan di tempat-tempat di mana Anda tidak selalu mengharapkan fasilitas globalisasi aktif. Contoh misalnya, kode berikut dapat menghasilkan jawaban yang berbeda tergantung pada runtime saat ini.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");

// The snippet prints:
//
// '3' when running on .NET Core 2.x - 3.x (Windows)
// '0' when running on .NET 5 or later (Windows)
// '0' when running on .NET Core 2.x - 3.x or .NET 5 (non-Windows)
// '3' when running on .NET Core 2.x or .NET 5+ (in invariant mode)

string s = "Hello\r\nworld!";
int idx = s.IndexOf("\n");
Console.WriteLine(idx);

// The snippet prints:
//
// '6' when running on .NET Core 3.1
// '-1' when running on .NET 5 or .NET Core 3.1 (non-Windows OS)
// '-1' when running on .NET 5 (Windows 10 May 2019 Update or later)
// '6' when running on .NET 6+ (all Windows and non-Windows OSs)

Untuk informasi selengkapnya, dapat dilihat API Globalisasi menggunakan pustaka ICU di Windows.

Menjaga terhadap perilaku tak terduga

Bagian ini menyediakan dua opsi untuk menangani perubahan perilaku tak terduga di .NET 5.

Aktifkan penganalisis kode

Penganalisis kode dapat mendeteksi situs panggilan buggy. Untuk membantu melindungi dari perilaku mengejutkan, sebaiknya aktifkan penganalisis platform kompilator .NET (Roslyn) pada proyek Anda. Penganalisis membantu menandai kode yang mungkin secara tidak sengaja menggunakan perbandingan linguistik ketika perbandingan ordinal yang kemungkinan dimaksud. Aturan berikut dapat membantu menandai masalah ini:

Aturan khusus ini tidak didukung oleh pengaturan default. Untuk mengaktifkannya dan menampilkan pelanggaran apa pun sebagai kesalahan build, atur properti berikut dalam file proyek Anda:

<PropertyGroup>
  <AnalysisMode>All</AnalysisMode>
  <WarningsAsErrors>$(WarningsAsErrors);CA1307;CA1309;CA1310</WarningsAsErrors>
</PropertyGroup>

Cuplikan berikut menunjukkan contoh kode yang menghasilkan peringatan atau kesalahan penganalisis kode yang relevan.

//
// Potentially incorrect code - answer might vary based on locale.
//
string s = GetString();
// Produces analyzer warning CA1310 for string; CA1307 matches on char ','
int idx = s.IndexOf(",");
Console.WriteLine(idx);

//
// Corrected code - matches the literal substring ",".
//
string s = GetString();
int idx = s.IndexOf(",", StringComparison.Ordinal);
Console.WriteLine(idx);

//
// Corrected code (alternative) - searches for the literal ',' character.
//
string s = GetString();
int idx = s.IndexOf(',');
Console.WriteLine(idx);

Demikian pula, saat menginisiasi kumpulan string yang diurutkan atau mengurutkan koleksi berbasis string yang ada, tentukan perbandingan eksplisit.

//
// Potentially incorrect code - behavior might vary based on locale.
//
SortedSet<string> mySet = new SortedSet<string>();
List<string> list = GetListOfStrings();
list.Sort();

//
// Corrected code - uses ordinal sorting; doesn't vary by locale.
//
SortedSet<string> mySet = new SortedSet<string>(StringComparer.Ordinal);
List<string> list = GetListOfStrings();
list.Sort(StringComparer.Ordinal);

Kembali ke perilaku NLS

Untuk mengembalikan aplikasi .NET 5+ ke perilaku NLS yang lebih lama saat berjalan di Windows, ikuti langkah-langkah dalam Globalisasi .NET dan ICU. Sakelar kompatibilitas di seluruh aplikasi ini harus diatur pada tingkat aplikasi. Pustaka individual tidak dapat ikut serta atau menolak perilaku ini.

Tip

Kami sangat menyarankan Anda mengaktifkan aturan analisis kode CA1307, CA1309, dan CA1310 untuk membantu meningkatkan kebersihan kode dan menemukan bug laten yang ada. Untuk informasi selengkapnya, lihat cara Mengaktifkan penganalisis kode.

API yang Terpengaruh

Sebagian besar aplikasi .NET tidak akan menemukan perilaku tak terduga karena perubahan .NET 5. Namun, karena jumlah API yang terpengaruh dan seberapa dasar API ini terhadap ekosistem .NET yang lebih luas, Anda harus menyadari potensi .NET 5 untuk memperkenalkan perilaku yang tidak diinginkan atau untuk mengekspos bug laten yang sudah ada di aplikasi Anda.

Peran yang terpengaruh meliputi:

Catatan

Ini bukan daftar lengkap API yang terpengaruh.

Semua API di atas menggunakan pencarian dan perbandingan string linguistik menggunakan utas kultur saat ini, secara default. Perbedaan antara pencarian linguistik dan ordinal serta perbandingan dipanggil dalam pencarian dan perbandingan Ordinal vs. linguistik.

Karena ICU menerapkan perbandingan string linguistik secara berbeda dari NLS, aplikasi berbasis Windows yang meningkatkan untuk .NET 5 dari versi .NET Core atau .NET Framework yang lebih lama dan yang memanggil salah satu API terpengaruh yang mungkin melihat bahwa API mulai menunjukkan perilaku yang berbeda.

Pengecualian

  • Jika API menerima eksplisit parameter StringComparison atau CultureInfo, parameter tersebut akan menggantikan perilaku default API.
  • anggota System.String tempat parameter pertama bertipe char (misalnya, String.IndexOf(Char)) menggunakan pencarian ordinal, kecuali pemanggil melewati argumen eksplisit StringComparison yang menentukan CurrentCulture[IgnoreCase] atau InvariantCulture[IgnoreCase].

Untuk analisis yang lebih rinci tentang perilaku default setiap String API, lihat bagian Jenis pencarian dan perbandingan default.

Pencarian dan perbandingan ordinal vs. linguistik

Pencarian dan perbandingan ordinal (juga dikenal sebagai non-linguistik) mencari dan menguraikan string ke dalam elemen char individualnya dan melakukan pencarian atau perbandingan karakter demi karakter. Misalnya, string "dog" dan "dog" bandingkan sama dengan di bawah perbandingan Ordinal, karena dua string terdiri dari urutan karakter yang sama persis. Namun, "dog" dan "Dog" bandingkan sebagai tidak sama di bawah perbandingan Ordinal, karena tidak terdiri dari urutan karakter yang sama persis. Artinya, U+0044 titik kode huruf besar 'D' terjadi sebelum U+0064 titik kode huruf kecil 'd', menghasilkan pengurutan "Dog" sebelum "dog".

Perbandingan OrdinalIgnoreCase masih beroperasi berdasarkan char-by-char, tetapi menghilangkan perbedaan kasus pada saat melakukan operasi. Di bawah perbandingan OrdinalIgnoreCase, pasangan karakter'd' dan 'D' dibandingkan sebagai sama, seperti halnya pasangan karakter 'á' dan 'Á'. Namun karakter yang tidak beraksen 'a' dibandingkan sebagai tidak sama dengan karakter beraksen 'á'.

Beberapa contoh ini telah tersedia dalam tabel berikut:

String 1 String 2 Perbandingan Ordinal Perbandingan OrdinalIgnoreCase
"dog" "dog" Setara Setara
"dog" "Dog" Tidak setara Setara
"resume" "résumé" Tidak setara Tidak setara

Unicode juga memungkinkan string untuk memiliki beberapa representasi dalam memori yang berbeda. Misalnya, e-acute (é) dapat diwakili dengan dua kemungkinan cara:

  • Karakter 'é' harfiah tunggal (juga ditulis sebagai '\u00E9').
  • Karakter 'e' tanpa aksen harfiah diikuti dengan karakter pengubah aksen gabungan '\u0301'.

Ini berarti bahwa empat string berikut semuanya ditampilkan sebagai "résumé", meskipun potongan konstituennya berbeda. String menggunakan kombinasi karakter 'é' harfiah atau karakter 'e' tanpa aksen harfiah ditambah pemodifikasi aksen kombinasi '\u0301'.

  • "r\u00E9sum\u00E9"
  • "r\u00E9sume\u0301"
  • "re\u0301sum\u00E9"
  • "re\u0301sume\u0301"

Di bawah perbandingan ordinal, string ini tidak ada yang dibandingkan satu sama lain. Ini karena semuanya berisi urutan karakter mendasar yang berbeda, meskipun ketika dirender ke layar, semuanya terlihat sama.

Saat melakukan operasi string.IndexOf(..., StringComparison.Ordinal), runtime mencari kecocokan substring yang persis. Hasilnya adalah sebagai berikut.

Console.WriteLine("resume".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e", StringComparison.Ordinal)); // prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf("e", StringComparison.Ordinal)); // prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("resume".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'

Rutinitas pencarian dan perbandingan ordinal tidak pernah terpengaruh oleh utas pengaturan budaya saat ini.

Rutinitas pencarian dan perbandingan linguistik menguraikan string menjadi elemen kolase dan melakukan pencarian atau perbandingan pada elemen-elemen ini. Belum tentu ada pemetaan 1:1 antara karakter string dan elemen kolase konstituennya. Misalnya, untai panjang 2 hanya terdiri dari satu elemen kolase. Ketika dua string dibandingkan dengan cara yang sadar linguistik, perbandingan memeriksa apakah elemen kolase dua string memiliki arti semantik yang sama, bahkan jika karakter secara harfiah berbeda string.

Pertimbangkan lagi string "résumé" dan empat representasinya yang berbeda. Tabel berikut menunjukkan setiap representasi yang dipecah menjadi elemen kolabasinya.

String Sebagai elemen kolase
"r\u00E9sum\u00E9" "r" + "\u00E9" + "s" + "u" + "m" + "\u00E9"
"r\u00E9sume\u0301" "r" + "\u00E9" + "s" + "u" + "m" + "e\u0301"
"re\u0301sum\u00E9" "r" + "e\u0301" + "s" + "u" + "m" + "\u00E9"
"re\u0301sume\u0301" "r" + "e\u0301" + "s" + "u" + "m" + "e\u0301"

Elemen kolase sesuai secara longgar dengan apa yang akan dipikirkan pembaca sebagai satu karakter atau kluster karakter. Hal ini secara konseptual mirip dengan kluster grapheme tetapi mencakup payung yang agak lebih besar.

Di bawah perbandingan linguistik, kecocokan persis tidak diperlukan. Elemen kolase malah dibandingkan berdasarkan makna semantiknya. Misalnya, perbandingan linguistik memperlakukan substring "\u00E9" dan "e\u0301" sama karena keduanya secara semantik berarti "huruf kecil e dengan pengubah aksen akut." Ini memungkinkan metode IndexOf untuk mencocokkan substring "e\u0301" dalam string yang lebih besar yang berisi substring "\u00E9" yang setara secara semantik, seperti yang ditunjukkan dalam sampel kode berikut.

Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e")); // prints '-1' (not found)
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("\u00E9")); // prints '1'
Console.WriteLine("\u00E9".IndexOf("e\u0301")); // prints '0'

Sebagai konsekuensi dari ini, dua string dengan panjang yang berbeda dapat dibandingkan sama jika perbandingan linguistik digunakan. Para pemanggil harus berhati-hati untuk tidak menangani logika kasus khusus yang berkaitan dengan panjang string dalam skenario tersebut.

Rutinitas pencarian dan perbandingan yang sadar budaya adalah bentuk khusus dari pencarian linguistik dan rutinitas perbandingan. Di bawah perbandingan sadar budaya, konsep elemen kolase diperluas untuk menyertakan informasi khusus untuk budaya yang telah ditentukan.

Misalnya, dalam alfabet Hungaria, ketika dua karakter <dz> muncul secara back-to-back, mereka dianggap sebagai huruf unik mereka sendiri berbeda dari <d> atau <z>. Ini berarti bahwa ketika <dz> terlihat dalam string, perbandingan sadar budaya Hungaria memperlakukannya sebagai elemen kolase tunggal.

String Sebagai elemen kolase Keterangan
"endz" "e" + "n" + "d" + "z" (menggunakan perbandingan linguistik standar)
"endz" "e" + "n" + "dz" (menggunakan perbandingan sadar budaya Hungaria)

Saat menggunakan perbandingan sadar budaya Hungaria, ini berarti bahwa string "endz"tidak berakhir dengan substring "z", karena <dz> dan <z> dianggap sebagai elemen kolase dengan arti semantik yang berbeda.

// Set thread culture to Hungarian
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("hu-HU");
Console.WriteLine("endz".EndsWith("z")); // Prints 'False'

// Set thread culture to invariant culture
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine("endz".EndsWith("z")); // Prints 'True'

Catatan

  • Perilaku: Perbandingan linguistik dan sadar budaya dapat menjalani penyesuaian perilaku dari waktu ke waktu. ICU dan fasilitas NLS Windows yang lebih lama diperbarui untuk mempertimbangkan bagaimana bahasa dunia berubah. Untuk informasi selengkapnya, lihat posting blog Churn data Lokal (budaya). Perilaku perbandingan Ordinal tidak akan pernah berubah karena melakukan pencarian dan perbandingan bitwise yang tepat. Namun, perilaku perbandingan OrdinalIgnoreCase dapat berubah saat Unicode tumbuh untuk mencakup lebih banyak set karakter dan memperbaiki kelalaian dalam data casing yang ada.
  • Penggunaan: Pembanding StringComparison.InvariantCulture dan StringComparison.InvariantCultureIgnoreCase merupakan pembanding linguistik yang tidak sadar budaya. Artinya, pembanding ini memahami konsep seperti karakter beraksen é yang memiliki beberapa kemungkinan representasi yang mendasar, dan bahwa semua representasi tersebut harus diperlakukan sama. Tetapi pembanding linguistik yang tidak sadar budaya tidak akan mengandung penanganan khusus untuk <dz> yang berbeda dari <d> atau <z>, seperti yang ditunjukkan di atas. Mereka juga tidak akan karakter kasus khusus seperti Eszett Jerman (ß).

.NET juga menawarkan mode globalisasi yang invarian. Mode keikutsertaan ini menonaktifkan jalur kode yang menangani pencarian linguistik dan rutinitas perbandingan. Dalam mode ini, semua operasi menggunakan perilaku Ordinal atau OrdinalIgnoreCase, terlepas dari argumen CultureInfo atau StringComparison yang disediakan pemanggil. Untuk informasi selengkapnya, lihat Opsi konfigurasi runtime untuk globalisasi dan Mode Invarian Globalisasi .NET Core.

Untuk informasi selengkapnya, lihat Praktik terbaik untuk membandingkan string di .NET.

Implikasi keamanan

Jika aplikasi Anda menggunakan API yang terpengaruh untuk pemfilteran, sebaiknya aktifkan aturan analisis kode CA1307 dan CA1309 untuk membantu menemukan tempat di mana pencarian linguistik mungkin secara tidak sengaja digunakan alih-alih pencarian ordinal. Pola kode seperti berikut ini mungkin rentan terhadap eksploitasi keamanan.

//
// THIS SAMPLE CODE IS INCORRECT.
// DO NOT USE IT IN PRODUCTION.
//
public bool ContainsHtmlSensitiveCharacters(string input)
{
    if (input.IndexOf("<") >= 0) { return true; }
    if (input.IndexOf("&") >= 0) { return true; }
    return false;
}

Karena metode string.IndexOf(string) ini menggunakan pencarian linguistik secara default, dimungkinkan bagi string untuk mengisi karakter harfiah '<' atau '&' dan agar rutinitas string.IndexOf(string) mengembalikan -1, menunjukkan bahwa substring pencarian tidak ditemukan. Aturan analisis kode CA1307 dan CA1309 menandai situs panggilan tersebut dan memperingatkan pengembang bahwa ada potensi masalah.

Jenis pencarian dan perbandingan default

Tabel berikut mencantumkan jenis pencarian dan perbandingan default untuk berbagai string dan string seperti API. Jika pemanggil menyediakan parameter eksplisit CultureInfo atau StringComparison, parameter tersebut akan dihormati selama default apa pun.

API Perilaku default Keterangan
string.Compare CurrentCulture
string.CompareTo CurrentCulture
string.Contains Urut
string.EndsWith Urut (ketika parameter pertama adalah char)
string.EndsWith CurrentCulture (ketika parameter pertama adalah string)
string.Equals Urut
string.GetHashCode Urut
string.IndexOf Urut (ketika parameter pertama adalah char)
string.IndexOf CurrentCulture (ketika parameter pertama adalah string)
string.IndexOfAny Urut
string.LastIndexOf Urut (ketika parameter pertama adalah char)
string.LastIndexOf CurrentCulture (ketika parameter pertama adalah string)
string.LastIndexOfAny Urut
string.Replace Urut
string.Split Urut
string.StartsWith Urut (ketika parameter pertama adalah char)
string.StartsWith CurrentCulture (ketika parameter pertama adalah string)
string.ToLower CurrentCulture
string.ToLowerInvariant InvariantCulture
string.ToUpper CurrentCulture
string.ToUpperInvariant InvariantCulture
string.Trim Urut
string.TrimEnd Urut
string.TrimStart Urut
string == string Urut
string != string Urut

Tidak seperti API string, semua API MemoryExtensions melakukan pencarian dan perbandingan Ordinal secara default, dengan pengecualian berikut.

API Perilaku default Keterangan
MemoryExtensions.ToLower CurrentCulture (ketika melewati argumen CultureInfo null)
MemoryExtensions.ToLowerInvariant InvariantCulture
MemoryExtensions.ToUpper CurrentCulture (ketika melewati argumen CultureInfo null)
MemoryExtensions.ToUpperInvariant InvariantCulture

Konsekuensinya adalah ketika mengonversi kode dari mengonsumsi string menjadi mengonsumsi ReadOnlySpan<char>, perubahan perilaku dapat diperkenalkan secara tidak sengaja. Contohnya adalah sebagai berikut.

string str = GetString();
if (str.StartsWith("Hello")) { /* do something */ } // this is a CULTURE-AWARE (linguistic) comparison

ReadOnlySpan<char> span = s.AsSpan();
if (span.StartsWith("Hello")) { /* do something */ } // this is an ORDINAL (non-linguistic) comparison

Cara yang disarankan untuk mengatasinya adalah dengan meneruskan parameter eksplisit StringComparison ke API ini. Aturan analisis kode CA1307 dan CA1309 dapat membantu hal ini.

string str = GetString();
if (str.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } // ordinal comparison

ReadOnlySpan<char> span = s.AsSpan();
if (span.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } // ordinal comparison

Lihat juga