Dizeleri .NET değişmez olduğu göz önüne alındığında, neden onlar yerine string.Substring()
O ( substring.Length
) zaman alır gibi tasarlanmış merak ediyorum O(1)
?
yani, eğer ödünleşimler nelerdi?
Dizeleri .NET değişmez olduğu göz önüne alındığında, neden onlar yerine string.Substring()
O ( substring.Length
) zaman alır gibi tasarlanmış merak ediyorum O(1)
?
yani, eğer ödünleşimler nelerdi?
Yanıtlar:
GÜNCELLEME: Bu soruyu çok beğendim, yeni blog yazdım. Bkz. Dizeler, değişmezlik ve kalıcılık
Kısa cevap: n büyüyemezse O (n) O (1) 'dir. Çoğu insan minik dizelerden minik dizeleri çıkarır, böylece karmaşıklığın asemptotik olarak nasıl büyüdüğü tamamen önemsizdir .
Uzun cevap:
Bir örnek üzerindeki işlemlerin, orijinalin belleğinin çok az miktarda (tipik olarak O (1) veya O (lg n)) kopyalama veya yeni ayırma ile yeniden kullanılmasına izin verecek şekilde oluşturulmuş değişmez bir veri yapısı, "kalıcı" olarak adlandırılır değişmez veri yapısı. .NET dizeleri değişmez; sorunuz aslında "neden ısrarcı değiller?"
Çünkü genellikle .NET programlarındaki dizelerde yapılan işlemlere baktığınızda, ilgili her şekilde tamamen yeni bir dize yapmak , hiç de kötü değildir . Karmaşık ve kalıcı bir veri yapısı oluşturmanın maliyeti ve zorluğu kendi başına ödeme yapmaz.
İnsanlar kısa bir dizeyi (örneğin, on ya da yirmi karakteri - biraz daha uzun bir dizeden - belki de birkaç yüz karakterden) ayıklamak için genellikle "alt dize" kullanırlar. Virgülle ayrılmış bir dosyada bir metin satırınız var ve soyadı olan üçüncü alanı ayıklamak istiyorsunuz. Satır belki birkaç yüz karakter uzunluğunda olacak, isim birkaç düzine olacak. Elli baytın dize tahsisi ve bellek kopyalama modern donanımda şaşırtıcı derecede hızlıdır . Varolan bir dizenin ortasına bir işaretçi artı bir uzunluktan oluşan yeni bir veri yapısı oluşturmanın da şaşırtıcı derecede hızlı olması önemsizdir; "Yeterince hızlı" tanımı gereği yeterince hızlı.
Ekstrakte edilen alt ipler tipik olarak küçük boyutlu ve ömür boyu kısa; çöp toplayıcı yakında onları geri alacak ve ilk etapta yığın üzerinde fazla yer kaplamadılar. Dolayısıyla, belleğin çoğunun yeniden kullanılmasını teşvik eden kalıcı bir strateji kullanmak da bir kazanç değildir; Yaptığınız tek şey çöp toplayıcınızı yavaşlatmaktır, çünkü şimdi iç işaretçileri kullanma konusunda endişelenmek zorunda.
İnsanların genellikle tellerde yaptıkları alt dize işlemleri tamamen farklı olsaydı, kalıcı bir yaklaşımla gitmek mantıklı olurdu. İnsanlar tipik olarak milyon karakter dizelerine sahipse ve yüz bin karakter aralığında boyutlarda binlerce örtüşen alt dizeyi çıkarıyorlarsa ve bu alt dizeler öbek üzerinde uzun süre yaşadıysa, kalıcı bir alt dize ile gitmek mükemmel mantıklı olurdu yaklaşmak; savurgan ve aldatıcı olmazdı. Ancak çoğu iş kolu programcısı bu tür şeyler gibi belirsiz bir şey yapmaz. .NET, İnsan Genom Projesinin ihtiyaçlarına göre uyarlanmış bir platform değildir; DNA analiz programcıları her gün bu dizi kullanım özellikleri ile ilgili problemleri çözmek zorundadır; oranlar iyi değil. Yakından maç kendi kalıcı veri yapıları inşa kim kaç onların kullanım senaryolarını.
Örneğin, ekibim siz yazdıkça C # ve VB kodunu anında analiz eden programlar yazar. Bu kod dosyalarından bazıları muazzamdır ve bu nedenle alt dizeleri ayıklamak veya karakter eklemek veya silmek için O (n) dize düzenleme yapamayız. Biz hızlı ve verimli mevcut dize veri kütlesini yeniden kullanmak için bize izin bir metin tampon üzerindeki düzenlemeleri temsil kalıcı iletmenin veri yapılarının bir demet kurmuş ve tipik bir düzenleme üzerinde mevcut sözcük ve sözdizim analizler. Bu, çözülmesi zor bir sorundu ve çözümü, C # ve VB kod düzenlemesinin belirli alan adına dar bir şekilde uyarlandı. Yerleşik dize türünün bu sorunu bizim için çözmesini beklemek gerçekçi olmaz.
string contents = File.ReadAllText(filename); foreach (string line in content.Split("\n")) ...
veya diğer sürümleri. Yani bütün bir dosyayı okudum, sonra çeşitli parçaları işledim. Bu tür bir kod çok daha hızlı olurdu ve bir dize kalıcı olsaydı daha az bellek gerektirirdi; her satırı kopyalamak yerine her zaman dosyanın tam olarak bir kopyasına sahip olursunuz, daha sonra her satırın işleminizdeki kısımları kopyalanır. Bununla birlikte, Eric'in dediği gibi - bu tipik kullanım durumu değildir.
String
, kalıcı bir veri yapısı olarak uygulanır (standartlarda belirtilmez, ancak bildiğim tüm uygulamalar bunu yapar).
Tam olarak Dizeler değişmez olduğundan.Substring
, orijinal dizenin en azından bir bölümünün bir kopyasını almalıdır. N baytın bir kopyasını oluşturmak O (n) zamanını almalıdır.
Bir grup baytı sabit zamanda nasıl kopyalayacağınızı düşünüyorsunuz ?
EDIT: Mehrdad dizeyi hiç kopyalamamanızı, ancak bir parçasına referansta bulunmanızı önerir.
Birinin aradığı çok megabaytlık bir dize olan .Net'i düşünün .SubString(n, n+3)
(dizenin ortasındaki n için).
Şimdi, ENTIRE dizesi sadece bir referans 4 karaktere bağlı olduğu için Çöp Toplanamaz mı? Bu saçma bir yer kaybı gibi görünüyor.
Ayrıca, alt dizelere yapılan referansların izlenmesi (hatta alt dizelerin içinde bile olabilir) ve GC'yi (yukarıda açıklandığı gibi) yenmekten kaçınmak için en uygun zamanlarda kopyalamaya çalışmak, konsepti bir kabus haline getirir. .SubString
Basit değişmez modeli kopyalamak ve sürdürmek çok daha basit ve daha güvenilirdir .
DÜZENLEME: İşte bu biraz okumak iyi büyük dizeleri içinde altdizgelerin başvurular tutma tehlikesi hakkında.
memcpy
hala O (n) olanı kullanır .
char*
alt dize alabilirsiniz .
NULL
sonlandırıldı. Lippert'in postunda açıklandığı gibi , ilk 4 bayt ipin uzunluğunu içerir. Skeet'in işaret ettiği gibi, \0
karakter içerebilirler .
Java (.NET'in aksine) iki yol sağlar Substring()
, sadece bir referans tutmak mı yoksa tüm alt dizeyi yeni bir bellek konumuna kopyalamak mı istediğinizi düşünebilirsiniz.
Basit .substring(...)
, dahili olarak kullanılan char
diziyi orijinal String nesnesiyle paylaşır; daha sonra new String(...)
gerektiğinde yeni bir diziye kopyalayabilirsiniz (orijinalin çöp toplanmasını engellemek için).
Bu tür bir esnekliğin bir geliştirici için en iyi seçenek olduğunu düşünüyorum.
.substring(...)
.
Java daha büyük dizelere başvururken kullanılır:
Yine de geliştirilebileceğini hissediyorum: neden kopyalamayı şartlı olarak yapmıyorsunuz?
Alt dize, üst öğenin en az yarısı boyutundaysa, üst öğeye başvurulabilir. Aksi takdirde sadece bir kopyasını alabilirsiniz. Bu, önemli bir fayda sağlarken çok fazla bellek sızmasını önler.
char[]
(başlangıç ve bitiş için farklı işaretçilerle) kullanmaktan yeni bir şey oluşturmaya değişmesidir String
. Bu, maliyet-fayda analizinin yeni bir tane yaratmak için bir tercih göstermesi gerektiğini açıkça göstermektedir String
.
Buradaki yanıtların hiçbiri "basamaklama sorununu" çözmemiştir, yani .NET'teki dizeler bir BStr (bellekte depolanan uzunluk "imlecin önündeki") ve bir CStr (dize bir '\ 0').
"Merhaba orada" dizesi,
0B 00 00 00 48 00 65 00 6C 00 6F 00 20 00 74 00 68 00 65 00 72 00 65 00 00 00
( char*
a- fixed
statüsünde bir a atanmışsa, işaretçi 0x48'i gösterir.)
Bu yapı, bir dizenin uzunluğunun hızlı bir şekilde aranmasına izin verir (birçok bağlamda yararlıdır) ve işaretçinin boş sonlandırılmış bir dizgi bekleyen bir P / Invoke to Win32 (veya diğer) API'lerinde geçirilmesine izin verir.
Bunu yaptığınızda Substring(0, 5)
size bir kopyasını yapmak gerekir diyor kural "Ah, ama ben son karakteri sonra boş karakterli orada olacağına dair söz". Alt dize sonunda olsa bile, diğer değişkenleri bozmadan uzunluğu koymak için yer olmazdı.
Bununla birlikte, bazen, "dizenin ortası" hakkında gerçekten konuşmak istersiniz ve P / Invoke davranışını önemsemeniz gerekmez. Son eklenen ReadOnlySpan<T>
yapı, kopyasız bir alt dize elde etmek için kullanılabilir:
string s = "Hello there";
ReadOnlySpan<char> hello = s.AsSpan(0, 5);
ReadOnlySpan<char> ell = hello.Slice(1, 3);
ReadOnlySpan<char>
" Alt dize" uzunluğu bağımsız olarak saklar ve değerin bitiminden sonra bir "\ 0" olduğunu garanti etmez. "Bir dize gibi" birçok şekilde kullanılabilir, ancak BStr veya CStr özelliklerine (her ikisinden de çok daha az) sahip olmadığı için "dize" değildir. Asla (doğrudan) P / Invoke yapmazsanız, çok fazla bir fark yoktur (aramak istediğiniz API'nin ReadOnlySpan<char>
aşırı yüklenmesi yoksa ).
ReadOnlySpan<char>
bir referans türünün alanı olarak kullanılamaz, bu yüzden de dolaylı bir yol olan ReadOnlyMemory<char>
( s.AsMemory(0, 5)
) vardır, bu ReadOnlySpan<char>
yüzden de aynı farklılıklar string
vardır.
Önceki cevapların bazı cevapları / yorumları, çöp toplayıcının yaklaşık 5 karakter konuşmaya devam ederken bir milyon karakter dizesi tutması gerektiğinin israf edildiğinden bahsetti. Tam olarak bu ReadOnlySpan<char>
yaklaşımla elde edebileceğiniz davranış budur . Sadece kısa hesaplamalar yapıyorsanız, ReadOnlySpan yaklaşımı muhtemelen daha iyidir. Bir süre devam etmeniz gerekiyorsa ve orijinal dizenin yalnızca küçük bir yüzdesini koruyacaksanız, (fazla veriyi kesmek için) uygun bir alt dize yapmak muhtemelen daha iyidir. Ortada bir yerde bir geçiş noktası var, ancak bu sizin özel kullanımınıza bağlıdır.