Tipe data string adalah salah satu tipe data yang paling signifikan dalam bahasa pemrograman apa pun. Anda hampir tidak dapat menulis program yang bermanfaat tanpanya. Namun demikian, banyak pengembang tidak mengetahui aspek-aspek tertentu dari jenis ini. Oleh karena itu, mari kita pertimbangkan aspek-aspek ini.
Representasi string dalam memori
Di .Net, string ditempatkan sesuai dengan aturan BSTR (string Dasar atau string biner). Metode representasi data string ini digunakan dalam COM (kata 'dasar' berasal dari bahasa pemrograman Visual Basic yang awalnya digunakan). Seperti yang kita ketahui, PWSZ (Pointer to Wide-character String, Zero-terminated) digunakan dalam C/C++ untuk representasi string. Dengan lokasi seperti itu di memori, null-terminated terletak di akhir string. Terminator ini memungkinkan untuk menentukan akhir string. Panjang string di PWSZ hanya dibatasi oleh volume ruang kosong.
Di BSTR, situasinya sedikit berbeda.
Aspek dasar representasi string BSTR dalam memori adalah sebagai berikut:
- Panjang string dibatasi oleh angka tertentu. Di PWSZ, panjang string dibatasi oleh ketersediaan memori bebas.
- String BSTR selalu menunjuk pada karakter pertama dalam buffer. PWSZ dapat menunjuk ke karakter apa pun di buffer.
- Dalam BSTR, mirip dengan PWSZ, karakter null selalu terletak di akhir. Di BSTR, karakter null adalah karakter yang valid dan dapat ditemukan di mana saja dalam string.
- Karena terminator nol terletak di akhir, BSTR kompatibel dengan PWSZ, tetapi tidak sebaliknya.
Oleh karena itu, string dalam .NET direpresentasikan dalam memori menurut aturan BSTR. Buffer berisi panjang string 4 byte diikuti oleh karakter dua byte dari string dalam format UTF-16, yang pada gilirannya diikuti oleh dua byte nol (\u0000).
Menggunakan implementasi ini memiliki banyak keuntungan:panjang string tidak boleh dihitung ulang karena disimpan di header, string dapat berisi karakter null di mana saja. Dan yang paling penting adalah alamat string (disematkan) dapat dengan mudah melewati kode asli di mana WCHAR* diharapkan.
Berapa banyak memori yang dibutuhkan objek string?
Saya menemukan artikel yang menyatakan bahwa ukuran objek string sama dengan size=20 + (length/2)*4, tetapi rumus ini tidak sepenuhnya benar.
Untuk memulainya, string adalah jenis tautan, jadi empat byte pertama berisi SyncBlockIndex dan empat byte berikutnya berisi pointer tipe.
Ukuran senar =4 + 4 + …
Seperti yang saya nyatakan di atas, panjang string disimpan dalam buffer. Ini adalah bidang tipe int, oleh karena itu kita perlu menambahkan 4 byte lagi.
Ukuran senar =4 + 4 + 4 + …
Untuk meneruskan string ke kode asli dengan cepat (tanpa menyalin), terminator nol terletak di akhir setiap string yang membutuhkan 2 byte. Oleh karena itu,
Ukuran senar =4 + 4 + 4 + 2 + …
Satu-satunya yang tersisa adalah mengingat bahwa setiap karakter dalam string ada dalam pengkodean UTF-16 dan juga membutuhkan 2 byte. Oleh karena itu:
Ukuran senar =4 + 4 + 4 + 2 + 2 * panjang =14 + 2 * panjang
Satu hal lagi dan kita selesai. Memori yang dialokasikan oleh manajer memori di CLR adalah kelipatan 4 byte (4, 8, 12, 16, 20, 24, ...). Jadi, jika panjang string membutuhkan total 34 byte, 36 byte akan dialokasikan. Kita perlu membulatkan nilai kita ke angka terdekat yang lebih besar yaitu kelipatan empat. Untuk ini, kita perlu:
Ukuran string =4 * ((14 + 2 * panjang + 3) / 4) (pembagian bilangan bulat)
Masalah versi :sampai .NET v4, ada tambahan m_arrayLength bidang tipe int di kelas String yang mengambil 4 byte. Bidang ini adalah panjang nyata dari buffer yang dialokasikan untuk string, termasuk terminator nol, yaitu panjang + 1. Dalam .NET 4.0, bidang ini dijatuhkan dari kelas. Akibatnya, objek tipe string menempati 4 byte lebih sedikit.
Ukuran string kosong tanpa m_arrayLength bidang (yaitu di .Net 4.0 dan lebih tinggi) sama dengan =4 + 4 + 4 + 2 =14 byte, dan dengan bidang ini (yaitu lebih rendah dari .Net 4.0), ukurannya sama dengan =4 + 4 + 4 + 4 + 2 =18 byte. Jika kita membulatkan 4 byte, ukurannya akan menjadi 16 dan 20 byte.
Aspek String
Jadi, kami mempertimbangkan representasi string dan ukurannya dalam memori. Sekarang, mari kita bicara tentang kekhasan mereka.
Aspek dasar string dalam .NET adalah sebagai berikut:
- String adalah tipe referensi.
- String tidak dapat diubah. Setelah dibuat, string tidak dapat dimodifikasi (dengan cara yang adil). Setiap pemanggilan metode kelas ini mengembalikan string baru, sedangkan string sebelumnya menjadi mangsa pemulung.
- String mendefinisikan ulang metode Object.Equals. Akibatnya, metode membandingkan nilai karakter dalam string, bukan nilai tautan.
Mari kita pertimbangkan setiap poin secara mendetail.
String adalah jenis referensi
String adalah tipe referensi nyata. Artinya, mereka selalu berada di tumpukan. Banyak dari kita mengacaukannya dengan tipe nilai, karena Anda berperilaku dengan cara yang sama. Misalnya, mereka tidak dapat diubah dan perbandingannya dilakukan berdasarkan nilai, bukan dengan referensi, tetapi kita harus ingat bahwa itu adalah tipe referensi.
String tidak dapat diubah
- String tidak dapat diubah untuk suatu tujuan. Kekekalan string memiliki sejumlah manfaat:
- Jenis string aman untuk utas, karena tidak ada satu utas pun yang dapat mengubah konten string.
- Penggunaan string yang tidak dapat diubah menyebabkan penurunan beban memori, karena tidak perlu menyimpan 2 instance dari string yang sama. Akibatnya, lebih sedikit memori yang dihabiskan, dan perbandingan dilakukan lebih cepat, karena hanya referensi yang dibandingkan. Dalam .NET, mekanisme ini disebut string interning (string pool). Kami akan membicarakannya nanti.
- Saat meneruskan parameter yang tidak dapat diubah ke suatu metode, kita tidak perlu khawatir bahwa itu akan dimodifikasi (tentu saja jika tidak diteruskan sebagai ref atau keluar).
Struktur data dapat dibagi menjadi dua jenis:ephemeral dan persisten. Struktur data fana hanya menyimpan versi terakhirnya. Struktur data persisten menyimpan semua versi sebelumnya selama modifikasi. Yang terakhir, pada kenyataannya, tidak dapat diubah, karena operasi mereka tidak mengubah struktur di situs. Sebagai gantinya, mereka mengembalikan struktur baru yang didasarkan pada struktur sebelumnya.
Mengingat fakta bahwa string tidak dapat diubah, mereka bisa menjadi persisten, tetapi sebenarnya tidak. String bersifat sementara di .Net.
Sebagai perbandingan, mari kita ambil string Java. Mereka tidak berubah, seperti di .NET, tetapi selain itu mereka gigih. Implementasi kelas String di Java terlihat sebagai berikut:
public final class String { private final char value[]; private final int offset; private final int count; private int hash; ..... }
Selain 8 byte di header objek, termasuk referensi ke jenis dan referensi ke objek sinkronisasi, string berisi bidang berikut:
- Referensi ke array char;
- Indeks karakter pertama dari string dalam array char (offset dari awal)
- Jumlah karakter dalam string;
- Kode hash dihitung setelah pertama kali memanggil HashCode() metode.
String di Java membutuhkan lebih banyak memori daripada di .NET, karena mengandung bidang tambahan yang memungkinkan mereka untuk menjadi persisten. Karena ketekunan, eksekusi String.substring() metode di Java membutuhkan O(1) , karena tidak memerlukan penyalinan string seperti pada .NET, di mana eksekusi metode ini membutuhkan O(n) .
Implementasi metode String.substring() di Java:
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) throw new StringIndexOutOfBoundsException(beginIndex); if (endIndex > count) throw new StringIndexOutOfBoundsException(endIndex); if (beginIndex > endIndex) throw new StringIndexOutOfBoundsException(endIndex - beginIndex); return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); } public String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count; }
Namun, jika string sumber cukup besar dan substring potongan terdiri dari beberapa karakter, seluruh array karakter dari string awal akan menunggu di memori sampai ada referensi ke substring. Atau, jika Anda membuat serial substring yang diterima dengan cara standar dan meneruskannya melalui jaringan, seluruh array asli akan diserialisasi dan jumlah byte yang dilewatkan melalui jaringan akan banyak. Oleh karena itu, alih-alih kode
s =ss.substring(3)
kode berikut dapat digunakan:
s =String baru(ss.substring(3)),
Kode ini tidak akan menyimpan referensi ke array karakter dari string sumber. Sebaliknya, itu hanya akan menyalin bagian array yang benar-benar digunakan. Omong-omong, jika kita memanggil konstruktor ini pada string yang panjangnya sama dengan panjang array karakter, penyalinan tidak akan terjadi. Sebagai gantinya, referensi ke array asli akan digunakan.
Ternyata, implementasi tipe string telah diubah di versi terakhir Java. Sekarang, tidak ada bidang offset dan panjang di kelas. hash32 baru (dengan algoritma hashing yang berbeda) telah diperkenalkan sebagai gantinya. Ini berarti bahwa string tidak persisten lagi. Sekarang, String.substring metode akan membuat string baru setiap kali.
String mendefinisikan ulang Onbject.Equals
Kelas string mendefinisikan ulang metode Object.Equals. Akibatnya, perbandingan terjadi, tetapi bukan berdasarkan referensi, tetapi berdasarkan nilai. Saya kira pengembang berterima kasih kepada pembuat kelas String karena mendefinisikan ulang operator ==, karena kode yang menggunakan ==untuk perbandingan string terlihat lebih mendalam daripada pemanggilan metode.
if (s1 == s2)
Dibandingkan dengan
if (s1.Equals(s2))
Omong-omong, di Jawa, operator ==membandingkan dengan referensi. Jika Anda perlu membandingkan string dengan karakter, kita perlu menggunakan metode string.equals().
Pelatihan Senar
Akhirnya, mari kita pertimbangkan magang string. Mari kita lihat contoh sederhana – kode yang membalikkan string.
var s = "Strings are immutuble"; int length = s.Length; for (int i = 0; i < length / 2; i++) { var c = s[i]; s[i] = s[length - i - 1]; s[length - i - 1] = c; }
Jelas, kode ini tidak dapat dikompilasi. Kompiler akan memunculkan kesalahan untuk string ini, karena kami mencoba mengubah konten string. Metode apa pun dari kelas String mengembalikan instance baru dari string, alih-alih modifikasi kontennya.
String dapat dimodifikasi, tetapi kita perlu menggunakan kode yang tidak aman. Mari kita perhatikan contoh berikut:
var s = "Strings are immutable"; int length = s.Length; unsafe { fixed (char* c = s) { for (int i = 0; i < length / 2; i++) { var temp = c[i]; c[i] = c[length - i - 1]; c[length - i - 1] = temp; } } }
Setelah eksekusi kode ini, elbatummi era sgnirtS akan ditulis ke dalam string, seperti yang diharapkan. Mutabilitas string mengarah ke kasus mewah terkait dengan string interning.
Pelatihan string adalah mekanisme di mana literal serupa direpresentasikan dalam memori sebagai objek tunggal.
Singkatnya, inti dari string interning adalah sebagai berikut:ada tabel internal hash tunggal dalam suatu proses (bukan dalam domain aplikasi), di mana string adalah kuncinya, dan nilai adalah referensinya. Selama kompilasi JIT, string literal ditempatkan ke dalam tabel secara berurutan (setiap string dalam tabel hanya dapat ditemukan sekali). Selama eksekusi, referensi ke string literal ditetapkan dari tabel ini. Selama eksekusi, kita dapat menempatkan string ke dalam tabel internal dengan String.Intern metode. Selain itu, kita dapat memeriksa ketersediaan string di tabel internal menggunakan String.IsInterned metode.
var s1 = "habrahabr"; var s2 = "habrahabr"; var s3 = "habra" + "habr"; Console.WriteLine(object.ReferenceEquals(s1, s2));//true Console.WriteLine(object.ReferenceEquals(s1, s3));//true
Perhatikan, bahwa hanya string literal yang diinternir secara default. Karena tabel internal hash digunakan untuk implementasi magang, pencarian terhadap tabel ini dilakukan selama kompilasi JIT. Proses ini membutuhkan waktu. Jadi, jika semua string diinternir, itu akan mengurangi optimasi menjadi nol. Selama kompilasi ke dalam kode IL, kompiler menggabungkan semua string literal, karena tidak perlu menyimpannya dalam beberapa bagian. Oleh karena itu, persamaan kedua mengembalikan true .
Sekarang, mari kembali ke kasus kita. Perhatikan kode berikut:
var s = "Strings are immutable"; int length = s.Length; unsafe { fixed (char* c = s) { for (int i = 0; i < length / 2; i++) { var temp = c[i]; c[i] = c[length - i - 1]; c[length - i - 1] = temp; } } } Console.WriteLine("Strings are immutable");
Tampaknya semuanya cukup jelas dan kode harus mengembalikan String tidak dapat diubah . Namun, tidak! Kode mengembalikan elbatummi era sgnirtS . Itu terjadi persis karena magang. Saat kami memodifikasi string, kami memodifikasi kontennya, dan karena ini literal, string tersebut diinternir dan diwakili oleh satu instance string.
Kita dapat mengabaikan pelatihan string jika kita menerapkan CompilationRelaxationsAttribute atribut ke majelis. Atribut ini mengontrol keakuratan kode yang dibuat oleh kompiler JIT dari lingkungan CLR. Konstruktor atribut ini menerima CompilationRelaxations enumeration, yang saat ini hanya menyertakan CompilationRelaxations.NoStringInterning . Akibatnya, majelis ditandai sebagai yang tidak memerlukan magang.
Omong-omong, atribut ini tidak diproses di .NET Framework v1.0. Itu sebabnya, tidak mungkin untuk menonaktifkan magang. Mulai dari versi 2, mscorlib perakitan ditandai dengan atribut ini. Jadi, ternyata string di .NET bisa dimodifikasi dengan kode yang tidak aman.
Bagaimana jika kita melupakan tidak aman?
Seperti yang terjadi, kita dapat memodifikasi konten string tanpa kode yang tidak aman. Sebagai gantinya, kita dapat menggunakan mekanisme refleksi. Trik ini berhasil di .NET hingga versi 2.0. Setelah itu, pengembang kelas String membuat kami kehilangan kesempatan ini. Di .NET 2.0, kelas String memiliki dua metode internal:SetChar untuk pemeriksaan batas dan InternalSetCharNoBoundsCheck yang tidak membuat pemeriksaan batas. Metode ini mengatur karakter tertentu dengan indeks tertentu. Implementasi metode terlihat sebagai berikut:
internal unsafe void SetChar(int index, char value) { if ((uint)index >= (uint)this.Length) throw new ArgumentOutOfRangeException("index", Environment.GetResourceString("ArgumentOutOfRange_Index")); fixed (char* chPtr = &this.m_firstChar) chPtr[index] = value; } internal unsafe void InternalSetCharNoBoundsCheck (int index, char value) { fixed (char* chPtr = &this.m_firstChar) chPtr[index] = value; }
Oleh karena itu, kita dapat memodifikasi konten string tanpa kode yang tidak aman dengan bantuan kode berikut:
var s = "Strings are immutable"; int length = s.Length; var method = typeof(string).GetMethod("InternalSetCharNoBoundsCheck", BindingFlags.Instance | BindingFlags.NonPublic); for (int i = 0; i < length / 2; i++) { var temp = s[i]; method.Invoke(s, new object[] { i, s[length - i - 1] }); method.Invoke(s, new object[] { length - i - 1, temp }); } Console.WriteLine("Strings are immutable");
Seperti yang diharapkan, kode mengembalikan elbatummi era sgnirtS .
Masalah versi :dalam versi .NET Framework yang berbeda, string.Empty dapat diintegrasikan atau tidak. Mari kita perhatikan kode berikut:
string str1 = String.Empty; StringBuilder sb = new StringBuilder().Append(String.Empty); string str2 = String.Intern(sb.ToString()); if (object.ReferenceEquals(str1, str2)) Console.WriteLine("Equal"); else Console.WriteLine("Not Equal");
Di .NET Framework 1.0, .NET Framework 1.1 dan .NET Framework 3.5 dengan paket layanan 1 (SP1), str1 dan str2 tidak sama. Saat ini, string.Empty tidak diasingkan.
Aspek Kinerja
Ada satu efek samping negatif dari magang. Masalahnya adalah bahwa referensi ke objek String yang disimpan oleh CLR dapat disimpan bahkan setelah pekerjaan aplikasi berakhir dan bahkan setelah pekerjaan domain aplikasi berakhir. Oleh karena itu, lebih baik untuk menghilangkan menggunakan string literal besar. Jika masih diperlukan, magang harus dinonaktifkan dengan menerapkan CompilationRelaxations atribut untuk perakitan.