Skip to content

Bölüm 3 / 13

String işleme

Interpolation, StringBuilder, format, kültür duyarlı karşılaştırma.

9 dakikalık okuma

Hat 02 iki senet bırakmıştı: 1. ders ".NET string'i bellekte UTF-16 tutar" demişti, 10. ders "döngüde += O(n²)'dir, çözümü Hat 03'te" diye not düşmüştü — ikisi de bu derste ödeniyor. String'in değişmezliğinden StringBuilder'ın mekanizmasına, biçimlendirme dilinden Türkçe İ'nin ünlü üretim tuzağına ve StringComparison kararlarına: metin, her projenin en çok dokunulan verisidir ve yanlışları sessizce yaşar.

01Ne öğreneceksin

  • string'in doğası: değişmez referans tipi, UTF-16 içyapı, Length'in gerçekte saydığı şey
  • Birleştirme ekonomisi: +'nın meşru olduğu yerler, döngüde StringBuilder, string.Join
  • Biçimlendirme: interpolation, format belirteçleri (N2, dd.MM.yyyy...), hizalama
  • Kültür: CurrentCulture / InvariantCulture, Türkçe İ tuzağı, StringComparison karar tablosu
  • Gündelik alet çantası: Split/Trim/Contains ailesi ve doğru aşırı yüklemeleri

02string'in doğası: değişmez, UTF-16, değer gibi davranan referans

1. dersten biliyorsun: string bir referans tipidir ama == içerik karşılaştırır. İkinci temel özellik her şeyi belirler: string değişmezdir (immutable). Hiçbir string metodu metni yerinde değiştirmez; hepsi yeni bir string döndürür. En sık yazılan hata bu yüzden sessizdir:

string ad = "kalem";
ad.ToUpper();                     // sonuç çöpe gitti — ad değişmedi!
ad = ad.ToUpper();                // doğrusu: dönüşü geri ata

char c = ad[0];                   // indeksle OKUMA serbest
// ad[0] = 'K';                   // DERLEME HATASI — yerinde değişiklik yok

Değişmezliğin getirisi büyüktür: string'ler korkusuzca paylaşılır (kopyalamadan metoda geçir, sözlük anahtarı yap, thread'ler arasında taşı — kimse altından değiştiremez) ve derleyici aynı sabit metinleri tek nesnede birleştirebilir (interning: kodundaki iki "kalem" sabiti aynı heap nesnesidir). Bedeli de birazdan geleceğiz: her "değişiklik" bir kopyadır.

İçyapı, Hat 02'nin 1. dersindeki senedin ödemesi: string, bellekte UTF-16 kod birimlerinin dizisidir; Length karakter değil UTF-16 birimi sayar ve indeksleyici char (bir birim) döndürür. Temel Çokdilli Düzlem dışındaki karakterler — emoji başta — iki birim (surrogate pair) kaplar:

Console.WriteLine("İzmir".Length);   // 5 — beklediğin gibi
Console.WriteLine("😀".Length);      // 2! tek "karakter", iki UTF-16 birimi
// karakter sayısı gerekiyorsa: new System.Globalization.StringInfo("😀").LengthInTextElements → 1

Pratik sonuç: Length ile "kullanıcının gördüğü karakter sayısı"nı saymak, emoji ve bazı dillerin girdiği her yerde yanlıştır; SMS/karakter limiti gibi işlerde bunu bilerek karar ver. Dosyaya/ağa yazarken ise kodlama dönüşümü devreye girer: Encoding.UTF8.GetBytes(s) — bellekteki UTF-16, dışarıda çoğu zaman UTF-8 olur (1. dersin katalog/kodlama ayrımı).

03Birleştirme ekonomisi: += ne zaman suç, StringBuilder nasıl çalışır

Hat 02'nin 10. dersindeki tuzağı hatırla: döngüde rapor += satir; her turda tüm metni yeni bir string'e kopyalar — 1+2+...+n kopya = O(n²). Neden başka türlü olamazdı, artık biliyorsun: string değişmez, "sonuna ekleme" diye bir işlemi yok. Çözüm, değişebilir bir tampon: StringBuilder. İçinde büyüyebilen bir karakter dizisi tutar; Append kopyalamaz, tampona yazar; tampon dolunca kapasitesini büyütür (itfa edilmiş O(1) — Hat 02'nin 8. dersindeki List büyümesiyle aynı ekonomi); ToString() sonucu tek seferde string'e çevirir:

var sb = new StringBuilder(kapasite: 4096);   // tahmin edebiliyorsan baştan ver
foreach (var satir in satirlar)
    sb.AppendLine(satir);                     // O(1) ekleme — kopya yok
string rapor = sb.ToString();                 // toplam O(n)

Ölçüyü kaçırma — + her yerde suç değildir:

  • Sabitlerin birleşimi ("ka" + "lem") derleme zamanında tek sabite katlanır — maliyet sıfır.
  • Tek ifadedeki birkaç parça (ad + " " + soyad) tek string.Concat çağrısıdır — StringBuilder açmak gereksiz tören olur.
  • Ayraçlı listeler için hazır araç varken elle döngü yazma: string.Join(", ", adlar) — hem kısa hem O(n).
  • Kural tek cümle: döngüde/parça sayısı bilinmezken StringBuilder; bir-iki parçada + ve interpolation; listede Join.

04Biçimlendirme: interpolation ve format dili

Modern C#'ta metin kurmanın varsayılanı string interpolation'dır: $"..." içinde {ifade}. Gücü, süslü parantezin içindeki format belirteci dilinden gelir — {ifade:belirteç}:

BelirteçÖrnekÇıktı (tr-TR)
N2 — binlik ayraç + 2 hane$"{1234567.891:N2}"1.234.567,89
C — para birimi$"{49.9m:C}"₺49,90
P1 — yüzde, 1 hane$"{0.874:P1}"%87,4
D5 — sıfır dolgulu tamsayı$"{42:D5}"00042
X — onaltılık$"{255:X}"FF
tarih deseni$"{simdi:dd.MM.yyyy HH:mm}"27.08.2026 14:05
// hizalama: {ifade,genişlik} — negatif sola, pozitif sağa yaslar
Console.WriteLine($"{"Ürün",-12}{"Tutar",10}");
Console.WriteLine($"{"Kalem",-12}{12.5m,10:N2}");
// Ürün             Tutar
// Kalem            12,50

Tablonun üçüncü sütunundaki başlık gözünden kaçmasın: çıktılar tr-TR kültürüne göre yazıldı. Aynı kod ABD sunucusunda 1,234,567.89 üretir — biçimlendirme, görünmez bir küresel ayara bağlıdır. O ayarın adıyla sıradaki bölüm bu dersin en kritik bölümüdür.

05Kültür: Türkçe İ tuzağı ve StringComparison kararı

Metni büyütmek, karşılaştırmak, sayıya/tarihe çevirmek dile bağlı işlerdir; .NET bu bağımlılığı kültür (CultureInfo) nesnesiyle yönetir ve hiçbir şey belirtmezsen thread'in CurrentCulture'ı — genelde işletim sisteminin dili — sessizce devreye girer. Türkçe makinede dünyaca ünlü bir tuzak doğar, Hat 02'nin 1. dersinde selam verip geçtiğimiz noktalı/noktasız i:

// Türkçe kültürde: 'i' büyüyünce 'İ' olur, 'I' küçülünce 'ı' olur
"file".ToUpper()                      // tr-TR'de "FİLE" — "FILE" DEĞİL!
"file".ToUpper() == "FILE"            // false — protokol/dosya adı kontrolü kırıldı

// kültürden bağımsız işler için Invariant aile:
"file".ToUpperInvariant() == "FILE"   // true
string.Equals("file", "FILE", StringComparison.OrdinalIgnoreCase)   // asıl doğru araç

Aynı sızıntı sayı ve tarihte de yaşar: double.Parse("1.5") tr-TR'de 15 okur (nokta binlik ayracıdır!), ToString() ondalığı virgülle yazar ve o CSV'yi ABD'deki sistem okuyamaz. Kural: kullanıcıya gösterilen metin CurrentCulture; dosyaya/ağa/protokole yazılan veri InvariantCulture: deger.ToString(CultureInfo.InvariantCulture), double.Parse(s, CultureInfo.InvariantCulture).

Karşılaştırmada karar mekanizması StringComparison enum'udur — her Equals/Compare/StartsWith/Contains/IndexOf çağrısında bilinçli seç:

SeçenekNasıl karşılaştırırNerede kullan
Ordinalkod birimlerini sayısal, bire biriç anahtarlar, dosya yolu, protokol, token — varsayılanın olsun
OrdinalIgnoreCaseordinal + büyük/küçük görmeze-posta, header, uzantı gibi ASCII kimlikler
CurrentCulturekullanıcının dil kurallarıylaekranda sıralama, kullanıcıya dönük karşılaştırma
InvariantCulturesabit nötr kültürlekültürel ama makineler arası tutarlı olması gereken nadir işler

Sıralamada fark somuttur: ordinal sıralama "Çelik"i Z'nin arkasına atar (kod noktası büyük), CurrentCulture tr-TR'de C'nin yanına koyar. Ekran → CurrentCulture, makine → Ordinal; bu iki refleks, kültür hatalarının neredeyse tamamını keser.

06Gündelik alet çantası: doğru aşırı yüklemeleriyle

var satir = "  Ali; Ayşe;; Zeynep  ";
string[] adlar = satir.Trim().Split(';',
    StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
// ["Ali", "Ayşe", "Zeynep"] — boş parça ve boşluk derdi tek satırda bitti

bool bos1 = string.IsNullOrEmpty(girdi);       // null ya da ""
bool bos2 = string.IsNullOrWhiteSpace(girdi);  // null, "" ya da yalnız boşluk — form girdisinde bu

string posta = "Ali@Ornek.COM";
bool esit = posta.Equals("ali@ornek.com", StringComparison.OrdinalIgnoreCase);
bool icerir = rapor.Contains("hata", StringComparison.OrdinalIgnoreCase);

string kesit = yol.Substring(yol.LastIndexOf('/') + 1);   // son parçayı al
string temiz = ad.Replace("  ", " ");                     // yeni string döndürür — geri ata!
  • Split seçenekleri (RemoveEmptyEntries, TrimEntries) elle temizlik döngülerini gereksiz kılar — CSV benzeri işlerin ekmeği.
  • IsNullOrWhiteSpace, kullanıcı girdisi doğrulamada IsNullOrEmpty'nin yerine geçer: " " da boştur.
  • Comparison parametreli aşırı yüklemeler — Contains, StartsWith, IndexOf... — 05'in kararını her çağrıya taşır; parametresiz hâlleri kültüre göre değişebilen sonuç verir.
  • Performans ucu: dilim almak için AsSpan ailesi kopyasız çalışır (Span<T>, 1. derste anılan ölçüm-kritik dünyanın aracı) — varlığını bil, ihtiyaçta derinleş.

07Sahada metin: üç senaryo, üç refleks

  • Log ve CSV üretimi: makinelerin okuyacağı her değer InvariantCulture ile yazılır; tarih için en sağlamı ISO 8601: tarih.ToString("O") ya da "yyyy-MM-dd". "Benim makinemde doğru görünüyordu" hatalarının yarısı buradan çıkar.
  • Kimlik karşılaştırması: kullanıcı adı, e-posta, dosya uzantısı, HTTP header — hepsi OrdinalIgnoreCase; sıralı liste ekrana çıkacaksa sıralamayı CurrentCulture ile yap. Kod incelemesinde parametresiz ToLower()== karşılaştırması gördüğünde ikaz refleksin çalışsın: hem kültür tuzağı hem gereksiz string tahsisi.
  • Rapor/metin üretimi: parça sayısı belirsizse StringBuilder + baştan kapasite; şablonlu satırlarda interpolation + hizalama; ayraçlı birleştirmede Join. Bu üçlü, metin üreten kodun tamamını kapsar.

08Mini alıştırma — Tuzakları kendi makinende yakala

Yaklaşık 20 dakika · dotnet run

  1. İ tuzağı: "file".ToUpper() == "FILE" ifadesini yazdır; sonra CultureInfo.CurrentCulture = new CultureInfo("en-US"); ile tekrarla. İki sonucu ve nedenini not et; doğru çözümü (OrdinalIgnoreCase) ekle.
  2. Kültür sızıntısı: double.Parse("1.5")'i tr-TR ve InvariantCulture ile çalıştır, iki sonucu gör (15 ve 1,5). Bir double değeri hem CurrentCulture hem InvariantCulture ile ToString'le, çıktıları karşılaştır.
  3. O(n²) kanıtı (Hat 02'nin senedi): 100.000 kısa satırı önce += ile, sonra StringBuilder ile birleştir, Stopwatch süreleriyle yazdır. Kaç kat fark çıktı? Sonra string.Join ile üçüncü ölçümü ekle.
  4. Length gerçeği: "İzmir", "😀" ve "👍🏽" için Length'i ve Encoding.UTF8.GetBytes(...).Length'i tablo hâlinde yazdır; her satır için "UTF-16 birimi / UTF-8 baytı" ayrımını bir cümleyle açıkla.
  5. Biçim provası: ürün-tutar listesini hizalama + N2 ile düzgün sütunlu yazdır; en alta bugünün tarihini dd.MM.yyyy HH:mm deseniyle ekle.

09Kendini sına

string'in değişmezliği ne demektir? Getirdiği iki avantaj ve bir maliyeti, birer örnekle anlat.

Hiçbir işlem string'i yerinde değiştirmez; ToUpper/Replace dahil her metot yeni string döndürür — dönüşü geri atamayan kod sessizce işe yaramaz. Avantajlar: güvenli paylaşım (kopyasız parametre geçişi, sözlük anahtarı, thread'ler arası taşıma — kimse değiştiremez) ve interning (aynı sabit metinlerin tek nesnede birleşmesi). Maliyet: her "değişiklik" kopyadır — döngüde += bunun O(n²)'ye büyümüş hâlidir, çözümü StringBuilder.

"😀".Length neden 2'dir? Length'in saydığı şey nedir ve bu hangi işlerde yanlış sonuç verir?

.NET string'i bellekte UTF-16 kod birimleri dizisidir ve Length karakter değil kod birimi sayar. Temel düzlem dışındaki karakterler — emoji gibi — iki birimlik surrogate pair kaplar, bu yüzden 2 çıkar. Kullanıcının gördüğü karakteri saymak gereken işlerde (karakter limiti, kesme) Length yanıltır; metin öğesi saymak için StringInfo kullanılır. Dosya/ağ tarafındaki bayt sayısı ise ayrı bir dünyadır: Encoding.UTF8.GetBytes ile kodlama dönüşümü yapılır.

Döngüde += neden O(n²)'dir ve StringBuilder bunu hangi mekanizmayla O(n)'e indirir? + ne zaman meşrudur?

string değişmez olduğundan her += o ana dek biriken tüm metni yeni string'e kopyalar; 1+2+...+n kopya O(n²) eder. StringBuilder içinde büyüyebilen bir karakter tamponu tutar: Append kopyasız yazar, tampon dolunca kapasite büyür (itfa edilmiş O(1)), ToString tek seferde sonucu üretir — toplam O(n). +, sabit birleşimlerinde (derlemede katlanır) ve tek ifadelik birkaç parçada (tek Concat çağrısı) meşrudur; ayraçlı listede doğru araç string.Join'dir.

Türkçe İ tuzağını bir kod örneğiyle anlat; StringComparison seçiminde ekran/makine kuralını ver.

Türkçe kültürde 'i' büyüyünce noktalı 'İ' olur: "file".ToUpper() tr-TR makinede "FİLE" üretir ve "FILE" ile karşılaştırma beklenmedik false döner — dosya adı/protokol kontrolleri böyle kırılır. Çözüm, kültürden bağımsız işlerde Invariant/Ordinal aile kullanmaktır; karşılaştırmanın doğru aracı StringComparison parametresidir. Kural: makineye dönük kimlikler (yol, token, e-posta) Ordinal/OrdinalIgnoreCase; kullanıcıya gösterilen sıralama ve karşılaştırma CurrentCulture; dosyaya/ağa yazılan sayı-tarih InvariantCulture.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) Değişmezlik çıktı sorusu: s.ToUpper(); sonrası s ne yazar (değişmedi — dönüş atanmadı). (2) Performans: "döngüde string birleştirmenin karmaşıklığı" (O(n²)) ve "hangi sınıf çözer" (StringBuilder) — Hat 02'nin Big-O sorusuyla birleşik gelir. (3) Kodlama: ".NET string bellekte hangi kodlamayı kullanır?" (UTF-16), Length'in kod birimi saydığı, UTF-8'in değişken uzunluklu olduğu (1-4 bayt). (4) Biçim eşleme: {255:X} → FF, {42:D5} → 00042 türü kısa sorular; kültür sorusu klasiği "1.5 hangi kültürde 15 okunur ve neden".

Sözlüde nasıl anlatılır — 2 dakikalık taslak

"String'te dikkat ettiğin performans ve doğruluk noktaları neler?" için düzen: (1) Doğa — "string değişmez, UTF-16 tabanlı bir referans tipidir; her değişiklik kopyadır" temeli. (2) Performans kararı — "döngüde ve bilinmeyen parça sayısında StringBuilder, bir-iki parçada + yeter, listede Join; kararı Stopwatch'la doğrularım" — ölçüm vurgusu kıdım gösterir. (3) Doğruluk kararı — "karşılaştırmada StringComparison'ı hep açık yazarım: makine kimliğinde Ordinal, ekranda CurrentCulture; Türkçe makinedeki ToUpper/FILE vakasını bilirim, bu yüzden kültürden bağımsız işlerde Invariant aile kullanırım." (4) Kapanış köprüsü: "Seri hâle getirilen her sayı ve tarihte InvariantCulture + ISO 8601 kullanmak, kültür hatalarını sistemden tümüyle çıkarır."