Ana içeriğe geç

ch08-02-strings

UTF-8 Kodu İçerik Metnini Değerlerle Saklama

Dördüncü bölümde dizgiler hakkında konuştuk, ancak şimdi onlara daha derinlemesine bakacağız. Yeni Rust geliştiricileri genellikle dizgiler hakkında üç nedenden dolayı zorlanır: Rust'ın olası hataları açığa çıkartma eğilimi, dizgilerin birçok programcının düşünüğünden daha karmaşık bir veri yapısı olması ve UTF-8. Bu faktörler, diğer programlama dillerinden geliyorken zorlayıcı gibi görünen bir şekilde birleşir.

ipucu

Dizgiler, belirli bir bağlamda, başka yerlerde depolanan bazı UTF-8 kodlu dize verilerine referanslar olan iç içe geçirilen baytların bir koleksiyonudur.

Bu bölümde, String üzerindeki her koleksiyon türünün sahip olduğu işlemleri, örneğin oluşturma, güncelleme ve okuma hakkında konuşacağız. Ayrıca, String'in diğer koleksiyonlardan nasıl farklı olduğunu, yani bir String'e indekslemenin insanların ve bilgisayarların String verisini yorumlama şekilleri arasındaki farklılıklarla nasıl karmaşıklaştığını da tartışacağız.

Dize Nedir?

Öncelikle dize terimiyle ne demek istediğimizi tanımlayalım. Rust'ın çekirdek dilinde yalnızca bir dize türü vardır; bu, genellikle borçlu formu olan dize dilimi &str olarak görülen str dize dilimidir. Dördüncü bölümde, başka bir yerde depolanan bazı UTF-8 kodlu dize verilerine referans olan dize dilimlerini konuştuk. Örneğin, dize literalleri programın ikili dosyasında depolanır ve dolayısıyla dize dilimleridir.

Rust'ın standart kütüphanesi tarafından sağlanan String türü, büyüyebilen, değiştirilebilen, sahipli, UTF-8 kodlu bir dize türüdür. Rust geliştiricileri Rust'taki "dizgiler" derken, yalnızca String veya dize dilimi &str türlerinden birine değil, her ikisine de atıfta bulunabilirler. Bu bölüm büyük ölçüde String hakkında olsa da, her iki tür de Rust'ın standart kütüphanesinde yoğun şekilde kullanılır ve hem String hem de dize dilimleri UTF-8 kodludur.

Yeni Bir Dize Oluşturma

Vec ile mevcut olan birçok aynı işlem String ile de mevcuttur çünkü String aslında bazı ek garantiler, kısıtlamalar ve yetenekler ile bir bayt vektörü etrafında bir sarmalayıcı olarak uygulanmıştır. Vec ve String ile aynı şekilde çalışan bir örnek işlev, bir örneği gösteren new işlevidir, Liste 8-11'de gösterilmiştir.

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-11/src/main.rs:here}}

Bu satır, s adında yeni, boş bir dize oluşturur, ardından içine veri yükleyebiliriz. Çoğu zaman, dizenin başlangıcında kullanmak istediğimiz bazı başlangıç verilerimiz olacaktır. Bunun için, dize literallerinin yaptığı gibi, Display trait'ini uygulayan herhangi bir türda mevcut olan to_string yöntemini kullanırız. Liste 8-12, iki örneği göstermektedir.

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-12/src/main.rs:here}}

Bu kod, initial contents içeren bir dize oluşturur.

Ayrıca, bir dize literali'nden String oluşturmak için String::from işlevini de kullanabiliriz. Liste 8-13'teki kod, to_string kullanan Liste 8-12'deki kod ile eşdeğerdir.

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-13/src/main.rs:here}}

Dizgiler birçok şey için kullanıldığından, bize birçok seçenek sunan birçok farklı genel API'yi dize için kullanabiliriz. Bazıları gereksiz görünebilir, ancak hepsinin kendine göre bir yeri vardır! Bu durumda, String::from ve to_string aynı şeyi yapar, bu yüzden hangisini seçtiğiniz bir stil ve okunabilirlik meselesidir.

Dizgilerin UTF-8 kodlu olduğunu unutmayın, bu nedenle içlerine uygun şekilde kodlanmış verileri dahil edebiliriz; bu Liste 8-14'te gösterilmektedir.

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:here}}

Tüm bunlar geçerli String değerleridir.

Bir Dizeyi Güncelleme

Bir String, Vec içindekiler gibi büyüyebilir ve içeriği değiştirilebilir, daha fazla veri eklediğinizde. Ayrıca String değerlerini birleştirmek için kolayca + operatörünü veya format! makrosunu kullanabilirsiniz.

push_str ve push ile Dizeye Ekleme

push_str yöntemini kullanarak bir dize dilimini String'e ekleyerek büyütebiliriz; bu Liste 8-15'te gösterilmiştir.

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-15/src/main.rs:here}}

Bu iki satırdan sonra, s foobar içerecektir. push_str yöntemi bir dize dilimi alır çünkü parametreyi sahiplenmek istemiyoruz. Örneğin, Liste 8-16'daki kodda, içeriğini s1'e ekledikten sonra s2'yi kullanabilmek istiyoruz.

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-16/src/main.rs:here}}

Eğer push_str yöntemi s2'nin sahipliğini alsa, son satırda değerini yazdırmamız mümkün olmazdı. Ancak bu kod, beklediğimiz gibi çalışır!

push yöntemi, bir karakteri bir parametre olarak alır ve String'e ekler. Liste 8-17, push yöntemi kullanarak bir String değerine l harfini ekler.

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-17/src/main.rs:here}}

Sonuç olarak, s lol içerecektir.

+ Operatörü veya format! Makrosu ile Birleştirme

Çoğu zaman, iki var olan dizgiyi birleştirmek istersiniz. Bir yol, Liste 8-18'de gösterildiği gibi + operatörünü kullanmaktır.

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-18/src/main.rs:here}}

String s3, Hello, world! içerecektir. s1'in eklemeden sonra artık geçerli olmaması ve s2'ye referans kullanmamızın nedeni, + operatörünü kullandığımızda çağrılan metodun imzasıyla ilgilidir. + operatörü add metodunu kullanır, imzası aşağıdakine benzer görünmektedir:

fn add(self, s: &str) -> String {

Standart kütüphanede add, jenerikler ve ilişkili türler kullanılarak tanımlanmıştır. Burada, bu yöntemi String değerleri ile çağırdığımızda olan şey budur; somut türlerde değiştirilmiştir. Bu imza, + operatörünün karmaşık kısımlarını anlamak için ihtiyacımız olan ipuçlarını verir.

Öncelikle, s2'nin bir &'ye sahip olduğunu görüyoruz, bu da ikinci dizenin bir referansını birinci dizenin üzerine eklediğimiz anlamına gelir. Bu, add işlevindeki s parametresinin nedenidir: yalnızca bir &str ekleyebiliriz; iki String değerini birleştiremeyiz. Ancak bekle—&s2 türü &String, değil mi? add'in ikinci parametresi &str olarak belirtilmiştir. O halde neden Liste 8-18 derleniyor?

tehlike

add metoduna &s2 kullanabilmemizin nedeni, derleyicinin &String argümanını &str'ye zorlamasıdır.

add metodunu çağırdığımızda, Rust bir dereferans zorlaması kullanır; burada &s2'yi &s2[..]'ye dönüştürür. Dereferans zorlaması üzerine daha detaylı olarak 15. bölümde tartışacağız. add fonksiyonu s parametresinin sahipliğini almadığı için, s2 bu işlemin ardından hala geçerli bir String olacaktır.

İkinci olarak, imzadan add'in self'in sahipliğini aldığını görebiliriz, çünkü self bir &'ye sahip değildir. Bu, Liste 8-18'de s1'in add çağrısına taşınacağı ve bundan sonra geçerli olmayacağı anlamına gelir. Bu nedenle, let s3 = s1 + &s2; ifadesi hem iki dizeyi kopyalayacak hem de yeni bir tane yaratacakmış gibi görünse de, bu ifade aslında s1'in sahipliğini alır, s2'nin içeriklerinden bir kopyayı ekler ve ardından sonucu geri döndürür. Diğer bir deyişle, birçok kopya yapıyormuş gibi görünüyor, ancak öyle değil; uygulama kopyalamaktan daha verimlidir.

Birden fazla dize birleştirmemiz gerekiyorsa, + operatörünün davranışı zahmetli hale gelir:

{{#rustdoc_include ../listings/ch08-common-collections/no-listing-01-concat-multiple-strings/src/main.rs:here}}

Bu noktada, s tic-tac-toe olacaktır. Tüm bu + ve " karakterleri ile ne olduğunu görmek zordur. Daha karmaşık yollarla dizeleri birleştirmek için, bunun yerine format! makrosunu kullanabiliriz:

{{#rustdoc_include ../listings/ch08-common-collections/no-listing-02-format/src/main.rs:here}}

Bu kod ayrıca s'yi tic-tac-toe olarak ayarlar. format! makrosu println! gibi çalışır, ancak çıktıyı ekrana yazdırmak yerine içerikle birlikte bir String döndürür. format! kullanan kodun versiyonu okumak için çok daha kolaydır ve format! makrosu tarafından üretilen kod, hiçbir parametresinin sahipliğini almadığı için referanslar kullanarak çalışır.

Dizelere İndeksleme

Birçok diğer programlama dilinde, bir dize içindeki bireysel karakterlere indeksle erişim, geçerli ve yaygın bir işlemdir. Ancak, Rust'ta indeksleme sözdizimini kullanarak bir String'in parçalarına erişmeye çalışırsanız, bir hata alırsınız. Liste 8-19'daki geçersiz koda bir göz atalım.

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-19/src/main.rs:here}}

Bu kod aşağıdaki hata ile sonuçlanacaktır:

{{#include ../listings/ch08-common-collections/listing-08-19/output.txt}}

Hata ve not, durumu anlatıyor: Rust dizgileri indekslemeyi desteklemiyor. Ama neden? Bu soruya yanıt vermek için Rust'ın dizgileri bellekte nasıl depoladığını tartışmamız gerekiyor.

İçsel Temsil

Bir String bir Vec etrafında bir sarmalayıcıdır. Liste 8-14'ten bazı düzgün kodlanmış UTF-8 örnek dizgilerimize bir göz atalım. Öncelikle bu:

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:spanish}}

Bu durumda, len 4 olacaktır, bu da "Hola" dizesini depolayan vektörün 4 bayt uzunluğunda olduğu anlamına gelir. Bu harflerin her biri UTF-8'de kodlandığında bir bayt alır. Ancak, aşağıdaki satır sizi şaşırtabilir (bu dizenin büyük Kiril harfi Ze ile başladığını not edin, sayı 3 ile değil):

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:russian}}

Dizenin ne kadar uzun olduğunu sorsaydınız, muhtemelen 12 dersiniz. Ancak, Rust'ın cevabı 24'tür: "Здравствуйте" dizisini UTF-8'de kodlamak için gereken bayt sayısıdır, çünkü o dizedeki her Unicode skalar değeri 2 bayt depolama alır. Bu nedenle, dizenin baytlarındaki bir indeks her zaman geçerli bir Unicode skalar değeri ile ilişkilendirilmez. Bunu göstermek için, bu geçersiz Rust koduna bakalım:

let hello = "Здравствуйте";
let answer = &hello[0];

Artık answerın З olmayacağını zaten biliyorsunuz; bu, ilk harf değil. UTF-8'de kodlandığında, З'nin ilk baytı 208 ve ikincisi 151dir, dolayısıyla answer'ın aslında 208 olması gerektiği görünmektedir, ancak 208 tek başına geçerli bir karakter değildir. Bir kullanıcı bu dizenin ilk harfini istediyse, 208 döndürmek muhtemelen istemediği bir şeydir; ancak bu, Rust'ın bayt indeks 0'da sahip olduğu tek veridir. Kullanıcılar genellikle geri dönen bayt değerini istemez, dizenin yalnızca Latin harfleri içermesi durumunda bile: &"hello"[0] geçerli bir kod olarak dönerse, 104 döner, h değil.

not

Sonuç olarak, beklenmeyen bir değer döndürmeyi ve hemen keşfedilmeyen hatalar yaratmayı önlemek için, Rust bu kodu hiç derlemez ve çelişkileri geliştirme aşamasının başında önler.

Baytlar ve Skalar Değerler ve Graphem Küme! Oh Hayır!

UTF-8 hakkında başka bir nokta, Rust'ın gözünde dizelere bakmanın aslında üç ilgili yolu olduğudur: baytlar, skalar değerler ve graphem kümeleri (en yakın isimlendirme ile harf olarak adlandırabileceğimiz şey).

Hindistan'daki "नमस्ते" kelimesini Devanagari alfabesinde yazarsak, bu şu şekilde depolanır:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

Bu 18 bayttır ve verilerin nihayetinde bilgisayarlar tarafından nasıl depolandığını gösterir. Unicode skalar değerleri (Rust'ın char tipi olarak adlandırdığı şey) açısından bakarsak, bu baytlar şöyle görünür:

['न', 'म', 'स', '्', 'त', 'े']

Burada altı char değeri vardır, ancak dördüncü ve altıncı değer harf değildir: bunlar kendi başına anlam ifade etmeyen diakritik işaretlerdir. Son olarak, bunlara graphem kümeleri açısından bakarsak, Hindistan'daki kelimeyi oluşturan dört harfi alırız:

["न", "म", "स्", "ते"]

Rust, bilgisayarların depoladığı ham dize verilerini yorumlamak için farklı yollar sağlayarak her programın ihtiyacına göre doğru yorumu seçmesine izin verir; bu, verilerin hangi insan dili kullandığına bakılmaksızın geçerlidir.

bilgi

Bir dizgedeki karakterleri indeksleme işlemini desteklememe nedeninin bir diğer nedeni, indeksleme işlemlerinin her zaman sabit süre (O(1)) alması beklenmesidir. Ancak String ile bunun performansını garanti etmek mümkün değildir, çünkü Rust, geçerli karakterlerin sayısını belirlemek için en baştan içeriği kontrol etmek zorundadır.

Dizeleri Dilimleme

Bir dizeye indeksleme yapmak temel olarak kötü bir fikirdir çünkü dize-indeksleme işleminin dönüş tipinin ne olması gerektiği net değildir: bir bayt değeri, bir karakter, bir graphem küme veya bir dize dilimi. Bu nedenle, dizeleri dilimlemek için gerçekten indeksler kullanmanız gerekiyorsa, Rust sizi daha spesifik olmaya zorlar.

Tek bir numara ile [] kullanmak yerine, belirli baytları içeren bir dize dilimi oluşturmak için bir aralık kullanarak [] kullanabilirsiniz:

let hello = "Здравствуйте";

let s = &hello[0..4];

Burada, s dizenin ilk dört baytını içeren bir &str olacaktır. Daha önce, her bir karakterin iki bayt olduğunu belirtmiştik; bu nedenle s Зд olacaktır.

Bir karakterin baytlarının yalnızca bir kısmını dilimlemeyi denediğimizde, örneğin &hello[0..1], Rust, bir vektörde geçersiz indeks erişimi varsa olduğu gibi çalışma zamanında panik yapacaktır:

{{#include ../listings/ch08-common-collections/output-only-01-not-char-boundary/output.txt}}

Menziller ile dize dilimleri oluştururken dikkatli olmalısınız; çünkü bu, programınızı çökertebilir.

Dizeler Üzerinde İterasyon Yöntemleri

Dizgilerin parçaları üzerinde çalışmanın en iyi yolu, karakterlerin veya baytların ne türde olduğunu açıkça belirtmektir. Tekil Unicode skalar değerleri için chars yöntemini kullanın. "Зд" üzerinde chars çağırmak, iki char tipi değeri ayırır ve her bir elemanı erişmek için sonuç üzerinde yineleyebilirsiniz:

for c in "Зд".chars() {
println!("{c}");
}

Bu kod aşağıdakileri yazdıracaktır:

З
д

Alternatif olarak, bytes yöntemi her bir ham baytı döndürür; bu sizin alanınıza uygun olabilir:

for b in "Зд".bytes() {
println!("{b}");
}

Bu kod, bu dizeyi oluşturan dört baytı yazdıracaktır:

208
151
208
180

Ancak geçerli Unicode skalar değerlerinin birden fazla bayttan oluşabileceğini hatırlamak önemlidir.

bilgi

Dizelerden graphem kümeleri almak, Devanagari alfabesi gibi, karmaşıktır, bu nedenle bu işlevsellik standart kütüphanede sağlanmaz. Eğer bu işlevselliğe ihtiyacınız varsa, crates.io üzerinde mevcut olan paketler bulunmaktadır.

Dizeler O Kadar Basit Değil

Özetlemek gerekirse, dizeler karmaşıktır. Farklı programlama dilleri, programcıya bu karmaşıklığı nasıl sunacakları konusunda farklı seçimler yaparlar. Rust, String verilerini doğru bir şekilde ele almayı tüm Rust programları için varsayılan davranış olarak seçmiştir; bu, programcıların UTF-8 verilerini baştan ele alma konusunda daha fazla düşünmek zorunda kalması anlamına gelir. Bu takas, diğer programlama dillerinde görünenden daha fazla dize karmaşıklığını açığa çıkarır, ancak sizin gelişim yaşam döngünüzde daha sonra ASCII olmayan karakterlerle ilgili hataları ele almanızı engeller.

İyi haber, standart kütüphanenin bu karmaşık durumları doğru bir şekilde ele almak için String ve &str türlerine dayanan birçok işlevsellik sunmasıdır. Bir dize içinde arama yapmak için contains gibi yararlı yöntemler ve bir dizedeki bazı kısımları başka bir dizi ile değiştirmek için replace gibi yöntemlerin belgelerine göz atmayı unutmayın.


Şimdi biraz daha az karmaşık bir şeye geçelim: hash haritaları!