C# String'ler: Interpolation, StringBuilder ve Türkçe I Tuzağı
C#'ta string'in değiştirilemez oluşu, interpolation ve biçim belirteçleri, escape sequence'lar ve verbatim string, sık kullanılan method'lar, StringBuilder ile ölçülmüş bellek farkı, StringComparison ve Türkçe I/ı/İ tuzağı.
Sürüm notu: Bu yazıdaki kodlar C# 10 ve .NET 6 API'leriyle yazılmıştır; .NET 10 SDK ile derlenip denenmiştir.
Önce Tahmin Et
Bir kitap başlığını önce büyük harfe çeviriyoruz, sonra içindeki bir kelimeyi değiştiriyoruz. En sonda başlığı yazdırıyoruz. Ekranda ne görürsün?
string title = "clean code";
title.ToUpper();
title.Replace("clean", "dirty");
Console.WriteLine(title);
clean code
Hiçbir şey değişmedi. Program derlendi, uyarı bile vermedi, ama başlık ilk hâliyle duruyor. Sebebi C# string'lerinin en önemli özelliği: string değiştirilemez (immutable). ToUpper ve Replace mevcut string'i değiştirmez; yeni bir string üretip döndürür. Biz o dönen değeri hiçbir yere koymadığımız için kayboldu. Doğrusu title = title.ToUpper();. Bu yazıda string'lerle çalışmanın temel araçlarını, immutability'nin sonuçlarını, StringBuilder'ı ve karşılaştırmada culture'ın neden önemli olduğunu görüyoruz.
String Değiştirilemez
Bir string oluşturulduktan sonra içindeki karakterler bir daha değişmez. Değişiyormuş gibi görünen her işlem aslında yeni bir string üretir. Bunu taşa kazınmış bir yazı gibi düşün: üzerinde değişiklik yapamazsın, ancak yeni bir taşa yeni bir yazı kazıyabilirsin. title = title.ToUpper(); dediğinde eski taş yerinde durur, title değişkeni artık yeni taşı gösterir.
Bu tasarımın güzel sonuçları var. Bir string'i bir method'a verdiğinde o method'un string'ini bozmasından korkmazsın. Aynı string'i birçok yerde güvenle paylaşabilirsin, birden fazla thread'in aynı anda okuması da sorun çıkarmaz. Bedeli ise performansta: bir string'i küçük parçalarla defalarca "değiştirdiğinde" her adımda yeni bir string oluşur. Bunu ilerleyen bölümde ölçeceğiz.
Interpolation ve Biçimlendirme
Değişkenleri bir metnin içine yerleştirmenin en okunaklı yolu string interpolation: string'in başına $ koyup değerleri süslü parantez içinde yazarsın.
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
string title = "Clean Code";
decimal price = 34.9m;
int stock = 7;
Console.WriteLine("Title: " + title + ", price: " + price);
Console.WriteLine($"Title: {title}, price: {price}");
Console.WriteLine($"Price with two decimals: {price:F2}");
Console.WriteLine($"Stock as three digits: {stock:D3}");
Console.WriteLine($"|{title,-15}|{price,8:F2}|");
Console.WriteLine($"Total: {price * stock:F2}, low stock: {stock < 10}");
Console.WriteLine($"Braces in text: {{ {stock} }}");
Title: Clean Code, price: 34.9
Title: Clean Code, price: 34.9
Price with two decimals: 34.90
Stock as three digits: 007
|Clean Code | 34.90|
Total: 244.30, low stock: True
Braces in text: { 7 }
İlk iki satır aynı sonucu verir ama ikincisi çok daha kolay okunur. Süslü parantezin içine sadece değişken değil, herhangi bir expression yazabilirsin: {price * stock} ya da {stock < 10}. İki noktadan sonra bir biçim belirtirsin: F2 iki ondalık basamak, D3 en az üç basamaklı tamsayı. Virgülden sonraki sayı hizalama: -15 değeri 15 karakterlik alana sola yaslar, 8 sağa yaslar. Konsolda tablo gibi görünen çıktılar için çok kullanışlı. Metnin içinde gerçek bir süslü parantez istersen onu çift yazarsın.
Sayıların ekrana yazılışı culture'a bağlı olduğu için örneklerin başında culture'ı sabitledik. Culture'ı ve parse işlemlerini bir sonraki yazıda ayrıntılı göreceğiz. C# 10 ile gelen interpolated string handler'lar sayesinde interpolation performans açısından da eskisinden çok daha verimli; artık "interpolation yavaştır, string.Format kullan" tavsiyesine gerek yok.
Kaçış Karakterleri ve Verbatim String
String'in içine tırnak, satır sonu ya da sekme koymak için ters eğik çizgiyle başlayan escape sequence'lar kullanırsın. Windows dosya yolları gibi bol ters eğik çizgi içeren metinler için ise verbatim string var:
string quote = "She said \"read this\" and left.";
string lines = "Line 1\nLine 2";
string columns = "Title\tPrice";
string windowsPath = "C:\\books\\new.txt";
string verbatimPath = @"C:\books\new.txt";
string verbatimQuote = @"She said ""read this"" and left.";
string shelf = "A";
string mixed = $@"C:\books\{shelf}\list.txt";
Console.WriteLine(quote);
Console.WriteLine(lines);
Console.WriteLine(columns);
Console.WriteLine(windowsPath == verbatimPath);
Console.WriteLine(verbatimQuote);
Console.WriteLine(mixed);
Console.WriteLine("caf\u00E9");
She said "read this" and left.
Line 1
Line 2
Title Price
True
She said "read this" and left.
C:\books\A\list.txt
café
En sık kullanılanlar \" (tırnak), \\ (ters eğik çizgi), \n (yeni satır), \t (sekme) ve \u ile başlayan Unicode karakter kodu. Başına @ koyduğun verbatim string'de ters eğik çizgi sıradan bir karakterdir; tırnağı ise iki kez yazarak eklersin. Verbatim string birden fazla satıra da yayılabilir. $ ile @'u birleştirip hem verbatim hem interpolated bir string yazabilirsin. JSON gibi bol tırnaklı metinler için C# 11 ile raw string literal'lar geldi; onları C# Sürümleri serisinde göreceğiz.
Sık Kullanılan Method'lar
String class'ının onlarca method'u var. Bir kitapçı uygulamasında en çok işine yarayanlar şunlar:
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
string raw = " Clean Code: A Handbook of Agile Software Craftsmanship ";
string title = raw.Trim();
Console.WriteLine(title.Length);
Console.WriteLine(title.ToUpper());
Console.WriteLine(title.Contains("Agile"));
Console.WriteLine(title.StartsWith("Clean"));
Console.WriteLine(title.IndexOf(':'));
Console.WriteLine(title.Substring(0, 10));
Console.WriteLine(title.Replace("Agile", "Modern"));
Console.WriteLine(title[0]);
string tags = "csharp,dotnet,,backend";
string[] parts = tags.Split(',', StringSplitOptions.RemoveEmptyEntries);
Console.WriteLine(parts.Length);
Console.WriteLine(string.Join(" | ", parts));
Console.WriteLine("42".PadLeft(5, '0'));
Console.WriteLine(string.IsNullOrWhiteSpace(" "));
54
CLEAN CODE: A HANDBOOK OF AGILE SOFTWARE CRAFTSMANSHIP
True
True
10
Clean Code
Clean Code: A Handbook of Modern Software Craftsmanship
C
3
csharp | dotnet | backend
00042
True
Trim baştaki ve sondaki boşlukları atar; kullanıcıdan gelen her girdiye önce bunu uygula. IndexOf aradığın şeyin ilk konumunu verir, bulamazsa -1 döner. Konumlar 0'dan başlar: title[0] ilk karakter, iki nokta 10. sırada. Substring(0, 10) 0. konumdan başlayıp 10 karakter alır. Split bir string'i parçalarına ayırır; RemoveEmptyEntries iki virgül arasındaki boş parçayı atar. string.Join bunun tersini yapar. string.IsNullOrWhiteSpace ise bir girdinin null, boş ya da sadece boşluktan oluşup oluşmadığını tek seferde kontrol eder. Unutma: bunların hiçbiri title'ı değiştirmez, hepsi yeni bir değer döndürür.
StringBuilder
Immutability'nin bedelini görmek için bin kitaplık bir rapor metni oluşturalım. Önce += ile, sonra StringBuilder ile. GC.GetAllocatedBytesForCurrentThread() o ana kadar ayrılan bellek miktarını verir; farkı alarak her yöntemin ne kadar bellek ayırdığını ölçüyoruz:
using System.Globalization;
using System.Text;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
long start = GC.GetAllocatedBytesForCurrentThread();
string slow = "";
for (int i = 0; i < 1000; i++)
{
slow += "Book " + i + ";";
}
long concatBytes = GC.GetAllocatedBytesForCurrentThread() - start;
start = GC.GetAllocatedBytesForCurrentThread();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
builder.Append("Book ").Append(i).Append(';');
}
string fast = builder.ToString();
long builderBytes = GC.GetAllocatedBytesForCurrentThread() - start;
Console.WriteLine(slow == fast);
Console.WriteLine($"Length: {fast.Length}");
Console.WriteLine($"+= : {concatBytes:N0} bytes");
Console.WriteLine($"StringBuilder : {builderBytes:N0} bytes");
True
Length: 8890
+= : 8,859,712 bytes
StringBuilder : 50,984 bytes
Bu çıktı Apple M5 işlemcili bir Mac'te alındı; kesin sayılar runtime sürümüne göre birkaç yüz byte oynayabilir ama oran değişmez. İki yöntem aynı 8890 karakterlik metni üretti. += yaklaşık 8,9 MB bellek ayırdı, StringBuilder 51 KB. Sebebi şu: += her adımda o ana kadarki bütün metni kopyalayan yeni bir string oluşturuyor. Metin uzadıkça her kopya daha pahalı hâle geliyor. StringBuilder ise genişleyebilen bir tampon tutar ve sadece en sonda, ToString() dediğinde tek bir string üretir.
Bu, her yerde StringBuilder kullan demek değil. İki üç parçayı birleştirmek için interpolation ya da + gayet iyi ve daha okunaklı. StringBuilder'ı, bir döngü içinde sayısını önceden bilmediğin kadar parça eklediğinde kullan.
Karşılaştırma ve StringComparison
C#'ta == iki string'in içeriğini karşılaştırır, büyük-küçük harfe duyarlıdır ve karakterleri kodlarına göre birebir eşleştirir (ordinal karşılaştırma).
string a = "Clean Code";
string b = "clean code";
string c = "Clean " + "Code".Trim();
Console.WriteLine(a == b);
Console.WriteLine(a == c);
Console.WriteLine(a.Equals(b, StringComparison.OrdinalIgnoreCase));
Console.WriteLine(string.Compare("apple", "Banana", StringComparison.Ordinal));
Console.WriteLine(string.Compare("apple", "Banana", StringComparison.InvariantCulture));
False
True
True
31
-1
c runtime'da birleştirilerek oluşturuldu, yine de a == c doğru; çünkü karşılaştırılan şey içerik. Büyük-küçük harfi yok saymak istediğinde ToLower() ile iki yeni string üretmek yerine StringComparison.OrdinalIgnoreCase ver. Sıralama tarafında fark daha belirgin: ordinal karşılaştırma karakter kodlarına bakar ve küçük a'nın kodu (97) büyük B'ninkinden (66) büyük olduğu için "apple"ı "Banana"dan sonraya koyar. Culture'a duyarlı (culture-sensitive) karşılaştırma ise bir insanın alfabetik sıra dediği şeyi verir ve "apple" öne geçer. Kural basit: dosya adı, key, komut, kod gibi makineye yönelik metinlerde ordinal; kullanıcıya gösterilen listeyi sıralarken culture'a duyarlı karşılaştırma.
Culture ve Türkçe I Tuzağı
Büyük-küçük harf dönüşümü de varsayılan olarak çalıştığı makinenin culture'ını kullanır. Türkçede "i"nin büyüğü "İ", "I"nın küçüğü "ı" olduğu için Türkçe ayarlı bir sunucuda İngilizce metinlerle çalışan kod beklenmedik şekilde bozulur:
using System.Globalization;
CultureInfo.CurrentCulture = new CultureInfo("tr-TR");
string command = "file";
Console.WriteLine(command.ToUpper());
Console.WriteLine(command.ToUpper() == "FILE");
Console.WriteLine(command.ToUpperInvariant() == "FILE");
Console.WriteLine("TITLE".ToLower());
Console.WriteLine(string.Equals("FILE", "file", StringComparison.CurrentCultureIgnoreCase));
Console.WriteLine(string.Equals("FILE", "file", StringComparison.OrdinalIgnoreCase));
Console.WriteLine("istanbul".ToUpper());
Console.WriteLine("istanbul".ToUpper(CultureInfo.InvariantCulture));
FİLE
False
True
tıtle
False
True
İSTANBUL
ISTANBUL
Bu yazıyı hazırlarken sık kullanılan method'lar örneğini culture'ı sabitlemeden çalıştırdığımda ToUpper() çıktısında "AGİLE" ve "CRAFTSMANSHİP" görmüştüm; bilgisayarım Türkçe ayarlı. Aynı sorun bir komut adını, bir e-posta adresini ya da bir dosya uzantısını karşılaştırırken sessizce hataya dönüşür: "FILE" ile "file" Türkçe culture'da büyük-küçük harf farkı gözetmeden bile eşit değil. Kural şu: makineye yönelik metinde ToUpperInvariant(), ToLowerInvariant() ve StringComparison.OrdinalIgnoreCase; kullanıcıya gösterilen Türkçe metinde ise culture'ı açıkça ver. "istanbul" kullanıcıya "İSTANBUL" olarak görünmeli, bu yüzden son iki satırın ikisi de kendi bağlamında doğru.
Sık Düşülen Tuzaklar
Dönen değeri atmak. title.Trim(); tek başına hiçbir şey yapmaz. String method'larının sonucunu her zaman bir değişkene ata.
Döngüde +=. Birkaç yüz parçadan sonra bellek ve süre maliyeti hızla büyür. Döngüde metin biriktiriyorsan StringBuilder kullan.
Karşılaştırma için ToLower(). a.ToLower() == b.ToLower() hem iki gereksiz string üretir hem de Türkçe culture'da yanlış sonuç verebilir. string.Equals(a, b, StringComparison.OrdinalIgnoreCase) yaz.
Culture'ı varsaymak. Kodun senin bilgisayarında İngilizce culture'la çalışıp Türkçe ayarlı bir sunucuda bozulabilir ya da tam tersi. Culture'a bağlı her işlemde (büyük-küçük harf, sıralama, sayı biçimi) hangi culture'ı istediğini açıkça belirt.
Küçük Bir Deneme
★☆☆ Önce Tahmin Et'teki kodu düzelt; ekranda "DIRTY CODE" görmelisin. Dikkat: büyük harfe çevirdikten sonra Replace içindeki "clean" artık eşleşmez; işlemlerin sırasını ayarla. Büyük harf dönüşümü için ToUpperInvariant() kullan, sebebini yazının sonunda gördük.
★★☆ "Robert C. Martin" gibi bir yazar adından baş harfleri (RCM) üreten kodu Split ve bir döngüyle yaz. Boşlukları fazla girilmiş bir adla da dene.
★★★ Kitap adlarını StringBuilder ile |Title | Price| biçiminde hizalı bir tabloya dök. Sonra programın başına CultureInfo.CurrentCulture = new CultureInfo("tr-TR"); ekle ve fiyat sütununda nelerin değiştiğine bak.
String'ler ve sayılar elimizde; ama kullanıcıdan gelen her şey önce string'dir ve onu sayıya çevirmen gerekir. Bir sonraki yazıda tip dönüşümlerini görüyoruz: implicit ve explicit dönüşüm, Parse ile TryParse, Convert, boxing'e ilk bakış ve culture'a bağlı parse'ın tuzakları.