Dilim Türü (The Slice Type)
Dilimler, bir koleksiyondaki bitişik bir eleman dizisine referans vermenizi sağlar. Bir dilim bir tür referanstır, bu nedenle sahipliği yoktur.
İşte küçük bir programlama problemi: Boşluklarla ayrılmış kelimelerden oluşan bir metin (string) alan ve o metinde bulduğu ilk kelimeyi döndüren bir fonksiyon yazın. Eğer fonksiyon metin içinde boşluk bulamazsa, tüm metin tek bir kelime olmalıdır, bu yüzden metnin tamamı döndürülmelidir.
Not: Dilimleri tanıtmak amacıyla, bu bölümde yalnızca ASCII karakterleri olduğunu varsayıyoruz; UTF-8 kullanımı ile ilgili daha kapsamlı bir tartışma Bölüm 8’deki “UTF-8 Kodlanmış Metni Stringler ile Saklamak” bölümünde yer almaktadır.
Dilimlerin çözeceği problemi anlamak için, dilim kullanmadan bu fonksiyonun imzasını nasıl yazacağımız üzerinde çalışalım:
fn ilk_kelime(metin: &String) -> ?
ilk_kelime fonksiyonunun &String türünde bir parametresi var. Sahipliğe ihtiyacımız yok, o yüzden bu gayet uygundur. (İdiyomatik Rust’ta, fonksiyonlar ihtiyaç duymadıkça argümanlarının sahipliğini almazlar ve bunun nedenleri ilerledikçe netleşecektir.) Peki ne döndürmeliyiz? Bir metnin bir kısmından bahsetmenin gerçekten bir yolu yok. Ancak, bir boşlukla belirtilen kelimenin sonunun indeksini döndürebiliriz. Liste 4-7’de gösterildiği gibi bunu deneyelim.
fn ilk_kelime(metin: &String) -> usize {
let baytlar = metin.as_bytes();
for (i, &oge) in baytlar.iter().enumerate() {
if oge == b' ' {
return i;
}
}
metin.len()
}
fn main() {}
String parametresine karşılık bir bayt indeksi değeri döndüren ilk_kelime fonksiyonuString’i eleman eleman incelememiz ve bir değerin boşluk olup olmadığını kontrol etmemiz gerektiğinden, as_bytes metodunu kullanarak String’imizi bir bayt dizisine dönüştürüyoruz.
fn ilk_kelime(metin: &String) -> usize {
let baytlar = metin.as_bytes();
for (i, &oge) in baytlar.iter().enumerate() {
if oge == b' ' {
return i;
}
}
metin.len()
}
fn main() {}
Daha sonra, iter metodunu kullanarak bayt dizisi üzerinde bir yineleyici oluşturuyoruz:
fn ilk_kelime(metin: &String) -> usize {
let baytlar = metin.as_bytes();
for (i, &oge) in baytlar.iter().enumerate() {
if oge == b' ' {
return i;
}
}
metin.len()
}
fn main() {}
Yineleyicileri Bölüm 13’te daha detaylı tartışacağız. Şimdilik iter’in bir koleksiyondaki her elemanı döndüren bir metot olduğunu ve enumerate’in iter sonucunu sarmaladığını ve bunun yerine her elemanı bir demetin parçası olarak döndürdüğünü bilin. enumerate’ten döndürülen demetin ilk elemanı indekstir ve ikinci eleman elemana bir referanstır. Bu, indeksi kendimiz hesaplamaktan biraz daha uygundur.
enumerate metodu bir demet döndürdüğü için, o demeti ayrıştırmak (destructure) amacıyla desenleri (patterns) kullanabiliriz. Desenler hakkında Bölüm 6’da daha fazla konuşacağız. for döngüsünde, demetteki indeks için i ve demetteki tek bayt için &oge olan bir desen belirtiyoruz. .iter().enumerate()’ten elemanın bir referansını aldığımız için desende & kullanırız.
for döngüsünün içinde, bayt literali (byte literal) sözdizimini kullanarak boşluğu temsil eden baytı ararız. Eğer bir boşluk bulursak, o konumu döndürürüz. Aksi takdirde, metin.len() kullanarak metnin uzunluğunu döndürürüz.
fn ilk_kelime(metin: &String) -> usize {
let baytlar = metin.as_bytes();
for (i, &oge) in baytlar.iter().enumerate() {
if oge == b' ' {
return i;
}
}
metin.len()
}
fn main() {}
Artık metindeki ilk kelimenin sonunun indeksini bulmanın bir yoluna sahibiz, ancak bir sorun var. Tek başına bir usize döndürüyoruz, ancak bu yalnızca &String bağlamında anlamlı bir sayıdır. Başka bir deyişle, String’den ayrı bir değer olduğu için gelecekte de geçerli kalacağının garantisi yoktur. Liste 4-7’deki ilk_kelime fonksiyonunu kullanan Liste 4-8’deki programı inceleyelim.
fn ilk_kelime(metin: &String) -> usize {
let baytlar = metin.as_bytes();
for (i, &oge) in baytlar.iter().enumerate() {
if oge == b' ' {
return i;
}
}
metin.len()
}
fn main() {
let mut metin = String::from("merhaba dünya");
let kelime = ilk_kelime(&metin); // kelime 7 değerini alacak
metin.clear(); // bu, String'i boşaltır ve onu "" değerine eşitler
// kelime burada hala 7 değerine sahip, ancak metin'in 7 değeriyle
// anlamlı bir şekilde kullanabileceğimiz hiçbir içeriği kalmadı,
// yani kelime artık tamamen geçersiz!
}
ilk_kelime fonksiyonunu çağırmaktan dönen sonucu saklayıp ardından String içeriğini değiştirmekBu program hiçbir hata olmadan derlenir ve metin.clear() çağırdıktan sonra kelime’yi kullansaydık da derlenirdi. kelime metin’in durumuna hiç bağlı olmadığı için, kelime hala 7 (veya "merhaba" kelimesinin uzunluğu) değerini içerir. İlk kelimeyi çıkarmayı denemek için metin değişkeni ile birlikte bu 7 değerini kullanabilirdik, ancak bu bir bug olurdu çünkü kelime’de 7’yi kaydettiğimizden bu yana metin’in içerikleri değişmiştir.
kelime’deki indeksin metin’deki verilerle senkronizasyonunun (uyumunun) bozulması konusunda endişelenmek yorucu ve hataya açıktır! Eğer bir ikinci_kelime fonksiyonu yazarsak bu indeksleri yönetmek daha da kırılgan (brittle) bir hal alır. İmzası şuna benzerdi:
fn ikinci_kelime(metin: &String) -> (usize, usize) {
Artık bir başlangıç ve bir bitiş indeksini izliyoruz; belirli bir durumdaki verilerden hesaplanmış ancak o duruma hiçbir şekilde bağlı olmayan daha da fazla değerimiz var. Etrafta dolaşan ve senkronize tutulması gereken, birbiriyle ilgisiz üç değişkenimiz var.
Neyse ki Rust’ın bu soruna bir çözümü var: string dilimleri.
String Dilimleri (String Slices)
Bir string dilimi, bir String’in elemanlarının bitişik bir dizisine referanstır ve şuna benzer:
fn main() {
let metin = String::from("merhaba dünya");
let merhaba = &metin[0..7];
let dunya = &metin[8..13];
}
Tüm String’e referans vermek yerine, merhaba, ekstra [0..7] bölümünde belirtilen String’in bir kısmına referanstır. Dilimleri [başlangıç_indeksi..bitiş_indeksi] belirterek köşeli parantezler içinde bir aralık kullanarak oluştururuz; burada başlangıç_indeksi dilimdeki ilk konumdur ve bitiş_indeksi dilimdeki son konumun bir fazlasıdır. Dahili olarak, dilim veri yapısı dilimin başlangıç konumunu ve uzunluğunu depolar; bu uzunluk bitiş_indeksi eksi başlangıç_indeksi değerine karşılık gelir. Yani, let dunya = &metin[8..13]; durumunda dunya, metin’in 8. indeksindeki bayta bir işaretçi ve 5 uzunluk değeri içeren bir dilim olacaktır.
Şekil 4-7 bunu bir diyagram halinde göstermektedir.
Şekil 4-7: String’in bir parçasına atıfta bulunan bir string dilimi
Rust’ın .. aralık sözdizimi ile, eğer indeks 0’dan başlamak istiyorsanız, iki noktadan önceki değeri bırakabilirsiniz. Başka bir deyişle, bunlar birbirine eşittir:
#![allow(unused)]
fn main() {
let metin = String::from("merhaba");
let dilim = &metin[0..2];
let dilim = &metin[..2];
}
Aynı şekilde, diliminiz String’in son baytını içeriyorsa, sondaki sayıyı da bırakabilirsiniz. Bu, şunların eşit olduğu anlamına gelir:
#![allow(unused)]
fn main() {
let metin = String::from("merhaba");
let uzunluk = metin.len();
let dilim = &metin[3..uzunluk];
let dilim = &metin[3..];
}
Tüm metnin bir dilimini almak için her iki değeri de bırakabilirsiniz. Dolayısıyla bunlar da eşittir:
#![allow(unused)]
fn main() {
let metin = String::from("merhaba");
let uzunluk = metin.len();
let dilim = &metin[0..uzunluk];
let dilim = &metin[..];
}
Not: String dilim aralığı indeksleri geçerli UTF-8 karakter sınırlarında gerçekleşmelidir. Çok baytlı (multibyte) bir karakterin ortasında bir string dilimi oluşturmaya çalışırsanız, programınız bir hatayla çıkış yapacaktır.
Tüm bu bilgileri göz önünde bulundurarak, ilk_kelime’yi bir dilim döndürecek şekilde yeniden yazalım. “String dilimi“ni ifade eden tür &str olarak yazılır:
fn ilk_kelime(metin: &String) -> &str {
let baytlar = metin.as_bytes();
for (i, &oge) in baytlar.iter().enumerate() {
if oge == b' ' {
return &metin[0..i];
}
}
&metin[..]
}
fn main() {}
Kelimenin sonu için indeksi Liste 4-7’de yaptığımız gibi, bir boşluğun ilk geçtiği yeri arayarak elde ederiz. Bir boşluk bulduğumuzda, metnin başlangıcını ve boşluğun indeksini sırasıyla başlangıç ve bitiş indeksleri olarak kullanarak bir string dilimi döndürürüz.
Artık ilk_kelime’yi çağırdığımızda, temel alınan (underlying) verilere bağlı tek bir değer geri alıyoruz. Bu değer, dilimin başlangıç noktasına yönelik bir referans ile dilimdeki eleman sayısından oluşur.
Bir dilim döndürmek ikinci_kelime fonksiyonu için de işe yarayacaktır:
fn ikinci_kelime(metin: &String) -> &str {
Derleyici String içindeki referansların geçerli kalmasını sağlayacağından artık bozması çok daha zor olan basit bir API’ye sahibiz. Liste 4-8’deki programda yer alan ve ilk kelimenin sonunun indeksini aldığımız, ancak daha sonra metni boşalttığımızda indeksimizin geçersiz hale geldiği hatayı hatırlıyor musunuz? O kod mantıksal olarak yanlıştı ancak anında herhangi bir hata göstermemişti. İlk kelime indeksini boşaltılmış bir metinle birlikte kullanmaya devam etseydik, sorunlar daha sonra ortaya çıkacaktı. Dilimler bu hatayı imkansız kılar ve kodumuzla ilgili bir sorunumuz olduğunu çok daha erken bilmemizi sağlar. ilk_kelime’nin dilim sürümünü kullanmak bir derleme zamanı hatası fırlatır:
fn ilk_kelime(metin: &String) -> &str {
let baytlar = metin.as_bytes();
for (i, &oge) in baytlar.iter().enumerate() {
if oge == b' ' {
return &metin[0..i];
}
}
&metin[..]
}
fn main() {
let mut metin = String::from("merhaba dünya");
let kelime = ilk_kelime(&metin);
metin.clear(); // hata!
println!("ilk kelime: {kelime}");
}
İşte derleyici hatası:
$ cargo run
Compiling ownership v0.1.0 ($PROJE/listings/ch04-understanding-ownership/no-listing-19-slice-error)
error[E0502]: cannot borrow `metin` as mutable because it is also borrowed as immutable
--> src/main.rs:19:5
|
17 | let kelime = ilk_kelime(&metin);
| ------ immutable borrow occurs here
18 |
19 | metin.clear(); // hata!
| ^^^^^^^^^^^^^ mutable borrow occurs here
20 |
21 | println!("ilk kelime: {kelime}");
| ------ immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Ödünç alma kurallarını hatırlayın: Eğer bir şeye değiştirilemez bir referansımız varsa, ona ayrıca değiştirilebilir bir referans alamayız. clear’ın String’i kesmesi (truncate) gerektiği için değiştirilebilir bir referans alması gerekir. clear çağrısından sonraki println!, kelime’deki referansı kullanır, dolayısıyla o noktada değiştirilemez referansın hala aktif olması gerekir. Rust, clear’daki değiştirilebilir referans ile kelime’deki değiştirilemez referansın aynı anda var olmasına izin vermez ve derleme başarısız olur. Rust yalnızca API’mizi daha kolay kullanılır hale getirmekle kalmadı, aynı zamanda bütün bir hata sınıfını derleme zamanında ortadan kaldırdı!
Dilim Olarak Sabit Metinler (String Literals as Slices)
Sabit metinlerin (string literals) ikili dosya içinde saklandığından bahsettiğimizi hatırlayın. Artık dilimleri bildiğimize göre sabit metinleri uygun bir şekilde anlayabiliriz:
#![allow(unused)]
fn main() {
let metin = "Merhaba, dünya!";
}
Buradaki metin değişkeninin türü &str’dir: İkili dosyanın belirli bir noktasına işaret eden bir dilimdir. Sabit metinlerin değiştirilemez olmasının nedeni de budur; &str değiştirilemez bir referanstır.
Parametre Olarak String Dilimleri
Sabitlerin (literals) ve String değerlerinin dilimlerini alabileceğinizi bilmek, ilk_kelime üzerinde yapacağımız bir iyileştirmeye, yani onun imzasına yönlendirir bizi:
fn ilk_kelime(metin: &String) -> &str {
Daha tecrübeli bir Rust geliştiricisi bunun yerine Liste 4-9’da gösterilen imzayı yazardı, çünkü bu imza aynı fonksiyonu hem &String hem de &str değerleri üzerinde kullanmamıza olanak tanır.
fn ilk_kelime(metin: &str) -> &str {
let baytlar = metin.as_bytes();
for (i, &oge) in baytlar.iter().enumerate() {
if oge == b' ' {
return &metin[0..i];
}
}
&metin[..]
}
fn main() {
let benim_metnim = String::from("merhaba dünya");
// `ilk_kelime` `String`lerin dilimleri (kısmi veya tam) üzerinde çalışır.
let kelime = ilk_kelime(&benim_metnim[0..7]);
let kelime = ilk_kelime(&benim_metnim[..]);
// `ilk_kelime` ayrıca, `String`lerin tam dilimlerine eşdeğer olan
// `String` referansları üzerinde de çalışır.
let kelime = ilk_kelime(&benim_metnim);
let sabit_metnim = "merhaba dünya";
// `ilk_kelime` sabit metinlerin dilimleri (kısmi veya tam) üzerinde çalışır.
let kelime = ilk_kelime(&sabit_metnim[0..7]);
let kelime = ilk_kelime(&sabit_metnim[..]);
// Sabit metinler zaten metin dilimleri (string slices) olduğu için,
// bu da dilim sözdizimi olmadan çalışır!
let kelime = ilk_kelime(sabit_metnim);
}
metin parametresi için bir string dilimi kullanarak ilk_kelime fonksiyonunu iyileştirmeEğer elimizde bir string dilimi varsa, bunu doğrudan geçirebiliriz. Eğer bir String’imiz varsa, bu String’in bir dilimini veya String’in referansını geçirebiliriz. Bu esneklik, Bölüm 15’teki “Fonksiyonlarda ve Metotlarda Deref Zorlamalarını Kullanma (Using Deref Coercions in Functions and Methods)” bölümünde ele alacağımız deref zorlamaları (deref coercions) özelliğinden yararlanır.
Bir fonksiyonu String referansı yerine bir string dilimi alacak şekilde tanımlamak, API’mizi hiçbir işlevselliğini kaybetmeden daha genel ve kullanışlı hale getirir:
fn ilk_kelime(metin: &str) -> &str {
let baytlar = metin.as_bytes();
for (i, &oge) in baytlar.iter().enumerate() {
if oge == b' ' {
return &metin[0..i];
}
}
&metin[..]
}
fn main() {
let benim_metnim = String::from("merhaba dünya");
// `ilk_kelime` `String`lerin dilimleri (kısmi veya tam) üzerinde çalışır.
let kelime = ilk_kelime(&benim_metnim[0..7]);
let kelime = ilk_kelime(&benim_metnim[..]);
// `ilk_kelime` ayrıca, `String`lerin tam dilimlerine eşdeğer olan
// `String` referansları üzerinde de çalışır.
let kelime = ilk_kelime(&benim_metnim);
let sabit_metnim = "merhaba dünya";
// `ilk_kelime` sabit metinlerin dilimleri (kısmi veya tam) üzerinde çalışır.
let kelime = ilk_kelime(&sabit_metnim[0..7]);
let kelime = ilk_kelime(&sabit_metnim[..]);
// Sabit metinler zaten metin dilimleri (string slices) olduğu için,
// bu da dilim sözdizimi olmadan çalışır!
let kelime = ilk_kelime(sabit_metnim);
}
Diğer Dilimler (Other Slices)
String dilimleri tahmin edebileceğiniz gibi stringlere özgüdür. Ancak daha genel bir dilim türü de vardır. Şu diziyi (array) ele alalım:
#![allow(unused)]
fn main() {
let d = [1, 2, 3, 4, 5];
}
Tıpkı bir stringin bir kısmına başvurmak (referans vermek) isteyebileceğimiz gibi, bir dizinin bir kısmına da başvurmak isteyebiliriz. Bunu şu şekilde yapardık:
#![allow(unused)]
fn main() {
let d = [1, 2, 3, 4, 5];
let dilim = &d[1..3];
assert_eq!(dilim, &[2, 3]);
}
Bu dilim &[i32] türündedir. İlk elemana bir referans ve bir uzunluk depolayarak tıpkı string dilimlerinin çalıştığı gibi çalışır. Bu tür dilimleri her türlü diğer koleksiyon için kullanacaksınız. Bu koleksiyonları, Bölüm 8’de vektörler hakkında konuştuğumuzda ayrıntılı olarak tartışacağız.
Özet
Sahiplik, ödünç alma ve dilimler kavramları Rust programlarında bellek güvenliğini (memory safety) derleme zamanında sağlar. Rust dili, tıpkı diğer sistem programlama dilleri gibi bellek kullanımınız üzerinde size kontrol verir. Ancak, veri sahibinin kapsam dışına çıktığında o veriyi otomatik olarak temizlemesi, bu kontrolü elde etmek için ekstra kod yazıp ayıklamak zorunda olmadığınız anlamına gelir.
Sahiplik, Rust’ın diğer pek çok parçasının çalışma şeklini etkiler, bu nedenle kitabın geri kalanında bu kavramlar hakkında daha fazla konuşacağız. Şimdi Bölüm 5’e geçelim ve veri parçalarını bir struct içinde gruplandırmaya bakalım.