Di C #, mengapa String adalah tipe referensi yang berperilaku seperti tipe nilai?

371

String adalah tipe referensi meskipun ia memiliki sebagian besar karakteristik tipe nilai seperti tidak berubah dan memiliki == kelebihan muatan untuk membandingkan teks daripada memastikan mereka mereferensikan objek yang sama.

Mengapa string bukan tipe nilai saja?

Davy8
sumber
Karena untuk tipe yang tidak dapat diubah perbedaannya sebagian besar merupakan implementasi-detail ( ismengesampingkan tes), jawabannya mungkin "karena alasan historis". Kinerja penyalinan tidak dapat menjadi alasan karena tidak perlu menyalin objek yang tidak dapat diubah secara fisik. Sekarang tidak mungkin untuk mengubah tanpa melanggar kode yang benar-benar menggunakan iscek (atau kendala serupa).
Elazar
BTW ini adalah jawaban yang sama untuk C ++ (walaupun perbedaan antara nilai dan tipe referensi tidak eksplisit dalam bahasa), keputusan untuk std::stringberperilaku seperti koleksi adalah kesalahan lama yang tidak dapat diperbaiki sekarang.
Elazar

Jawaban:

333

String bukan tipe nilai karena bisa sangat besar, dan harus disimpan di heap. Jenis nilai (dalam semua implementasi CLR sampai saat ini) disimpan di stack. String pengalokasian tumpukan akan memecah segala hal: tumpukan hanya 1MB untuk 32-bit dan 4MB untuk 64-bit, Anda harus mem-box setiap string, menimbulkan penalti penyalinan, Anda tidak bisa mengintip string, dan penggunaan memori akan balon, dll ...

(Sunting: Klarifikasi tambahan tentang penyimpanan tipe nilai menjadi detail implementasi, yang mengarah ke situasi ini di mana kami memiliki tipe dengan semantik nilai yang tidak diwarisi dari System.ValueType. Terima kasih Ben.)

codekaizen
sumber
75
Saya tertarik di sini, tetapi hanya karena memberi saya kesempatan untuk menautkan ke posting blog yang relevan dengan pertanyaan: tipe nilai tidak harus disimpan di tumpukan. Ini paling sering benar di ms.net, tetapi sama sekali tidak ditentukan oleh spesifikasi CLI. Perbedaan utama antara nilai dan tipe referensi adalah, bahwa tipe referensi mengikuti semantik nilai-demi-nilai. Lihat blogs.msdn.com/ericlippert/archive/2009/04/27/… dan blogs.msdn.com/ericlippert/archive/2009/05/04/…
Ben Schwehn
8
@ Qwertie: Stringbukan ukuran variabel. Ketika Anda menambahkannya, Anda sebenarnya membuat Stringobjek lain , mengalokasikan memori baru untuknya.
codekaizen
5
Yang mengatakan, string bisa, secara teori, adalah tipe nilai (sebuah struct), tetapi "nilai" tidak lebih dari referensi ke string. Para perancang. NET secara alami memutuskan untuk memotong perantara (penanganan penanganan tidak efisien dalam .NET 1.0, dan itu wajar untuk mengikuti Java, di mana string sudah didefinisikan sebagai referensi, bukan primitif, jenis. Ditambah, jika string adalah tipe nilai kemudian mengubahnya menjadi objek akan membutuhkannya untuk kotak, inefisiensi yang tidak perlu).
Qwertie
7
@codekaizen Qwertie benar tetapi saya pikir kata-katanya membingungkan. Satu string mungkin ukuran yang berbeda dari string lain dan dengan demikian, tidak seperti tipe nilai sebenarnya, kompiler tidak dapat mengetahui sebelumnya berapa banyak ruang yang dialokasikan untuk menyimpan nilai string. Sebagai contoh, sebuah Int32selalu 4 byte, sehingga kompiler mengalokasikan 4 byte setiap kali Anda mendefinisikan variabel string. Berapa banyak memori yang harus dialokasikan oleh kompiler ketika menemukan intvariabel (jika itu tipe nilai)? Pahamilah bahwa nilainya belum ditetapkan pada saat itu.
Kevin Brock
2
Maaf, salah ketik dalam komentar saya yang tidak dapat saya perbaiki sekarang; yang seharusnya .... Misalnya, Int32selalu 4 byte, sehingga kompiler mengalokasikan 4 byte setiap kali Anda mendefinisikan intvariabel. Berapa banyak memori yang harus dialokasikan oleh kompiler ketika menemukan stringvariabel (jika itu tipe nilai)? Pahamilah bahwa nilainya belum ditetapkan pada saat itu.
Kevin Brock
57

Ini bukan tipe nilai karena kinerja (ruang dan waktu!) Akan mengerikan jika itu tipe nilai dan nilainya harus disalin setiap kali dilewatkan ke dan dikembalikan dari metode, dll.

Ini memiliki nilai semantik untuk menjaga dunia tetap waras. Bisakah Anda bayangkan betapa sulitnya kode jika

string s = "hello";
string t = "hello";
bool b = (s == t);

diatur bmenjadi false? Bayangkan betapa sulitnya menyandi hampir semua aplikasi.

jason
sumber
44
Java tidak dikenal sebagai empulur.
jason
3
@ Mat: tepatnya. Ketika saya beralih ke C # ini agak membingungkan, karena saya selalu menggunakan (dan kadang-kadang masih). Equals (..) untuk membandingkan string sementara rekan tim saya hanya menggunakan "==". Saya tidak pernah mengerti mengapa mereka tidak meninggalkan "==" untuk membandingkan referensi, meskipun jika Anda berpikir, 90% dari waktu Anda mungkin ingin membandingkan konten bukan referensi untuk string.
Juri
7
@ Juri: Sebenarnya saya pikir itu tidak pernah diinginkan untuk memeriksa referensi, karena kadang new String("foo");- kadang dan yang lain new String("foo")dapat mengevaluasi dalam referensi yang sama, yang bukan yang Anda harapkan dilakukan oleh newoperator. (Atau dapatkah Anda memberi tahu saya sebuah kasus di mana saya ingin membandingkan referensi?)
Michael
1
@ Michael Yah, Anda harus memasukkan perbandingan referensi di semua perbandingan untuk menangkap perbandingan dengan nol. Tempat lain yang baik untuk membandingkan referensi dengan string, adalah ketika membandingkan daripada membandingkan kesetaraan. Dua string yang setara, bila dibandingkan harus mengembalikan 0. Memeriksa kasus ini meskipun memakan waktu sepanjang menjalankan seluruh perbandingan, jadi bukan jalan pintas yang bermanfaat. Memeriksa ReferenceEquals(x, y)adalah tes cepat dan Anda dapat segera mengembalikan 0, dan ketika digabungkan dengan uji nol Anda bahkan tidak menambah pekerjaan lagi.
Jon Hanna
1
... memiliki string menjadi tipe nilai dari gaya itu daripada menjadi tipe kelas akan berarti nilai default dari a stringbisa berperilaku sebagai string kosong (seperti pada sistem pre-.net) daripada sebagai referensi nol. Sebenarnya, preferensi saya sendiri adalah memiliki tipe nilai Stringyang berisi tipe referensi NullableString, dengan yang pertama memiliki nilai default yang setara dengan String.Emptydan yang terakhir memiliki default null, dan dengan aturan tinju / unboxing khusus (seperti meninju default- dihargai NullableStringakan menghasilkan referensi ke String.Empty).
supercat
26

Perbedaan antara tipe referensi dan tipe nilai pada dasarnya adalah tradeoff kinerja dalam desain bahasa. Jenis referensi memiliki beberapa overhead pada konstruksi dan penghancuran dan pengumpulan sampah, karena mereka dibuat di heap. Tipe nilai di sisi lain memiliki overhead pada pemanggilan metode (jika ukuran data lebih besar dari pointer), karena seluruh objek disalin daripada hanya pointer. Karena string dapat (dan biasanya) jauh lebih besar dari ukuran pointer, mereka dirancang sebagai tipe referensi. Juga, seperti yang ditunjukkan Servy, ukuran tipe nilai harus diketahui pada waktu kompilasi, yang tidak selalu berlaku untuk string.

Pertanyaan tentang mutabilitas adalah masalah yang terpisah. Baik tipe referensi dan tipe nilai bisa berubah-ubah atau tidak berubah. Jenis nilai biasanya tidak berubah, karena semantik untuk jenis nilai yang dapat berubah dapat membingungkan.

Jenis referensi umumnya bisa berubah, tetapi dapat dirancang sebagai tidak berubah jika masuk akal. String didefinisikan sebagai tidak dapat diubah karena memungkinkan pengoptimalan tertentu. Misalnya, jika string literal yang sama muncul beberapa kali dalam program yang sama (yang cukup umum), kompiler dapat menggunakan kembali objek yang sama.

Jadi mengapa "==" kelebihan beban untuk membandingkan string dengan teks? Karena itu adalah semantik yang paling berguna. Jika dua string sama dengan teks, mereka mungkin atau mungkin tidak menjadi referensi objek yang sama karena optimasi. Jadi membandingkan referensi sama sekali tidak berguna, sementara membandingkan teks hampir selalu seperti yang Anda inginkan.

Berbicara lebih umum, Strings memiliki apa yang disebut semantik nilai . Ini adalah konsep yang lebih umum daripada tipe nilai, yang merupakan detail implementasi spesifik C #. Tipe nilai memiliki semantik nilai, tetapi tipe referensi juga memiliki semantik nilai. Ketika suatu tipe memiliki semantik nilai, Anda tidak dapat benar-benar mengetahui apakah implementasi yang mendasarinya adalah tipe referensi atau tipe nilai, sehingga Anda dapat mempertimbangkannya sebagai detail implementasi.

JacquesB
sumber
Perbedaan antara tipe nilai dan tipe referensi sama sekali bukan tentang kinerja. Ini tentang apakah variabel berisi objek aktual atau referensi ke objek. Suatu string tidak mungkin menjadi tipe nilai karena ukuran string adalah variabel; harus konstan untuk menjadi tipe nilai; kinerja hampir tidak ada hubungannya dengan itu. Jenis referensi juga tidak mahal untuk dibuat sama sekali.
Servy
2
@Sevy: Ukuran string adalah konstan.
JacquesB
Karena itu hanya berisi referensi ke array karakter, yang berukuran variabel. Memiliki tipe nilai yang hanya "nilai" nyata adalah tipe referensi akan semakin membingungkan, karena masih memiliki semantik referensi untuk semua tujuan intensif.
Servy
1
@Sevy: Ukuran array konstan.
JacquesB
1
Setelah Anda membuat array ukurannya konstan, tetapi semua array di seluruh dunia tidak semuanya berukuran sama persis. Itu maksudku. Untuk sebuah string menjadi tipe nilai, semua string yang ada harus semuanya berukuran persis sama, karena itulah jenis nilai dirancang dalam .NET. Perlu dapat memesan ruang penyimpanan untuk tipe nilai seperti itu sebelum benar-benar memiliki nilai , jadi ukurannya harus diketahui pada waktu kompilasi . A seperti stringjenis akan perlu memiliki buffer char beberapa ukuran tetap, yang akan menjadi baik membatasi dan sangat tidak efisien.
Servy
16

Ini adalah jawaban terlambat untuk pertanyaan lama, tetapi semua jawaban lain tidak ada gunanya, yaitu .NET tidak memiliki obat generik hingga .NET 2.0 pada 2005.

Stringadalah tipe referensi bukan tipe nilai karena itu sangat penting bagi Microsoft untuk memastikan bahwa string dapat disimpan dengan cara yang paling efisien dalam koleksi non-generik , seperti System.Collections.ArrayList.

Menyimpan tipe-nilai dalam koleksi non-generik membutuhkan konversi khusus ke tipe objectyang disebut tinju. Ketika CLR mengotakkan tipe nilai, itu membungkus nilai di dalam a System.Objectdan menyimpannya di tumpukan yang dikelola.

Membaca nilai dari koleksi membutuhkan operasi terbalik yang disebut unboxing.

Baik tinju maupun unboxing memiliki biaya yang tidak dapat diabaikan: tinju membutuhkan alokasi tambahan, unboxing membutuhkan pemeriksaan jenis.

Beberapa jawaban mengklaim secara keliru bahwa stringtidak pernah dapat diimplementasikan sebagai tipe nilai karena ukurannya variabel. Sebenarnya mudah untuk menerapkan string sebagai struktur data panjang tetap menggunakan strategi Optimasi String Kecil: string akan disimpan dalam memori secara langsung sebagai urutan karakter Unicode kecuali untuk string besar yang akan disimpan sebagai pointer ke buffer eksternal. Kedua representasi dapat dirancang untuk memiliki panjang tetap yang sama, yaitu ukuran pointer.

Jika generik sudah ada sejak hari pertama saya kira memiliki string sebagai tipe nilai mungkin akan menjadi solusi yang lebih baik, dengan semantik yang lebih sederhana, penggunaan memori yang lebih baik, dan lokalitas cache yang lebih baik. Hanya List<string>berisi string kecil bisa menjadi satu blok memori yang berdekatan.

ZunTzu
sumber
Wah, terima kasih atas jawaban ini! Saya telah melihat semua jawaban lain yang mengatakan hal-hal tentang alokasi heap dan stack, sementara stack adalah detail implementasi . Lagi pula, stringhanya berisi ukuran dan penunjuk ke chararray, jadi itu tidak akan menjadi "tipe nilai besar". Tapi ini adalah alasan sederhana dan relevan untuk keputusan desain ini. Terima kasih!
V0ldek
8

Bukan hanya string yang merupakan tipe referensi yang tidak berubah. Delegasi multi-pemain juga. Itu sebabnya aman untuk menulis

protected void OnMyEventHandler()
{
     delegate handler = this.MyEventHandler;
     if (null != handler)
     {
        handler(this, new EventArgs());
     }
}

Saya kira string tidak dapat diubah karena ini adalah metode paling aman untuk bekerja dengannya dan mengalokasikan memori. Mengapa mereka bukan tipe Nilai? Penulis sebelumnya benar tentang ukuran tumpukan dll. Saya juga akan menambahkan bahwa membuat string sebagai jenis referensi memungkinkan untuk menghemat ukuran perakitan ketika Anda menggunakan string konstan yang sama dalam program ini. Jika Anda mendefinisikan

string s1 = "my string";
//some code here
string s2 = "my string";

Kemungkinannya adalah bahwa kedua instance dari konstanta "string saya" akan dialokasikan hanya sekali dalam perakitan Anda.

Jika Anda ingin mengelola string seperti tipe referensi biasa, masukkan string ke dalam StringBuilder baru (string s). Atau gunakan MemoryStreams.

Jika Anda ingin membuat pustaka, tempat Anda mengharapkan string besar untuk diteruskan dalam fungsi Anda, baik menentukan parameter sebagai StringBuilder atau sebagai Stream.

Bogdan_Ch
sumber
1
Ada banyak contoh tipe referensi yang tidak dapat diubah. Dan contoh string, yang memang cukup banyak dijamin di bawah implementasi saat ini - secara teknis itu adalah per modul (bukan per-perakitan) - tapi itu hampir selalu hal yang sama ...
Marc Gravell
5
Kembali ke poin terakhir: StringBuilder tidak membantu jika Anda mencoba untuk meneruskan string besar (karena sebenarnya tetap diterapkan sebagai string) - StringBuilder berguna untuk memanipulasi string beberapa kali.
Marc Gravell
Apakah yang Anda maksud delegate handler, bukan hadler? (maaf untuk pilih-pilih .. tapi itu sangat dekat dengan a) nama (tidak umum saya tahu ....)
Pure.Krome
6

Juga, cara string diimplementasikan (berbeda untuk setiap platform) dan ketika Anda mulai menjahitnya bersama-sama. Suka menggunakan a StringBuilder. Ini mengalokasikan buffer untuk Anda salin ke, setelah Anda mencapai akhir, itu mengalokasikan lebih banyak memori untuk Anda, dengan harapan bahwa jika Anda melakukan kinerja penggabungan besar tidak akan terhalang.

Mungkin Jon Skeet dapat membantu di sini?

Chris
sumber
5

Ini terutama masalah kinerja.

Memiliki string berperilaku SEPERTI nilai nilai membantu ketika menulis kode, tetapi memilikinya BE jenis nilai akan membuat hit kinerja besar.

Untuk tampilan yang lebih mendalam, lihat artikel yang bagus tentang string dalam kerangka .net.

Denis Troller
sumber
3

Dengan kata yang sangat sederhana, nilai apa pun yang memiliki ukuran pasti dapat diperlakukan sebagai tipe nilai.

saurav.net
sumber
Ini harus menjadi komentar
ρяσѕρєя K
lebih mudah dimengerti untuk ppl baru ke c #
LONG
2

Bagaimana Anda tahu stringjenis referensi? Saya tidak yakin itu penting bagaimana itu diterapkan. String dalam C # tidak dapat diubah dengan tepat sehingga Anda tidak perlu khawatir tentang masalah ini.


sumber
Ini adalah tipe referensi (saya percaya) karena tidak berasal dari System.ValueType Dari MSDN Keterangan pada System.ValueType: Tipe data dipisahkan menjadi tipe nilai dan tipe referensi. Jenis nilai dapat dialokasikan atau dialokasikan inline dalam struktur. Jenis referensi dialokasikan tumpukan.
Davy8
Kedua tipe referensi dan nilai diturunkan dari Object kelas dasar utama. Dalam kasus di mana perlu untuk tipe nilai untuk berperilaku seperti objek, pembungkus yang membuat tipe nilai terlihat seperti objek referensi dialokasikan pada heap, dan nilai tipe nilai disalin ke dalamnya.
Davy8
Pembungkus ditandai sehingga sistem tahu bahwa itu berisi tipe nilai. Proses ini dikenal sebagai tinju, dan proses sebaliknya dikenal sebagai unboxing. Boxing dan unboxing memungkinkan semua jenis diperlakukan sebagai objek. (Di situs belakang, mungkin seharusnya hanya tertaut ke artikel.)
Davy8
2

Sebenarnya string memiliki sedikit kemiripan dengan tipe nilai. Sebagai permulaan, tidak semua tipe nilai tidak dapat diubah, Anda dapat mengubah nilai Int32 yang Anda inginkan dan itu akan tetap menjadi alamat yang sama pada stack.

String tidak dapat diubah karena alasan yang sangat bagus, string tidak ada hubungannya dengan itu menjadi tipe referensi, tetapi banyak hubungannya dengan manajemen memori. Ini hanya lebih efisien untuk membuat objek baru ketika ukuran string berubah daripada menggeser hal-hal di tumpukan terkelola. Saya pikir Anda mencampurkan bersama nilai / tipe referensi dan konsep objek yang tidak berubah.

Sejauh "==": Seperti yang Anda katakan "==" adalah kelebihan operator, dan sekali lagi itu diterapkan untuk alasan yang sangat baik untuk membuat kerangka kerja lebih berguna ketika bekerja dengan string.

WebMatrix
sumber
Saya menyadari bahwa tipe nilai menurut definisi tidak dapat diubah, tetapi sebagian besar praktik terbaik tampaknya menyarankan bahwa mereka seharusnya ketika membuat Anda sendiri. Saya mengatakan karakteristik, tidak sifat jenis nilai, yang bagi saya berarti bahwa sering jenis nilai menunjukkan ini, tetapi tidak harus dengan definisi
Davy8
5
@ WebMatrix, @ Davy8: Tipe primitif (int, dobel, bool, ...) tidak berubah.
jason
1
@ Alasan, saya pikir istilah abadi sebagian besar berlaku untuk objek (tipe referensi) yang tidak dapat berubah setelah inisialisasi, seperti string ketika nilai string berubah, secara internal contoh baru dari string dibuat, dan objek asli tetap tidak berubah. Bagaimana ini berlaku untuk tipe nilai?
WebMatrix
8
Entah bagaimana, dalam "int n = 4; n = 9;", itu bukan berarti variabel int Anda "tidak berubah", dalam arti "konstan"; itu karena nilai 4 tidak dapat diubah, tidak berubah menjadi 9. Variabel int Anda "n" pertama-tama memiliki nilai 4 dan kemudian nilai yang berbeda, 9; tetapi nilai-nilai itu sendiri tidak berubah. Terus terang, bagi saya ini sangat dekat dengan wtf.
Daniel Daranas
1
+1. Saya muak mendengar ini "string seperti tipe nilai" padahal sebenarnya tidak.
Jon Hanna
1

Tidak sesederhana Strings yang terdiri dari array karakter. Saya melihat string sebagai array karakter []. Oleh karena itu mereka berada di heap karena lokasi memori referensi disimpan di stack dan menunjuk ke awal lokasi memori array di heap. Ukuran string tidak diketahui sebelum dialokasikan ... sempurna untuk heap.

Itulah sebabnya sebuah string benar-benar tidak dapat diubah karena ketika Anda mengubahnya walaupun ukurannya sama, kompiler tidak mengetahui hal itu dan harus mengalokasikan array baru dan menetapkan karakter ke posisi dalam array. Masuk akal jika Anda menganggap string sebagai cara bahasa melindungi Anda dari keharusan mengalokasikan memori dengan cepat (baca C seperti pemrograman)

BionicCyborg
sumber
1
"ukuran string tidak diketahui sebelum dialokasikan" - ini tidak benar di CLR.
codekaizen
-1

Dengan risiko mendapat lagi suara misterius ... fakta bahwa banyak yang menyebutkan tumpukan dan memori sehubungan dengan tipe nilai dan tipe primitif adalah karena mereka harus masuk ke dalam register di mikroprosesor. Anda tidak dapat mendorong atau mengeluarkan sesuatu ke / dari tumpukan jika membutuhkan lebih banyak bit daripada register yang memiliki .... instruksinya adalah, misalnya "pop eax" - karena eax memiliki lebar 32 bit pada sistem 32-bit.

Tipe primitif floating-point ditangani oleh FPU, yang lebar 80 bit.

Ini semua diputuskan jauh sebelum ada bahasa OOP untuk mengaburkan definisi tipe primitif dan saya berasumsi bahwa tipe nilai adalah istilah yang telah dibuat khusus untuk bahasa OOP.

jinzai
sumber