Rust'ta String sahipli, büyüyebilen ve değiştirilebilen bir UTF-8 metin tipidir; &str ise başka bir yerde duran UTF-8 metne bakan, ödünç alınmış bir dilimdir. Kısaca: metni oluşturup değiştirecek ya da bir yapının içinde saklayacaksanız String, yalnızca okuyacaksanız &str kullanırsınız. Fonksiyon parametrelerinde &str tercih etmek, fonksiyonun hem String hem de sabit metin kabul etmesini sağlar.
Bu yazıda iki tipin nasıl oluşturulduğunu, birleştirmenin sahiplik açısından ne anlama geldiğini, Rust'ın metinlere neden s[0] ile erişime izin vermediğini ve Türkçe karakterlerin (ç, ğ, ı, İ, ö, ş, ü) bayt, karakter ve büyük/küçük harf dönüşümünde nasıl davrandığını gerçek çıktılarla göreceksiniz. Serinin geri kalanı için Rust öğrenme rehberi sayfasına bakabilirsiniz.
İki string tipi: str ve String

The Rust Book'a göre çekirdek dilde tek bir string tipi vardır: str. Bu tip neredeyse her zaman ödünç alınmış hâliyle, yani &str olarak görülür. Programa yazdığınız "merhaba" gibi sabit metinler derlenmiş programın içinde saklanır ve tipleri &str'dir. String ise dilin değil standart kütüphanenin sağladığı bir tiptir: içeriğinin sahibidir, büyüyebilir ve değiştirilebilir. İkisi de her zaman geçerli UTF-8 içerir.
String'in iç yapısı bir Vec<u8> sarmalayıcısıdır. Belgeye göre bir işaretçi, bayt cinsinden uzunluk ve bayt cinsinden kapasiteden oluşur; tampon her zaman yığındadır (heap). Bu yüzden Rust Vec yazısındaki kapasite ve yeniden ayırma kurallarının çoğu String için de geçerlidir: String::new() bellek ayırmaz, String::with_capacity(n) en az n baytlık tampon ayırır.
| Özellik | String | &str |
|---|---|---|
| Sahiplik | Metnin sahibidir | Ödünç alır, sahibi başkasıdır |
| Değiştirme | push_str, push, insert, clear... | Değiştirilemez görünüm |
| Bellek | Tampon her zaman heap üzerinde | Programın içinde, heap ya da stack üzerinde olabilir |
| Tipik kaynak | String::from, to_string, format! | Sabit metinler, &String, dilimleme |
| Fonksiyon parametresi | Sahiplik gerekiyorsa | Yalnızca okunacaksa (önerilen) |
| Struct alanı | Genellikle bu | Lifetime notasyonu gerektirir |
String oluşturma ve büyütme
Bir String oluşturmanın birkaç eşdeğer yolu vardır. String::from("...") ve "...".to_string() aynı sonucu verir; kitap hangisini seçeceğinizin bir üslup meselesi olduğunu söyler. to_string yalnızca metinlerde değil, Display trait'ini uygulayan her tipte çalışır, örneğin sayılarda. Var olan bir String'e metin eklemek için push_str, tek karakter eklemek için push kullanılır.
fn main() {
let mut s = String::new();
s.push_str("Merhaba");
s.push(',');
s.push(' ');
let a = String::from("dünya");
let b = "dünya".to_string();
let c: String = "dünya".into();
println!("{} {}", a == b, b == c);
s.push_str(&a);
println!("{s}");
let s1 = String::from("tik");
let s2 = String::from("tak");
let s3 = s1 + "-" + &s2;
println!("{s3}");
let gun = "Pazartesi";
let saat = 9;
let mesaj = format!("{gun} saat {saat}:00");
println!("{mesaj}");
println!("s2 hâlâ kullanılabilir: {s2}");
}
// çıktı: true true
// çıktı: Merhaba, dünya
// çıktı: tik-tak
// çıktı: Pazartesi saat 9:00
// çıktı: s2 hâlâ kullanılabilir: takpush_str parametre olarak &str aldığı için eklenen metnin sahipliğini almaz; örnekte a'yı ekledikten sonra hâlâ kullanabilirdik. format! makrosu println! gibi çalışır ama ekrana yazmak yerine yeni bir String döndürür ve argümanlarının sahipliğini almaz. Birden fazla parçayı birleştirirken en okunur yol genellikle budur. Biçim belirteçlerinin tamamı Rust Display ve Debug formatlama yazısında.
+ operatörü neden ilk String'i tüketir?
s1 + &s2 ifadesi iki metni kopyalayıp yeni bir metin üretiyormuş gibi görünür ama öyle değildir. + operatörü arka planda imzası kabaca fn add(self, s: &str) -> String olan bir metodu çağırır. Birinci operand self olarak sahipliğiyle birlikte alınır, ikinci operand ise yalnızca ödünç alınır. Yani s1'in tamponu yeniden kullanılır, sonuna s2'nin içeriği eklenir ve sonuç döndürülür. Bu kopyalamaktan daha verimlidir ama işlemden sonra s1 artık kullanılamaz:
fn main() {
let s1 = String::from("Merhaba, ");
let s2 = String::from("dünya");
let s3 = s1 + &s2;
println!("{s1} {s3}");
}
// error[E0382]: borrow of moved value: `s1`Peki &s2'nin tipi &String olduğu hâlde add neden &str beklerken bunu kabul ediyor? Çünkü String, Deref<Target = str> trait'ini uygular ve derleyici &String'i gerektiğinde otomatik olarak &str'ye çevirir (deref coercion). Aynı nedenle iki String'i s1 + s2 biçiminde toplayamazsınız; sağ tarafın referans olması gerekir. Bu taşıma (move) hatasının çözüm yolları E0382 borrow of moved value hatası yazısında anlatılıyor.
Bayt, karakter ve Türkçe harfler

Rust'ta bir metnin len() değeri karakter sayısı değil bayt sayısıdır. UTF-8'de ASCII harfler 1 bayt tutar, Türkçeye özgü ç, ğ, ı, İ, ö, ş, ü harfleri ise 2'şer bayt tutar. Bu yüzden Türkçe metinlerde len() çoğu zaman beklediğinizden büyük çıkar. Karakter saymak için chars().count() kullanılır. bytes() ham baytları, char_indices() ise her karakteri bayt konumuyla birlikte verir:
fn main() {
let sehir = "İstanbul";
println!("bayt: {}, char: {}", sehir.len(), sehir.chars().count());
let kelime = "şğü";
for b in kelime.bytes() {
print!("{b} ");
}
println!();
for (i, c) in kelime.char_indices() {
print!("{i}:{c} ");
}
println!();
println!("3. harf: {:?}", "çiçek".chars().nth(2));
println!("ascii mi: {} {}", "kedi".is_ascii(), "köpek".is_ascii());
}
// çıktı: bayt: 9, char: 8
// çıktı: 197 159 196 159 195 188
// çıktı: 0:ş 2:ğ 4:ü
// çıktı: 3. harf: Some('ç')
// çıktı: ascii mi: true false"İstanbul" sekiz harftir ama "İ" 2 bayt tuttuğu için len() 9 döndürür. char_indices çıktısındaki 0, 2 ve 4 konumları her Türkçe harfin iki bayt kapladığını açıkça gösterir. Kitap metne üç farklı açıdan bakılabileceğini anlatır: baytlar, Unicode skaler değerleri (Rust'ın char tipi) ve insanların "harf" dediği şeye en yakın olan grafem kümeleri. Türkçe metinlerde birleşik karakter kullanılmadığı sürece char ile harf genellikle örtüşür; ancak bazı dillerde tek bir harf birden çok char'dan oluşur. Grafem kümesi ayırma standart kütüphanede yoktur; belge bunun için crates.io'daki kütüphanelere yönlendirir.
Neden s[0] yazılamıyor?
Birçok dilde s[0] ilk karakteri verir. Rust'ta bu kod derlenmez:
fn main() {
let s = String::from("merhaba");
let h = s[0];
println!("{h}");
}
// error[E0277]: the type `str` cannot be indexed by `{integer}`Kitap üç neden sayar. Birincisi, tek bir bayt indeksi her zaman geçerli bir karaktere karşılık gelmez; "şeker" kelimesinin 0 numaralı baytı tek başına bir harf değildir. İkincisi, s[i]'nin ne döndürmesi gerektiği belirsizdir: bayt mı, karakter mi, grafem kümesi mi? Üçüncüsü, indeksleme işlemlerinin sabit zamanlı olması beklenir ama i'nci karakteri bulmak için metnin başından itibaren yürümek gerekir. Rust bu belirsizliği derleme aşamasında keserek, ASCII dışı karakterlerde ancak çok sonra fark edilecek hataları önler.
Ne istediğinizi açıkça söylediğinizde sorun kalmaz: i'nci karakter için s.chars().nth(i), i'nci bayt için s.as_bytes()[i] kullanılır. Aralıkla dilimleme (&s[0..2]) ise bayt indeksiyle çalışır ve bir &str döndürür; ancak aralık bir karakterin ortasına denk gelirse program panikler. Paniklemeyen sürüm get'tir:
fn main() {
let s = "şeker";
println!("{:?}", s.get(0..1));
println!("{:?}", s.get(0..2));
let parca = &s[0..1];
println!("{parca}");
}
// çıktı: None
// çıktı: Some("ş")
// panik: end byte index 1 is not a char boundary; it is inside 'ş' (bytes 0..2 of string)Dilimleme sözdizimi, karakter sınırları ve is_char_boundary gibi araçlar Rust slice ve str dilimleri yazısında ayrıntılı olarak anlatılıyor.
Büyük/küçük harf dönüşümü ve Türkçe I sorunu
to_uppercase ve to_lowercase Unicode standardındaki genel kurallara göre çalışır ve yeni bir String döndürür, çünkü bazı karakterler dönüşümde birden fazla karaktere açılabilir. Belge açıkça bu metotların Türkçe ve Azericedeki yerele özgü I, ı, İ, i kurallarını uygulamadığını belirtir. Gerçek davranış şöyle:
fn main() {
println!("{}", "istanbul ırmak".to_uppercase());
let kucuk = "İSTANBUL".to_lowercase();
println!("{kucuk:?} bayt: {}", kucuk.len());
println!("{:?}", "İ".to_lowercase().chars().collect::<Vec<char>>());
println!("{}", "IŞIK".to_lowercase());
println!("{}", "Kedi".eq_ignore_ascii_case("KEDİ"));
println!("{}", "Kedi".to_lowercase() == "KEDI".to_lowercase());
}
// çıktı: ISTANBUL IRMAK
// çıktı: "i\u{307}stanbul" bayt: 10
// çıktı: ['i', '\u{307}']
// çıktı: işik
// çıktı: false
// çıktı: trueÇıktılar Türkçe açısından üç sorunu gösteriyor. "istanbul" büyütülünce "İSTANBUL" değil "ISTANBUL" oluyor, çünkü genel kurala göre küçük i'nin büyüğü noktasız I'dır. "İ" küçültülünce tek bir "i" yerine "i" artı birleştirici nokta (U+0307) olmak üzere iki char çıkıyor; sonuç ekranda doğru görünse bile başka bir "istanbul" metniyle karşılaştırıldığında eşit çıkmaz. "IŞIK" ise "ışık" yerine "işik" oluyor. Türkçe metinlerde doğru büyük/küçük harf dönüşümü gerekiyorsa, I/ı/İ/i harflerini dönüşümden önce kendiniz eşleyen küçük bir fonksiyon yazmanız ya da yerel ayar destekli bir kütüphane kullanmanız gerekir (bkz. https://docs.rs). eq_ignore_ascii_case adından da anlaşılacağı gibi yalnızca ASCII harfleri eşler, Türkçe harflerde işe yaramaz.
Fonksiyonlarda &str kabul etmek
Standart kütüphane belgesi fonksiyonların, belirli bir nedenle String gerekmedikçe &str kabul etmesini önerir. &String'den &str'ye dönüşüm çok ucuzdur ve derleyici bunu sizin yerinize yapar. Böylece aynı fonksiyon sabit metinleri, String değişkenlerini ve dilimleri kabul eder:
fn selamla(ad: &str) -> String {
format!("Selam {ad}!")
}
fn main() {
let sahipli = String::from("Ayşe");
let sabit = "Mehmet";
println!("{}", selamla(&sahipli));
println!("{}", selamla(sabit));
println!("{}", selamla(sahipli.as_str()));
let mut tampon = String::with_capacity(16);
tampon.push_str("abc");
println!("len {} kapasite en az 16: {}", tampon.len(), tampon.capacity() >= 16);
let dilim: &str = &sahipli;
let geri: String = dilim.to_owned();
println!("{dilim} {geri}");
}
// çıktı: Selam Ayşe!
// çıktı: Selam Mehmet!
// çıktı: Selam Ayşe!
// çıktı: len 3 kapasite en az 16: true
// çıktı: Ayşe AyşeTers yön, yani &str'den sahipli bir String elde etmek, yeni bellek ayırıp metni kopyalamak demektir; bunun için to_owned(), to_string() ya da String::from kullanılır. Belge, otomatik dönüşümün yetmediği bir durumu da anlatır: fonksiyon generic bir trait sınırı bekliyorsa derleyici iki adımlı dönüşümü kendisi yapamaz; o zaman s.as_str() ya da &*s yazarak dönüşümü açıkça belirtmeniz gerekir.
Baytlardan String üretmek ve sık kullanılan metotlar
Dosyadan ya da ağdan gelen ham baytları metne çevirirken geçerli UTF-8 olup olmadıklarını kontrol etmeniz gerekir. String::from_utf8 bir Vec<u8> alır ve Result döndürür; baytlar geçersizse hata içinde nedeni ve verdiğiniz vektör bulunur. String::from_utf8_lossy ise geçersiz dizileri U+FFFD yerine koyma karakteriyle değiştirerek her durumda bir metin verir:
fn main() {
let gecerli = vec![103, 195, 188, 108];
println!("{:?}", String::from_utf8(gecerli));
let bozuk = vec![103, 195, 108];
match String::from_utf8(bozuk.clone()) {
Ok(s) => println!("metin: {s}"),
Err(e) => println!("hata: {e}"),
}
println!("{}", String::from_utf8_lossy(&bozuk));
let metin = " elma, armut ,kiraz ";
let meyveler: Vec<&str> = metin.trim().split(',').map(|p| p.trim()).collect();
println!("{meyveler:?}");
println!("{}", "kar kış kar".replace("kar", "yağmur"));
println!("{}", "Rust öğreniyorum".contains("öğren"));
println!("{:?}", "bir iki üç".split_whitespace().collect::<Vec<_>>());
let n: i32 = " 42 ".trim().parse().unwrap();
println!("{}", n + 1);
}
// çıktı: Ok("gül")
// çıktı: hata: invalid utf-8 sequence of 1 bytes from index 1
// çıktı: g�l
// çıktı: ["elma", "armut", "kiraz"]
// çıktı: yağmur kış yağmur
// çıktı: true
// çıktı: ["bir", "iki", "üç"]
// çıktı: 43trim, split, split_whitespace ve lines yeni metin üretmez, orijinal metne bakan &str dilimleri verir; bu yüzden hızlıdırlar. replace, to_uppercase ve format! ise yeni bir String üretir. parse bir Result döndürür ve hatalı girdide paniklemez; burada kısalık için unwrap kullandık, gerçek kodda nasıl ele alınacağı Rust Result ve hata yönetimi yazısında.
String'i yerinde değiştirmek: pop, insert, remove, truncate
String, vektördeki ekleme ve silme metotlarının metne uyarlanmış karşılıklarını da sunar; ama burada konumlar karakter sırası değil bayt konumudur ve her biri o konumun bir karakter sınırında olmasını şart koşar. pop son karakteri Option<char> olarak çıkarır, insert ve insert_str verilen bayt konumuna ekler, remove verilen bayt konumundaki karakteri çıkarıp döndürür, retain koşulu sağlamayan karakterleri atar, truncate metni verilen bayt uzunluğuna kısaltır, clear ise metni boşaltır:
fn main() {
let mut s = String::from("güneş");
println!("{:?}", s.pop());
s.insert(0, 'G');
s.insert_str(1, "-");
println!("{s}");
let silinen = s.remove(0);
println!("{silinen} {s}");
s.retain(|c| c != '-');
println!("{s}");
s.truncate(3);
println!("{s} (len {})", s.len());
let kapasite_once = s.capacity();
s.clear();
println!("boş mu: {}, kapasite aynı mı: {}", s.is_empty(), s.capacity() == kapasite_once);
}
// çıktı: Some('ş')
// çıktı: G-güne
// çıktı: G -güne
// çıktı: güne
// çıktı: gü (len 3)
// çıktı: boş mu: true, kapasite aynı mı: truetruncate(3) "güne" metnini "gü" yaptı, çünkü "g" 1 bayt, "ü" 2 bayt tutar ve 3 tam bir karakter sınırıdır. Aynı metinde truncate(2) deneseydik konum "ü"nün ortasına düşeceği için program paniklerdi. Belge truncate, insert ve remove için bu panik koşulunu açıkça yazar. Türkçe bir kelimede bunu kendiniz görebilirsiniz:
fn main() {
let mut s = String::from("çay");
s.truncate(1);
println!("{s}");
}
// panik: assertion failed: self.is_char_boundary(new_len)Belgenin bir uyarısı daha var: remove ve insert, işlem yapılan konumdan sonraki bütün baytları kaydırır; bu yüzden bir döngü içinde tekrar tekrar çağrılmaları karesel (quadratic) süreye yol açabilir. Çok sayıda karakter silecekseniz tek geçişte çalışan retain daha uygundur. truncate ve clear, vektörde olduğu gibi ayrılmış kapasiteye dokunmaz; örneğin son satırı da bunu gösteriyor.
Sık yapılan hatalar

len()'i harf sayısı sanmak: Türkçe metinlerde bayt sayısı harf sayısından büyüktür. Kullanıcıya gösterilecek uzunluk içinchars().count()kullanın.- Bayt aralığıyla dilimlemek:
&s[0..1]ASCII metinde çalışıp Türkçe metinde paniğe yol açabilir. Emin değilsenizgetya dachar_indiceskullanın. +sonrası ilk değişkeni kullanmak:s1 + &s2s1'i tüketir. Değişkeni korumak istiyorsanızformat!("{s1}{s2}")yazın.- Fonksiyon parametresini
&Stringyapmak: Bu, sabit metin geçirilmesini zorlaştırır.&strhem&String'i hem sabit metni kabul eder. - Türkçe büyük/küçük harf için
to_lowercase'e güvenmek: I/ı/İ/i dönüşümleri yerel kurala uymaz; karşılaştırma ve arama kodunuzu Türkçe örneklerle test edin.
Sık Sorulan Sorular
Struct alanında String mi &str mi kullanmalıyım?
Struct verinin sahibi olacaksa String kullanın; çoğu durumda doğru seçim budur. &str alanı, struct'ın referansın sahibinden daha uzun yaşamayacağını belirten bir lifetime notasyonu gerektirir; bu konu Rust lifetime yazısında anlatılıyor.
String::from ile to_string arasında performans farkı var mı?
Kitap bu ikisinin sabit metinlerde aynı işi yaptığını ve seçimin üslup ile okunabilirlik meselesi olduğunu söyler. to_string'in avantajı, sayılar gibi Display uygulayan her tipte çalışmasıdır.
Bir metnin son karakterini nasıl alırım?
s.chars().last() ya da s.chars().next_back() bir Option<char> döndürür; boş metinde None verir. Bayt indeksleriyle son karakteri hesaplamaya çalışmak Türkçe metinlerde hataya açıktır.
Bir metni ters çevirmenin güvenli yolu nedir?
Baytları değil karakterleri ters çevirin: let ters: String = "çiçekçi".chars().rev().collect(); satırı içkeçiç sonucunu verir. Baytları ters çevirmek, çok baytlı Türkçe harfleri bozar ve geçersiz UTF-8 üretir; bu yüzden String bunu doğrudan sunmaz. Birleştirici işaret içeren metinlerde chars düzeyinde ters çevirmenin de işareti yanlış harfe taşıyabileceğini unutmayın.
UTF-8 olmayan metinlerle nasıl çalışırım?
Belge, UTF-8 zorunluluğu olmayan bir metin tipi gerekiyorsa işletim sistemi metinleri için OsString'e bakmanızı önerir. Ham veri için doğrudan Vec<u8> kullanmak da bir seçenektir. Dosya yollarıyla çalışmak Rust dosya okuma ve yazma yazısında.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Rust öğrenme rehberi · Slice ve str dilimleri · Vec kullanımı · Formatlama · E0382 hatası · HashMap kullanımı