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) tekstring.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çenek | Nasıl karşılaştırır | Nerede kullan |
|---|---|---|
Ordinal | kod birimlerini sayısal, bire bir | iç anahtarlar, dosya yolu, protokol, token — varsayılanın olsun |
OrdinalIgnoreCase | ordinal + büyük/küçük görmez | e-posta, header, uzantı gibi ASCII kimlikler |
CurrentCulture | kullanıcının dil kurallarıyla | ekranda sıralama, kullanıcıya dönük karşılaştırma |
InvariantCulture | sabit nötr kültürle | kü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
AsSpanailesi 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
- İ tuzağı:
"file".ToUpper() == "FILE"ifadesini yazdır; sonraCultureInfo.CurrentCulture = new CultureInfo("en-US");ile tekrarla. İki sonucu ve nedenini not et; doğru çözümü (OrdinalIgnoreCase) ekle. - 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). Birdoubledeğeri hem CurrentCulture hem InvariantCulture ile ToString'le, çıktıları karşılaştır. - O(n²) kanıtı (Hat 02'nin senedi): 100.000 kısa satırı önce
+=ile, sonra StringBuilder ile birleştir,Stopwatchsüreleriyle yazdır. Kaç kat fark çıktı? Sonrastring.Joinile üçüncü ölçümü ekle. - Length gerçeği:
"İzmir","😀"ve"👍🏽"için Length'i veEncoding.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. - Biçim provası: ürün-tutar listesini hizalama +
N2ile düzgün sütunlu yazdır; en alta bugünün tarihinidd.MM.yyyy HH:mmdeseniyle 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."
