C # 'da, String neden bir değer türü gibi davranan bir başvuru türüdür?


371

Bir String, aynı nesneye başvurduklarından emin olmak yerine, metni karşılaştırmak için değişmez olma ve == aşırı yüklenme gibi bir değer türünün özelliklerinin çoğuna sahip olmasına rağmen bir referans türüdür.

Neden string sadece bir değer türü değil?


Değişmez tipler için ayrım çoğunlukla bir uygulama detayı olduğundan ( istestleri bir kenara bıraktığından ), cevap muhtemelen "tarihsel nedenlerle" dir. Değişmeyen nesneleri fiziksel olarak kopyalamaya gerek olmadığından, kopyalama performansı bunun nedeni olamaz. Artık gerçekte iskontrolleri (veya benzer kısıtlamaları) kullanan kodu kırmadan değiştirmek mümkün değildir .
Elazar

BTW bu C ++ için aynı cevaptır (değer ve referans türleri arasındaki ayrım dilde açık olmasa da), std::stringbir koleksiyon gibi davranma kararı şu anda düzeltilemeyen eski bir hatadır.
Elazar

Yanıtlar:


333

Dizeler, büyük olabileceğinden ve yığın üzerinde depolanması gerektiğinden değer türleri değildir. Değer türleri (henüz CLR'nin tüm uygulamalarında) yığın üzerinde depolanır. Yığın ayırma dizeleri her türlü şeyi kıracaktır: yığın 32 bit için sadece 1 MB ve 64 bit için 4 MB, her dizeyi kutlamanız, bir kopya cezası vermeniz, sabit dizeler ve bellek kullanımı yapmamanız gerekir balon, vs ...

(Düzenleme: Değer türü depolamanın, bir uygulama ayrıntısı olmasıyla ilgili açıklama eklendi; bu, System.ValueType öğesinden devralmayan değer sinemasına sahip bir türümüzün olduğu bu duruma yol açar.


75
Ben burada nitpicking, ama sadece bana soru ile ilgili bir blog yazısı bağlantı vermek için bir fırsat verdiği için: değer türleri mutlaka yığın üzerinde depolanmaz. Çoğu zaman ms.net içinde doğrudur, ancak CLI belirtiminde belirtilmez. Değer ve referans türleri arasındaki temel fark, referans türlerinin değer-değer kopya semantiğini izlemesidir. Bkz. Blogs.msdn.com/ericlippert/archive/2009/04/27/… ve blogs.msdn.com/ericlippert/archive/2009/05/04/…
Ben

8
@Qwertie: Stringdeğişken boyutlu değil. Buna eklediğinizde, aslında başka bir Stringnesne yaratıyorsunuz ve bunun için yeni bellek tahsis ediyorsunuz .
codekaizen

5
Bununla birlikte, bir dize, teorik olarak, bir değer türü (bir yapı) olabilir, ancak "değer", dizeye yapılan bir referanstan başka bir şey olamazdı. .NET tasarımcıları doğal olarak aracıyı kesmeye karar verdiler (yapı işleme .NET 1.0'da verimsizdi ve dizelerin ilkel tip yerine bir referans olarak zaten tanımlandığı Java'yı takip etmek doğaldı. daha sonra onu nesneye dönüştüren bir değer türü, kutunun kutuya alınmasını gerektirir, gereksiz bir verimsizlik).
Qwertie

7
@codekaizen Qwertie haklı ama sanırım kafa karıştırıcıydı. Bir dize, başka bir dizeden farklı bir boyutta olabilir ve bu nedenle, gerçek bir değer türünün aksine, derleyici önceden dize değerini depolamak için ne kadar alan ayrılacağını bilemezdi. Örneğin, a Int32her zaman 4 bayttır, bu nedenle bir dize değişkeni tanımladığınızda derleyici 4 bayt ayırır. Derleyici bir intdeğişkenle karşılaştığında ne kadar bellek ayırmalıdır (bir değer türüyse)? Değerin henüz atanmadığını anlayın.
Kevin Brock

2
Üzgünüm, yorumumda şimdi düzeltemediğim bir yazım hatası; Örneğin, a Int32her zaman 4 bayttır, bu nedenle derleyici her intdeğişken tanımladığınızda 4 bayt ayırır. Derleyici bir stringdeğişkenle karşılaştığında ne kadar bellek ayırmalıdır (bir değer türüyse)? Değerin henüz atanmadığını anlayın.
Kevin Brock

57

Bu bir değer türü değildir, çünkü değer (alan ve zaman!), Bir değer türü olsaydı ve yöntemlere her aktarıldığında ve yöntemlerden döndürüldüğünde değerinin kopyalanması gerektiğinde korkunç olurdu.

Dünyayı aklı başında tutmak için değer semantiğine sahiptir. Kodlamanın ne kadar zor olacağını hayal edebiliyor musunuz?

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

set bolmak false? Herhangi bir uygulama hakkında kodlamanın ne kadar zor olacağını düşünün.


44
Java'nın özlü olduğu bilinmemektedir.
Jason

3
@Matt: kesinlikle. C # 'a geçtiğimde bu takım kafa karıştırıcıydı, çünkü takım arkadaşlarım sadece "==" kullanılırken dizeleri karşılaştırmak için her zaman kullandım. Neden referansları karşılaştırmak için "==" bırakmadıklarını anlamadım, ancak düşünürseniz, zamanın% 90'ı muhtemelen dizeleri için referansları değil karşılaştırmak isteyeceksiniz.
Juri

7
@Juri: Aslında referansları kontrol etmenin asla arzu edilmediğini düşünüyorum, çünkü bazen new String("foo");ve başka new String("foo")biri aynı referansta değerlendirebilir, bu tür bir newoperatörün yapmasını beklediğiniz şey değildir . (Ya da referansları karşılaştırmak istediğim bir durum söyleyebilir misiniz?)
Michael

1
@Michael Null ile karşılaştırmayı yakalamak için tüm karşılaştırmalara bir referans karşılaştırması eklemelisiniz. Referansları dizelerle karşılaştırmak için iyi bir yer de eşitlik karşılaştırmasından ziyade karşılaştırma yapılır. Karşılaştırıldığında iki eşdeğer dizge 0 döndürmelidir. Yine de, bu durumun kontrol edilmesi, tüm karşılaştırmayı yine de yürütmek kadar uzun sürdüğü için, yararlı bir kısayol değildir. Kontrol ReferenceEquals(x, y)etmek hızlı bir testtir ve hemen 0'a dönebilirsiniz ve null testinizle karıştırıldığında daha fazla iş bile eklemez.
Jon Hanna

1
... dizelerin sınıf tipi olmak yerine o stilin bir değer türü olması, a değerinin varsayılan değerinin, stringboş bir dize olarak değil, boş bir dize gibi davranabileceği anlamına gelir (pre.net sistemlerinde olduğu gibi). Aslında, kendi tercihim Stringreferans tipi içeren bir değer türüne NullableStringsahip olmak, birincisinin varsayılan değeri eşdeğer String.Emptyve ikincisinin varsayılan değeri nullve özel boks / kutudan çıkarma kuralları ( değerinde NullableStringbir referans verir String.Empty).
supercat

26

Referans türleri ve değer türleri arasındaki ayrım, temel olarak dilin tasarımında bir performans dengesidir. Referans türlerinin, yığın üzerinde oluşturuldukları için inşaat ve imha ve çöp toplama üzerinde bazı ek yükleri vardır. Öte yandan değer türleri, yöntem çağrılarında (veri boyutu bir işaretçiden büyükse) ek yüke sahiptir, çünkü tüm nesne yalnızca bir işaretçi yerine kopyalanır. Dizeler bir işaretçinin boyutundan çok daha büyük (ve tipik olarak) olduğundan, başvuru türleri olarak tasarlanmıştır. Ayrıca, Servy'nin işaret ettiği gibi, bir değer türünün boyutu derleme zamanında bilinmelidir, bu da dizeler için her zaman geçerli değildir.

Değişebilirlik sorunu ayrı bir konudur. Hem referans tipleri hem de değer tipleri değişebilir ya da değişmez olabilir. Değişken değer türleri için semantikler kafa karıştırıcı olabileceğinden değer türleri tipik olarak değişmezdir.

Referans tipleri genellikle değişkendir, ancak mantıklıysa değişmez olarak tasarlanabilir. Dizeler değişmez olarak tanımlanır, çünkü belirli optimizasyonları mümkün kılar. Örneğin, aynı dize değişmez değeri aynı programda (bu oldukça yaygın olan) birden çok kez oluşursa, derleyici aynı nesneyi yeniden kullanabilir.

Öyleyse neden dizeleri metne göre karşılaştırmak için "==" aşırı yüklenmiş? Çünkü en kullanışlı anlambilimdir. İki dize metne eşitse, optimizasyonlar nedeniyle aynı nesne başvurusu olabilir veya olmayabilir. Bu yüzden referansları karşılaştırmak oldukça işe yaramazken, metni karşılaştırmak neredeyse her zaman istediğiniz şeydir.

Daha genel olarak konuşmak gerekirse, Dizeler değer semantiği olarak adlandırılır . Bu, C #'a özgü bir uygulama ayrıntısı olan değer türlerinden daha genel bir kavramdır. Değer türlerinde değer semantiği vardır, ancak başvuru türlerinde değer semantiği de olabilir. Bir tür değer semantiğine sahip olduğunda, temeldeki uygulamanın bir referans türü mü yoksa değer türü mü olduğunu gerçekten anlayamazsınız, bu nedenle bir uygulama ayrıntısını düşünebilirsiniz.


Değer türleri ve referans türleri arasındaki ayrım aslında performansla ilgili değildir. Bir değişkenin gerçek bir nesne mi yoksa bir nesne referansı içerip içermediğiyle ilgilidir. Bir dize hiçbir zaman bir değer türü olamaz çünkü bir dizenin boyutu değişkendir; bir değer türü olmak için sabit olması gerekir; performansın bununla hemen hemen hiçbir ilgisi yoktur. Referans türlerinin oluşturulması da pahalı değildir.
13'te

2
@Sevy: bir dize boyutu olan sabit.
JacquesB

Çünkü sadece değişken boyutlu bir karakter dizisine başvuru içerir. Sadece gerçek "değer" olan bir değer türüne sahip olmak bir referans türüydü, çünkü tüm yoğun amaçlar için hala referans anlambilimine sahip olacaktı.
13'te

1
@Sevy: Bir dizinin boyutu sabittir.
JacquesB

1
Bir dizi oluşturduktan sonra boyutu sabittir, ancak tüm dünyadaki tüm diziler tamamen aynı boyutta değildir. Demek istediğim bu. Bir dizenin değer türü olması için, varolan tüm dizelerin tamamen aynı boyutta olması gerekir, çünkü .NET'te değer türleri bu şekilde tasarlanmıştır. Bir değere sahip olmadan önce bu tür değer türleri için depolama alanı ayırabilmesi gerekir, bu nedenle boyut derleme zamanında bilinmelidir . Böyle bir stringtipin, hem kısıtlayıcı hem de son derece verimsiz olacak bazı sabit boyutlu bir char tamponuna sahip olması gerekir.
Servy

16

Bu eski bir soruya geç bir cevaptır, ancak diğer tüm cevaplar noktayı kaçırmaktadır, yani 2005'te .NET 2.0'a kadar .NET'in jenerikleri yoktu.

StringÇünkü yerine bir değer türünde bir referans türüdür Microsoft bu dizeleri olmayan jenerik koleksiyonlarında en verimli şekilde saklanabilir sağlamak için daha büyük önem taşımaktadır oldu gibi System.Collections.ArrayList.

Bir değer türünü genel olmayan bir koleksiyonda depolamak object, boks adı verilen türe özel bir dönüşüm gerektirir . CLR bir değer türünü işaretlediğinde, a içindeki değeri sarar System.Objectve yönetilen yığında saklar.

Koleksiyondaki değerin okunması, kutudan çıkarma adı verilen ters işlem gerektirir.

Hem boks hem de boksun ihmal edilemez maliyeti vardır: boks ek bir tahsis gerektirir, kutudan çıkarma tip kontrolü gerektirir.

Bazı cevaplar string, boyutu değişken olduğu için hiçbir zaman değer türü olarak uygulanamayacağını yanlış iddia ediyor . Aslında bir Küçük Dize Optimizasyonu stratejisi kullanarak dizeyi sabit uzunluklu bir veri yapısı olarak uygulamak kolaydır: dizeler, harici bir arabelleğe işaretçi olarak depolanacak büyük dizeler dışında doğrudan Unicode karakter dizisi olarak depolanır. Her iki gösterim de aynı sabit uzunluğa, yani bir işaretçinin boyutuna sahip olacak şekilde tasarlanabilir.

Eğer jenerikler ilk günden itibaren mevcut olsaydı, değer tipi olarak dizgiye sahip olmak sanırım daha basit anlambilim, daha iyi bellek kullanımı ve daha iyi önbellek konumuyla daha iyi bir çözüm olurdu. Bir List<string>içeren sadece küçük şeritler bellek tek bir bitişik blok olabilirdi.


Benim, bu cevap için teşekkürler! Yığın bir uygulama detayı iken, yığın ve yığın tahsisleri hakkında şeyler söyleyerek diğer tüm cevaplara bakıyordum . Sonuçta, yine de stringsadece boyutunu ve chardiziye bir işaretçi içerir , bu yüzden bir "büyük değer türü" olmaz. Ancak bu, bu tasarım kararının basit ve ilgili bir nedenidir. Teşekkürler!
V0ldek

8

Sadece dizgiler değişmez referans tipleridir. Çoklu oyuncu delegeleri de. Bu yüzden yazmak güvenlidir

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

Bu dizeleri değişmez olduğunu düşünüyorum çünkü bu onlarla çalışmak ve bellek ayırmak için en güvenli yöntemdir. Neden Değer türleri değil? Önceki yazarlar yığın boyutu vb. Hakkında haklıyım. Ayrıca, dizelerde bir başvuru türünün, programda aynı sabit dizeyi kullandığınızda montaj boyutundan tasarruf etmesini sağladığını da ekleyeceğim. Eğer tanımlarsan

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

Şansınız, "dizem" sabitinin her iki örneğinin de derlemenize yalnızca bir kez tahsis edilmesidir.

Dizeleri normal referans türü gibi yönetmek istiyorsanız, dizeyi yeni bir StringBuilder (dize s) içine yerleştirin. Veya MemoryStreams kullanın.

İşlevlerinizde büyük bir dizenin geçmesini beklediğiniz bir kütüphane oluşturacaksanız, bir parametreyi StringBuilder veya Stream olarak tanımlayın.


1
Değişmez referans türlerinin birçok örneği vardır. Ve yine dize örneği, bu gerçekten de mevcut uygulamalar altında çok garantili - teknik olarak modül başına (montaj başına değil) - ama neredeyse her zaman aynı şey ...
Marc Gravell

5
Re son nokta: StringBuilder büyük bir dize geçmeye çalışıyorsanız (aslında bir dize olarak zaten uygulandığından) yardımcı olmaz - StringBuilder bir dizeyi birden çok kez işlemek için yararlıdır .
Marc Gravell

U hadler değil delege işleyicisi mi demek istediniz? (seçici olduğum için özür dilerim ... ama bildiğim (yaygın olmayan) bir soyadına çok yakın ....)
Pure.Krome

6

Ayrıca, dizelerin uygulanma şekli (her platform için farklıdır) ve bunları birleştirmeye başladığınızda. A kullanmak gibi StringBuilder. Kopyalamanız için bir arabellek ayırır, sonuna ulaştığınızda, büyük bir birleştirme performansının engellenmemesi umuduyla sizin için daha da fazla bellek ayırır.

Belki Jon Skeet buraya yardım edebilir mi?


5

Esas olarak bir performans sorunudur.

Dizelerin LIKE değer türüne sahip olması, kod yazarken yardımcı olur, ancak bir değer türünün BE olması, büyük bir performans isabeti yaratacaktır.

Derinlemesine bir görünüm için, .net çerçevesindeki dizelerle ilgili güzel bir makaleye göz atın .


3

Çok basit bir deyişle, belirli bir boyuta sahip herhangi bir değer, bir değer türü olarak ele alınabilir.


Bu bir yorum olmalı
ρяσѕρєя K

c # için yeni ppl için anlamak daha kolay
UZUN

2

stringBir referans türü olduğunu nasıl anlarsınız ? Nasıl uygulandığından emin değilim. C # dizeleri tam olarak değişmez, böylece bu sorun hakkında endişelenmenize gerek yok.


System.ValueType öğesinden türetilmediğinden, bu tür bir başvuru türü olduğuna inanıyorum. Değer türleri, bir yapıda yığınla ayrılmış veya satır içi olarak ayrılmıştır. Referans türleri öbek olarak ayrılmıştır.
Davy8

Hem başvuru hem de değer türleri, nihai temel sınıf Object'ten türetilir. Bir değer türünün nesne gibi davranması gerektiğinde, değer türünü referans nesneye benzeyen bir sarmalayıcı yığın üzerinde ayrılır ve değer türünün değeri bu nesneye kopyalanır.
Davy8

Sarıcı işaretlenir, böylece sistem bir değer türü içerdiğini bilir. Bu işlem boks olarak bilinir ve ters işlem kutudan çıkarma olarak bilinir. Boks ve kutudan çıkarma, her türün nesne olarak ele alınmasını sağlar. (Arka sitede, muhtemelen makaleye bağlantı
vermeliydim

2

Aslında dizelerin değer türlerine çok az benzerlikleri vardır. Yeni başlayanlar için, tüm değer türleri değiştirilemez, bir Int32'nin değerini istediğiniz gibi değiştirebilirsiniz ve yine de yığın üzerinde aynı adres olacaktır.

Dizeler çok iyi bir nedenden dolayı değişmez, referans türü olmasıyla hiçbir ilgisi yoktur, ancak bellek yönetimi ile ilgisi vardır. Dize boyutu değiştiğinde, yönetilen öbek üzerinde bir şeyleri kaydırmak yerine yeni bir nesne oluşturmak daha etkilidir. Bence değer / referans türlerini ve değişmez nesne kavramlarını karıştırıyorsunuz.

"==": Dediğiniz gibi "==" bir operatör aşırı yüküdür ve yine dizelerle çalışırken çerçeveyi daha kullanışlı hale getirmek için çok iyi bir nedenden dolayı uygulanmıştır.


Değer türlerinin tanım gereği değişmez olmadığının farkındayım, ancak en iyi uygulama, kendinizinkini oluştururken olması gerektiğini gösteriyor. Değer türlerinin özellikleri değil, dedim ki, bu genellikle değer türlerinin bunları sergilediği anlamına gelir, ancak mutlaka tanım gereği değil
Davy8

5
@WebMatrix, @ Davy8: İlkel tipler (int, double, bool, ...) değişmez.
Jason

1
@Jason, değişmez terimin çoğunlukla, başlatmadan sonra değiştirilemeyen, dizeler değeri değiştiğinde dizeler gibi, dahili olarak yeni bir dize örneği oluşturulduğunu ve orijinal nesnenin değişmeden kaldığını (referans türleri) uyguladığını düşündüm. Bu değer türleri için nasıl geçerlidir?
WebMatrix

8
Her nasılsa, "int n = 4; n = 9;" de, int değişkeni "sabit" anlamında "değişmez" değildir; 4 değerinin değişmez olduğu, 9 olarak değişmediğidir. int "n" değişkeniniz önce 4 değerine, sonra 9 değerine farklı bir değere sahiptir; ancak değerlerin kendileri değişmezdir. Açıkçası, bu bana wtf çok yakın.
Daniel Daranas

1
+1. Oldukça basit değilken bu "dizeleri değer türleri gibidir" duymaktan bıktım.
Jon Hanna

1

Dizeler karakter dizilerinden oluştuğu kadar basit değildir. Dizelere karakter dizileri olarak bakıyorum []. Bu nedenle, yığın bellekte olduğundan, yığın bellekte saklanır ve dizinin yığın üzerindeki bellek konumunun başlangıcına işaret eder. Dize boyutu ayrılmadan önce bilinmemektedir ... yığın için mükemmeldir.

Bu nedenle bir dize gerçekten değişmezdir, çünkü aynı boyutta olsa bile değiştirdiğinizde derleyici bunu bilmez ve yeni bir dizi tahsis etmeli ve dizideki konumlara karakterler atamalıdır. Dizeleri, dillerin sizi anında bellek ayırmaktan koruyacak bir yol olarak düşünmeniz mantıklıdır (C gibi programlama okuyun)


1
"dize boyutu ayrılmadan önce bilinmiyor" - bu CLR'de yanlış.
codekaizen

-1

Bir başka gizemli aşağı oy alma riski altında ... birçoğunun değer türlerine ve ilkel türlere göre yığın ve hafızadan bahsetmesi, mikroişlemcideki bir sicile uymaları gerektiğidir. Bir kaydın sahip olduğundan daha fazla bit alırsa yığına / yerden bir şey itemez veya patlatamazsınız ... talimatlar örneğin "pop eax" - çünkü eax 32 bit sistemde 32 bit genişliğindedir.

Kayan noktalı ilkel tipler 80 bit genişlikteki FPU tarafından ele alınır.

Tüm bunlara, ilkel tür tanımını gizlemek için bir OOP dili gelmeden çok önce karar verildi ve değer türünün özellikle OOP dilleri için yaratılmış bir terim olduğunu varsayıyorum.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.