Rust Programlama Dili

Steve Klabnik ve Carol Nichols tarafından, Rust Topluluğunun katkılarıyla

Yazının bu versiyonu, Rust 1.59 ya da daha yenisini kullandığınızı varsayar (2022-02-24 tarihinde yayınlandı). Rust'ı güncellemek ya da yüklemek için Bölüm 1'in “Yükleme” kısmına bakınız

HTML formatı https://doc.rust-lang.org/stable/book/ sayfasından erişilebilir ve rustup; rustup docs --book ile açılabilir.

Sayılı çeviriler translations kısmında mevcuttur.

Bu yazı No Starch Press'den baskılı ve e-kitap formatı şeklinde temin edilebilir.

Ön söz

Her zaman bu kadar net değildi ancak Rust programlama dili temel olarak güçlendirme ile ilgilidir: şu anda ne tarz bir kod yazdığının bir önemi yok. Rust, daha önce yaptığınızdan çok ama çok daha geniş bir alan yelpazesinde güvenle programlama yapmanız için size güç verir.

Örneğin, bellek yönetimi, veri gösterimi ve eşzamanlılığın alt düzey ayrıntılarıyla ilgilenen "sistemsel düzeyde" bir çalışmayı ele alalım. Geleneksel olarak, bu programlama alanı gizemli olarak görülür ve yalnızca yıllarını kötü şöhretli tuzaklarından kaçınmayı öğrenmeye adayan seçkin birkaç kişi tarafından erişilebilir. Ve bunu uygulayanlar bile, kodları istismarlara, çökmelere veya ağır sıkıntılara açık olmasın diye bunu dikkatli yapıyorlar.

Rust, eski tuzakları ortadan kaldırarak ve yol boyunca size yardımcı olacak samimi, cilalı bir araç seti sağlayarak bu engelleri ortadan kaldırır. Daha düşük seviyeli kontrole "dalması" gereken programcılar, bunu Rust ile, geleneksel çökme veya güvenlik açıkları riskini üstlenmeden ve kararsız bir alet zincirinin ince noktalarını öğrenmek zorunda kalmadan yapabilirler. Daha da iyisi, bu dil, sizi hız ve bellek kullanımı açısından verimli, güvenilir koda doğru doğal olarak yönlendirmek için tasarlanmıştır.

Halihazırda düşük seviyeli kodla çalışan programcılar, hedeflerini yükseltmek için Rust'ı kullanabilir. Örneğin, Rust'a paralellik eklemek nispeten düşük riskli bir işlemdir: derleyici sizin için klasik hataları yakalayacaktır. Ve kazara çökmeler veya güvenlik açıkları oluşturmayacağınıza güvenerek kodunuzdaki daha agresif optimizasyonların üstesinden gelebilirsiniz.

Ancak Rust, sadece düşük seviyeli sistem programlama ile sınırlı değildir. Komut satırı uygulamalarını, web sunucularını ve diğer birçok kod türünü yazmayı oldukça keyifli kılacak kadar etkileyici ve ergonomiktir — kitabın ilerleyen bölümlerinde her ikisinin de basit örneklerini bulacaksınız. Rust ile çalışmak, bir alandan diğerine aktarılan beceriler oluşturmanıza olanak tanır; Rust'ı bir web uygulaması yazarak öğrenebilir, ardından aynı becerileri Raspberry Pi'nizi hedeflemek için kullanabilirsiniz.

Bu kitap, Rust'ın kullanıcılarını güçlendirme potansiyelini tamamen benimsiyor. Bu, yalnızca Rust hakkındaki bilginizi değil, aynı zamanda genel olarak bir programcı olarak erişiminizi ve buna karşı olan güveninizi artırmanıza yardımcı olmayı amaçlayan samimi ve ulaşılabilir bir metindir. Öyleyse dalın, öğrenmeye hazır olun—ve Rust Topluluğuna hoş geldiniz!

— Nicholas Matsakis ve Aaron Turon

Başlangıç

Not: Kitabın bu sürümü İngilizce olarak Rust Programlama Dili'da ve basılı ve e-kitap formatında No Starch Press'dan temin edilebilir.

Rust Programlama Diline hoş geldiniz. Bu kitap size Rust'ı tanıtacaktır. Rust programlama dili size daha hızlı, daha güvenli program yazmanız için yardım eder. Yüksek seviyeli ergonomi ve düşük seviyeli kontrol, programlama dili tasarımında genellikle çelişkili olarak lanse edilir; Rust, işte bu çatışmaya meydan okur. Güçlü teknik kapasiteyi ve harika bir geliştirici deneyimini dengeleyen Rust, size düşük seviyeli ayrıntıları (bellek kullanımı gibi) bu tür kontrollerle geleneksel olarak ilişkilendirilen tüm güçlükler olmadan kontrol etme seçeneği sunar.

Rust Kimin İçin

Rust çeşitli nedenlerden dolayı çoğu kişi için idealdir. Hadi bazı çok önemli gruplara göz atalım.

Geliştirici Ekipleri

Rust, farklı düzeylerde sistem programlama bilgisi olan büyük geliştirici ekipleri arasında iş birliği yapmak için üretken bir araç olduğunu kanıtlıyor. Düşük seviyeli kod, diğer birçok dilde yalnızca deneyimli geliştiriciler tarafından kapsamlı testler ve dikkatli kod incelemesi yoluyla yakalanabilen çeşitli ince hatalara yönelimlidir. Rust'ta derleyici, eşzamanlılık hataları da dahil olmak üzere bu anlaşılması zor hatalarla kod derlemeyi reddederek bir kapı bekçisi rolü oynar. Derleyici ile birlikte çalışarak ekip, zamanlarını hataları aramak yerine programın mantığına odaklanarak geçirebilir.

Rust ayrıca çağdaş geliştirici araçlarını sistem programlama dünyasına getiriyor:

  • Cargo, dahili bağımlılık yöneticisi ve inşa aracı, eklemeler yapar, derler, sorunsuzca bağımlılıkları yönetir ve Rust ekosisteminin en önemli parçalarından birisidir.
  • Rustfmt, geliştiriciler arasında tutarlı bir kodlama stili sağlar.
  • Rust Dil Sunucusu, Entegre Geliştirme Ortamına (IDE) kod tamamlama ve hata mesajları için güç verir.

Geliştiriciler, Rust ekosisteminde bunları ve farklı araçları kullanarak sistem programlama seviyesinde daha üretken olabilirler.

Öğrenciler

Rust, öğrenciler ve sistem kavramlarını öğrenmek isteyenler içindir. Rust'ı kullanarak birçok kişi, işletim sistemi geliştirme gibi konuları öğrendi. Topluluk çok sıcakkanlı ve öğrencilerin sorularını yanıtlamaktan mutluluk duyuyor. Rust ekipleri, bu kitap gibi çaba göstererek sistem kavramlarını, özellikle programlamada yeni olanlar için daha fazla insan için daha erişilebilir hale getirmek istiyor.

Şirketler

Büyük ya da küçük yüzlerce şirket, çeşitli görevler için üretimde Rust kullanıyor. Bu görevler arasında komut satırı araçları, web hizmetleri, DevOps araçları, gömülü cihazlar, ses ve video analizi ve kod dönüştürme, kripto para birimleri, biyoinformatik, arama motorları, Nesnelerin İnterneti uygulamaları, makine öğrenimi ve hatta Firefox web tarayıcısının büyük bölümleri de yer alıyor.

Açık Kaynak Geliştiricileri

Rust, Rust programlama dili, topluluk, geliştirici araçları ve kitaplıklar oluşturmak isteyenler içindir. Rust diline katkıda bulunmanızı çok isteriz.

Hıza ve Kararlılığa Önem Verenler

Rust, bir dilde hız ve istikrar isteyen insanlar içindir. Hız derken, Rust ile oluşturabileceğiniz programların hızını ve Rust'ın bunları yazmanıza izin verdiği hızı kastediyoruz. Rust derleyicisinin denetimleri, yeni özellik eklemeleri ve yeniden düzenleme yoluyla kararlılık sağlar. Bu, geliştiricilerin genellikle değiştirmekten korktukları, bu kontrollerin olmadığı dillerdeki kırılgan eski kodun aksine. Rust, sıfır maliyetli soyutlamalar, elle yazılan kod kadar hızlı bir şekilde daha düşük seviyeli koda derlenen daha yüksek seviyeli özellikler için çabalayarak her güvenli kodun da hızlı kod olmasını sağlamaya çalışır.

Rust dili, diğer birçok kullanıcıyı da desteklemeyi umuyor; burada bahsedilenler sadece en büyük paydaşlardan bazılarıdır. Genel olarak, Rust'ın en büyük amacı, güvenlik ve üretkenlik olmak üzere artı olarak hız ve ergonomi sağlayarak geliştiriclerin on yıllardır kabul ettiği ödünleri ortadan kaldırmaktır. Rust'ı deneyin ve seçeneklerinin sizin için işe yarayıp yaramadığını görün.

Bu Kitap Kimler İçin

Bu kitap, başka bir programlama dilinde kod yazdığınızı varsayar. Kitabı çok çeşitli programlama geçmişlerinden gelenler için geniş çapta erişilebilir hale getirmeye çalıştık. Programlamanın ne olduğu veya onun hakkında nasıl düşünüleceği hakkında konuşmak için çok zaman harcamıyoruz. Programlama konusunda tamamen yeniyseniz, özellikle programlamaya giriş sağlayan bir kitap okuyarak daha fazla bilgi alarak bu serüvene atılabilirsiniz.

Kitabı Nasıl Kullanmalı

Genel olarak, bu kitap önden arkaya sırayla okuduğunuzu varsayarak anlatır. Sonraki bölümler, önceki bölümlerdeki kavramların üzerine inşa edilmiştir ve önceki bölümler bir konunun ayrıntılarına girmeyebilir; Konuyu genellikle daha sonraki bir bölümde tekrar ele alırız.

Bu kitapta iki tür bölüm bulacaksınız: kavram bölümleri ve proje bölümleri. Konsept bölümlerinde Rust'ın farklı bir yönü hakkında bilgi edineceksiniz. Proje bölümlerinde, şimdiye kadar öğrendiklerinizi uygulayarak birlikte küçük programlar oluşturacağız. Bölüm 2, 12 ve 20 proje bölümleridir; geri kalanı kavram bölümleridir.

Bölüm 1, Rust'ın nasıl kurulacağını, “Hello, World!”'ün nasıl yazılacağını, Rust'ın paket yöneticisi ve oluşturma aracı olan Cargo'nun nasıl kullanılacağını açıklar. 2. Bölüm, Rust diline uygulamalı bir giriştir. Burada kavramları yüksek düzeyde ele alıyoruz. Eğer kavramlarla kafanız karışmışsa sonraki bölümlerdeki ek ayrıntılar size ek bilgiler verecektir. Ellerinizi hemen kirletmek istiyorsanız, bunun yeri Bölüm 2'dir. İlk başta, diğer programlama dillerine benzer Rust özelliklerini kapsayan Bölüm 3'ü atlayabilir ve Rust'ın sahiplik sistemi hakkında bilgi edinmek için doğrudan Bölüm 4'e gidebilirsiniz. Ancak, bir sonrakine geçmeden önce her ayrıntıyı öğrenmeyi tercih eden özellikle titiz bir öğrenciyseniz, Bölüm 2'yi atlayıp doğrudan Bölüm 3'e geçebilir, bir konu üzerinde çalışmak istediğinizde Bölüm 2'ye dönebilirsiniz. öğrendiğiniz detayları uygulayarak projelendirmeyi unutmayın.

Bölüm 5 yapıları ve yöntemleri tartışır ve Bölüm 6 numaralandırmaları, eşleşme ifadelerini ve if let kontrol akışı yapısını kapsar. Rust'ta özel türler oluşturmak için yapıları ve numaralandırmaları kullanacaksınız.

Bölüm 7'de, Rust'ın modül sistemi ve kodunuzu ve onun genel Uygulama Programlama Arayüzü'nü (API) düzenlemek için gereken gizlilik kuralları hakkında bilgi edineceksiniz. Bölüm 8, vektörler, diziler ve karma haritalar (hash map) gibi standart kitaplığın sağladığı bazı ortak veri toplama yapılarını tartışır. 9. Bölüm, Rust'ın hata işleme felsefesini ve tekniklerini anlatır.

Bölüm 10, size birden çok tür için geçerli olan kodu tanımlama gücü veren yaygın türler, özellikler ve ömürlükleri inceler. Bölüm 11, programınızın mantığının doğru olduğundan emin olmak için Rust'ın güvenlik garantileriyle bile gerekli olan testlerle ilgilidir. 12. Bölümde, dosyalar içinde metin arayan "grep" komut satırı aracının yaptığıyla benzer olarak kendi uygulamamızı oluşturacağız. Bunun için önceki bölümlerde tartıştığımız kavramların birçoğunu kullanacağız.

Bölüm 13, kapanış ifadelerini ve yineleyicileri anlatıyor: işlevsel programlama dillerinden gelen Rust özellikleri. 14. Bölümde, Cargo'yu daha derinlemesine inceleyeceğiz ve kitaplıklarınızı başkalarıyla paylaşmak için kullanılabilecek en iyi tekniklerden bahsedeceğiz. Bölüm 15'te, standart kütüphanenin sağladığı akıllı işaretçileri ve bunların işlevselliğini sağlayan özellikleri tartışacağız.

Bölüm 16'da, farklı eşzamanlı programlama modellerini inceleyeceğiz ve Rust'ın birden çok iş parçacığında korkusuzca programlamanıza nasıl yardımcı olduğu hakkında konuşacağız. Bölüm 17, Rust deyimlerinin aşina olabileceğiniz nesne yönelimli programlama ilkeleriyle nasıl karşılaştırıldığını inceler.

Bölüm 18, Rust programları boyunca fikirleri ifade etmenin güçlü yolları olan kalıplar ve kalıp eşleştirme hakkında bir referanstır. Bölüm 19, güvenli olmayan Rust, makrolar ve ömürlükler, tanımlar, türler, fonksiyonlar ve kapanış türleri hakkında daha fazlası dahil olmak üzere ileri düzey ilgi çekici konulardan oluşan bir İsveç masasını içerir.

Bölüm 20'de, düşük seviyeli çok iş parçacıklı bir web sunucusu uygulayacağımız bir projeyi tamamlayacağız!

Son olarak, Ekleme A, Rust'ın anahtar sözcüklerini, Ekleme B, Rust'ın operatörlerini ve sembollerini kapsar, Ekleme C, standart kütüphane tarafından sağlanan türevlenebilir özellikleri kapsar, Ekleme D, bazı yararlı geliştirme araçlarını kapsar ve Ekleme E, Rust sürümlerini açıklar.

Bu kitabı okumanın yanlış bir yolu yok: Nasıl ilerlemek istiyorsanız, devam edin! Herhangi bir karışıklık yaşarsanız, önceki bölümlere geri dönmeniz gerekebilir. Ama bu kitabı istediğin gibi kullanabilirsin, yapabileceğinin en iyisini yap!

Rust öğrenme sürecinin önemli bir parçası, derleyicinin görüntülediği hata mesajlarının nasıl okunacağını öğrenmektir: bunlar sizi çalışma koduna yönlendirecektir. Bu nedenle, derleyicinin her durumda size göstereceği hata mesajıyla birlikte derlenmeyen birçok örnek sunacağız. Rastgele bir örnek girip çalıştırırsanız derlenmeyebileceğini bilin! Çalıştırmaya çalıştığınız örneğin hata amaçlı olup olmadığını görmek için çevreleyen metni okuduğunuzdan emin olun. Ferris, çalışması mümkün olmayan ya da düzgün çalışmayan kodları ayırt etmenize de yardımcı olacaktır:

FerrisAnlamı
Ferris with a question markBu kod derlenmiyor!
Ferris throwing up their handsBu kod paniğe sahip!
Ferris with one claw up, shruggingBu kod belirtilen davranışı sergilemiyor.

Çoğu durumda, sizi derlenmeyen herhangi bir kodun derlenen doğru sürümüne yönlendireceğiz.

Kaynak Kod

Bu kitabın oluşturulduğu kaynak dosyalar şu adreste bulunabilir: GitHub, Rust deposu. Türkçeye çevrilmiş kaynak dosyaları şu adreste bulunabilir: GitHub, ferhatgec deposu.

Başlarken

Hadi sizin Rust yolculuğunuza başlayalım! Öğrenecek çok şey var... Ama her yolculuk bir yerden başlar. Bu bölümde şunları tartışacağız:

  • Linux, macOS, and Windows platformları için Rust'ı yüklemek
  • Hello, world! yazan bir program yazmak
  • Rust'ın paket yöneticisi ve inşa sistemi olan cargo'yu kullanmak

Kurulum

İlk adım Rust'ı kurmaktır. Rust'ı, Rust sürümlerini ve ilgili araçları yönetmek için bir komut satırı aracı olan rustup aracılığıyla indireceğiz. İndirmek için bir internet bağlantısına ihtiyacınız olacak.

Not: Eğer bir nedenden ötürü rustup kullanmak istemiyorsanız, lütfen Rust'ı Kurmanın Diğer Yolları sayfasına bir göz atın.

Aşağıdaki adımlar, Rust derleyicisinin en son kararlı sürümünü yükler. Rust'ın kararlılık garantisi, kitaptaki tüm örneklerin daha yeni Rust sürümleriyle derlenmeye devam etmesini sağlar. Çıktı, sürümler arasında biraz farklılık gösterebilir, çünkü Rust yeni sürümlerde genellikle hata mesajlarını ve uyarıları iyileştirir. Başka bir deyişle, bu adımları kullanarak kurduğunuz her yeni, kararlı Rust sürümü bu kitabın içeriğiyle beklendiği gibi çalışmalıdır.

Komut Satırı Gösterimi

Bu bölümde hatta kitabın çoğu yerinde komutları uçbirimde kullanıldığı haliyle göstereceğiz. Yazacağınız satırlar $ ile başlamalıdır. Bu karakteri yazmanıza gerek yoktur, sadece komutun başladığını belirtir ve her komutta belirir $ ile başlamayan satırlar çoğu zaman önceki komutun çıktısını gösterir. Farklı olarak, PowerShell özelindeki örneklerde > karakterini kullanacağız.

Linux ya da macOS üzerinde rustup'ı indirmek

Eğer Linux ya da macOS kullanıyorsanız, uçbirimi açın ve şu komutu girin.

$ curl --proto '=https' --tlsv1.3 https://sh.rustup.rs -sSf | sh

Bu komut bir betik indirir ve Rust'ın son stabil sürümünü kuran araç olan rustup'ı başlatır. Eğer çıkış hariç herhangi bir seçeneği seçerseniz şifrenizi girmeniz gerektiği belirtilmiş olmalıdır. Eğer kurulum başarılı olursa, şu satırlar görünmüş olmalıdır:

Rust is installed now. Great!

Ayrıca bir Rust'ın kullandığı, derlenmiş çıktıları tek dosyala toplayan bir bağlayıcıya ihtiyacınız olacak. Büyük ihtimalle sizde bir tanesi vardır, eğer herhangi bir bağlayıcı hatası alıyorsanız Bağlayıcı içeren bir C derleyicisi yüklemeniz gerekir. Bir C derleyicisi ayrıca C kodu içeren Rust paketlerini derlemek için de kullanışlıdır.

macOS'ta C derleyicisini şu kodu çalıştırarak elde edebilirsiniz:

$ xcode-select --install

Linux kullanıcıları dağıtımlarının dokümantasyonlarına bağlı olarak genel olarak GCC ya da Clang yüklemelidir. Örnek olarak, eğer Ubuntu kullanıyorsanız, build-essential paketini yükleyebilirsiniz.

Windows üzerinde rustup'ı indirmek

Windows'ta, https://www.rust-lang.org/tools/install sitesine gidin ve Rust'ı kurmak için belirtilen yönergeleri uygulayın. Yüklemenin bazı noktalarında Visual Studio 2013 ya da yeni sürümleri için C++ inşa araçlarına ihtiyacınız olduğuna dair bir mesaj alacaksınız. En kolay yolla gerekli inşa araçlarını alabilmek için Build Tools for Visual Studio 2019'ı kurabilirsiniz. Her ne zaman hangilerini indirmeniz gerektiği sorulduğunda “C++ inşa araçları”, Windows 10 SDK ve İngilizce dil paketi dahil edilmş olmalıdır.

Bu kitap hem cmd.exe de hem de PowerShell de çalışan komutları kullanmaktadır. Eğer bir farklılık var ise hangisini kullanmanız gerektiğini açıklayacağız.

Güncelleme ve Kaldırma

rustup yoluyla kurduktan sonra son sürüme güncelleştirmek aşırı kolaydır. Kabuğunuzdan (PowerShell) şu güncelleme betiğini çalıştırın:

$ rustup update

Rust ve rustup'ı kaldırmak için kabuğunuzdan şu kaldırma betiğini çalıştırın:

$ rustup self uninstall

Sorun giderme

Rust'ı doğru yüklediğinizden emin olmak için kabuğunuzu açın ve şu betiği girin:

$ rustc --version

Versiyon sayısını, son stabil sürüm için depoya gönderilen gönderim tarihini görmüş olmalısınızdır:

rustc x.y.z (abcabcabc yyyy-mm-dd)

Eğer bu bilgileri görebiliyorsanız, Rust'ı doğru bir biçimde kurmuşsunuz demektir! Eğer göremiyorsanız ve Windows üzerindeyseniz, %PATH% sistem değişkenini kontrol edinç Eğer her şey yolunda ve Rust hala çalışmıyorsa, yardım alabileceğiniz birçok yer vardır. En kolay yol, resmi Rust Discord sunucusunda #beginners kanalına mesaj atmaktır. Burada, size yardımcı olabilecek diğer Rustseverler (Rustaceans) ile mesajlaşabilirsiniz. Diğer güzel kaynaklara Kullanıcılar forumu ve Stack Overflow örnek verilebilir.

Yerel Dokümantasyon

Rust kurulumu ayrıca dokümantasyonun bir kopyasını yerelde tutar, yani bunu çevrimdışı da okuyabilirsiniz. Tarayıcınızda okumak için rustup doc komutunu çalıştırabilirsiniz.

Standart kütüphanede bulunan ve nasıl ya da nerede kullanacağınızı bilmediğiniz tür ya da fonksiyonları uygulama programlama arayüzü (API) dokümantasyonunu kullanarak bulabilirsiniz!

Merhaba, Dünya!

Artık Rust'u yüklediğinize göre ilk Rust programınızı yazalım. Yeni bir dil öğrenirken Hello, world! metnini yazdıran küçük bir program yazmak gelenekseldir. Bu yüzden burada da aynısını yapacağız!

Not: Bu kitap, komut satırına temel düzeyde aşina olduğunuzu varsayar. Rust, araçlarınızı ya da kodunuzun nerede tutulduğunu umursamaz, çoğu editörde Rust dili desteği vardır. Bu nedenle komut satırı yerine entegre bir geliştirme ortamı (IDE) kullanmayı tercih ederseniz, favori IDE'nizi kullanmaktan çekinmeyin. Birçok IDE artık bir dereceye kadar Rust desteğine sahiptir; ayrıntılar için IDE dokümantasyonlarına bakabilirsiniz. Son zamanlarda, Rust ekibi harika IDE desteği sağlamaya odaklandı ve bu cephede hızla ilerleme kaydedildi!

Proje Dizini Oluşturma

Rust kodunuzu tutmak için bir dizine ihtiyacınız var, Rust bunu otomatik olarak oluşturur. Normalde, Rust için kodunuzun nerede tutulduğu hiç de önemli değildir ancak bu kitaptaki alıştırmalar ve projeler için ana dizininizde bir proje dizini oluşturmanızı ve tüm projelerinizi orada tutmanızı öneririz.

Bir proje dizini ve başlangıç kodu oluşturmak için bir uçbirim açın ve aşağıdaki komutları girin:

Linux'ta, macOS'ta ve PowerShell'de (Windows'ta) çalıştırmak için şunları girin:

$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world

Windows Komut Satırı (CMD) için şunları girin:

> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world

Rust Programı Yazma ve Çalıştırma

Ardından, yeni bir kaynak dosya oluşturun ve main.rs olarak adlandırın. Rust dosyaları her zaman .rs uzantısıyla biter. Dosya adınızda birden fazla kelime kullanıyorsanız, bunları ayırmak için alt çizgi kullanın. Örneğin, helloworld.rs yerine hello_word.rs kullanın.

Şimdi az önce oluşturduğunuz main.rs dosyasını açın ve Liste 1-1'deki kodu yapıştırın.

Dosya adı: main.rs

fn main() {
    println!("Hello, world!");
}

Liste 1-1: Hello, world! Yazan Bir Program

Dosyayı kaydedin ve uçbirime geri dönün. Linux'ta ya da macOS'ta derlemek ve çalıştırmak için şu komutları girin:

$ rustc main.rs
$ ./main
Hello, world!

Windows'ta, komutu ./main şeklinde değil .\main.exe şeklinde girin:

> rustc main.rs
> .\main.exe
Hello, world!

İşletim sisteminizden bağımsız olarak Hello, world! dizgisi uçbirime yazılmış olmalıdır. Eğer çıktıyı göremiyorsanız, “Hata giderme” kısmına giderek çözüm arayın, büyük ihtimalle bir şeyi kaçırmışssınızdır!

Eğer Hello, world! çıktısını gördüyseniz, tebrikleeeeeeer! Rust programı yazdınız. Bu da sizi Rust programcısı yapar—Hoş geldiniz!

Bir Rust Programının Anatomisi

Hadi sizin “Hello, world!” programınızda nelerin olduğunu detaylıca inceleyelim. Burası yapbozun ilk parçası:

fn main() {

}

Bu satırlar Rust'ta bir fonksiyonu tanımlar. main fonksiyonu özel bir fonksiyondur: yürütülebilir her Rust programında çalışan ilk koddur. İlk satır, parametresi olmayan ve hiçbir şey döndürmeyen main adlı bir işlev bildirir. Parametreler olsaydı, parametreler parantez () içine girerlerdi.

Ayrıca, fonksiyon gövdesinin {} süslü parantezlerine sarıldığına dikkat edin. Rust'ta bunlar gereklidir. Aralarına bir boşluk ekleyerek, giriş süslü parantezini fonksiyon ile aynı satıra yerleştirmek iyi bir stildir.

Rust projelerinde standart bir stile bağlı kalmak istiyorsanız, kodunuzu belirli bir stille biçimlendirmek için rustfmt adlı otomatik biçimlendirici aracı kullanabilirsiniz. Rust ekibi, bu aracı rustc gibi standart Rust dağıtımına dahil etti, bu nedenle bilgisayarınızda zaten yüklü olmalıdır! Daha fazla ayrıntı için çevrimiçi belgelere bakın.

main fonksiyonunun içinde aşağıdaki kod bulunur:


#![allow(unused)]
fn main() {
    println!("Hello, world!");
}

Bu satır, bu küçük programdaki tüm işi yapar: metni ekrana yazdırır. Burada dikkat edilmesi gereken dört önemli detay var.

İlk olarak, Rust stili bir TAB karakteriyle değil, dört boşlukla girinti yapar.

İkincisi, println! bir Rust makrosu çağırır. Bunun yerine bir fonksiyon çağırsaydı, println olarak girilirdi (! olmadan). Rust makrolarını Bölüm 19'da daha ayrıntılı olarak tartışacağız. Şimdilik, bir ! normal bir fonksiyon yerine bir makro çağırdığınız ve makroların her zaman fonksiyonlarla aynı kurallara uymadığı anlamına gelir.

Üçüncüsü, "Hello, world!" dizgisi. Bu dizgiyi println!'e bir argüman olarak iletiyoruz ve dizgi ekrana yazdırılıyor.

Dördüncü olarak, satırı noktalı virgül (;) ile bitiriyoruz, bu karakter ile ifadenin bittiğini ve bir sonrakinin başlamaya hazır olduğunu gösteriyoruz. Rust kodunun çoğu satırı noktalı virgülle biter.

Derleme ve Çalıştırma Ayrı Adımlardır

Az önce yeni oluşturulmuş bir programı çalıştırdınız, bu yüzden süreçteki her adımı inceleyelim. Bir Rust programını çalıştırmadan önce, Rust derleyicisini kullanarak, yani rustc komutunu girerek ve kaynak dosyanızın adını ona şu şekilde ileterek onu derlemelisiniz:

$ rustc main.rs

C veya C++ geçmişiniz varsa, bunun GCC veya Clang'a benzediğini fark edeceksiniz. Başarılı bir şekilde derlendikten sonra Rust, derlenmiş ve yürütülebilir dosya çıkarır. Linux, macOS ve PowerShell'de (yani Windows'ta), kabuğunuza ls komutunu girerek yürütülebilir dosyayı görebilirsiniz. Linux ve macOS'ta iki dosya göreceksiniz. PowerShell (Windows) ile, CMD kullanarak göreceğiniz aynı üç dosyayı göreceksiniz.

$ ls
main  main.rs

Windows'ta CMD kullanırsanız, şunları göreceksinizdir:

> dir /B %= /B seçeneği yalnızca dosya adlarını göstermeyi garantiler =%
main.exe
main.pdb
main.rs

Bu, .rs uzantılı kaynak kod dosyasını, yürütülebilir dosyayı (Windows'ta main.exe, ancak diğer tüm platformlarda main) ve Windows kullananlar için .pdb uzantılı hata ayıklama bilgilerini içeren bir dosyayı gösterir. Buradan main veya main.exe dosyasını şu şekilde çalıştırırsınız:

$ ./main # ya da Windows üzerinde .\main.exe şeklinde kullanılabilir

Eğer derlenmiş main.rs, “Hello, world!” programı ise, çalıştırdığınız zaman Hello, world! çıktısını uçbiriminizde görmüşsünüzdür.

Ruby, Python veya JavaScript gibi dinamik bir dille haşır neşirseniz, bir programı ayrı adımlar olarak derlemeye ve çalıştırmaya alışkın olmayabilirsiniz. Rust derlenmeye ihtiyaç duyan bir dildir, yani bir programı derleyebilir ve yürütülebilir dosyayı başka birine verebilirsiniz ve onlar Rust'u kurmadan bile çalıştırabilirler. Birine .rb, .py veya .js dosyası verirseniz, o kişinin (sırasıyla) bir Ruby, Python veya JavaScript süreklemesine sahip olması gerekir. Ancak bu dillerde, programınızı derlemek ve çalıştırmak için yalnızca bir komuta ihtiyacınız vardır. Dil tasarımında her şey bir değiş tokuştur.

Basit programları rustc kullanarak derlemek hoştur fakat projeniz büyüdükçe kodunuzu kolayca paylaşabilmeniz ve yönetebilmeniz için bir yöneticiye ihtiyacınız olacaktır. Sonraki aşamalarda size, siz gerçek-dünya programları yazarken size yardımcı olacağını düşündüğümüz Cargo aracını tanıtacağız.

Merhaba, Cargo!

Cargo, Rust'ın yapı sistemi ve paket yöneticisidir. Çoğu Rustsever, Rust projelerini yönetmek için bu aracı kullanır çünkü Cargo, kodunuzu oluşturmak, kodunuzun bağlı olduğu kitaplıkları indirmek ve bu kitaplıkları derlemek gibi birçok görevi sizin yerinize gerçekleştirir.

Şimdiye kadar yazdığımız gibi en basit Rust programlarının herhangi bir bağımlılığı yoktur. Yani “Hello, world!” projeniz Cargo ile oluşturulduğunda, yalnızca Cargo'nun kodunuzu oluşturmayı yöneten bölümünü kullanır. Daha karmaşık Rust programları yazdıkça, bağımlılıklar ekleyeceksiniz ve Cargo kullanarak bir projeye başlarsanız, bağımlılıkları eklemek çok daha kolay olacaktır.

Rust projelerinin büyük çoğunluğu Kargo kullandığından, bu kitabın tamamında sizin de Cargo kullandığınız varsayılır. Eğer resmi yükleyicileri “Yükleme” kısmından çekerek çalıştırdıysanız, Cargp önceden yüklü olarak gelmiş olur. Eğer farklı yollarla Rust'ı yüklediyseniz, Cargo'nun yüklü olup olmadığını şu kodu uçbiriminizde çalıştırarak öğrenebilirsiniz:

$ cargo --version

Eğer sürüm numarasını görüyorsanız, zaten yüklüdür! Eğer hata görüyorsanız, mesela komut bulunamadı, yükleme metodunuzun dokümantasyonuna bakarak Cargo'yu nasıl ayrı olarak kurabileceğinizi bulabilirsiniz.

Cargo'yla Proje Oluşturmak

Hadi Cargo ile yeni proje oluşturalım ve orijinal “Hello, world!” projesiyle olan farklılıklarına bir göz gezdirelim. Projelerinizi tuttuğunuz (örneğin projects dizini) dizine ya da kodunuzu tutmak istediğiniz dizinde herhangi bir işletim sistemi farklılığı gözetmeksizin şu komutu çalıştırın:

$ cargo new hello_cargo
$ cd hello_cargo

İlk komut hello_cargo adında yeni bir dizin oluşturdu. Biz projemize hello_cargo adını vermek istedik ve Cargo aynı addaki dizine temel dosyaları oluşturdu.

hello_cargo dizinine gidin ve dosyaları listeleyin. Göreceksiniz ki Cargo sizin için iki tane dosya oluşturmuş: bir Cargo.toml dosyası ve içinde main.rs'i tutan bir src dizini.

Cargo ayrıca yeni bir Git deposunu .gitignore dosyası oluşturmakla beraber başlatır. Git dosyaları eğer halihazırda bir Git deposundaysanız cargo new komutuyla oluşturulmaz. Bu davranışı cargo new --vcs=git komutunu kullanarak değiştirebilirsiniz ve Git dosyaları otomatik olarak oluşturulmuş olur.

Not: Git yaygın bir versiyon kontrol sistemidir. cargo new komutunu --vcs argümanıyla birlikte farklı bir versiyon kontrol sistemiyle ya da VKS (VCS) olmadan da kullanabilirsiniz. cargo new --help komutunu çalıştırarak seçenekleri görebilirsiniz.

Cargo.toml dosyasını yazı editörünüzle açabilirsiniz. Liste 1-2'dekine benzer bir kodla karşılaşmanız beklenir.

Dosya adı: Cargo.toml

[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

[dependencies]

Liste 1-2: Cargo.toml'ın içeriği cargo new tarafından oluşturulmuştur

Bu dosya, TOML (Tom’un Bariz, Minimal Dili) Cargo'nun kullandığı dahili konfigürasyon formatıyla oluşturulmuştur.

İlk satır [package], konu başlığını belirtir. Bu başlık bize üye yapıları hakkında bazı bilgiler verir ve onları sınırlandırmamızı sağlar.

Sonraki üç satır Cargo'nun kodunuzu derlemesi için gerekli konfigürasyon bilgilerini içerir: paketinizin adı, sürümü ve hangi Rust sürümünü kullandığı. Ekleme E'de edition anahtarı hakkında daha fazla konuşacağız.

Son satırda [dependencies], projenizin kullandığı bağımlılıkların bir listesidir. Rust'ta, kod paketleri kasalar olarak adlandırılır. Bu basit proje için bir diğer kasaya ihtiyacımız yok fakat Bölüm 2'de bağımlılıklar konusunu işleyeceğiz.

Şimdi src/main.rs dosyasını açın ve bir bakış atın:

Dosya adı: src/main.rs

fn main() {
    println!("Hello, world!");
}

Cargo sizin için bir “Hello, world!” programı oluşturmuştu aynı Liste 1-1'de yazdığımız gibi! Çok yakın, eski projemiz ile arasındaki farklardan bazıları: Cargo kodu src dizininde oluşturdu, biz ise kök dizinde oluşturduk. Ayrıca biz Cargo.toml şeklinde bir dosya oluşturmadık.

Cargo sizin tüm kaynak dosyalarınızın src dizininde olmasını bekler. Kök dizin daha çok BENİOKU (README) dosyaları, lisans bilgileri, konfigürasyon dosyaları gibi çeşitli yardımcı elemanlar için kullanılması beklenir. Burada her şey için bir yer var ve her şeyin de bir yeri var ve her şey yerli yerinde olmalı.

Cargo kullanmayan “Hello, world!” projenizi Cargo kullanabilir hale getirmek getirmek için tüm kodlarınızı src dizinine taşıyabilir ve Cargo.toml adında bir konfigürasyon dosyası oluşturabilirsiniz.

Cargo Projesini Derleme ve Çalıştırma

Şimdi “Hello, world!” projesinde neyin farklı olduğunu bulalım! hello_cargo dizininizde, projenizi şu kodla derleyebilirsiniz:

$ cargo build
   Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs

Bu komut kök dizinde oluşturmak yerine target/debug/hello_cargo dizininde yürütülebilir bir dosya oluşturur (Windows'ta target\debug\hello_cargo.exe dizininde). Bu dosyayı şu komutla çalıştırabilirsiniz:

$ ./target/debug/hello_cargo # Windows'ta .\target\debug\hello_cargo.exe komutunu kullanın 
Hello, world!

Eğer her şey yolunda gitmişse, Hello, world! uçbirimde yazılmış olmalıdır. cargo build komutunu ilk defa çalıştırmak ayrıca kök dizinde Cargo.lock adında bir dosya oluşturur. Bu dosya projenizin bağımlılıklarını kayıtta tutar. Tabii bu projenin standart kütüphane hariç herhangi bir bağımlılığı olmadığından dolayı dosya biraz boş görünebilir. Bu dosyayı elle değiştirmeniz gerekmez, Cargo bunu sizin için otomatik yönetir.

cargo build ile derledik ve ./target/debug/hello_cargo ile çalıştırdık, ama ayrıca cargo run komutunu da kullanarak kodu derleyip çalıştırabiliriz:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/hello_cargo`
Hello, world!

Not olarak, eğer kaynak kodunuzu değiştirmediyseniz, Cargo herhangi bir derleme gereksinimi olmadan programınızı çalıştıracaktır. Eğer kaynak kodunuzu değiştirdiyseniz, Cargo projenizi yeniden derleyecektir ve eğer kodunuz sorunsuz ise şu çıktıyı görmeniz olasıdır:

$ cargo run
   Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs
     Running `target/debug/hello_cargo`
Hello, world!

Cargo ayrıca cargo check adında bir komut sunar. Bu komut kodunuzu hızlıca kontrol eder ve onu derlenebilir hale sokar fakat herhangi bir yürütülebilir dosya oluşturmaz:

$ cargo check
   Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs

Niye yürütülebilir dosya istenmesin ki? Yaygın olarak, cargo check cargo build'ten daha hızlıdır, çünkü yürütülebilir dosya oluşturma kısmını es geçer. Eğer kod yazarken kodunuzu sürekli kontrol ediyorsanız cargo check kullanmak işlemlerinize hız katacaktır! Ayrıca, çoğu Rustsever cargo check komutunu derleneceğinden emin olabilmek için sıklıkla çalıştırır. Onlar ayrıca cargo build komutunu her ne zaman proje yürütülebilirliğe hazır olduğu vakit çalıştırırlar.

Hadi şimdi Cargo ile neler öğrendiğimize yakından bakalım:

  • cargo new ile proje oluşturabiliyoruz.
  • cargo build ile proje derleyebiliyoruz.
  • cargo run ile hem derleyip hem çalıştırabiliyoruz.
  • Yürütülebilir kod oluşturmadan cargo check ile kodumuzu kontrol edebiliyoruz.
  • Aynı dizinde yürütülebilirleri tutmak yerine Cargo'nun target/debug dizininde tuttuğunu artık biliyoruz.

Ayrıca iyi bir avantaj olaraktan, Cargo her ne işletim sistemini kullanıyorsanız olun aynı komutlara ve işleve sahiptir. Yani, bu saatten sonra işletim sistemlerine yönelik spesifik talimatlar sunmayacağız.

Yayın için Derlemek

Her ne zaman projeniz yayınlanmak için hazırsa, cargo build --release komutunu kullanarak kodunuzu optimizasyonlarla derleyebilirsiniz. Bu komut, yürütülebilir dosyaları target/debug dizini yerine target/release dizininde tutar. Optimizasyonlar Rust kodunuzu hızlandırır fakat derlenmesi için gerekli yer ve sürenizi artırır. İşte bu neden iki farklı profil türüne sahip olduğumuzu açıklar. Eğer kodunuzun çalıştırılma zamanını merak ediyor ve bunu test etmek istiyorsanız cargo build --release komutuyla derlediğinizden emin olun.

Cargo Hakkında

Basit projelerde Cargo direkt rustc kullanmanın önüne aşırı yenilikler katmıyor fakat bu halen kodunuzun büyüdükçe ve karmaşıklaştıkça, farklı farklı kasalarla birlikte kullanıldığında Cargo ile koordine bir biçimde derlememenin daha kolay olacağı kanaatine varıyorsunuz.

Hatta hello_cargo projesi basit bir proje olmasına rağmen Rust kariyerinizde her zaman kullanacağınız önemli araca ev sahipliği yapmış oluyor. Halihazırda var olan projeler üzerinde çalışmak için şu komutla yerelde bu depoyu tutabilir, depoyu derleyebilirsiniz:

$ git clone example.org/someproject
$ cd someproject
$ cargo build

Cargo hakkında daha fazla bilgi almak için, kontrol edin its documentation.

Özet

Rust serüveninize iyi bir başlangıç yaptınız! Tüm bu bölümde birçok yeni şey öğrendiniz, bunlardan bazıları:

  • rustup ile son stabil sürümü yükleme
  • En sonki Rust sürümüne güncelleme
  • Yereldeki dokümantasyonu açma
  • Direkt rustc komutunu kullanarak “Hello, world!” programı yazıp çalıştırma
  • Cargo projesi oluşturma ve çalıştırma

Okuduklarınız ve yazdıklarınızla daha karmaşık programlar yazmanın tam zamanı. Yani, Bölüm 2'de bir tahmin oyunu inşa edeceğiz. Eğer daha önceden yaygın programlama kavramlarını öğrenmek istiyorsanız, Bölüm 3'e bakabilirsiniz ve sonra tekrar Bölüm 2'ye dönebilirsiniz.

Programming a Guessing Game

Let’s jump into Rust by working through a hands-on project together! This chapter introduces you to a few common Rust concepts by showing you how to use them in a real program. You’ll learn about let, match, methods, associated functions, using external crates, and more! In the following chapters, we’ll explore these ideas in more detail. In this chapter, you’ll practice the fundamentals.

We’ll implement a classic beginner programming problem: a guessing game. Here’s how it works: the program will generate a random integer between 1 and 100. It will then prompt the player to enter a guess. After a guess is entered, the program will indicate whether the guess is too low or too high. If the guess is correct, the game will print a congratulatory message and exit.

Setting Up a New Project

To set up a new project, go to the projects directory that you created in Chapter 1 and make a new project using Cargo, like so:

$ cargo new guessing_game
$ cd guessing_game

The first command, cargo new, takes the name of the project (guessing_game) as the first argument. The second command changes to the new project’s directory.

Look at the generated Cargo.toml file:

Filename: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

As you saw in Chapter 1, cargo new generates a “Hello, world!” program for you. Check out the src/main.rs file:

Filename: src/main.rs

fn main() {
    println!("Hello, world!");
}

Now let’s compile this “Hello, world!” program and run it in the same step using the cargo run command:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     Running `target/debug/guessing_game`
Hello, world!

The run command comes in handy when you need to rapidly iterate on a project, as we’ll do in this game, quickly testing each iteration before moving on to the next one.

Reopen the src/main.rs file. You’ll be writing all the code in this file.

Processing a Guess

The first part of the guessing game program will ask for user input, process that input, and check that the input is in the expected form. To start, we’ll allow the player to input a guess. Enter the code in Listing 2-1 into src/main.rs.

Filename: src/main.rs

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Listing 2-1: Code that gets a guess from the user and prints it

This code contains a lot of information, so let’s go over it line by line. To obtain user input and then print the result as output, we need to bring the io input/output library into scope. The io library comes from the standard library, known as std:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

By default, Rust has a set of items defined in the standard library that it brings into the scope of every program. This set is called the prelude, and you can see everything in it in the standard library documentation.

If a type you want to use isn’t in the prelude, you have to bring that type into scope explicitly with a use statement. Using the std::io library provides you with a number of useful features, including the ability to accept user input.

As you saw in Chapter 1, the main function is the entry point into the program:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

The fn syntax declares a new function, the parentheses, (), indicate there are no parameters, and the curly bracket, {, starts the body of the function.

As you also learned in Chapter 1, println! is a macro that prints a string to the screen:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

This code is printing a prompt stating what the game is and requesting input from the user.

Storing Values with Variables

Next, we’ll create a variable to store the user input, like this:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Now the program is getting interesting! There’s a lot going on in this little line. We use the let statement to create the variable. Here’s another example:

let apples = 5;

This line creates a new variable named apples and binds it to the value 5. In Rust, variables are immutable by default, meaning once we give the variable a value, the value won't change. We’ll be discussing this concept in detail in the “Variables and Mutability” section in Chapter 3. To make a variable mutable, we add mut before the variable name:

let apples = 5; // immutable
let mut bananas = 5; // mutable

Note: The // syntax starts a comment that continues until the end of the line. Rust ignores everything in comments. We’ll discuss comments in more detail in Chapter 3.

Returning to the guessing game program, you now know that let mut guess will introduce a mutable variable named guess. The equal sign (=) tells Rust we want to bind something to the variable now. On the right of the equals sign is the value that guess is bound to, which is the result of calling String::new, a function that returns a new instance of a String. String is a string type provided by the standard library that is a growable, UTF-8 encoded bit of text.

The :: syntax in the ::new line indicates that new is an associated function of the String type. An associated function is a function that’s implemented on a type, in this case String. This new function creates a new, empty string. You’ll find a new function on many types, because it’s a common name for a function that makes a new value of some kind.

In full, the let mut guess = String::new(); line has created a mutable variable that is currently bound to a new, empty instance of a String. Whew!

Receiving User Input

Recall that we included the input/output functionality from the standard library with use std::io; on the first line of the program. Now we’ll call the stdin function from the io module, which will allow us to handle user input:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

If we hadn’t imported the io library with use std::io at the beginning of the program, we could still use the function by writing this function call as std::io::stdin. The stdin function returns an instance of std::io::Stdin, which is a type that represents a handle to the standard input for your terminal.

Next, the line .read_line(&mut guess) calls the read_line method on the standard input handle to get input from the user. We’re also passing &mut guess as the argument to read_line to tell it what string to store the user input in. The full job of read_line is to take whatever the user types into standard input and append that into a string (without overwriting its contents), so we therefore pass that string as an argument. The string argument needs to be mutable so the method can change the string’s content.

The & indicates that this argument is a reference, which gives you a way to let multiple parts of your code access one piece of data without needing to copy that data into memory multiple times. References are a complex feature, and one of Rust’s major advantages is how safe and easy it is to use references. You don’t need to know a lot of those details to finish this program. For now, all you need to know is that like variables, references are immutable by default. Hence, you need to write &mut guess rather than &guess to make it mutable. (Chapter 4 will explain references more thoroughly.)

Handling Potential Failure with the Result Type

We’re still working on this line of code. We’re now discussing a third line of text, but note that it’s still part of a single logical line of code. The next part is this method:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

We could have written this code as:

io::stdin().read_line(&mut guess).expect("Failed to read line");

However, one long line is difficult to read, so it’s best to divide it. It’s often wise to introduce a newline and other whitespace to help break up long lines when you call a method with the .method_name() syntax. Now let’s discuss what this line does.

As mentioned earlier, read_line puts whatever the user enters into the string we pass to it, but it also returns a Result value. Result is an enumeration, often called an enum, which is a type that can be in one of multiple possible states. We call each possible state a variant.

Chapter 6 will cover enums in more detail. The purpose of these Result types is to encode error-handling information.

Result's variants are Ok and Err. The Ok variant indicates the operation was successful, and inside Ok is the successfully generated value. The Err variant means the operation failed, and Err contains information about how or why the operation failed.

Values of the Result type, like values of any type, have methods defined on them. An instance of Result has an expect method that you can call. If this instance of Result is an Err value, expect will cause the program to crash and display the message that you passed as an argument to expect. If the read_line method returns an Err, it would likely be the result of an error coming from the underlying operating system. If this instance of Result is an Ok value, expect will take the return value that Ok is holding and return just that value to you so you can use it. In this case, that value is the number of bytes in the user’s input.

If you don’t call expect, the program will compile, but you’ll get a warning:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

warning: `guessing_game` (bin "guessing_game") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s

Rust warns that you haven’t used the Result value returned from read_line, indicating that the program hasn’t handled a possible error.

The right way to suppress the warning is to actually write error handling, but in our case we just want to crash this program when a problem occurs, so we can use expect. You’ll learn about recovering from errors in Chapter 9.

Printing Values with println! Placeholders

Aside from the closing curly bracket, there’s only one more line to discuss in the code so far:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

This line prints the string that now contains the user’s input. The {} set of curly brackets is a placeholder: think of {} as little crab pincers that hold a value in place. You can print more than one value using curly brackets: the first set of curly brackets holds the first value listed after the format string, the second set holds the second value, and so on. Printing multiple values in one call to println! would look like this:


#![allow(unused)]
fn main() {
let x = 5;
let y = 10;

println!("x = {} and y = {}", x, y);
}

This code would print x = 5 and y = 10.

Testing the First Part

Let’s test the first part of the guessing game. Run it using cargo run:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 6.44s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

At this point, the first part of the game is done: we’re getting input from the keyboard and then printing it.

Generating a Secret Number

Next, we need to generate a secret number that the user will try to guess. The secret number should be different every time so the game is fun to play more than once. We’ll use a random number between 1 and 100 so the game isn’t too difficult. Rust doesn’t yet include random number functionality in its standard library. However, the Rust team does provide a rand crate with said functionality.

Using a Crate to Get More Functionality

Remember that a crate is a collection of Rust source code files. The project we’ve been building is a binary crate, which is an executable. The rand crate is a library crate, which contains code intended to be used in other programs and can't be executed on its own.

Cargo’s coordination of external crates is where Cargo really shines. Before we can write code that uses rand, we need to modify the Cargo.toml file to include the rand crate as a dependency. Open that file now and add the following line to the bottom beneath the [dependencies] section header that Cargo created for you. Be sure to specify rand exactly as we have here, with this version number, or the code examples in this tutorial may not work.

Filename: Cargo.toml

rand = "0.8.3"

In the Cargo.toml file, everything that follows a header is part of that section that continues until another section starts. In [dependencies] you tell Cargo which external crates your project depends on and which versions of those crates you require. In this case, we specify the rand crate with the semantic version specifier 0.8.3. Cargo understands Semantic Versioning (sometimes called SemVer), which is a standard for writing version numbers. The number 0.8.3 is actually shorthand for ^0.8.3, which means any version that is at least 0.8.3 but below 0.9.0.

Cargo considers these versions to have public APIs compatible with version 0.8.3, and this specification ensures you’ll get the latest patch release that will still compile with the code in this chapter. Any version 0.9.0 or greater is not guaranteed to have the same API as what the following examples use.

Now, without changing any of the code, let’s build the project, as shown in Listing 2-2.

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.3
  Downloaded libc v0.2.86
  Downloaded getrandom v0.2.2
  Downloaded cfg-if v1.0.0
  Downloaded ppv-lite86 v0.2.10
  Downloaded rand_chacha v0.3.0
  Downloaded rand_core v0.6.2
   Compiling rand_core v0.6.2
   Compiling libc v0.2.86
   Compiling getrandom v0.2.2
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.10
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.3
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s

Listing 2-2: The output from running cargo build after adding the rand crate as a dependency

You may see different version numbers (but they will all be compatible with the code, thanks to SemVer!), different lines (depending on the operating system), and the lines may be in a different order.

When we include an external dependency, Cargo fetches the latest versions of everything that dependency needs from the registry, which is a copy of data from Crates.io. Crates.io is where people in the Rust ecosystem post their open source Rust projects for others to use.

After updating the registry, Cargo checks the [dependencies] section and downloads any crates listed that aren’t already downloaded. In this case, although we only listed rand as a dependency, Cargo also grabbed other crates that rand depends on to work. After downloading the crates, Rust compiles them and then compiles the project with the dependencies available.

If you immediately run cargo build again without making any changes, you won’t get any output aside from the Finished line. Cargo knows it has already downloaded and compiled the dependencies, and you haven’t changed anything about them in your Cargo.toml file. Cargo also knows that you haven’t changed anything about your code, so it doesn’t recompile that either. With nothing to do, it simply exits.

If you open up the src/main.rs file, make a trivial change, and then save it and build again, you’ll only see two lines of output:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs

These lines show Cargo only updates the build with your tiny change to the src/main.rs file. Your dependencies haven’t changed, so Cargo knows it can reuse what it has already downloaded and compiled for those.

Ensuring Reproducible Builds with the Cargo.lock File

Cargo has a mechanism that ensures you can rebuild the same artifact every time you or anyone else builds your code: Cargo will use only the versions of the dependencies you specified until you indicate otherwise. For example, say that next week version 0.8.4 of the rand crate comes out, and that version contains an important bug fix, but it also contains a regression that will break your code. To handle this, Rust creates the Cargo.lock file the first time you run cargo build, so we now have this in the guessing_game directory.

When you build a project for the first time, Cargo figures out all the versions of the dependencies that fit the criteria and then writes them to the Cargo.lock file. When you build your project in the future, Cargo will see that the Cargo.lock file exists and use the versions specified there rather than doing all the work of figuring out versions again. This lets you have a reproducible build automatically. In other words, your project will remain at 0.8.3 until you explicitly upgrade, thanks to the Cargo.lock file. Because the Cargo.lock file is important for reproducible builds, it's often checked into source control with the rest of the code in your project.

Updating a Crate to Get a New Version

When you do want to update a crate, Cargo provides the command update, which will ignore the Cargo.lock file and figure out all the latest versions that fit your specifications in Cargo.toml. Cargo will then write those versions to the Cargo.lock file. Otherwise, by default, Cargo will only look for versions greater than 0.8.3 and less than 0.9.0. If the rand crate has released the two new versions 0.8.4 and 0.9.0 you would see the following if you ran cargo update:

$ cargo update
    Updating crates.io index
    Updating rand v0.8.3 -> v0.8.4

Cargo ignores the 0.9.0 release. At this point, you would also notice a change in your Cargo.lock file noting that the version of the rand crate you are now using is 0.8.4. To use rand version 0.9.0 or any version in the 0.9.x series, you’d have to update the Cargo.toml file to look like this instead:

[dependencies]
rand = "0.9.0"

The next time you run cargo build, Cargo will update the registry of crates available and reevaluate your rand requirements according to the new version you have specified.

There’s a lot more to say about Cargo and its ecosystem which we’ll discuss in Chapter 14, but for now, that’s all you need to know. Cargo makes it very easy to reuse libraries, so Rustaceans are able to write smaller projects that are assembled from a number of packages.

Generating a Random Number

Let’s start using rand to generate a number to guess. The next step is to update src/main.rs, as shown in Listing 2-3.

Filename: src/main.rs

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Listing 2-3: Adding code to generate a random number

First, we add the line use rand::Rng. The Rng trait defines methods that random number generators implement, and this trait must be in scope for us to use those methods. Chapter 10 will cover traits in detail.

Next, we’re adding two lines in the middle. In the first line, we call the rand::thread_rng function that gives us the particular random number generator that we’re going to use: one that is local to the current thread of execution and seeded by the operating system. Then we call the gen_range method on the random number generator. This method is defined by the Rng trait that we brought into scope with the use rand::Rng statement. The gen_range method takes a range expression as an argument and generates a random number in the range. The kind of range expression we’re using here takes the form start..=end and is inclusive on the lower and upper bounds, so we need to specify 1..=100 to request a number between 1 and 100.

Note: You won’t just know which traits to use and which methods and functions to call from a crate, so each crate has documentation with instructions for using it. Another neat feature of Cargo is that running the cargo doc --open command will build documentation provided by all of your dependencies locally and open it in your browser. If you’re interested in other functionality in the rand crate, for example, run cargo doc --open and click rand in the sidebar on the left.

The second new line prints the secret number. This is useful while we’re developing the program to be able to test it, but we’ll delete it from the final version. It’s not much of a game if the program prints the answer as soon as it starts!

Try running the program a few times:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

You should get different random numbers, and they should all be numbers between 1 and 100. Great job!

Comparing the Guess to the Secret Number

Now that we have user input and a random number, we can compare them. That step is shown in Listing 2-4. Note that this code won’t compile quite yet, as we will explain.

Filename: src/main.rs

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    // --snip--
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Listing 2-4: Handling the possible return values of comparing two numbers

First we add another use statement, bringing a type called std::cmp::Ordering into scope from the standard library. The Ordering type is another enum and has the variants Less, Greater, and Equal. These are the three outcomes that are possible when you compare two values.

Then we add five new lines at the bottom that use the Ordering type. The cmp method compares two values and can be called on anything that can be compared. It takes a reference to whatever you want to compare with: here it’s comparing the guess to the secret_number. Then it returns a variant of the Ordering enum we brought into scope with the use statement. We use a match expression to decide what to do next based on which variant of Ordering was returned from the call to cmp with the values in guess and secret_number.

A match expression is made up of arms. An arm consists of a pattern to match against, and the code that should be run if the value given to match fits that arm’s pattern. Rust takes the value given to match and looks through each arm’s pattern in turn. Patterns and the match construct are powerful Rust features that let you express a variety of situations your code might encounter and make sure that you handle them all. These features will be covered in detail in Chapter 6 and Chapter 18, respectively.

Let’s walk through an example with the match expression we use here. Say that the user has guessed 50 and the randomly generated secret number this time is 38. When the code compares 50 to 38, the cmp method will return Ordering::Greater, because 50 is greater than 38. The match expression gets the Ordering::Greater value and starts checking each arm’s pattern. It looks at the first arm’s pattern, Ordering::Less, and sees that the value Ordering::Greater does not match Ordering::Less, so it ignores the code in that arm and moves to the next arm. The next arm’s pattern is Ordering::Greater, which does match Ordering::Greater! The associated code in that arm will execute and print Too big! to the screen. The match expression ends after the first successful match, so it won’t look at the last arm in this scenario.

However, the code in Listing 2-4 won’t compile yet. Let’s try it:

$ cargo build
   Compiling libc v0.2.86
   Compiling getrandom v0.2.2
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.10
   Compiling rand_core v0.6.2
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.3
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:22:21
   |
22 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `String`, found integer
   |
   = note: expected reference `&String`
              found reference `&{integer}`

error[E0283]: type annotations needed for `{integer}`
   --> src/main.rs:8:44
    |
8   |     let secret_number = rand::thread_rng().gen_range(1..=100);
    |         -------------                      ^^^^^^^^^ cannot infer type for type `{integer}`
    |         |
    |         consider giving `secret_number` a type
    |
    = note: multiple `impl`s satisfying `{integer}: SampleUniform` found in the `rand` crate:
            - impl SampleUniform for i128;
            - impl SampleUniform for i16;
            - impl SampleUniform for i32;
            - impl SampleUniform for i64;
            and 8 more
note: required by a bound in `gen_range`
   --> /Users/carolnichols/.cargo/registry/src/github.com-1ecc6299db9ec823/rand-0.8.3/src/rng.rs:129:12
    |
129 |         T: SampleUniform,
    |            ^^^^^^^^^^^^^ required by this bound in `gen_range`
help: consider specifying the type arguments in the function call
    |
8   |     let secret_number = rand::thread_rng().gen_range::<T, R>(1..=100);
    |                                                     ++++++++

Some errors have detailed explanations: E0283, E0308.
For more information about an error, try `rustc --explain E0283`.
error: could not compile `guessing_game` due to 2 previous errors

The core of the error states that there are mismatched types. Rust has a strong, static type system. However, it also has type inference. When we wrote let mut guess = String::new(), Rust was able to infer that guess should be a String and didn’t make us write the type. The secret_number, on the other hand, is a number type. A few of Rust’s number types can have a value between 1 and 100: i32, a 32-bit number; u32, an unsigned 32-bit number; i64, a 64-bit number; as well as others. Unless otherwise specified, Rust defaults to an i32, which is the type of secret_number unless you add type information elsewhere that would cause Rust to infer a different numerical type. The reason for the error is that Rust cannot compare a string and a number type.

Ultimately, we want to convert the String the program reads as input into a real number type so we can compare it numerically to the secret number. We do so by adding this line to the main function body:

Filename: src/main.rs

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    // --snip--

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

The line is:

let guess: u32 = guess.trim().parse().expect("Please type a number!");

We create a variable named guess. But wait, doesn’t the program already have a variable named guess? It does, but helpfully Rust allows us to shadow the previous value of guess with a new one. Shadowing lets us reuse the guess variable name rather than forcing us to create two unique variables, such as guess_str and guess for example. We’ll cover this in more detail in Chapter 3, but for now know that this feature is often used when you want to convert a value from one type to another type.

We bind this new variable to the expression guess.trim().parse(). The guess in the expression refers to the original guess variable that contained the input as a string. The trim method on a String instance will eliminate any whitespace at the beginning and end, which we must do to be able to compare the string to the u32, which can only contain numerical data. The user must press enter to satisfy read_line and input their guess, which adds a newline character to the string. For example, if the user types 5 and presses enter, guess looks like this: 5\n. The \n represents “newline”. (On Windows, pressing enter results in a carriage return and a newline, \r\n). The trim method eliminates \n or \r\n, resulting in just 5.

The parse method on strings converts a string to another type. Here, we use it to convert from a string to a number. We need to tell Rust the exact number type we want by using let guess: u32. The colon (:) after guess tells Rust we’ll annotate the variable’s type. Rust has a few built-in number types; the u32 seen here is an unsigned, 32-bit integer. It’s a good default choice for a small positive number. You’ll learn about other number types in Chapter 3. Additionally, the u32 annotation in this example program and the comparison with secret_number means that Rust will infer that secret_number should be a u32 as well. So now the comparison will be between two values of the same type!

The parse method will only work on characters that can logically be converted into numbers and so can easily cause errors. If, for example, the string contained A👍%, there would be no way to convert that to a number. Because it might fail, the parse method returns a Result type, much as the read_line method does (discussed earlier in “Handling Potential Failure with the Result Type”). We’ll treat this Result the same way by using the expect method again. If parse returns an Err Result variant because it couldn’t create a number from the string, the expect call will crash the game and print the message we give it. If parse can successfully convert the string to a number, it will return the Ok variant of Result, and expect will return the number that we want from the Ok value.

Let’s run the program now!

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

Nice! Even though spaces were added before the guess, the program still figured out that the user guessed 76. Run the program a few times to verify the different behavior with different kinds of input: guess the number correctly, guess a number that is too high, and guess a number that is too low.

We have most of the game working now, but the user can make only one guess. Let’s change that by adding a loop!

Allowing Multiple Guesses with Looping

The loop keyword creates an infinite loop. We’ll add a loop to give users more chances at guessing the number:

Filename: src/main.rs

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    // --snip--

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        // --snip--


        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

As you can see, we’ve moved everything from the guess input prompt onward into a loop. Be sure to indent the lines inside the loop another four spaces each and run the program again. The program will now ask for another guess forever, which actually introduces a new problem. It doesn’t seem like the user can quit!

The user could always interrupt the program by using the keyboard shortcut ctrl-c. But there’s another way to escape this insatiable monster, as mentioned in the parse discussion in “Comparing the Guess to the Secret Number”: if the user enters a non-number answer, the program will crash. We can take advantage of that to allow the user to quit, as shown here:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Typing quit will quit the game, but as you’ll notice so will entering any other non-number input. This is suboptimal to say the least; we want the game to also stop when the correct number is guessed.

Quitting After a Correct Guess

Let’s program the game to quit when the user wins by adding a break statement:

Filename: src/main.rs

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Adding the break line after You win! makes the program exit the loop when the user guesses the secret number correctly. Exiting the loop also means exiting the program, because the loop is the last part of main.

Handling Invalid Input

To further refine the game’s behavior, rather than crashing the program when the user inputs a non-number, let’s make the game ignore a non-number so the user can continue guessing. We can do that by altering the line where guess is converted from a String to a u32, as shown in Listing 2-5.

Filename: src/main.rs

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        // --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Listing 2-5: Ignoring a non-number guess and asking for another guess instead of crashing the program

We switch from an expect call to a match expression to move from crashing on an error to handling the error. Remember that parse returns a Result type and Result is an enum that has the variants Ok and Err. We’re using a match expression here, as we did with the Ordering result of the cmp method.

If parse is able to successfully turn the string into a number, it will return an Ok value that contains the resulting number. That Ok value will match the first arm’s pattern, and the match expression will just return the num value that parse produced and put inside the Ok value. That number will end up right where we want it in the new guess variable we’re creating.

If parse is not able to turn the string into a number, it will return an Err value that contains more information about the error. The Err value does not match the Ok(num) pattern in the first match arm, but it does match the Err(_) pattern in the second arm. The underscore, _, is a catchall value; in this example, we’re saying we want to match all Err values, no matter what information they have inside them. So the program will execute the second arm’s code, continue, which tells the program to go to the next iteration of the loop and ask for another guess. So, effectively, the program ignores all errors that parse might encounter!

Now everything in the program should work as expected. Let’s try it:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 4.45s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

Awesome! With one tiny final tweak, we will finish the guessing game. Recall that the program is still printing the secret number. That worked well for testing, but it ruins the game. Let’s delete the println! that outputs the secret number. Listing 2-6 shows the final code.

Filename: src/main.rs

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Listing 2-6: Complete guessing game code

Summary

At this point, you’ve successfully built the guessing game. Congratulations!

This project was a hands-on way to introduce you to many new Rust concepts: let, match, functions, the use of external crates, and more. In the next few chapters, you’ll learn about these concepts in more detail. Chapter 3 covers concepts that most programming languages have, such as variables, data types, and functions, and shows how to use them in Rust. Chapter 4 explores ownership, a feature that makes Rust different from other languages. Chapter 5 discusses structs and method syntax, and Chapter 6 explains how enums work.

Yaygın Programlama Kavramları

Bu bölüm, hemen hemen her programlama dilinde görünen kavramları ve bunların Rust'ta nasıl çalıştığını kapsar. Birçok programlama dilinin özünde çokça ortak nokta vardır. Bu bölümde sunulan kavramların hiçbiri Rust'a özgü değildir, ancak bunları Rust bağlamında tartışacağız ve bu kavramların kullanımına ilişkin kuralları açıklayacağız. Özellikle değişkenler, temel türler, fonksiyonlar, yorum satırları ve kontrol akışı hakkında bilgi edineceksiniz. Bu temeller her Rust programında yer alacaktır ve bunları erkenden öğrenmek size başlangıç için güçlü bir temel sağlayacaktır.

Anahtar Sözcükler

Rust dilinde, diğer dillerde olduğu gibi, yalnızca dil tarafından kullanılmak üzere ayrılmış bir dizi anahtar sözcük vardır. Bu kelimeleri değişken veya fonksiyon adı olarak kullanamayacağınızı unutmayın. Anahtar kelimelerin çoğunun özel anlamları vardır ve bunları Rust programlarınızda çeşitli görevleri yapmak için kullanacaksınız; birkaçının kendileriyle ilişkili mevcut bir işlevi yoktur, ancak gelecekte Rust'a eklenebilecek işlevsellik için ayrılmıştır. Anahtar kelimelerin bir listesini Ekleme A'da bulabilirsiniz.

Değişkenler ve Değişkenlik

“Değişkenlerle Değerleri Saklama” bölümünde belirtildiği gibi, varsayılan olarak değişkenler değişmezdir. Bu, Rust'ın size sunduğu güvenlik ve kolay eşzamanlılıktan yararlanarak kodunuzu yazmanız için size verdiği birçok dürtüden biridir. Ancak yine de değişkenlerinizi değiştirilebilir yapma seçeneğiniz vardır. Rust'ın sizi değişmezliği tercih etmeye nasıl ve neden teşvik ettiğini ve bazen neden vazgeçmek isteyebileceğinizi keşfedelim.

Bir değişken değişmez olduğunda, bir değer bir ada bağlandıktan sonra bu değeri değiştiremezsiniz. Bunu örneklemek için, cargo new variables komutunu kullanarak proje dizininizde variables adında yeni bir proje oluşturalım.

Ardından, yeni variables dizininizde bulunan src/main.rs dosyasını açın ve kodunu aşağıdaki kodla değiştirin. Bu kod henüz derlenmeyecek, bundan dolayı önce değişmezlik hatasını inceleyeceğiz.

Dosya adı: src/main.rs

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

cargo run komutunu kullanarak programı kaydedin ve çalıştırın. Bu çıktıda gösterildiği gibi bir hata mesajı almalısınız:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     println!("The value of x is: {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` due to previous error

Bu örnek, derleyicinin programlarınızdaki hataları bulmanıza nasıl yardımcı olduğunu gösterir. Derleyici hataları can sıkıcı olabilir, ancak gerçekte bunlar yalnızca programınızın henüz yapmak istediğiniz şeyi güvenli bir şekilde yapmadığı anlamına gelir; iyi bir programcı olmadığınız anlamına gelmezler! Deneyimli Rustseverler hala derleyici hataları alıyor.

Hata mesajı, hatanın nedeninin, değişmez x değişkenine ikinci bir değer atamaya çalıştığınız için değişmez "x" değişkenine ikinci kez atayamamanız olduğunu gösterir.

Değişmez olarak belirlenmiş bir değeri değiştirmeye çalıştığımızda derleme zamanı hataları almamız önemlidir çünkü bu durum hatalara yol açabilir. Kodumuzun bir kısmı, bir değerin asla değişmeyeceği varsayımıyla çalışıyorsa ve kodumuzun başka bir kısmı bu değeri değiştiriyorsa, kodun ilk kısmının tasarlandığı şeyi yapmaması olasıdır. Bu tür bir hatanın nedenini, özellikle ikinci kod parçası değeri yalnızca bazen değiştirdiğinde, olaydan sonra bulmak zor olabilir. Rust derleyicisi, bir değerin değişmeyeceğini belirttiğinizde, gerçekten değişmeyeceğini garanti eder, bu nedenle onu kendiniz takip etmek zorunda kalmazsınız. Bu nedenle kodunuzun akıl yürütmesi daha kolaydır.

Ancak değişebilirlik çok faydalı olabilir ve kod yazmayı daha uygun hale getirebilir. Değişkenler yalnızca varsayılan olarak değişmezdir; Bölüm 2'de yaptığınız gibi, değişken adının önüne mut ekleyerek bunları değiştirilebilir yapabilirsiniz. mut eklemek ayrıca kodun diğer bölümlerinin bu değişkenin değerini değiştireceğini belirterek kodun gelecekteki okuyucularına niyet iletir.

Örneğin, src/main.rs'yi aşağıdaki şekilde değiştirelim:

Dosya adı: src/main.rs

fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

Programı şimdi çalıştırdığımızda şunu görürüz:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

mut kullanıldığında x'e bağlı değeri 5'ten 6'ya değiştirmemize izin verilir. Nihayetinde, mut kullanıp kullanmamaya karar vermek size bağlıdır.

Sabitler

Değişmez değişkenler gibi, sabitler de bir ada bağlı ve değişmesine izin verilmeyen değerlerdir, ancak sabitler ve değişkenler arasında birkaç fark vardır.

İlk olarak, mut'u sabitlerle kullanamazsınız. Sabitler yalnızca varsayılan olarak değişmez değildir, her zaman değişmezdirler. Sabitleri let anahtar sözcüğü yerine const anahtar sözcüğünü kullanarak bildirirsiniz ve değerin türü verilmiş olmalıdır. Bir sonraki “Veri Türleri” bölümünde türleri ve tür ek açıklamalarını ele alacağız, bu nedenle şu anda ayrıntılar için endişelenmeyin. Her zaman türe değer vermeniz gerektiğini bilin. Sabitler, global kapsam da dahil olmak üzere herhangi bir kapsamda bildirilebilir, bu da onları kodun birçok bölümünün bilmesi gereken değerler için faydalı kılar. Son fark olarak, sabitlerin çalışma zamanında hesaplanabilecek bir değerin sonucu olarak değil, yalnızca sabit bir ifadeye ayarlanabilmesidir. İşte bir sabit bildirim örneği:


#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}

Sabitin adı THREE_HOURS_IN_SECONDS'dır ve değeri 60 (bir dakikadaki saniye sayısı) ile 60 (bir saatteki dakika sayısı) ile 3 (bu programda saymak istediğimiz saat sayısı) çarpılmasının sonucuna ayarlanır. Rust'ın sabitler için adlandırma kuralı, sözcükler arasında alt çizgi ile tüm büyük harfleri kullanmaktır. Derleyici, derleme zamanında sınırlı bir dizi işlemi değerlendirebilir; bu, bu sabiti 10,800 değerine ayarlamak yerine, bu değeri anlaşılması ve doğrulanması daha kolay bir şekilde yazmayı seçmemize olanak tanır. Sabitleri bildirirken hangi işlemlerin kullanılabileceği hakkında daha fazla bilgi için Rust Reference’ın sabitleri hesaplama bölümüne bakın. Sabitler, bir programın çalıştığı tüm süre boyunca, bildirildikleri kapsam dahilinde geçerlidir. Bu özellik, sabitleri, uygulama etki alanınızdaki, herhangi bir maksimum nokta sayısı gibi, programın birden fazla bölümünün bilmesi gerekebilecek değerler için kullanışlı hale getirir. Programınız boyunca kullanılan sabit kodlanmış değerleri sabitler olarak adlandırmak, bu değerin anlamını kodun gelecekteki koruyucularına iletmede faydalıdır. Ayrıca, gelecekte sabit kodlanmış değerin güncellenmesi gerekiyorsa değiştirmeniz gereken kodunuzun yalnızca bir bölümü olacaktır.

Gölgeleme

Bölüm 2'deki tahmin oyununda gördüğünüz gibi, önceki değişkenle aynı ada sahip yeni bir değişken bildirebilirsiniz. Rustseverler, ilk değişkenin ikinci tarafından gölgelendiğini söylüyor, bu da ikinci değişkenin, değişkenin adını kullandığınızda derleyicinin göreceği şey olduğu anlamına geliyor. Gerçekte, ikinci değişken birinciyi gölgede bırakır ve değişken adının herhangi bir kullanımını kendisi gölgelenene veya kapsam sona erene kadar kendisine alır. Aynı değişkenin adını kullanarak ve let anahtar sözcüğünü aşağıdaki gibi tekrarlayarak bir değişkeni gölgeleyebiliriz:

Dosya adı: src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x is: {x}");
}

Bu program önce x'e 5 değerini atar, sonra let x ='i tekrar ederek, orijinal değeri alıp 1 ekleyerek yeni bir x değişkeni oluşturur, böylece x değeri 6 olur. Parantez içindeki üçüncü let ifadesi de x'i gölgeler ve x'e 12 değerini vermek için önceki değeri 2 ile çarparak yeni bir değişken oluşturur. Bu kapsam sona erdiğinde, iç gölgeleme sona erer ve x, 6'ya döner. Bu programı çalıştırdığımızda, aşağıdaki çıktıyı verecektir:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6

Gölgeleme, bir değişkeni mut olarak işaretlemekten farklıdır çünkü let anahtar sözcüğünü kullanmadan yanlışlıkla bu değişkene yeniden atamaya çalışırsak derleme zamanı hatası alırız. let kullanarak, bir değer üzerinde birkaç dönüşüm gerçekleştirebiliriz, ancak bu dönüşümler tamamlandıktan sonra değişkenin değişmez olmasını sağlayabiliriz.

mut ve shadowing arasındaki diğer fark, let anahtar sözcüğünü tekrar kullandığımızda etkin bir şekilde yeni bir değişken oluşturduğumuz için, değerin türünü değiştirebilir ancak aynı adı yeniden kullanabiliriz. Örneğin, programımızın bir kullanıcıdan boşluk karakterleri girerek bazı metinler arasında kaç boşluk istediğini göstermesini istediğini ve ardından bu girişi bir sayı olarak saklamak istediğimizi varsayalım:

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
}

İlk spaces değişkeni bir dize türüdür ve ikinci spaces değişkeni bir sayı türüdür. Böylece gölgeleme, bizi space_str ve space_num gibi farklı isimler kullanmaktan kurtarır; bunun yerine, daha basitçe spaces adını yeniden kullanabiliriz. Ancak, burada gösterildiği gibi bunun için mut kullanmaya çalışırsak, bir derleme zamanı hatası alırız:

fn main() {
    let mut spaces = "   ";
    spaces = spaces.len();
}

Hata, bu değişkenin türünü değiştirmemize izin verilmediğini söylüyor:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error

Artık değişkenlerin nasıl çalıştığını anladığımıza göre, sahip olabilecekleri daha fazla veri türüne bakalım.

Veri Türleri

Rust'taki her değer, Rust'a bu verilerle nasıl çalışacağını bilmesi için ne tür verilerin belirtildiğini söyleyen belirli bir veri türündendir. İki veri türü alt kümesine bakacağız: skaler ve bileşik.

Rust'ın statik yazılmış bir dil olduğunu unutmayın; bu, derleme zamanında tüm değişkenlerin türlerini bilmesi gerektiği anlamına gelir. Derleyici genellikle değere ve onu nasıl kullandığımıza bağlı olarak ne tür kullanmak istediğimizi çıkarabilir. Birçok türün mümkün olduğu durumlarda, örneğin Bölüm 2'deki “Tahminle Gizli Numarayı Karşılaştırma” bölümünde parse'ı kullanarak bir String'i sayısal bir türe dönüştürdüğümüzde, aşağıdaki gibi bir tür ek açıklaması eklemeliyiz:


#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

Yukarıdaki gibi : u32 tipini eklemezsek, Rust aşağıdaki hatayı döndürür, bu da derleyicinin hangi türü kullanmak istediğimizi bilmesi için bizden daha fazla bilgiye ihtiyacı olduğu anlamına gelir:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^ consider giving `guess` a type

For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations` due to previous error

Diğer veri türleri için farklı tür ek açıklamaları göreceksiniz.

Skaler Tipler

Bir skaler tip, tek bir değeri temsil eder. Rust'ın dört birincil skaler türü vardır: tamsayılar, kayan noktalı sayılar, Boole'ler ve karakterler. Bunları diğer programlama dillerinden tanıyabilirsiniz. Hadi Rust'ta nasıl çalıştıklarına geçelim.

Tam Sayı Türleri

Tam sayı, kesirli bileşeni olmayan bir sayıdır. Bölüm 2'de bir tam sayı türü olan u32 türü kullandık. Bu tür bildirimi, ilişkilendirildiği değerin 32 bit yer kaplayan işaretsiz bir tam sayı (işaretli tamsayı türleri u yerine i ile başlar) olması gerektiğini belirtir. Tablo 3-1, Rust'taki yerleşik tam sayı türlerini gösterir. Bir tam sayı değerinin türünü bildirmek için bu değişkenlerden herhangi birini kullanabiliriz.

Tablo 3-1: Rust'ta Tam Sayı Türleri

Büyüklüğüİşaretliİşaretsiz
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
(mimariye bağlı)isizeusize

Her varyant işaretli veya işaretsiz olabilir ve açık bir boyutu vardır. İşaretli ve işaretsiz, sayının negatif olmasının mümkün olup olmadığına - başka bir deyişle, sayının onunla bir işareti olması gerekip gerekmediğine (işaretli) veya yalnızca pozitif olup olmayacağına ve bu nedenle işaretsiz temsil edilip edilemeyeceğine (işaretsiz) atıfta bulunur. Kağıda sayılar yazmak gibidir: işaret önemli olduğunda, bir sayı artı veya eksi işaretiyle gösterilir; ancak, sayının pozitif olduğunu varsaymak güvenli olduğunda, işaretsiz olarak gösterilir. İşaretli sayılar, ikinin tümleyen gösterimi kullanılarak saklanır.

Her işaretli varyant -(2n - 1) ile 2n - 1 - 1 arasındaki sayıları saklayabilir; burada n, varyantın kullandığı bit sayısıdır.

Böylece bir i8, -(27) ile 27 - 1 arasındaki sayıları saklayabilir, bu da -128 ile 127'ye eşittir. İşaretsiz değişkenler 0 ile 2n - 1 arasındaki sayıları saklayabilir, dolayısıyla bir u8 0 ile 28 - 1 arasındaki sayıları saklayabilir, bu da 0 ila 255'e eşittir.

Ek olarak, isize ve usize, programınızın üzerinde çalıştığı bilgisayarın mimarisine bağlıdır; bu, tabloda “arch” olarak gösterilir: 64 bit mimarideyseniz 64 bit; 32 bit eğer 32 bit mimarideyseniz. Tam sayı değişmezlerini Tablo 3-2'de gösterilen herhangi bir biçimde yazabilirsiniz. Birden çok sayısal tür olabilen sayı değişmezlerinin, 57u8 gibi bir tür son ekinin türü belirlemesine izin verdiğini unutmayın. Sayı değişmezleri, 1000 şeklinde belirttiğiniz gibi aynı değere sahip olacak 1_000 gibi sayının okunmasını kolaylaştırmak için görsel ayırıcı olarak _'yi de kullanabilir.

Tablo 3-2: Rust'ta Tamsayı Değişmezlerit

Sayı değişmezleriÖrnek
Ondalıklı98_222
On altılık tabanda0xff
Sekizlik tabanda0o77
İkilik tabanda0b1111_0000
Bit (sadece u8 türü için)b'A'

Peki hangi tamsayı türünü kullanacağınızı nasıl bileceksiniz? Emin değilseniz, Rust'ın varsayılanları genellikle başlamak için iyi yerlerdir: tam sayı türleri varsayılan olarak i32'dir. isize veya usize kullandığınız birincil durum, bir tür koleksiyonu dizine eklerken olur.

Tam sayı taşması

Diyelim ki 0 ile 255 arasında değerler tutabilen u8 tipinde bir değişkeniniz var. Değişkeni bu aralığın dışında, örneğin 256 gibi bir değerle değiştirmeye çalışırsanız, tam sayı taşması meydana gelir ve bu iki davranıştan biriyle sonuçlanabilir. Hata ayıklama modunda derleme yaparken; Rust, bu davranış ortaya çıkarsa programınızın çalışma zamanında panik yapmasına neden olan tam sayı taşması için bazı kontroller içerir. Rust, bir program bir hatayla çıktığında panikleme terimini kullanır; panic!'le Kurtarılamayan Hatalar” bölümünde panikleri daha derinlemesine tartışacağız. --release bayrağıyla yayın modunda derlediğinizde, Rust paniklere neden olan tam sayı taşması denetimlerini içermez. Bunun yerine, taşma meydana gelirse, Rust iki tamamlayıcı sarmayı gerçekleştirir. Kısacası, türün tutabileceği maksimum değerden daha büyük değerler, türün tutabileceği değerlerin minimumuna "sarılır". Bir u8 durumunda, 256 değeri 0 olur, 257 değeri 1 olur ve bu böyle devam eder. Program paniğe kapılmaz, ancak değişken muhtemelen beklediğiniz değerde olmayan bir değere sahip olacaktır. Tam sayı taşmasının sarma davranışına güvenmek bir hata olarak kabul edilir.

Taşma olasılığını açıkça ele almak için, ilkel sayı türleri için standart kütüphane tarafından sağlanan bu metodları kullanabilirsiniz:

  • wrapping_add gibi wrapping_* metodlarını kullanabilirsiniz
  • checked_* yöntemlerinde taşma varsa None değerini döndürebilirsiniz
  • overflowing_* yöntemleriyle taşma olup olmadığını gösteren değeri Boole olarak döndürebilirsiniz
  • saturating_* yöntemleri ile değerin minimum veya maksimum değerlerinde 'doyurun'

Kayan Nokta Türleri

Rust ayrıca ondalık basamaklı sayılar olan kayan noktalı sayılar için iki temel türe sahiptir. Rust'ın kayan nokta türleri, sırasıyla 32 bit ve 64 bit boyutunda olan f32 ve f64'tür. Varsayılan tür f64'tür çünkü modern CPU'larda kabaca f32 ile aynı hızdadır ancak daha fazla hassasiyete sahiptir. Tüm kayan nokta türleri işaretlidir.

İşte kayan noktalı sayılarını çalışırken gösteren bir örnek:

Dosya adı: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Kayan nokta sayıları, IEEE-754 standardına göre tanımlanmıştır. f32 türü tek duyarlıklı bir kayan noktadır ve f64 çift duyarlıklıdır.

Sayısal İşlemler

Rust, tüm sayı türleri için beklediğiniz temel matematiksel işlemleri destekler: toplama, çıkarma, çarpma, bölme ve kalan.

Tam sayı bölümü en yakın tam sayıya yuvarlar.

Aşağıdaki kod, bir let ifadesinde her bir sayısal işlemi nasıl kullanacağınızı gösterir:

Dosya adı: src/main.rs

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let floored = 2 / 3; // Results in 0

    // remainder
    let remainder = 43 % 5;
}

Bu ifadelerdeki her ifade bir matematiksel operatör kullanır ve daha sonra bir değişkene bağlanan tek bir değer olarak değerlendirilir. Ekleme B, Rust'ın sağladığı tüm operatörlerin bir listesini içerir.

Boole Türü

Diğer programlama dillerinin çoğunda olduğu gibi, Rust'ta da bir Boole türünün iki olası değeri vardır: true ve false. Boole'ların boyutu bir bayttır. Rust'taki Boole türü bool kullanılarak belirtilir.

Örneğin:

Dosya adı: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

Boole değerlerini kullanmanın ana yolu, if ifadesi gibi koşullu ifadelerdir. Rust'ta ifadelerin nasıl çalışacağını “Kontrol Akışı” bölümünde ele alacağız.

Karakter Türü

Rust'ın char türü, dilin en temel alfabetik türüdür.

Aşağıda, char değerlerinin bildirilmesine ilişkin bazı örnekler verilmiştir:

Dosya adı: src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

Çift tırnak kullanan dize değişmezlerinin aksine, tek tırnaklı char değişmezlerini belirttiğimize dikkat edin. Rust'ın char türü, dört bayt boyutundadır ve bir Unicode Skaler Değerini temsil eder; bu, yalnızca ASCII'den çok daha fazlasını temsil edebileceği anlamına gelir.

Aksanlı harfler; Çince, Japonca ve Korece karakterler; emoji; ve sıfır genişlikli boşlukların tümü Rust'taki geçerli karakter değerleridir.

Unicode Skaler Değerleri, U+0000 ile U+D7FF ve U+E000 ile U+10FFFF dahil arasında değişir. Bununla birlikte, bir char, Unicode'da gerçekten bir kavram değildir, bu nedenle, bir “karakterin” ne olduğuna ilişkin insan sezginiz, Rust'ta bir karakterin ne olduğu ile eşleşmeyebilir. Bu konuyu Bölüm 8'deki “UTF-8 Kodlu Metinleri Dizgilerde Depolama” bölümünde ayrıntılı olarak tartışacağız.

Bileşik Türler

Bileşik türler birden çok değeri tek bir türde gruplayabilir. Rust'ın iki temel bileşik türü vardır: demetler ve diziler.

Demet Türü

Demet, çeşitli türlere sahip bir dizi değeri tek bir bileşik türde gruplandırmanın genel bir yoludur. Demetlerin sabit bir uzunluğu vardır: bir kez bildirildiğinde, boyut olarak büyüyemez veya küçülemezler.

Parantez içinde virgülle ayrılmış bir değerler listesi yazarak bir demet oluşturuyoruz. Tanımlama grubundaki her konumun bir türü vardır ve tanımlama grubundaki farklı değerlerin türlerinin aynı olması gerekmez. Bu örnekte isteğe bağlı tür ek açıklamaları ekledik:

Dosya adı: src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Değişken tup'a bütün bir demet atanır, çünkü bir demet tek bir bileşik eleman olarak kabul edilir. Bir tanımlama grubundan tek tek değerleri elde etmek için, bir tanımlama grubunu yok etmek için model eşleştirmeyi kullanabiliriz, bunun gibi:

Dosya adı: src/main.rs

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

Bu program önce bir tanımlama grubu oluşturur ve onu tup değişkenine atar. Daha sonra let tup'u alıp onu x, y ve z olmak üzere üç ayrı değişkene dönüştüren bir model kullanır. Buna yıkım denir, çünkü tek demeti üç parçaya böler. Son olarak, program y'nin 6.4 değerini yazdırır.

Ayrıca, bir nokta (.) ve ardından erişmek istediğimiz değerin sırasını (indeksini) kullanarak bir tanımlama grubu öğesine doğrudan erişebiliriz.

Örneğin:

Dosya adı: src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

Bu program, x demetini oluşturur ve ardından ilgili dizinleri kullanarak grubun her bir öğesine erişir. Çoğu programlama dilinde olduğu gibi, bir demet içindeki ilk sıra 0'dır.

Herhangi bir değeri olmayan demetin özel bir adı vardır, birim. Bu değer ve buna karşılık gelen tür yazılır () ve boş bir değeri veya boş bir dönüş türünü temsil eder. İfadeler, başka bir değer döndürmezlerse örtük olarak birim değerini döndürür.

Dizi Türü

Birden çok değerden oluşan bir koleksiyona sahip olmanın başka bir yolu da dizi kullanmaktır. Demetlerden farklı olarak, bir dizinin her elemanı aynı tipte olmalıdır. Diğer bazı dillerdeki dizilerin aksine, Rust'taki dizilerin sabit bir uzunluğu vardır.

Bir dizideki değerleri köşeli parantezler içinde virgülle ayrılmış bir liste olarak yazıyoruz:

Dosya adı: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Diziler, verilerinizin yığın yerine yığına atanmasını istediğinizde (yığın (stack) ve yığıtı (heap) Bölüm 4'te daha fazla tartışacağız) veya her zaman sabit sayıda öğeye sahip olduğunuzdan emin olmak istediğinizde yararlıdır. Yine de bir dizi, vektör türü kadar esnek değildir. Vektör, standart kütüphane tarafından sağlanan ve boyutunun büyümesine veya küçülmesine izin verilen benzer bir koleksiyon türüdür. Dizi mi yoksa vektör mü kullanacağınızdan emin değilseniz, büyük olasılıkla bir vektör kullanmalısınız. Bölüm 8, vektörleri daha ayrıntılı olarak tartışır.

Ancak diziler, eleman sayısının değişmesi gerekmediğini bildiğiniz zaman daha kullanışlıdır. Örneğin, bir programda ayın adlarını kullanıyor olsaydınız, her zaman 12 öğe içereceğini bildiğiniz için vektör yerine muhtemelen bir dizi kullanırdınız:


#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

Her öğenin türü, noktalı virgül ve ardından dizideki öğelerin sayısıyla birlikte köşeli parantezler kullanarak bir dizinin türünü yazarsınız, şöyle:


#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

Burada i32, her bir elemanın tipidir. Noktalı virgülden sonraki 5 sayısı, dizinin beş eleman içerdiğini gösterir.

Ayrıca, burada gösterildiği gibi ilk değeri, ardından noktalı virgül ve ardından dizinin uzunluğunu köşeli parantez içinde belirterek her öğe için aynı değeri içerecek bir dizi tanımlayabilirsiniz:


#![allow(unused)]
fn main() {
let a = [3; 5];
}

a adlı dizi, tümü başlangıçta 3 değerine ayarlanacak 5 öğe içerecektir. Bu, let a = [3, 3, 3, 3, 3]; ile aynıdır fakat daha yalındır.

Dizi Öğelerine Erişim

Dizi, yığına ayrılabilen, bilinen, sabit boyuttaki tek bir bellek yığınıdır. Dizinin öğelerine aşağıdaki gibi dizin oluşturmayı kullanarak erişebilirsiniz:

Dosya adı: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

Bu örnekte, first olarak adlandırılan değişken 1 değerini alacaktır, çünkü bu, dizideki [0] dizinindeki değerdir. second adlı değişken, dizideki [1] dizininden 2 değerini alacaktır.

Geçersiz Dizi Öğesine Erişmek

Dizinin sonunu aşan bir dizinin bir öğesine erişmeye çalışırsanız ne olacağını birlikte görelim. Kullanıcıdan bir dizi sırası almak için Bölüm 2'deki tahmin oyununa benzer şekilde bu kodu çalıştırdığınızı varsayalım:

Dosya adı: src/main.rs

use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

Bu kod başarıyla derlenir. Bu kodu cargo run komutunu kullanarak çalıştırırsanız ve girdi olarak 0, 1, 2, 3 veya 4 girerseniz, program dizideki o sırada karşılık gelen değeri yazdıracaktır. Bunun yerine, 10 gibi; dizinin sonunu aşan bir sayı girerseniz, şöyle bir çıktı görürsünüz:

thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Program, sırasına koyma (indeksleme) işleminde geçersiz bir değer kullanma noktasında bir çalışma zamanı hatasıyla sonuçlandı. Program bir hata mesajıyla çıktı ve son eklenen println'i çalıştırmadı! Sırasına koyma işlemini kullanarak bir elemana erişmeye çalıştığınızda Rust, belirttiğiniz sıranın dizi uzunluğundan küçük olup olmadığını kontrol eder. Sıra, uzunluktan büyük veya ona eşitse, Rust panikleyecektir. Bu kontrol, özellikle bu durumda ve bu kodda, çalışma zamanında yapılmalıdır, çünkü derleyici, bir kullanıcının kodu daha sonra çalıştırdığında hangi değeri gireceğini muhtemelen bilemez.

Bu, Rust'ın bellek güvenliği ilkelerinin uygulamadaki bir örneğidir. Birçok düşük seviyeli dilde bu tür bir kontrol yapılmaz ve yanlış bir sıra verdiğinizde geçersiz hafızaya erişilebilir. Rust, belleğe erişime izin vermek ve devam etmek yerine hemen çıkarak sizi bu tür hatalara karşı korur.

Bölüm 9, Rust'ın hata işlemesini ve panik yaratmayan veya geçersiz bellek erişimine izin vermeyen okunabilir, güvenli kodu nasıl yazabileceğinizi daha fazla tartışıyor olacak.

Fonksiyonlar

Fonksiyonlar Rust kodunda sıklıkla kullanılır. Dildeki en önemli fonksiyonlardan birini zaten gördünüz: birçok programın giriş noktası olan main fonksiyonu. Ayrıca, yeni fonksiyonlar tanımlamanızı sağlayan fn anahtar sözcüğünü de gördünüz.

Rust kodu, tüm harflerin küçük olduğu ve ayrı sözcüklerin altını çizdiği, fonksiyon ve değişken adları için geleneksel stil olan yılan stilini kullanır.

Örnek bir fonksiyon tanımı içeren bir program:

Dosya adı: src/main.rs

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

Rust'ta fn ve ardından bir fonksiyon adı ve bir parantez dizisi girerek bir fonksiyon tanımlarız. Parantezler derleyiciye fonksiyon argüman gövdesinin nerede başlayıp nerede bittiğini söyler.

Tanımladığımız herhangi bir fonksiyonu, adını ve ardından bir parantez dizisini girerek çağırabiliriz. Programda another_function tanımlı olduğundan, main fonksiyonu içinden çağrılabilir. Kaynak kodda ana fonksiyondan sonra another_function'u tanımladığımızı unutmayın; daha önce de tanımlayabilirdik. Rust, fonksiyonlarınızı nerede tanımladığınızla ilgilenmez, yalnızca arayanın görebileceği bir kapsamda bir yerde tanımlanmalarını ister. Kapsam dışı fonksiyonları geleneksel yöntemle çağıramazsınız.

İşlevleri daha fazla keşfetmek için functions adında yeni bir proje başlatalım. other_function örneğini src/main.rs içine atın ve çalıştırın. Aşağıdaki çıktıyı görmelisiniz:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hello, world!
Another function.

Satırlar, ana fonksiyonda göründükleri sırayla yürütülür. İlk olarak, “Hello, world!” ekrana yazdırılır ve ardından another_function fonksiyonu çağrılır ve belirtilen parametrelerle birlikte yürütülür.

Parametreler

Fonksiyonları, bir fonksiyonun yapısının parçası olan özel değişkenler olan parametrelere sahip olacak şekilde tanımlayabiliriz. Bir fonksiyonun parametreleri olduğunda, ona bu parametreler için somut değerler sağlayabilirsiniz. Teknik olarak somut değerlere argümanlar denir, ancak gündelik konuşmalarda insanlar parametre ve argüman kelimelerini bir fonksiyonun tanımındaki değişkenler veya bir fonksiyonu çağırdığınızda iletilen somut değerler için birbirinin yerine kullanma eğilimindedir.

another_function'un bu sürümünde bir parametre ekliyoruz:

Dosya adı: src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

Şimdi bu kodu çalıştırmaya çalışın; şu çıktıyı almalısınız:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

another_function tanımı, x adında bir parametreye sahiptir. x'in tipi i32 olarak belirtilmiştir. 5'i another_function fonksiyonuna parametre olarak verdiğimizde, println! makrosu, x'i içeren parametre listesinde x yerine 5'i koyar.

Fonksiyon yapılarında, her parametrenin türünü belirtmelisiniz. Bu, Rust'ın tasarımında bilinçli olarak verilmiş bir karardır: fonksiyon tanımlarında tip açıklamalarının zorunluluğu, derleyicinin, ne türü istediğinizi anlamak için kodun başka bir yerinde bu tarz yaygın tür tanımlarını kullanmanıza neredeyse hiç ihtiyaç duymadığı anlamına gelir. Derleyici, fonksiyonun ne tür beklediğini biliyorsa, daha yararlı hata mesajları da verebilir.

Birden çok parametre tanımlarken, parametre bildirimlerini aşağıdaki gibi virgülle ayırın:

Dosya adı: src/main.rs

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

Bu örnek, iki parametreli print_labeled_measurement adlı bir fonksiyon oluşturur. İlk parametre value olarak adlandırılmıştır ve türü i32'dir. İkincisi, unit_label olarak adlandırılmıştır ve char türündendir. Fonksiyon hem value hem de unit_label'in değerini içeren metni ekrana yazdırır.

Bu kodu çalıştırmayı deneyelim. functions klasörünüzün src/main.rs dosyasındaki mevcut programı önceki örnekle değiştirin ve cargo run komutunu kullanarak çalıştırın:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
The measurement is: 5h

Fonksiyonu value'nin değeri 5 ve unit_label'in değeri 'h' olacak şekilde çağırdığımız için program çıktısı bu değerleri içerecektir.

İfade Yapıları ve İfadeler

Fonksiyon gövdeleri, isteğe bağlı olarak ifade yapılarıyla biten bir dizi ifadeden oluşur. Şimdiye kadar ele aldığımız fonksiyonlar bir bitiş ifadesi içermemişti, ancak bir ifade yapısının parçası olarak ifadeler görmüştünüz. Rust ifade tabanlı bir dil olduğundan, bu anlaşılması gereken önemli bir ayrımdır. Diğer diller aynı ayrımlara sahip değildir, o halde şimdi ifade yapılarının ve ifadelerin ne olduğuna ve farklılıklarının fonksiyon gövdelerini nasıl etkilediğine bakalım.

İfade yapıları, bazı eylemleri gerçekleştiren ve bir değer döndürmeyen talimatlardır. İfadeler bir sonuç değeri olarak değerlendirilir. Bazı örneklere bakalım.

Aslında zaten ifade yapılarını ve ifadeleri kullandık. let anahtar sözcüğü ile bir değişken oluşturmak ve ona bir değer atamak bir ifade yapısıdır. Liste 3-1'deki let y = 6; bir ifade yapısıdır.

Dosya adı: src/main.rs

fn main() {
    let y = 6;
}

Liste 3-1: Bir ifade yapısı içeren main fonksiyonu tanımı

Fonksiyon tanımları da ayrıca ifade yapıları olarak değerlendirilir. Önceki örneğin tamamı kendi içinde bir ifade yapısıdır.

İfade yapıları herhangi bir değer döndürmez. Statements do not return values. Buna göre, let ifade yapısıyla başka bir değeri halihazırda tanımlanmış bir değerle eşitleyemezsiniz. Bunu denerseniz, şu hatayı alacaksınızdır:

Dosya adı: src/main.rs

fn main() {
    let x = (let y = 6);
}

Bu programı çalıştırırsanız, şuna benzer bir hata alacaksınızdır:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

error[E0658]: `let` expressions in this position are experimental
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information
  = help: you can write `matches!(<expr>, <pattern>)` instead of `let <pattern> = <expr>`

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^         ^
  |
  = note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
  |
2 -     let x = (let y = 6);
2 +     let x = let y = 6;
  | 

For more information about this error, try `rustc --explain E0658`.
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` due to 2 previous errors; 1 warning emitted

let y = 6 ifade yapısı bir değer döndürmez, dolayısıyla x'e atanacak bir şey yoktur. Bu, atamanın; atanan değeri döndürdüğü C ve Ruby gibi diğer dillerden farklıdır. Bu dillerde x = y = 6 yazabilir ve hem x hem de y'nin 6 değerine sahip olmasını sağlayabilirsiniz; ancak Rust'ta durum böyle değil.

İfadeler bir değer olarak değerlendirilir ve Rust'ta yazacağınız kodun geri kalanının çoğunu oluşturur. 11 değerini veren bir ifade olan 5 + 6 gibi bir matematik işlemini düşünün. İfadeler ifade yapılarının bir parçası olabilir: Liste 3-1'de, let y = 6 ifade yapısındaki 6; 6 değerini y'ye veren bir ifadedir. Bir fonksiyonu çağırmak bir ifadedir. Makro çağırmak bir ifadedir. Parantezlerle oluşturulan bir kapsam bloğu bir ifadedir, örneğin:

Dosya adı: src/main.rs

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {y}");
}

Bu ifade:

{
    let x = 3;
    x + 1
}

bu durumda 4 olarak değerlendirilen bir bloktur. Bu değer, let ifade yapısının bir parçası olarak y'ye atanır. Şu ana kadar gördüğünüz çoğu satırın aksine x + 1 satırının sonunda noktalı virgül bulunmadığına dikkat edin. İfadeler en sona noktalı virgül eklenmesini gerektirmez. Bir ifadenin sonuna noktalı virgül eklerseniz, onu bir ifade yapısına dönüştürürsünüz ve o, bir değer döndürmeyecektir. Bir sonraki başlığımız olacak olan fonksiyon dönüş değerlerini ve ifadelerini keşfederken bunu aklınızda bulundurun.

Dönüş Değerleri Olan Fonksiyonlar

Fonksiyonlar, onları çağıran koda değerler döndürebilir. Dönüş değerlerini adlandırmıyoruz, ancak türlerini bir oktan (->) sonra bildirmeliyiz. Rust'ta, fonksiyonun dönüş değeri, bir fonksiyonun gövdesinin bloğundaki son ifadenin değeri ile eş anlamlıdır. return anahtar sözcüğünü kullanarak ve ona bir değer belirterek bir fonksiyondan erken dönebilirsiniz, ancak çoğu fonksiyon son ifadeyi örtük yapıda döndürür. Değer döndüren bir fonksiyon örneği:

Dosya adı: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {x}");
}

five fonksiyonunda hiçbir fonksiyon çağrısı, makro ve hatta let ifade yapısı yoktur; yalnızca 5 sayısı tek başınadır. Bu, Rust'ta tamamen geçerli bir fonksiyondur. Fonksiyon dönüş türünün de -> i32 olarak belirtildiğine dikkat edin.

Bu kodu çalıştırmayı deneyin -hatasız çalışmalıdır-, çıktı şöyle görünmelidir:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

five'ın içindeki 5, fonksiyonun dönüş değeridir, bu nedenle fonksiyonun dönüş türü i32'dir. Bunu daha detaylı inceleyelim. İki önemli yer vardır: birincisi, let x = five(); satırı bir değişkeni başlatmak için bir fonksiyonun dönüş değerini kullandığımızı gösterir. five fonksiyonu 5 döndürdüğünden, bu satır aşağıdakiyle tamamen aynıdır:


#![allow(unused)]
fn main() {
let x = 5;
}

Ayrıca, five fonksiyonunda parametre yoktur ve dönüş değerinin türünü tanımlayan herhangi bir ifade yoktur, ancak işlevin gövdesi, değerini döndürmek istediğimiz bir ifade olduğu için noktalı virgül içermez. Yalnızca 5 yazabiliriz.

Başka bir daha örneğe bakalım:

Dosya adı: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

Bu kodu çalıştırmak bize The value of x is: 6 çıktısını verecektir. Ancak x + 1'i içeren satırın sonuna noktalı virgül koyarsak, onu bir ifadeden bir ifade yapısına çevirmiş oluruz ve bir hata alırız.

Dosya adı: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}

Bu kodun derlenmesi aşağıdaki gibi bir hata üretir:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: consider removing this semicolon

For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` due to previous error

Hata mesajı, “uyumsuz türler“ bu kodla ilgili temel sorunu ortaya koymaktadır. plus_one fonksiyonunun tanımı, bir i32 döndüreceğini söyler, ancak ifade yapısı () ile ifade edilen bir değer olarak değerlendirilmez. İstenilen şey i32 türünde döndürmektir, bir ifade yapısının döndürdüğü gibi ()'i döndürmek değildir. Bu nedenle, fonksiyon tanımıyla çelişen ve bir hatayla sonuçlanan hiçbir şey döndürülmemiş olur. Bu çıktıda Rust, muhtemelen bu sorunun düzeltilmesine yardımcı olacak bir mesaj gösterir: Rust, noktalı virgülün kaldırılmasını önerir, bu da hatayı düzeltir.

Yorum Satırları

Tüm programcılar, kodlarının anlaşılmasını kolaylaştırmak için çaba gösterir, ancak bazen ek açıklamalar gerekir. Bu durumlarda, programcılar kaynak kodlarında derleyicinin görmezden geleceği yorum satırları bırakırlar. Bunları kaynak kodu okuyan insanlar faydalı bulabilir.

İşte basit bir yorum satırı:


#![allow(unused)]
fn main() {
// Merhaba, Dünya!
}

Rust'ta, deyimsel yorum stili bir yoruma iki eğik çizgi ile başlar ve yorum satırın sonuna kadar devam eder. Tek bir satırın ötesine geçen yorumlar için, her satıra // eklemeniz gerekir, örneğin:


#![allow(unused)]
fn main() {
// Yani burada karmaşık şeyler yapıyoruz sanki, bu kadar uzun
// ve satırlarca süren bir yorum satırı ancak karışık
// fonksiyonlar içindir diye düşünmeliyim gibime geliyor
}

Yorum satırları ayrıca kodunuzun sonuna da konabilir:

Dosya adı: src/main.rs

fn main() {
    let lucky_number = 7; // I’m feeling lucky today
}

Ancak, bunların şu biçimde kullanıldığını daha sık göreceksiniz:

Dosya adı: src/main.rs

fn main() {
    // I’m feeling lucky today
    let lucky_number = 7;
}

Rust'ın ayrıca, Bölüm 14'ün "Bir Kasayı Crates.io'da Yayınlamak" bölümünde tartışacağımız belgeleme yorumları gibi başka bir yorum satırı çeşidi daha vardır.

Kontrol Akışı

Bir koşulun doğru olup olmadığına bağlı olarak bazı kodları çalıştırma veya bir koşul doğruyken bazı kodları tekrar tekrar çalıştırma yeteneği, çoğu programlama dili için temel yapı taşıdır. Rust kodunun yürütme akışını kontrol etmenizi sağlayan en yaygın yapılar if ifadeleri ve döngülerdir.

if İfadeleri

Bir if ifadesi, koşullara bağlı olarak kodunuzu dallandırmanıza olanak tanır. Bir koşul sağlarsınız ve ardından “Eğer bu koşul karşılanırsa, bu kod bloğunu çalıştırın. Koşul karşılanmazsa, bu kod bloğunu çalıştırmayın” emrini verirsiniz.

if ifadesini keşfetmek için proje dizininizde branches adında yeni bir proje oluşturun. src/main.rs dosyasına aşağıdakini girin:

Dosya adı: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Tüm if ifadeleri, if anahtar sözcüğüyle başlar ve ardından bir koşul gelir. Bu durumda koşul, number'ın 5'ten küçük bir değere sahip olup olmadığını kontrol eder. Koşul doğruysa yürütülecek kod bloğunu, koşulun hemen ardından süslü parantezler içine yerleştiririz. if ifadelerindeki koşullarla ilişkili kod blokları, tıpkı Bölüm 2'deki “Tahmin ile Gizli Numarayı Karşılaştırma” bölümünde tartıştığımız match ifadelerindeki kollar gibi bazen kol olarak adlandırılır.

İsteğe bağlı olarak, koşulun yanlış olarak değerlendirilmesi durumunda programa yürütülecek alternatif bir kod bloğu vermek için burada yapmayı seçtiğimiz başka bir ifade de ekleyebiliriz. Başka bir ifade sağlamazsanız ve koşul yanlışsa, program if bloğunu atlar ve bir sonraki kod parçasına geçer.

Bu kodu çalıştırmayı deneyin, aşağıdaki çıktıyı görmelisiniz:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was true

Ne olduğunu görmek için numberın değerini koşulu false yapan bir değerle değiştirmeyi deneyelim:

fn main() {
    let number = 7;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Programı tekrar çalıştırın ve çıktıya bakın:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was false

Bu koddaki koşulun bool türünden olması gerektiğini de belirtmekte fayda var. Koşul bool değilse, bir hata alırız. Örneğin, aşağıdaki kodu çalıştırmayı deneyin:

Dosya adı: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

if koşulu bu sefer 3 değerini değerlendiriyor ve Rust buna karşılık bir hata veriyor:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error

Hata, Rust'ın bir bool beklediğini ancak bir tam sayı aldığını gösteriyor. Ruby ve JavaScript gibi dillerin aksine Rust, Boole olmayan türleri otomatik olarak Boole'a dönüştürmeye çalışmaz. Açık olmalısınız ve her zaman koşulun Boole olup olmadığını sağlamalısınız. Örneğin if kod bloğunun yalnızca bir sayı 0'a eşit olmadığında çalışmasını istiyorsak, if ifadesini aşağıdaki gibi değiştirebiliriz:

Dosya adı: src/main.rs

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");
    }
}

Bu kodu çalıştırmak bize şu çıktıyı verecektir: number was something other than zero.

else if ile Birden Çok Koşulun İşlenmesi

Bir else if ifadesini if ve else ile birleştirerek birden çok koşul durumunda kullanabilirsiniz. Örneğin:

Dosya adı: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

Bu programın alabileceği dört olası durum vardır. Çalıştırdıktan sonra aşağıdaki çıktıyı görmelisiniz

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

Bu program yürütüldüğünde, sırayla her bir if ifadesini kontrol eder ve koşulun doğru olduğu ilk gövdeyi yürütür. 6'nın 2'ye bölünebilmesine rağmen, çıktıda number is divisible by 2'yu görmüyor ve else'e rağmen çıktıda number is not divisible by 4, 3, or 2'yu görmüyoruz. Bunun nedeni, Rust'ın bloğu yalnızca ilk gerçek koşul için çalıştırmasıdır ve bir kez doğru koşulu bulduğunda gerisini kontrol bile etmez.

Çok fazla else if ifadesi kullanmak kodunuzu karıştırabilir, bu nedenle birden fazla varsa, kodunuzu yeniden düzenlemek isteyebilirsiniz. Bölüm 6, bu durumlar için match adı verilen güçlü bir Rust dallanma yapısını açıklar.

if'i let'te İfade Yapısı Olarak Kullanmak

if bir ifade olduğu için, Liste 3-2'de olduğu gibi sonucu bir değişkene atamak için let ifadesinin sağ tarafında kullanabiliriz.

Dosya adı: src/main.rs

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}

Liste 3-2: Bir if ifadesinin sonucunu bir değişkene atama

number değişkenine, if ifadesinin sonucuna göre bir değer atanacaktır. Ne olduğunu görmek için bu kodu çalıştırın:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
The value of number is: 5

Kod bloklarının içlerindeki son ifadeyi değerlendirdiğini ve sayıların kendi başlarına da ifadeler olduğunu unutmayın. Bu durumda, if ifadesinin tamamının değeri, hangi kod bloğunun yürütüldüğüne bağlıdır. Bu, if'in her bir kolundan sonuç alma potansiyeline sahip değerlerin aynı tür olması gerektiği anlamına gelir; Liste 3-2'de, hem if kolunun hem de else kolunun sonuçları i32 tam sayı türündendi. Aşağıdaki örnekte olduğu gibi, türler uyumsuzsa bir hata alırız:

Dosya adı: src/main.rs

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("The value of number is: {number}");
}

Bu kodu derlemeye çalıştığımızda bir hata alacağız. if ve else kollarının uyumsuz değer türleri vardır ve Rust, sorunun programda tam olarak nerede bulunacağını bir hata mesajıyla belirtir:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error

if bloğundaki ifade bir tam sayı olarak değerlendirilir ve else bloğundaki ifade bir dizgi olarak değerlendirilir. Bu işe yaramaz çünkü değişkenlerin tek bir türü olması gerekir ve Rust'ın derleme zamanında sayı değişkeninin ne tür olduğunu kesin olarak bilmesi gerekir. Sayının türünü bilmek, derleyicinin sayıyı kullandığımız her yerde türün geçerli olduğunu doğrulamasını sağlar. Sayının türü yalnızca çalışma zamanında belirlenmiş olsaydı Rust bunu yapamazdı; derleyici daha karmaşık olurdu ve herhangi bir değişken için birden çok varsayımsal türü takip etmesi gerekiyorsa kod hakkında daha az garanti verirdi.

Döngülerle Yinelemek

Bir kod bloğunu bir kereden fazla yürütmek genellikle yararlıdır. Bu görev için Rust, döngü gövdesi içindeki kodu sonuna kadar çalıştıracak ve ardından hemen baştan başlayacak birkaç döngü sağlar. Döngüleri denemek için loops adında yeni bir proje yapalım.

Rust'un üç tür döngüsü vardır: loop, while ve for. Her birini deneyelim.

loop ile Kod Yinelemek

loop anahtar sözcüğü, Rust'a bir kod bloğunu sonsuza kadar veya siz açıkça durmasını söyleyene kadar tekrar tekrar yürütmesini söyler.

Örnek olarak, loops dizininizdeki src/main.rs dosyasını şöyle görünecek şekilde değiştirin:

Dosya adı: src/main.rs

fn main() {
    loop {
        println!("again!");
    }
}

Bu programı çalıştırdığımızda, programı manuel olarak durdurana kadar sürekli olarak again! yazdırıldığını göreceğiz. Çoğu uçbirim ctrl-c kısayolunu döngüden çıkabilmek için sunar. Bir şans verin:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

^C sembolü, ctrl-c tuşlarına bastığınız yeri gösterir. Kesme sinyalini aldığınızda kodun döngüde nerede olduğuna bağlı olarak ^C'den sonra döngü durur.

Neyse ki, Rust ayrıca kod kullanarak bir döngüden çıkmanın bir yolunu da sağlar. Programa döngüyü yürütmeyi ne zaman durduracağını söylemek için break anahtar sözcüğünü döngünün içine yerleştirebilirsiniz. Bunu Bölüm 2'deki “Doğru Tahminden Sonra Çıkma” bölümündeki tahmin oyununda, kullanıcı doğru sayıyı tahmin ederek oyunu kazandığında programdan çıkmak için yaptığımızı hatırlayın.

Ayrıca, bir döngüde programa döngünün bu yinelemesinde kalan herhangi bir kodu atlamasını ve bir sonraki yinelemeye geçmesini söyleyen tahmin oyununda continue'ı kullandık.

Döngülerden Değer Döndürmek

loop'un kullanımlarından biri, bir iş parçacığının işini tamamlayıp tamamlamadığını kontrol etmek gibi başarısız olabileceğini bildiğiniz bir işlemi yeniden denemektir. Ayrıca, bu işlemin sonucunu döngüden kodunuzun geri kalanına aktarmanız gerekebilir. Bunu yapmak için, döngüyü durdurmak için kullandığınız break ifadesinden sonra döndürülmesini istediğiniz değeri ekleyebilirsiniz; bu değer, burada gösterildiği gibi kullanabilmeniz için döngüden döndürülecektir:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

Döngüden önce counter adında bir değişken tanımlıyoruz ve onu 0 olarak başlatıyoruz. Ardından döngüden dönen değeri tutacak result adında bir değişken tanımlıyoruz. Döngünün her yinelemesinde counter değişkenine 1 ekliyoruz ve ardından counter'ın 10'a eşit olup olmadığını kontrol ediyoruz. Eşitse counter * 2 değerini break anahtar sözcüğüyle kullanıyoruz. Döngüden sonra noktalı virgül kullanıyoruz. result'a değer atayan ifadeyi bitirmek için; son olarak, 20 olan result değerini yazdırıyoruz.

Birden Çok Döngü Arasındaki Belirsizliği Gidermek için Döngü Etiketleri

Döngüler içinde döngüleriniz varsa, o noktada en içteki döngü için break ve continue ifadeleri uygulanır. İsteğe bağlı olarak bir döngü üzerinde bir döngü etiketi belirleyebilirsiniz ve daha sonra bu anahtar sözcüklerin en içteki döngü yerine etiketli döngüye uygulanacağını belirtmek için break veya continue ile kullanabiliriz. İşte iki iç içe döngü içeren bir örnek:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

Dış döngü 'counting_up etiketine sahiptir ve 0'dan 2'ye kadar sayar. Etiketsiz iç döngü 10'dan 9'a geri sayım yapar. Bir etiket belirtmeyen ilk break yalnızca iç döngüden çıkar. break 'counting_up; ifadesi dış döngüden çıkar:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

while ile Koşullu Döngüler

Bir programın genellikle bir döngü içindeki bir koşulu değerlendirmesi gerekir. Koşul doğru olduğunda döngü çalışır. Koşul doğru olmadığında, program break'i çağırarak döngüyü durdurur. Böyle bir davranışı loop, if, else ve break kombinasyonunu kullanarak uygulamak mümkündür; İsterseniz bunu şimdi bir programda deneyebilirsiniz. Ancak, bu model o kadar yaygındır ki, Rust'ın bunun için while döngüsü adı verilen yerleşik bir dil yapısı vardır. Liste 3-3'te, programı üç kez döngüye almak, her seferinde geri saymak ve ardından döngüden sonra bir mesaj yazdırıp çıkmak için while kullanıyoruz.

Dosya adı: src/main.rs

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

Liste 3-3: Bir koşul doğruyken kodu çalıştırmak için while döngüsü kullanma

Bu yapı, loop, if, else ve break kullandıysanız gerekli olacak birçok iç içe yerleştirmeyi ortadan kaldırır ve daha nettir. Bir koşul doğru olduğunda kod çalışır; aksi takdirde döngüden çıkar.

for ile Bir Koleksiyonda Yineleme Yapmak

Dizi gibi bir koleksiyonun öğeleri üzerinde döngü oluşturmak için while yapısını kullanmayı seçebilirsiniz. Örneğin, Liste 3-4'teki döngü a dizisindeki her öğeyi yazdırır.

Dosya adı: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

Liste 3-4: Bir while döngüsü kullanarak bir koleksiyonun her bir öğesi arasında döngü yapmak

Burada kod, dizideki öğeleri sayar. 0 dizininde başlar ve ardından dizideki son dizine ulaşana kadar döner (yani index < 5 doğru olmayana kadar). Bu kodu çalıştırmak dizideki her öğeyi yazdıracaktır.

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

Beş dizi değerinin tümü beklendiği gibi terminalde görünür. Dizin bir noktada 5 değerine ulaşacak olsa da, diziden altıncı bir değer getirmeye çalışmadan önce döngü kendini yürütmeyi durdurur.

Ancak bu yaklaşım hataya açıktır; index değeri veya test koşulu yanlışsa programın paniğe kapılmasına neden olabiliriz. Örneğin, a dizisinin tanımını dört öğeye sahip olacak şekilde değiştirdiyseniz ancak index < 4 iken koşulu güncellemeyi unuttuysanız, kod panikleyecektir. Ayrıca bu yavaştır, çünkü derleyici döngü boyunca her yinelemede dizinin dizinin sınırları içinde olup olmadığının koşullu kontrolünü gerçekleştirmek için çalışma zamanı kodu ekler.

Daha özlü bir alternatif olarak, bir for döngüsü kullanabilir ve bir koleksiyondaki her öğe için bir miktar kod çalıştırabilirsiniz. for döngüsü, Liste 3-5'teki koda benzer.

Dosya adı: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

Liste 3-5: Bir for döngüsü kullanarak bir koleksiyonun her bir öğesi arasında yinelemek

Bu kodu çalıştırdığımızda, Liste 3-4'teki çıktının aynısını göreceğiz. Daha da önemlisi, artık kodun güvenliğini artırdık ve dizinin sonunun ötesine geçmek veya yeterince uzağa gitmemek ve bazı öğeleri kaçırmaktan kaynaklanabilecek hata olasılığını ortadan kaldırdık.

for döngüsünü kullanarak, Liste 3-4'te kullanılan yöntemde olduğu gibi dizideki değerlerin sayısını değiştirdiyseniz, başka herhangi bir kodu değiştirmeniz gerekmez.

for döngülerinin güvenliği ve kısa olması, onları Rust'ta en yaygın kullanılan döngü yapısı haline getirir. Liste 3-3'te while döngüsü kullanan geri sayım örneğinde olduğu gibi, bazı kodları belirli sayıda çalıştırmak istediğiniz durumlarda bile, çoğu Rustacean bir for döngüsü kullanır. Bunu yapmanın yolu, bir sayıdan başlayıp diğer bir sayıdan önce biten tüm sayıları sırayla üreten standart kütüphane tarafından sağlanan Range tanımını kullanmaktır.

Aralığı tersine çevirmek için bir for döngüsü ve henüz bahsetmediğimiz başka bir yöntem olan rev kullanarak geri sayım işleminin nasıl görüneceği aşağıda açıklanmıştır:

Dosya adı: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

Bu kod sizce de daha hoş durmuyor mu?

Özet

Başardın! Bu oldukça büyük bir bölümdü: değişkenler, skaler ve bileşik veri türleri, fonksiyonlar, yorumlar, if ifadeleri ve döngüler hakkında çokça bilgi edindiniz! Bu bölümde tartışılan kavramlarla pratik yapmak için aşağıdaki programları oluşturmaya çalışın:

  • Fahrenheit ve Santigrat türleri arasında dönüşüm yapan programı yazın.
  • n'inci Fibonaccı sayısını oluşturan programı yazın.
  • Şarkıdaki tekrarlardan yararlanarak Noel şarkısı “The Twelve Days of Christmas”'ın sözlerini yazdırın.

Devam etmeye hazır olduğunuzda, Rust'ta diğer programlama dillerinde olmayan bir kavram olan sahiplikten bahsedeceğiz.

Sahipliği Anlamak

Sahiplik, Rust'ın en benzersiz özelliğidir ve dilin geri kalanı için derin etkileri vardır. Bu benzersiz özellik, Rust'ın bir çöp toplayıcıya ihtiyaç duymadan bellek güvenliği garantisi vermesini sağlar, bu nedenle sahipliğin nasıl çalıştığını anlamak önemlidir. Bu bölümde, sahiplik ve ilgili birkaç özellik hakkında konuşacağız: ödünç alma, dilimler ve Rust'ın verileri belleğe nasıl yerleştirdiği.

Sahiplik Nedir?

Sahiplik, bir Rust programının belleği nasıl yönettiğini yöneten bir dizi kuraldır. Tüm programlar, çalışırken bilgisayarın belleğini kullanma şeklini yönetmek zorundadır. Bazı dillerde, program çalışırken düzenli olarak artık kullanılmayan belleği arayan çöp toplama vardır; diğer dillerde, programcı belleği açıkça tahsis etmeli ve serbest bırakmalıdır. Rust üçüncü bir yaklaşım kullanır: bellek, derleyicinin kontrol ettiği bir dizi kurala sahip bir sahiplik sistemi aracılığıyla yönetilir. Kurallardan herhangi biri ihlal edilirse program derlenmeyecektir. Sahiplik özelliklerinin hiçbiri, çalışırken programınızı yavaşlatmaz.

Sahiplik birçok programcı için yeni bir kavram olduğu için alışması biraz zaman alıyor. İyi haber şu ki, Rust ve sahiplik sisteminin kuralları konusunda ne kadar deneyimli olursanız, doğal olarak güvenli ve verimli kod geliştirmeyi o kadar kolay bulacaksınız. Öğrenmeye devam edin!

Sahipliği anladığınızda, Rust'ı benzersiz kılan özellikleri anlamak için sağlam bir temele sahip olacaksınız. Bu bölümde, çok yaygın bir veri yapısına odaklanan bazı örnekler üzerinde çalışarak sahipliği öğreneceksiniz (bkz. dizgiler).

Yığıt ve Yığın

Çoğu programlama dili, yığıt ve yığın hakkında çok sık düşünmenizi gerektirecek özelliklere sahip değildir. Ancak Rust gibi bir sistem programlama dilinde, bir değerin yığıtta mı yoksa yığında mı olması dilin nasıl davrandığını ve neden belirli kararlar vermeniz gerektiğini etkiler. Sahiplik, bu bölümün ilerleyen kısımlarında yığıt ve yığınla ilgili olarak açıklanacaktır, bu nedenle burada hazırlık aşamasında olan kısa bir açıklama bulunmaktadır. Hem yığıt hem de yığın, çalışma zamanında kodunuzun kullanabileceği bellek parçalarıdır, ancak bunlar farklı şekillerde yapılandırılmıştır. Yığıt, değerleri aldığı sırayla saklar ve değerleri ters sırada tutar. Buna son giren ilk çıkar (LIFO) denir. Bir tabak yığıtını düşünün: daha fazla tabak eklediğinizde, onları yığıtın üstüne koyarsınız ve bir tabağa ihtiyacınız olduğunda üstten bir tane alırsınız. Ortadan veya alttan tabak eklemek veya çıkarmak da işe yaramaz! Veri eklemeye yığıta itme denir ve veri kaldırmaya yığıttan çıkarma denir. Yığıtta depolanan tüm verilerin bilinen, sabit bir boyutu olmalıdır. Derleme zamanında bilinmeyen bir boyuta veya değişebilecek bir boyuta sahip veriler, yığın (heap) üzerinde depolanmalıdır.

Yığın daha az organizedir: yığına veri koyduğunuzda, belirli bir miktar alan talep edersiniz. Bellek ayırıcı, yığında yeterince büyük boş bir nokta bulur, onu kullanımda olarak işaretler ve o konumun adresi olan bir işaretçi döndürür. Bu işleme yığın üzerinde ayırma denir ve bazen sadece ayırma olarak kısaltılır (değerleri yığına itmek ayırma olarak kabul edilmez). Yığın işaretçisi bilinen, sabit bir boyut olduğundan, işaretçiyi yığında saklayabilirsiniz, ancak gerçek verileri istediğinizde işaretçiyi izlemelisiniz. Bir restoranda oturduğunuzu düşünün. Girdiğinizde grubunuzdaki kişi sayısını belirtiyorsunuz ve görevliler herkese uygun boş bir masa bulup sizi oraya yönlendiriyor. Grubunuzdan biri geç gelirse, sizi bulmak için nerede oturduğunuzu sorabilir. Yığıta itme, yığında tahsis etmekten daha hızlıdır, çünkü ayırıcı hiçbir zaman yeni verileri depolamak için bir yer aramak zorunda kalmaz; bu konum her zaman yığıtın en üstündedir. Nispeten, yığın üzerinde alan tahsis etmek daha fazla iş gerektirir, çünkü tahsis edenin önce verileri tutacak kadar büyük bir alan bulması ve ardından bir sonraki tahsise hazırlanmak için defter tutma yapması gerekir. Yığındaki verilere erişmek, oraya ulaşmak için bir işaretçiyi izlemeniz gerektiğinden yığıttaki verilere erişmekten daha yavaştır. Çağdaş işlemciler, bellekte daha az zıplarlarsa daha hızlı olarak kabul edilir. Analojiye devam edersek, bir restoranda birçok masadan sipariş alan bir sunucu düşünün. Bir sonraki masaya geçmeden önce tüm siparişleri bir masada toplamak en verimli yöntemdir. A masasından bir sipariş, sonra B masasından, sonra tekrar A'dan ve sonra tekrar B'den bir sipariş almak çok daha yavaş bir süreç olacaktır. Aynı şekilde, bir işlemci daha uzakta (yığın üzerinde olabileceği gibi) yerine diğer verilere yakın olan (yığıt üzerinde olduğu gibi) veriler üzerinde çalışıyorsa işini daha iyi yapabilir.

Kodunuz bir fonksiyonu çağırdığında, fonksiyona iletilen değerler (potansiyel olarak yığın üzerindeki verilere işaretçiler de dahil) ve işlevin yerel değişkenleri yığıta aktarılır. Fonksiyon bittiğinde, bu değerler yığıttan atılır. Kodun hangi bölümlerinin yığındaki hangi verileri kullandığını takip etmek, yığındaki yinelenen veri miktarını en aza indirmek ve alanınız bitmemek için yığındaki kullanılmayan verileri temizlemek, sahipliğin ele aldığı sorunlardır. Sahipliği anladıktan sonra, yığıt ve yığın hakkında çok sık düşünmenize gerek kalmayacak, ancak sahipliğin asıl amacının yığın verilerini yönetmek olduğunu bilmek, neden böyle çalıştığını açıklamaya yardımcı olabilir.

Sahiplik Kuralları

İlk olarak, sahiplik kurallarına bir göz atalım. Bunları gösteren örnekler üzerinde çalışırken bu kuralları aklınızda bulundurun:

  • Rust'ta her değerin bir sahibi vardır.
  • Aynı zamanda sadece bir sahip olabilir.
  • Sahip kapsam dışına çıktığında değer düşürülür.

Değişken Kapsamı

Artık temel Rust söz dizimini geride bıraktığımıza göre, örneklere fn main() { kodunu dahil etmeyeceğiz, bu nedenle takip ediyorsanız, aşağıdaki örnekleri main fonksiyonunun içine koyduğunuzdan emin olun. Sonuç olarak, örneklerimiz biraz daha kısa olacak ve ortak kod yerine gerçek ayrıntılara odaklanmamıza izin verecek.

İlk sahiplik örneği olarak, bazı değişkenlerin kapsamına bakacağız. Kapsam, bir öğenin geçerli olduğu bir program içindeki aralıktır. Aşağıdaki değişkeni bakın:


#![allow(unused)]
fn main() {
let s = "hello";
}

s değişkeni, dizgi değerinin programımızın metnine sabit kodlanmış olduğu bir dizgi değişmezi anlamına gelir. Değişken, bildirildiği noktadan geçerli kapsamın sonuna kadar geçerlidir. Liste 4-1, s değişkeninin nerede geçerli olacağını açıklayan açıklamalar içeren bir programı gösterir.

fn main() {
    {                      // s is not valid here, it’s not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s
    }                      // this scope is now over, and s is no longer valid
}

Liste 4-1: Bir değişken ve geçerli olduğu kapsam

Başka bir deyişle, burada iki önemli nokta vardır:

  • s'in kapsama girmesi geçerli bir durumdur.
  • kapsam dışına çıkana kadar geçerliliğini korur.

Bu noktada, kapsamlar ve değişkenlerin ne zaman geçerli olduğu arasındaki ilişki diğer programlama dillerindekine benzer. Şimdi String türünü tanıtarak bu anlayışın üzerine inşa edeceğiz.

String Türü

Sahiplik kurallarını göstermek için, Bölüm 3'ün “Veri Türleri” bölümünde ele aldıklarımızdan daha karmaşık bir veri tipine ihtiyacımız var. Yığıt, kapsamı sona erdiğinde ve kodun başka bir bölümünün aynı değeri farklı bir kapsamda kullanması gerekiyorsa, yeni, bağımsız bir örnek oluşturmak için hızlı ve önemsiz bir şekilde kopyalanabilir. Ancak yığında depolanan verilere bakmak ve Rust'ın bu verileri ne zaman temizleyeceğini nasıl bildiğini keşfetmek istiyoruz ve String türü bu durum için harika bir örnek.

String'in sahiplikle ilgili kısımlarına odaklanacağız. Bu yönler, standart küyüphane tarafından sağlanmış veya sizin tarafınızdan oluşturulmuş olsun (ki diğer karmaşık veri türleri için de geçerlidir). String'i Bölüm 8'de daha derinlemesine tartışacağız.

Bir dizgi değerinin programımıza sabit kodlanmış olduğu dizgi değişmezlerini zaten gördük. Dizgi değişmezleri uygundur, ancak metin kullanmak isteyebileceğimiz her durum için uygun değildirler. Bunun bir nedeni, değişmez olmalarıdır. Bir diğeri ise, kodumuzu yazarken her dizgi değeri bilinemez: örneğin, kullanıcı girdisini alıp depolamak istersek ne olur? Bu durumlar için Rust'ın ikinci bir dizgi türü vardır, String. Bu tür, yığına ayrılan verileri yönetir ve bu nedenle derleme zamanında bizim için bilinmeyen bir miktarda metin depolayabilir. from fonksiyonunu kullanarak bir dizgi değişmezinden bir String oluşturabilirsiniz, şöyle:


#![allow(unused)]
fn main() {
let s = String::from("hello");
}

Çift iki nokta üst üste :: operatörü, string_from gibi bir tür isim kullanmak yerine, bu özel fonksiyondan String tipi altında isim-alanına izin verir. Bu söz dizimini Bölüm 5'in “Metod Söz Dizimi” bölümünde ve Bölüm 7'deki “Modül Ağacındaki Bir Öğeye Başvurma Yolları” daha fazla tartışacağız.

Bu tür bir dizgi de değiştirilebilir:

fn main() {
    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str() appends a literal to a String

    println!("{}", s); // This will print `hello, world!`
}

Peki, buradaki fark nedir? Neden String değiştirilebilir, ancak değişmezler (adı üstünde) değişemezler? Aradaki fark, bu iki türün bellekle nasıl başa çıktığıdır.

Bellek ve Tahsis

Bir dizgi değişmezi durumunda, içeriğini derleme zamanında biliyoruz, bu nedenle metin doğrudan son yürütülebilir dosyaya sabit kodlanmıştır. Bu nedenle dize değişmezleri hızlı ve verimlidir. Ancak bu özellikler yalnızca dize değişmezinin değişmezliğinden gelir. Ne yazık ki, derleme zamanında boyutu bilinmeyen ve programı çalıştırırken boyutu değişebilecek her metin parçası için yürütülebilir ikili dosyaya bir bellek bloğu koyamıyoruz.

String türüyle; değişebilir, büyütülebilir bir metin parçasını desteklemek ve içeriğini tutmak için yığın üzerinde derleme zamanında bilinmeyen bir miktar bellek ayırmamız gerekir.

Bu, şu anlama gelir:

  • Bellek, çalışma zamanında bellek ayırıcıdan talep edilmelidir.
  • String ile işimiz bittiğinde bu hafızayı ayırıcıya geri döndürmenin bir yoluna ihtiyacımız vardır.

Bu ilk kısım bizim tarafımızdan yapılır: String::from'u çağırdığımızda, süreklemesi ihtiyaç duyduğu hafızayı ister. Bu, programlama dillerinde oldukça evrenseldir.

Ancak ikinci kısım farklıdır. Çöp toplayıcı (GC) olan dillerde, GC artık kullanılmayan belleği izler ve temizler. Bellek tahsisi hakkında düşünmemize gerek yoktur. GC olmayan çoğu dilde, belleğin artık kullanılmadığını belirlemek ve tıpkı bizim talep ettiğimiz gibi, belleği açıkça boşaltmak için kodu çağırmak bizim sorumluluğumuzdadır. Bunu doğru yapmak, tarihsel olarak zor bir programlama problemi olmuştur. Unutursak, hafızayı boşa harcarız. Çok erken yaparsak geçersiz bir değişkenimiz olur. İki kez yaparsak, bu da bir hatadır. Tam olarak bir tahsisi tam olarak bir geri verme ile eşleştirmemiz gerekiyor.

Rust farklı bir yol izler: sahip olduğu değişken kapsam dışına çıktığında bellek otomatik olarak döndürülür. Aşağıda, bir dizgi değişmezi yerine bir String kullanarak Liste 4-1'deki kapsam örneğimizin farklı bir sürümü verilmiştir:

fn main() {
    {
        let s = String::from("hello"); // s is valid from this point forward

        // do stuff with s
    }                                  // this scope is now over, and s is no
                                       // longer valid
}

String'imizin ihtiyaç duyduğu belleği ayırıcıya döndürebileceğimiz doğal bir nokta vardır: s'in kapsam dışına çıkması. Bir değişken kapsam dışına çıktığında Rust bizim için özel bir fonksiyon çağırır. Bu fonksiyona drop denir ve String yazarının belleği geri döndürmek için kodu koyabileceği yerdir. Rust çağrıları, kapanış parantezinde otomatik olarak drop fonksiyonunu çağırır.

Not: C++'ta, bir öğenin kullanım süresinin sonunda kaynakları serbest bırakma modeline Resource Acquisition Is Initialization (RAII) adı verilir. RAII kalıplarını kullandıysanız, Rust'taki drop fonksiyonu size tanıdık gelecektir.

Bu model, Rust kodunun yazılma şekli üzerinde derin bir etkiye sahiptir. Şu anda basit görünebilir, ancak yığında tahsis ettiğimiz verileri birden çok değişkenin kullanmasını istediğimizde, daha karmaşık durumlarda kodun davranışı beklenmedik olabilir. Şimdi bu durumlardan bazılarını inceleyelim.

Değişkenlerin ve Veri Etkileşiminin Yolları: Hareket Ettirme

Birden çok değişken, Rust'ta aynı verilerle farklı şekillerde etkileşime girebilir. Liste 4-2'de, tam sayı kullanan bir örneğe bakalım.

fn main() {
    let x = 5;
    let y = x;
}

Liste 4-2: x değişkeninin tam sayı değerini y'ye atama

Muhtemelen bunun ne yaptığını tahmin edebiliriz: “5'i x'e ata; sonra x'deki değerin bir kopyasını al ve onu y'ye ata.". Yani iki değişkenimiz olmuş oluyor, x ve y'nin her ikisi de 5'e eşittir. Bu gerçekten olan şeydir, çünkü tam sayılar bilinen, sabit bir boyuta sahip basit değerlerdir ve bu iki 5 değeri yığına itilir.

Hadi String versiyonuna bakalım:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
}

Bu çok benzer duruyor, bu yüzden çalışma şeklinin aynı olacağını varsayabiliriz: yani ikinci satır s1'deki değerin bir kopyasını alır ve onu s2'ye atar. Ama bu tam olarak ne olduğunu açıklamıyor.

Arka kapılar ardında String'e ne olduğunu görmek için Şekil 4-1'e bakın. Bir String, solda gösterilen üç bölümden oluşur: dizginin içeriğini, uzunluğunu ve kapasitesini tutan belleğe yönelik bir işaretçi. Bu veri grubu yığıtta depolanır. Sağda, içeriği tutan yığın üzerindeki bellek bulunur.

Bellekteki String

Şekil 4-1: s1'e atanmış "hello" değerini tutan bir String'in bellekteki temsili

Uzunluk, String içeriğinin bayt cinsinden ne kadar bellek kullandığıdır. Kapasite, String'in ayırıcıdan aldığı bayt cinsinden toplam bellek miktarıdır. Uzunluk ve kapasite arasındaki fark önemlidir, ancak bu bağlamda değil, bu nedenle şimdilik kapasiteyi göz ardı etmekte fayda var.

s1'i s2'ye atadığımızda, String verileri kopyalanır, yani yığıttaki işaretçiyi, uzunluğu ve kapasiteyi kopyalarız. İşaretçinin başvurduğu yığın üzerindeki verileri kopyalamayız. Başka bir deyişle, bellekteki veri gösterimi Şekil 4-2'ye benzer.

s1 ve s2 aynı değere işaret ediyor

Şekil 4-2: İşaretçinin, uzunluğun ve s1 kapasitesinin bir kopyasına sahip olan s2 değişkeninin bellekteki temsili

Temsil, Şekil 4-3'e benzemiyor; bu, Rust'un yığın verilerini de kopyalaması durumunda belleğin nasıl görüneceğini gösterir. Bunu Rust yapsaydı, yığın üzerindeki veriler de büyük olsaydı s2 = s1 işlemi çalışma zamanı performansı açısından çok pahalı olabilirdi.

s1 ve s2 iki farklı yeri işaret ediyor

Şekil 4-3: Rust yığın verilerini de kopyalasaydı s2 = s1'in neler yapabileceğine dair başka bir olasılık

Daha önce, bir değişken kapsam dışına çıktığında Rust'ın otomatik olarak drop fonksiyonunu çağırdığını ve o değişken için yığın belleğini temizlediğini söylemiştik. Ancak Şekil 4-2, aynı konuma işaret eden her iki veri işaretçisini de göstermektedir. Bu bir sorundur: s2 ve s1 kapsam dışına çıktığında, ikisi de aynı belleği boşaltmaya çalışacaklardır. Bu, çifte serbest bırakma hatası olarak bilinir ve daha önce bahsettiğimiz bellek güvenlik hatalarından biridir. Belleği iki kez boşaltmak bellek bozulmasına neden olabilir ve bu da potansiyel olarak güvenlik açıklarına yol açabilir.

Bellek güvenliğini sağlamak için, let s2 = s1 satırından sonra Rust, s1'in artık geçerli olmadığını düşünür. Bu nedenle, s1 kapsam dışına çıktığında Rust'ın hiçbir şeyi serbest bırakmasına gerek yoktur. s2 oluşturulduktan sonra s1'i kullanmaya çalıştığınızda ne olduğuna bakın; çalışmayacaktır:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}, world!", s1);
}

Rust, geçersiz kılınan referansı kullanmanızı engellediği için şöyle bir hata alırsınız:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 | 
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error

Diğer dillerle çalışırken sığ kopyalama ve derin kopyalama terimlerini duymuşsanız, verileri kopyalamadan işaretçiyi, uzunluğu ve kapasiteyi kopyalama kavramı muhtemelen sığ bir kopya oluşturmaya benziyor. Ancak Rust aynı zamanda ilk değişkeni de geçersiz kıldığı için, onu sığ bir kopya olarak adlandırmak yerine hareket olarak adlandırırız. Bu örnekte, s1'in s2'ye hareket ettiğini söyleyebiliriz. Yani gerçekte ne olduğu Şekil 4-4'te gösterilmektedir.

s1, s2'ye hareket ettirildi

Şekil 4-4: s1 geçersiz kılındıktan sonra hafızadaki temsili

Bu bizim sorunumuzu çözüyor! Yalnızca s2 geçerli olduğunda, kapsam dışına çıktığında tek başına belleği boşaltır ve yapmamız gereken bir ley kalmaz.

Ek olarak, bununla ima edilen bir tasarım seçeneği vardır: Rust, verilerinizin “derin” kopyalarını asla otomatik olarak oluşturmaz. Bu nedenle, herhangi bir otomatik kopyalamanın çalışma zamanı performansı açısından ucuz olduğu varsayılabilir.

Değişkenlerin ve Veri Etkileşiminin Yolları: Klonlama

Yalnızca yığıt verilerini değil, String'in yığın verilerini de derinlemesine kopyalamak istiyorsak, clone adı verilen ortak metodu kullanabiliriz. Metod söz dizimini Bölüm 5'te tartışacağız, ancak metodlar birçok programlama dilinde ortak bir özellik olduğundan, muhtemelen onları daha önce görmüşsünüzdür.

İşte çalışma halindeki clone metodunun bir örneği:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {}, s2 = {}", s1, s2);
}

Bu gayet iyi çalışır ve yığın verilerinin kopyalandığı Şekil 4-3'te gösterilen davranışı açıkça üretir.

Bir clone çağrısı gördüğünüzde, bazı rastgele kodların yürütüldüğünü ve bu kodun pahalı olabileceğini bilirsiniz. Bu, farklı bir şeyin olup bittiğinin bir göstergesidir.

Yalnızca Yığıt Kullanan Veriler: Kopyalama

Henüz bahsetmediğimiz başka bir olay daha var. Bir kısmı Liste 4-2'de gösterilen tam sayıları kullanan bu kod çalışır ve geçerlidir:

fn main() {
    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);
}

Ancak bu kod, az önce öğrendiklerimizle çelişiyor gibi görünüyor: clone çağrımız yok, ancak x hala geçerli ve y'ye hareket ettirilmedi.

Bunun nedeni, derleme zamanında boyutu bilinen tam sayılar gibi türlerin tamamen yığıtta saklanmasıdır, bu nedenle gerçek değerlerin kopyaları hızlı bir şekilde oluşturulur. Bu, y değişkenini oluşturduktan sonra x'in geçerli olmasını engellemek istememiz için hiçbir neden olmadığı anlamına gelir. Başka bir deyişle, burada derin ve sığ kopyalama arasında bir fark yoktur, bu nedenle clone çağırmak normal sığ kopyalamadan farklı bir şey yapmaz ve bunu dışarıda bırakabiliriz.

Rust'ın, tam sayılar gibi yığıtta depolanan türlere yerleştirebileceğimiz Copy tanımı adı verilen özel bir açıklaması vardır (tanımlar hakkında Bölüm 10'da daha fazla konuşacağız). Bir tür, Copy özelliğini süreklerse (uygularsa), onu kullanan değişkenler hareket etmez, bunun yerine önemsiz bir şekilde kopyalanır, bu da onları başka bir değişkene atandıktan sonra hala geçerli hale getirir.

Rust, tür veya türün herhangi bir parçası Drop tanımını süreklemişse, bir türe Copy ile açıklama eklememize izin vermez. Değer kapsam dışına çıktığında türün özel bir şeye ihtiyacı varsa ve bu türe Copy ek açıklamasını eklersek, bir derleme zamanı hatası alırız. Niteliği uygulamak için Copy ek açıklamasını türünüze nasıl ekleyeceğinizi öğrenmek için Ek C'deki “Türetilebilir Tanımlar” başlığına bakın.

Peki, Copy tanımını hangi türler sürekler? Emin olmak için verilen türün dokümantasyonunu kontrol edebilirsiniz, ancak genel bir kural olarak, herhangi bir basit skaler değer grubu Copy'i sürekleyebilir. Copy'i sürekleyen türlerden bazıları şunlardır:

  • Tüm tam sayı türleri, meselax u32.
  • Boole türü, bool, true ve false.
  • Tüm kayan nokta türleri, mesela f64.
  • Karakter türü, char.
  • Demetler, eğer tutulan tür de Copy'i süreklemiş ise. Mesela, (i32, i32), Copyi sürekler fakat (i32, String) süreklemez.

Sahiplik ve Fonksiyonlar

Bir fonksiyona değer aktarmanın mekanikleri, bir değişkene değer atamaya benzer. Bir fonksiyona değişken iletmek, tıpkı atamada olduğu gibi taşınır veya kopyalanır. Liste 4-3, değişkenlerin nerede kapsam içine girip nerede kapsam dışında kaldığını gösteren bazı açıklamalar içeren bir örneğe sahiptir.

Dosya adı: src/main.rs

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it's okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

Liste 4-3: Sahiplik ve kapsam açıklamalı fonksiyonlar

takes_ownership çağrısından sonra s'i kullanmaya çalışırsak, Rust bir derleme zamanı hatası verir. Bu tarz statik kontroller bizi hatalardan korur. Bunları nerede kullanabileceğinizi ve sahiplik kurallarının bunu yapmanızı nerede engellediğini görmek için s ve x kullanan kodu main'e eklemeyi deneyin.

Dönüş Değerleri ve Kapsam

Dönen değerler de sahipliği aktarabilir. Liste 4-4, Liste 4-3'tekilere benzer açıklamalarla bir değer döndüren bir fonksiyon örneğini gösterir.

Dosya adı: src/main.rs

fn main() {
    let s1 = gives_ownership();         // gives_ownership moves its return
                                        // value into s1

    let s2 = String::from("hello");     // s2 comes into scope

    let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                        // takes_and_gives_back, which also
                                        // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {             // gives_ownership will move its
                                             // return value into the function
                                             // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                              // some_string is returned and
                                             // moves out to the calling
                                             // function
}

// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                      // scope

    a_string  // a_string is returned and moves out to the calling function
}

Liste 4-4: Dönüş değerlerinin sahipliğini aktarma

Bir değişkenin sahipliği her seferinde aynı kalıbı takip eder: başka bir değişkene bir değer atamak onu hareket ettirir. Yığın üzerindeki verileri içeren bir değişken kapsam dışına çıktığında, verilerin sahipliği başka bir değişkene taşınmadıkça değer drop ile temizlenir.

Bu işe yararken, sahiplik almak ve ardından her fonksiyonla birlikte sahipliğini iade etmek biraz sıkıcıdır. Ya bir fonksiyonun bir değer kullanmasına izin vermek istiyorsak ancak sahipliğini almak istemiyorsak? Döndürmek isteyebileceğimiz fonksiyonun gövdesinden kaynaklanan herhangi bir veriye ek olarak, tekrar kullanmak istiyorsak ilettiğimiz herhangi bir şeyin de geri iletilmesinin gerekmesi oldukça can sıkıcıdır.

Rust, Liste 4-5'te gösterildiği gibi, bir demet kullanarak birden çok değer döndürmemize izin verir.

Dosya adı: src/main.rs

fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String

    (s, length)
}

Liste 4-5: Parametrelerin sahipliğini geri döndürme

Ancak bu, yaygın olması gereken bir konsept için çok fazla başımıza iş açıyor. Şansımıza Rust, referans adı verilen, sahipliği devretmeden bir değeri kullanma özelliğine sahip.

Referanslar ve Ödünç Alma

Liste 4-5'teki tanımlama grubu koduyla ilgili sorun, String'i çağıran fonksiyona döndürmemizi gerektirmesidir, böylece String, calculate_length çağrısından sonra hala String'i kullanabiliriz, çünkü String, içine taşınmıştır. Bunun yerine, String değerine bir referans sağlayabiliriz. Referans, bir işaretçi gibidir, çünkü o adreste depolanan verilere erişmek için takip edebileceğimiz bir adrestir; bu veriler başka bir değişkene aittir. Bir işaretçiden farklı olarak, bir referansın, o referansın ömrü boyunca belirli bir türün geçerli bir değerine işaret etmesi garanti edilir. Değerin sahipliğini almak yerine parametre olarak bir nesneye referansı olan bir calculate_length fonksiyonunu nasıl tanımlayacağınız ve kullanacağınız aşağıda açıklanmıştır:

Değerin sahipliğini almak yerine parametre olarak bir nesneye referansı olan bir calculate_length fonksiyonunu nasıl tanımlayacağınız ve kullanacağınız aşağıda açıklanmıştır:

Dosya adı: src/main.rs

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

İlk olarak, değişken bildirimindeki tüm tanımlama grubu kodunun ve fonksiyonun dönüş değerinin kaybolduğuna dikkat edin. İkinci olarak, &s1'i calculate_length'e ilettiğimizi ve tanımında String yerine &String'i kullandığımızı unutmayın. Bu ve bunun işaretleri, referansları temsil eder ve sahipliğini almadan bazı değerlere başvurmanıza izin verir. Şekil 4-5 bu konsepti tasvir ediyor.

&String s String s1'e işaret ediyor

Şekil 4-5: String s1'i gösteren &String s diyagramı

Not: & kullanılarak yapılan referansın tersi, referanstan çıkarma operatörü * ile gerçekleştirilen referanstan çıkarma'dır. Bölüm 8'de referans kaldırma operatörünün bazı kullanımlarını göreceğiz ve Bölüm 15'te referans kaldırmanın ayrıntılarını tartışacağız.

Fonksiyon çağrısına daha yakından bakalım:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

&s1 söz dizimi, s1 değerine refere eden ancak kendisine ait olmayan bir referans oluşturmamıza izin verir. Kendisine ait olmadığı için, referansın kullanımı durduğunda işaret ettiği değer düşürülmeyecektir. Benzer şekilde, fonksiyonun imzası, s parametresinin türünün bir referans olduğunu belirtmek için & kullanır.

Bazı açıklayıcı notlar ekleyelim:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, it is not dropped.

s değişkeninin geçerli olduğu kapsam, herhangi bir fonksiyonla aynıdır. Sahipliği olmadığı için s kullanımı durduğunda parametrenin kapsamı bırakılır. Ancak referansın gösterdiği değer bırakılmaz. Sahipliği geri vermek için değerleri döndürmelisiniz, çünkü hiç sahibi olmadınız.

Peki ödünç aldığımız bir şeyi değiştirmeye çalışırsak ne olur? Liste 4-6'daki kodu deneyin. Uyarı: çalışmıyor!

Dosya adı: src/main.rs

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

Liste 4-6: Ödünç alınan bir değeri değiştirmeye çalışmak

İşte hata:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error

Değişkenler varsayılan olarak değişmez olduğu gibi, referanslar da öyledir. Referansımız olan bir şeyi değiştirmemize izin verilmez.

Değişebilir Referanslar

Liste 4-6'daki kodu, ödünç alınan bir değeri, bunun yerine değişken bir referans kullanan sadece birkaç küçük ince ayar ile değiştirmemize izin verecek şekilde düzeltebiliriz.

Dosya adı: src/main.rs

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

İlk olarak, s'i mut olarak değiştiriyoruz. Sonra &mut s ile değiştirilebilen bir referans yaratır ve burada change fonksiyonunu çağırırız ve fonksiyon imzasını, some_string: &mut String ile değiştirilebilir bir referansı kabul edecek şekilde güncelleriz. Bu, change fonksiyonunun ödünç aldığı değeri değiştireceğini açıkça ortaya koymaktadır.

Değişken referansların büyük bir kısıtlaması vardır: bir değere değiştirilebilir referansınız varsa, o değere başka referansınız olamaz. s için iki değişken referans oluşturmaya çalışan bu kod başarısız olur:

Dosya adı: src/main.rs

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);
}

İşte hata:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 | 
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error

Bu hata, s'i bir defada birden fazla değişken olarak ödünç alamayacağımız için bu kodun geçersiz olduğunu söylüyor. İlk değişken ödünç alma r1'dedir ve println!'de kullanılana kadar sürmelidir, ancak bu değişken referansın oluşturulması ve kullanımı arasında, r2'de r1 ile aynı verileri ödünç alan başka bir değişken referans oluşturmaya çalıştık.

Aynı anda aynı verilere birden fazla değişken referansı engelleyen kısıtlama, değişkenliğe izin verir, ancak çok kontrollü bir şekilde. Bu, yeni Rustseverlerin uğraştığı bir şey çünkü çoğu dil, istediğiniz zaman değişkenliğe uğramanıza izin veriyor. Bu kısıtlamaya sahip olmanın yararı, Rust'ın derleme zamanında veri yarışlarını önleyebilmesidir. Bir veri yarışı, bir yarış durumuna benzer ve şu üç davranış gerçekleştiğinde gerçekleşir:

  • İki veya daha fazla işaretçi aynı verilere aynı anda erişir.
  • Verilere yazmak için işaretçilerden en az biri kullanılıyor.
  • Verilere erişimi senkronize etmek için kullanılan hiçbir mekanizma yoktur.

Veri yarışları tanımsız davranışlara neden olur ve bunları çalışma zamanında izlemeye çalışırken teşhis edilmesi ve düzeltilmesi zor olabilir; Rust, veri yarışlarıyla dolu kodu derlemeyi reddederek bu sorunu önler!

Her zaman olduğu gibi, yeni bir kapsam oluşturmak için süslü parantezleri kullanabiliriz ve birden çok değişken referansa izin verebiliriz:

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.

    let r2 = &mut s;
}

Rust, değiştirilebilir ve değişmez referansları birleştirmek için benzer bir kural uygular. Bu kod hata verir:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);
}

İşte hata:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 | 
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

Vay be! Aynı değere değişmez bir referansımız varken değiştirilebilir bir referansımız da olamaz.

Değişmez bir referansın kullanıcıları, değerin aniden altlarında değişmesini beklemezler! Bununla birlikte, birden fazla değişmez referansa izin verilir, çünkü yalnızca verileri okuyan hiç kimse, başka birinin verileri okumasını etkileme yeteneğine sahip değildir.

Bir referansın kapsamının tanıtıldığı yerden başladığını ve o referansın son kullanıldığı zamana kadar devam ettiğini unutmayın. Örneğin, değişmez referansların son kullanımı olan println!, değiştirilebilir referans tanıtılmadan önce gerçekleştiği için bu kod derlenecektir:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{}", r3);
}

Değişmez r1 ve r2 referanslarının kapsamları println!'den sonra biter. Bu kapsamlar çakışmaz, bu nedenle bu koda izin verilir. Derleyicinin, kapsamın bitiminden önceki bir noktada bir referansın artık kullanılmadığını söyleyebilmesi, Sözcük Dışı Ömürlükler (kısaca NLL) olarak adlandırılır ve bununla ilgili daha fazla bilgiyi The Edition Guide'dan okuyabilirsiniz.

Ödünç alma hataları bazen can sıkıcı olsa da, Rust derleyicisinin olası bir hatayı erkenden (çalışma zamanından ziyade derleme zamanında) ve sorunun tam olarak nerede olduğunu size gösterdiğini unutmayın. O zaman verilerinizin neden düşündüğünüz gibi olmadığını bulmak zorunda değilsiniz.

Sarkan Referanslar

İşaretçileri olan dillerde, belleğin bir kısmını boşaltırken, o belleğe bir işaretçiyi koruyarak, yanlışlıkla sarkan bir işaretçi (bellekte başka birine verilmiş olabilecek bir konuma başvuran bir işaretçi) oluşturmak kolaydır. Buna karşılık Rust'ta derleyici, referansların asla sarkan referanslar olmayacağını garanti eder: eğer bazı verilere referansınız varsa, derleyici, veriye yapılan referanstan önce verilerin kapsam dışına çıkmamasını sağlayacaktır.

Rust'ın derleme zamanı hatasıyla bunları nasıl önlediğini görmek için sarkan bir referans oluşturmaya çalışalım:

Dosya adı: src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

İşte hata:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error

Bu hata mesajı, henüz ele almadığımız bir özelliğe atıfta bulunuyor: ömürler. Ömürleri Bölüm 10'da ayrıntılı olarak tartışacağız. Ancak, ömürlerle ilgili kısımları göz ardı ederseniz, mesaj bu kodun neden bir sorun olduğunun anahtarını içerir:

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
bu fonksiyonun dönüş türü ödünç alınan bir değer içeriyor, ancak ödünç alınacak bir değer yok

Sarkan kodumuzun her aşamasında tam olarak neler olduğuna daha yakından bakalım.

Dosya adı: src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!

s, dangle içinde oluşturulduğundan, dangle kodu bittiğinde, s serbest bırakılır. Ama ona bir referans döndürmeye çalıştık. Bu, bu referansın geçersiz bir String'e işaret edeceği anlamına gelir. Bu iyi bir fikir değildir! Rust bunu yapmamıza izin vermez.

Buradaki çözüm, String'i doğrudan döndürmektir:

fn main() {
    let string = no_dangle();
}

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

Bu herhangi bir sorun olmadan çalışır. Sahiplik taşınır ve hiçbir şey serbest bırakılmaz.

Referans Kuralları

Referanslar hakkında konuştuklarımızı gözden geçirelim:

  • Herhangi bir zamanda ya bir değişken referansa ya da istediğiniz sayıda değişmez referansa sahip olabilirsiniz.

  • Referanslar her zaman geçerli olmalıdır.

Bundan sonraki başlığımızda, farklı bir referans türü olan dilimlere bakacağız.

Dilim Türü

Dilimler, koleksiyonun tamamı yerine bir koleksiyondaki bitişik bir öğe dizisine başvurmanıza izin verir. Bir dilim bir tür referanstır, dolayısıyla sahipliği yoktur.

İşte küçük bir programlama problemi: boşluklarla ayrılmış bir dizi sözcük alan ve bu dizgide bulduğu ilk sözcüğü döndüren bir fonksiyon yazın. Fonksiyon dizgide bir boşluk bulamazsa, dizgi döndürülmelidir.

Dilimlerin çözeceği problemi anlamak için dilimleri kullanmadan bu fonksiyonun imzasını nasıl yazacağımız üzerinde çalışalım:

fn first_word(s: &String) -> ?

first_word fonksiyonu, parametre olarak bir &String'e sahiptir. Sahiplik istemiyoruz, bu yüzden referansını kullanıyoruz. Ama neyi iade etmeliyiz? Bir dizginin bir parçası hakkında konuşmanın gerçekten bir yolu yok. Ancak, bir boşlukla gösterilen kelimenin sonunun indeksini döndürebiliriz.

Bunu Liste 4-7'de gösterildiği gibi deneyelim.

Dosya adı: src/main.rs

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Liste 4-7: String parametresine bir bayt indeks değeri döndüren first_word fonksiyonu

String öğesini eleman bazında gözden geçirmemiz ve bir değerin boşluk olup olmadığını kontrol etmemiz gerektiğinden, as_bytes metodunu kullanarak String'imizi bir bayt dizgisine dönüştüreceğiz:

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Ardından, iter metodunu kullanarak bayt dizgisi üzerinde bir yineleyici oluştururuz.

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Yineleyicileri Bölüm 13'te daha ayrıntılı olarak tartışacağız. Şimdilik, iter'ın bir koleksiyondaki her öğeyi döndüren bir metod olduğunu ve numaralandırmanın yinelemenin sonucunu sararak bunun yerine her öğeyi bir demetin parçası olarak döndürdüğünü bilin.

Numaralandırmadan döndürülen grubun ilk öğesi indekstir ve ikinci öğe öğeye bir referanstır.

Bu, indeksi kendimiz hesaplamaktan biraz daha uygundur.

Numaralandırma metodu bir tanımlama grubu döndürdüğü için, bu tanımlama grubunu yok etmek için modelleri kullanabiliriz. Modelleri Bölüm 6'da daha fazla tartışacağız. for döngüsünde, tanımlama grubundaki indeks için i ve tanımlama grubundaki tek bayt için &item içeren bir model belirledik. .iter().enumerate() öğesinden öğeye bir referans aldığımız için, kalıpta & kullanırız.

for döngüsünün içinde, değişmez bayt söz dizimini kullanarak alanı temsil eden baytı ararız. Bir boşluk bulursak, konumu döndürürüz. Aksi takdirde, s.len() kullanarak dizginin uzunluğunu döndürürüz:

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Artık dizgideki ilk kelimenin sonunun indeksini bulmanın bir yolu var, ancak bir sorun var. usize'ı kendi başına 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 hala geçerli olacağının garantisi yoktur. Liste 4-7'deki first_word fonksiyonunu kullanan Liste 4-8'deki programı düşünün.

Dosya adı: src/main.rs

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word will get the value 5

    s.clear(); // this empties the String, making it equal to ""

    // word still has the value 5 here, but there's no more string that
    // we could meaningfully use the value 5 with. word is now totally invalid!
}

Liste 4-8: first_word fonksiyonunun sonucunu saklama ve ardından String içeriğini değiştirme

Bu program hatasız derlenir ve s.clear()'ı çağırdıktan sonra word kullansaydık da derlenecekti. word, s durumuna hiç bağlı olmadığı için, word 5 değerini içerir. İlk word'ü çıkarmaya çalışmak için bu 5 değerini s değişkeni ile kullanabiliriz, ancak bu bir hata olur çünkü word'e 5'i atadığımızdan beri s'in içeriği değişti.

word'deki indeksin s'deki verilerle senkronizasyondan çıkması konusunda endişelenmek sıkıcı ve hataya açık! Bir second_word fonksiyonu yazarsak, bu dizginleri yönetmek daha rahat olacak. Yapısı şöyle görünmelidir:

fn second_word(s: &String) -> (usize, usize) {

Şimdi bir başlangıç ve bitiş indeksini izliyoruz ve belirli bir durumdaki verilerden hesaplanan ancak bu duruma hiç bağlı olmayan daha da fazla değere sahibiz. Senkronize tutulması gereken, etrafta dolaşan üç alakasız değişkenimiz var.

Şansımıza ki, Rust'ın bu soruna bir çözümü var: String dilimleri.

Dizgi Dilimleri

Dizgi dilimi, bir dizginin parçasına yapılan bir başvurudur ve şöyle görünür:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];
}

hello, String'in tamamına bir referans yerine, String'in fazladan [0..5] bitinde belirtilen bir bölümüne referanstır. [starting_index..end_index]'i belirterek parantez içinde bir aralık kullanarak dilimler oluştururuz, burada starting_index dilimdeki ilk konumdur ve end_index dilimdeki son konumdan bir fazlasıdır. Dahili olarak, dilim veri yapısı, son eksi başa karşılık gelen başlangıç konumunu ve dilimin uzunluğunu saklar. Yani let world = &s[6..11]; durumunda; world s'in indeks 6'sındaki bayta işaretçi ve uzunluk değeri 5 olan bir dilim olacaktır.

world, s'in indeks 6'daki pointerını ve 5 uzunluğunu tutan işaretçiyi tutar

Şekil 4-6: Bir String parçasına atıfta bulunan dizgi dilimi

Rust'ın .. aralık söz dizimi ile indeks sıfırdan başlamak istiyorsanız, değeri iki noktadan önce bırakabilirsiniz. Başka bir deyişle, bunlar eşittir:


#![allow(unused)]
fn main() {
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
}

Aynı şekilde, diliminiz String'in son baytını içeriyorsa, sondaki sayıyı bırakabilirsiniz. Demek ki bunlar eşit:


#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
}

Ayrıca tüm dizgiden bir dilim almak için her iki değeri de bırakabilirsiniz:


#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];
}

Not: String dilim aralığı indeksleri, geçerli UTF-8 karakter sınırlarında oluşmalıdır. Çok baytlı bir karakterin ortasında bir dizgi dilimi oluşturmaya çalışırsanız, programınız bir hata ile çıkacaktır. Dize dilimlerini tanıtmak amacıyla, sadece bu bölümde ASCII'yi varsayıyoruz; UTF-8 işleme hakkında daha ayrıntılı bir tartışma, Bölüm 8'in “UTF-8 Kodlu Metni Dizgilerde Depolama” kısmındadır.

Tüm bu bilgileri göz önünde bulundurarak, bir dilim döndürmek için first_word'ü yeniden yazalım. “Dizgi dilimi” anlamına gelen tür &str olarak yazılır:

Dosya adı: src/main.rs

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {}

Bir boşluğun ilk oluşumunu arayarak, Liste 4-7'de yaptığımız gibi, kelimenin sonu için indeksi elde ederiz. Bir boşluk bulduğumuzda, başlangıç ve bitiş indeksleri olarak dizgenin başlangıcını ve boşluğun indeksini kullanarak bir dizgi dilimi döndürürüz.

Şimdi first_word'ü çağırdığımızda, temeldeki verilere bağlı tek bir değeri geri alıyoruz. Değer, dilimin başlangıç noktasına yapılan bir referanstan ve dilimdeki eleman sayısından oluşur.

Bir dilim döndürmek, second_word fonksiyonu için de işe yarar:

fn second_word(s: &String) -> &str {

Derleyici, String'e yapılan referansların geçerli kalmasını sağlayacağından, artık karıştırılması çok daha zor olan basit bir API'ye sahibiz. Liste 4-8'deki programdaki hatayı hatırlıyor musunuz? İndeksi ilk kelimenin sonuna kadar aldık ama sonra dizgiyi temizledik, böylece dizgimiz geçersiz olmuştu. Bu kod mantıksal olarak yanlıştı ancak herhangi bir hata göstermemişti. İlk kelime dizgisini boş bir dizgiyle kullanmaya devam edersek, sorunlar daha sonra ortaya çıkacaktı. Dilimler bu hatayı imkansız kılar ve kodumuzla ilgili bir sorunumuz olduğunu çok daha erken bildirir. first_word'ün dilim sürümünü kullanmak derleme zamanı hatası verir:

Dosya adı: src/main.rs

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {}", word);
}

Derleyici hatası:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 | 
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 | 
20 |     println!("the first word is: {}", word);
   |                                       ---- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

Ödünç alma kurallarından hatırlayın, eğer bir şeye değişmez bir referansımız varsa, aynı zamanda değişken bir referans alamayız. clear'ın String'i temizlemesi gerektiğinden, değişken bir referans alması gerekir. println! clear çağrısı word'deki referansı kullandıktan sonra, değişmez referansın o noktada hala aktif olması gerekir. Rust, clear değiştirilebilir referansın ve word'deki değişmez referansın aynı anda var olmasına izin vermez ve derleme başarısız olur. Rust yalnızca API'mizin kullanımını kolaylaştırmakla kalmadı, aynı zamanda derleme zamanında bir dizi hatayı da ortadan kaldırdı!

Dizgi Değişmezleri Bir Tür Dilimdir

İkili dosyada saklanan dizgi değişmezlerinden bahsettiğimizi hatırlayın. Artık dilimleri bildiğimize göre, dizgi değişmezlerini doğru bir şekilde anlayabiliriz:


#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

Buradaki s'in türü &str'dir: ikili dosyanın o belirli noktasına işaret eden bir dilimdir. Bu aynı zamanda dizgi değişmezlerinin değişmez olmasının nedenidir; &str değişmez bir referanstır.

Parametre Olarak Dizgi Dilimleri

Hazır bilgi ve String değerlerinin dilimlerini alabileceğinizi bilmek, bizi first_word üzerinde bir iyileştirmeye daha götürüyor ve bu da onun yapısı:

fn first_word(s: &String) -> &str {

Daha deneyimli bir Rustsever, bunun yerine Liste 4-9'da gösterilen yapıyı kullanırdı çünkü aynı fonksiyonu hem &String hem de &str değerlerinde kullanmamıza izin verir.

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or whole
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Liste 4-9: s parametresinin türü için bir dizgi dilimi kullanarak first_word fonksiyonunu geliştirme

Eğer elimizde bir dizgi dilimi varsa onu direkt argüman olarak verebiliriz. Bir String'imiz varsa, String'in bir dilimini veya String'e bir referansı iletebiliriz. Bu esneklik, Bölüm 15'in “Fonksiyonlar ve Metodlarla Örtülü Deref Zorlamaları” bölümünde ele alacağımız bir özellik olan deref zorlamalarından yararlanır. Bir String referansı yerine bir dizgi dilimi alacak bir fonksiyon tanımlamak, API'mizi daha genel ve herhangi bir işlevsellik kaybetmeden kullanmamızı sağlar:

Dosya adı: src/main.rs

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or whole
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Diğer Dilimler

Dizgi dilimleri, tahmin edebileceğiniz gibi, dizgilere özgüdür. Ancak daha genel bir dilim türü de var. Bu diziyi düşünün:


#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}

Tıpkı bir dizginin bir kısmına atıfta bulunmak isteyebileceğimiz gibi, bir dizinin bir kısmına atıfta bulunmak isteyebiliriz. Biz böyle yapardık:


#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}

Bu dilim &[i32] türüne sahip. İlk öğeye ve bir uzunluğa bir referans depolayarak, dizgi dilimlerinin yaptığını yapar. Bu tür dilimi diğer tüm koleksiyonlar için kullanacaksınız. Bölüm 8'de vektörler hakkında konuşurken bu koleksiyonları ayrıntılı olarak tartışacağız.

Özet

Sahiplik, ödünç alma ve dilim kavramları, derleme zamanında Rust programlarında bellek güvenliğini sağlar. Rust dili, diğer sistem programlama dilleriyle aynı şekilde bellek kullanımınız üzerinde kontrol sağlar, ancak veri sahibinin kapsam dışına çıktığında bu verileri otomatik olarak temizlemesi, fazladan kod yazmanız ve hata ayıklamanız gerekmediği anlamına gelir. Bu kontrolü elde etmek için.

Sahiplik, Rust'ın diğer birçok bölümünün nasıl çalıştığını etkiler, bu yüzden kitabın geri kalanında bu kavramlar hakkında daha fazla konuşacağız. Bölüm 5'e geçelim ve veri parçalarını bir struct içinde gruplandırmaya bakalım.

İlgili Verileri Yapılandırmak için Yapıları Kullanma

Yapı, ya da yapı :), anlamlı bir grup oluşturan ilgili birden çok değeri bir araya getirmenize ve adlandırmanıza olanak tanıyan özel bir veri türüdür. Nesne yönelimli bir dile aşina iseniz, yapı bir nesnenin veri tanımlayıcıları gibidir. Bu bölümde, zaten bildiklerinizin üzerine inşa etmek ve yapıların ne zaman verileri gruplamak için daha iyi bir yol olduğunu göstermek için demet türlerini yapılarla karşılaştıracağız. Yapıların nasıl tanımlanacağını ve somutlaştırılacağını göstereceğiz. Bir yapı türüyle ilişkili davranışı belirtmek için ilişkili fonksiyonların, özellikle metod adı verilen ilişkili fonksiyonların nasıl tanımlanacağını tartışacağız. Yapılar ve numaralandırılmış yapılar (Bölüm 6'da ele alınmıştır), Rust'ın derleme zamanı tür kontrolünden tam olarak yararlanmak için programınızın etki alanında yeni türler oluşturmak için yapı taşlarıdır.

Yapıları Tanımlama ve Örnekleme

Yapılar, her ikisinin de birden çok ilişkili değeri içermesi bakımından “Demet Türü” bölümünde tartışılan demetlere benzer. Demetler gibi, bir yapının parçaları farklı türleri olabilir. Demetlerden farklı olarak, bir yapı içinde, değerlerin ne anlama geldiğini netleştirmek için her bir veri parçasını adlandıracaksınız. Bu adların eklenmesi, yapıların tanımlama gruplarından daha esnek olduğu anlamına gelir: bir örneğin değerlerini belirtmek veya bunlara erişmek için verilerin sırasına güvenmeniz gerekmez.

Bir yapı tanımlamak için, struct anahtar sözcüğünü girer ve tüm yapıyı adlandırırız. Bir yapının adı, birlikte gruplandırılan veri parçalarının önemini açıklamalıdır. Ardından süslü parantezler içinde üye dediğimiz veri parçalarının adlarını ve türlerini tanımlarız. Örneğin, Liste 5-1, bir kullanıcı hesabı hakkında bilgi depolayan bir yapı gösterir.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}

Liste 5-1: Bir User yapı tanımı

Bir yapıyı tanımladıktan sonra kullanmak için, üyelerin her biri için somut değerler belirterek o yapının bir örneğini yaratırız. Yapının adını belirterek bir örnek oluşturuyoruz ve ardından anahtarların üye adları olduğu ve değerlerin bu üyelerde depolamak istediğimiz veriler olduğu anahtar: değer çiftlerini içeren süslü parantezleri ekliyoruz. Üyeleri struct içinde belirttiğimiz sırayla belirtmemize gerek yok. Başka bir deyişle, struct tanımı, tür için genel bir şablon gibidir ve örnekler, türün değerlerini oluşturmak için bu şablonu belirli verilerle doldurur. Örneğin, Liste 5-2'de gösterildiği gibi belirli bir kullanıcıyı bildirebiliriz.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
}

Liste 5-2: User yapısının bir örneğini oluşturma

Bir yapıdan belirli bir değer elde etmek için nokta gösterimini kullanırız. Örneğin, bu kullanıcının e-posta adresine erişmek için user1.email kullanıyoruz. Örnek değişken ise, nokta gösterimini kullanarak ve belirli bir alana atayarak bir değeri değiştirebiliriz. Liste 5-3, değiştirilebilir bir User örneğinin email alanındaki değerin nasıl değiştirileceğini gösterir.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let mut user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}

Liste 5-3: Bir User örneğinin email alanındaki değeri değiştirme

Tüm örneğin değiştirilebilir olması gerektiğini unutmayın; Rust, yalnızca belirli alanları değiştirilebilir olarak işaretlememize izin verme. Herhangi bir ifadede olduğu gibi, bu yeni örneği örtük olarak döndürmek için fonksiyon gövdesindeki son ifade olarak yapının yeni bir örneğini oluşturabiliriz.

Liste 5-4, verilen email ve username ile bir User örneği döndüren bir build_user fonksiyonunu gösterir. active üyesi true değerini, sign_in_count ise 1 değerini alır.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Liste 5-4: Bir e-posta ve kullanıcı adı alan ve bir User örneği döndüren bir build_user fonksiyonu

Fonksiyon parametrelerini yapı üyeleriyle aynı adla adlandırmak mantıklıdır, ancak email ve username üye adlarını ve değişkenlerini tekrarlamak biraz sıkıcıdır. Yapının daha fazla üyesi olsaydı, her adı tekrarlamak daha da can sıkıcı olurdu.

Neyse ki, uygun bir kısayol var!

Üye Başlatıcı Kısayolu Kullanma

Parametre adları ve yapı üye adları Liste 5-4'te tamamen aynı olduğundan, build_user'ı yeniden yazmak için üye başlatıcı kısayolu söz dizimini kullanabiliriz, böylece tam olarak aynı şeyi elde ederiz ve email ve username tekrarı olmaz. Liste 5-5'te gösterilmiştir.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Liste 5-5: email ve username parametreleri yapı üyeleriyle aynı ada sahip olduğundan üye başlatıcı kısayolunu kullanan build_user fonksiyonu

Burada, email adlı bir üyeye sahip olan User yapısının yeni bir örneğini oluşturuyoruz. email üyesinin değerini build_user fonksiyonunun email parametresindeki değere atamak istiyoruz. email üyesi ve email parametresi aynı ada sahip olduğundan, email: email yerine sadece email yazmamız gerekiyor.

Yapı Güncelleme Söz Dizimi ile Diğer Örneklerden Örnekler Oluşturma

Başka bir örnekteki değerlerin çoğunu içeren ancak bazılarını değiştiren bir yapının yeni bir örneğini oluşturmak genellikle yararlıdır. Bunu yapı güncelleme söz dizimini kullanarak yapabilirsiniz.

İlk olarak, Liste 5-6'da, güncelleme söz dizimi olmadan user2'de düzenli olarak yeni bir User örneğinin nasıl oluşturulacağını gösteriyoruz. email için yeni bir değer belirledik, ancak bunun dışında user1'den Liste 5-2'de oluşturduğumuz aynı değerleri kullanıyoruz

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}

Liste 5-6: user1'deki değerlerden birini kullanarak yeni bir User örneği oluşturma

Yapı güncelleme söz dizimini kullanarak, Liste 5-7'de gösterildiği gibi aynı etkiyi daha az kodla elde edebiliriz. .. söz dizimi, açıkça ayarlanmayan kalan üyelerin verilen örnekteki üyelerle aynı değere sahip olması gerektiğini belirtir.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}

Liste 5-7: Bir User örneği için yeni bir email değeri ayarlamak için yapı güncelleme söz dizimini user1'den kalan değerlerle kullanmak.

Liste 5-7'deki kod ayrıca user2'de email için farklı bir değere sahip olan ancak user1'den username, active ve sign_in_count üyeleri için aynı değerlere sahip bir örnek oluşturur. ..user1'de kalan üyelerin değerlerini user1'deki karşılık gelen üyelerden alması gerektiğini belirtmek için en son gelmelidir, ancak üyelerin sırasına bakılmaksızın herhangi bir sırayla istediğimiz kadar üye için değer belirtmeyi seçebiliriz.

Yapı güncelleme söz diziminin bir atama gibi = kullandığını unutmayın; bunun nedeni, tıpkı “Değişkenlerin ve Veri Etkileşiminin Yolları: Hareket Ettirme” bölümünde gördüğümüz gibi verileri hareket ettirmesidir. Bu örnekte, user1'in username üyesindeki String, user2'ye taşındığından, user2 oluşturulduktan sonra artık user1'i kullanamayız. user2'ye hem email hem de username için yeni String değerleri vermiş olsaydık ve bu nedenle yalnızca user1'den active ve sign_in_count değerlerini kullansaydık, o zaman user1, user2 oluşturulduktan sonra da geçerli olurdu. active ve sign_in_count türleri, Copy tanımını uygulayan türlerdir, bu nedenle “Sadece Yığıtı Kullanan Tür: Copy” bölümünde tartıştığımız davranış geçerli olur.

Farklı Türler Oluşturmak için Adlandırılmış Alanlar Olmadan Demet Yapılarını Kullanma

Rust ayrıca, demet yapıları adı verilen demetlere benzeyen yapıları da destekler. Demet yapıları, yapı adının sağladığı ek anlama sahiptir, ancak alanlarıyla ilişkilendirilmiş adları yoktur; daha ziyade, sadece alanların türlerine sahiptirler. Demet yapıları, tüm demete bir ad vermek ve demeti diğer demetlerden farklı bir tür yapmak istediğinizde ve her alanı normal bir yapıdaki gibi adlandırmak ayrıntılı veya gereksiz olduğunda yararlıdır.

Bir demet yapısı tanımlamak için, struct anahtar sözcüğü ve yapı adıyla başlayın ve ardından demetteki türleri takip edin. Örneğin, burada Color ve Point adında iki demet yapısı tanımlıyor ve kullanıyoruz:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

black ve origin değerlerinin farklı türler olduğuna dikkat edin, çünkü bunlar farklı demet yapılarının örnekleridir. Tanımladığınız her yapı, yapı içindeki üyeler aynı türlere sahip olsa bile kendine özgü türe sahiptir. Örneğin, Color türünde bir parametre alan bir fonksiyon, her iki tür de üç i32 değerinden oluşsa bile argüman olarak bir Point alamaz.

Üyesiz Birim Benzeri Yapılar

Ayrıca üyesi olmayan yapılar da tanımlayabilirsiniz! Bunlara birim benzeri yapılar denir, çünkü “Demet Türü” bölümünde bahsettiğimiz birim tipine () benzer şekilde davranırlar. Birim benzeri yapılar, bir tür üzerinde bir özellik uygulamanız gerektiğinde ancak türün kendisinde depolamak istediğiniz herhangi bir veriniz olmadığında faydalı olabilir. Nitelikleri Bölüm 10'da tartışacağız. İşte AlwaysEqual adlı bir birim yapısının bildirilmesine ve somutlaştırılmasına bir örnek:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

AlwaysEqual'ı tanımlamak için struct anahtar sözcüğünü, istediğimiz adı ve ardından noktalı virgül kullanırız. Süslü parantezlere veya parantezlere gerek yok! Daha sonra, benzer bir şekilde konu değişkeninde AlwaysEqual örneğini alabiliriz: tanımladığımız adı kullanarak, herhangi bir süslü parantez kullanmadan. Daha sonra, her AlwaysEqual örneğinin her zaman diğer herhangi bir türün her örneğine eşit olduğu, belki de test amacıyla bilinen bir sonuca sahip olacak şekilde bu tür için davranış uygulayacağımızı hayal edin. Bu davranışı uygulamak için herhangi bir veriye ihtiyacımız olmazdı! Bölüm 10'da özelliklerin nasıl tanımlanacağını ve birim benzeri yapılar da dahil olmak üzere herhangi bir türe nasıl uygulanacağını göreceksiniz.

Yapı Verilerinin Sahipliği

Liste 5-1'deki User yapısı tanımında, &str dizgi dilim tipi yerine sahip olunan String tipini kullandık. Bu bilinçli bir seçimdir çünkü bu yapının her örneğinin tüm verilerine sahip olmasını ve bu verilerin tüm yapı geçerli olduğu sürece geçerli olmasını istiyoruz.

Yapıların başka bir şeye ait verilere başvuruları depolaması da mümkündür, ancak bunu yapmak için yaşam sürelerinin kullanılması gerekir; bu, Bölüm 10'da tartışacağımız bir Rust özelliğidir. Ömürler, bir yapı tarafından başvurulan verilerin aşağıdakiler için geçerli olmasını sağlar. yapı olduğu sürece. Diyelim ki aşağıdaki gibi yaşam süreleri belirtmeden bir struct içinde bir referans depolamaya çalışıyorsunuz; bu işe yaramaz:

Dosya adı: src/main.rs

struct User {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}

Derleyici, ömürlük belirteçlere ihtiyaç duyduğundan hata verecektir:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` due to 2 previous errors

Bölüm 10'da, referansları yapılarda saklayabilmeniz için bu hataların nasıl düzeltileceğini tartışacağız, ancak şimdilik, &str gibi referanslar yerine String gibi sahip olunan türleri kullanarak bu gibi hataları düzelteceğiz.

Yapıları Kullanan Örnek Bir Program

Yapıları ne zaman kullanmak isteyebileceğimizi anlamak için, bir dikdörtgenin alanını hesaplayan bir program yazalım. Tek değişkenler kullanarak başlayacağız ve ardından bunun yerine yapıları kullanana kadar programı yeniden düzenleyeceğiz.

Piksel cinsinden belirtilen bir dikdörtgenin genişlik ve yüksekliğini alacak rectangles adlı, Cargo ile yeni bir ikili proje oluşturalım ve dikdörtgenin alanını hesaplayalım. Liste 5-8, projemizin src/main.rs dosyasında tam olarak bunu yapmanın bir yolunu içeren kısa bir program göstermektedir.

Dosya adı: src/main.rs

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

Liste 5-8: Ayrı genişlik ve yükseklik değişkenleri tarafından belirtilen bir dikdörtgenin alanını hesaplama

Şimdi, bu programı cargo run kullanarak çalıştırın.

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.

Bu kod, her bir boyutta alan fonksiyonunu çağırarak dikdörtgenin alanını bulmayı başarır, ancak bu kodu açık ve okunabilir hale getirmek için daha fazlasını yapabiliriz.

Bu kodla ilgili sorun, area'nın imzasında belirgindir:

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

area fonksiyonunun bir dikdörtgenin alanını hesaplaması gerekiyor, ancak yazdığımız fonksiyonun iki parametresi var ve programımızın hiçbir yerinde parametrelerin ilişkili olduğu net değil. Genişliği ve yüksekliği birlikte gruplamak daha okunaklı ve daha yönetilebilir olurdu. Bunu, Bölüm 3'ün “Demet Türü” bölümünde, demetleri kullanarak yapabileceğimizin bir yolunu zaten tartışmıştık.

Demetlerle Yeniden Düzenleme

Liste 5-9, programımızın demet kullanan başka bir sürümünü gösterir.

Dosya adı: src/main.rs

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

Liste 5-9: Bir demet ile dikdörtgenin genişliğini ve yüksekliğini belirtme

Bir bakıma bu program daha iyi. Demetler yapı eklememize izin verir ve biz sadece bir argümanı geçeceğiz. Ancak başka bir şekilde, bu versiyon daha az açıktır: demetler öğelerini adlandırmaz, bu nedenle demetin bölümlerine indekslememiz gerekir, bu da hesaplamamızı daha az belirgin hale getirir.

Genişlik ve yüksekliğin karıştırılması alan hesabı için önemli değil, ancak dikdörtgeni ekranda çizmek istiyorsak önemlidir! Genişliğin 0 küme indeksi ve yüksekliğin küme indeksi 1 olduğunu aklımızda tutmalıyız. Eğer bizim kodumuzu kullanacak olsaydı, başka birinin bunu anlaması ve akılda tutması daha da zor olurdu. Verilerimizin anlamını kodumuzda iletmediğimiz için, hataları tanıtmak artık daha kolay.

Yapılarla Yeniden Düzenleme: Daha Fazla Anlam Ekleme

Verileri etiketleyerek anlam eklemek için yapılar kullanırız. Kullandığımız demeti, Liste 5-10'da gösterildiği gibi, parçaların adlarının yanı sıra bütün için bir ad içeren bir yapıya dönüştürebiliriz.

Dosya adı: src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

Liste 5-10: Bir Rectangle yapı tanımlama

Burada bir yapı tanımladık ve onu Rectangle olarak adlandırdık. Kıvrımlı parantezler içinde, her ikisi de u32 tipinde olan alanları width ve height olarak tanımladık. Ardından, ana olarak, 30 genişliğinde ve 50 yüksekliğinde belirli bir Rectangle tanımı oluşturduk.

area fonksiyonumuz şimdi, türü bir struct Rectangle tanımının değişmez bir ödünç alma şekli olan rectangle adını verdiğimiz bir parametre ile tanımlanır. Bölüm 4'te bahsedildiği gibi, yapıyı sahiplenmek yerine ödünç almak istiyoruz. Bu şekilde main, sahipliğini korur ve fonksiyon imzasında ve fonksiyonu çağırdığımız yerde &'yi kullanmamızın nedeni olan rect1'i kullanmaya devam edebilir.

area fonksiyonu, Rectangle tanımının width ve height üyelerine erişir (ödünç alınan bir yapı tanımının üyelerine erişmenin üye değerlerini hareket ettirmediğini, bu nedenle sık sık yapı ödünçlerini gördüğünüzü unutmayın). area için fonksiyon imzamız şimdi tam olarak ne demek istediğimizi söylüyor: width ve height üyelerini kullanarak Rectangle alanını hesaplayın. Bu, genişlik ve yüksekliğin birbiriyle ilişkili olduğunu ifade eder ve 0 ve 1'lik demet indeks değerlerini kullanmak yerine değerlere açıklayıcı isimler verir. Bu, netlik için bir kazançtır.

Türetilmiş Tanımlar ile Faydalı İşlevsellik Ekleme

Programımızda hata ayıklarken bir Rectangle tanımını yazdırabilmek ve tüm üyelerin değerlerini görebilmek faydalı olacaktır. Liste 5-11 println! makrosunu önceki bölümlerde kullandığımız gibi kullanmayı dener. Ancak bu işe yaramayacaktır.

Dosya adı: src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {}", rect1);
}

Liste 5-11: Bir Rectangle tanımını yazdırmaya çalışmak

Bu kodu derlediğimizde, bir hata alıyoruz:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

println! makrosu birçok türde biçimlendirme yapabilir ve varsayılan olarak süslü parantezler println!'e Display olarak bilinen biçimlendirmeyi kullanmasını söyler. Şimdiye kadar gördüğümüz ilkel tipler varsayılan olarak Display'i uygular, çünkü bunu yapabileceğiniz tek yoldur.

   = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

Hadi deneyelim! println! makro çağrısı şimdi println!("rect1 is {:?}", rect1);. :? belirtecini küme parantezlerinin içine koymak şunu söyler: println! için Debug adında bir çıktı formatı kullanmak istiyoruz. Debug tanımı yapımızı geliştiriciler için yararlı olacak şekilde yazdırmamızı sağlar, böylece kodumuz ayıklanırken değerini görebilirsiniz.

Kodu bu değişiklikle derleyin. Hala bir hata alıyoruz:

error[E0277]: `Rectangle` doesn't implement `Debug`

Ama yine de derleyici bize yardımcı olacak bir not veriyor:

   = help: the trait `Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`

Rust, hata ayıklama bilgilerini yazdırma fonksiyonunu içerir, ancak bu fonksiyonu yapımız için kullanılabilir hale getirmek için açıkça seçmeliyiz. Bunu yapmak için, Liste 5-12'de gösterildiği gibi, yapı tanımından hemen önce #[derive(Debug)] dış niteliğini ekleriz.

Dosya adı: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1);
}

Liste 5-12: Debug tanımını türetmek için özniteliği ekleme ve hata ayıklama biçimlendirmesini kullanarak Rectangle tanımı yazdırma

Şimdi programı çalıştırdığımızda herhangi bir hata almayacağız ve aşağıdaki çıktıyı göreceğiz:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

Güzel! En güzel çıktı değil, ancak hata ayıklama sırasında kesinlikle yardımcı olacak bu örnek için tüm alanların değerlerini gösterir. Daha büyük yapılarımız olduğunda, okunması biraz daha kolay çıktılara sahip olmak yararlıdır; bu durumlarda println!'de {:?} yerine {:#?} kullanabiliriz. Bu örnekte, {:#?} stilinin kullanılması şu sonucu verecektir:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle {
    width: 30,
    height: 50,
}

Debug formatını kullanarak bir değer yazdırmanın başka bir yolu da dbg! makrosudur. Bu makro ifadenin sahipliğini alır (bir referans alan println!'nin aksine) ve bu ifadenin sonuç değeriyle birlikte kodunuzda gerçekleşir ve değerin sahipliğini döndürür.

Not: dbg!'yi çağırmak, println!'nin aksine standart hata konsolu akışına (stderr) yazdırır. Bölüm 12'deki “Standart Çıktı Yerine Standart Hataya Hata Mesajları Yazma” bölümünde stderr ve stdout hakkında daha fazla konuşacağız.

İşte, genişlik alanına atanan değerle ve ayrıca rect1'deki tüm yapının değeriyle ilgilendiğimiz bir örnek:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

30 * scale ifadesine dbg! makrosunu koyabiliriz çünkü dbg! ifadenin değerinin sahipliğini döndürür. Biz ise dbg!'nin rect1'in sahipliğini almasını istemiyor olduğumuzdan dolayı bir diğer çağrıda rect1'in bir referansını kullanacağız.

Bu örneğin çıktısı şuna benzemelidir:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rectangles`
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Çıktının ilk bitinin src/main.rs 10. satırdan geldiğini görebiliriz, burada 30 * scale ifadesinin hatalarını ayıklıyoruz. dbg! src/main.rs dosyasının 14. satırındaki çağrı, Rectangle yapısı olan &rect1 değerini verir. Bu çıktı, Rectangle türünün güzel Debug biçimlendirmesini kullanır. dbg! makrosu, kodunuzun ne yaptığını anlamaya çalışırken gerçekten yardımcı olabilir!

Rust, Debug tanımına ek olarak, derive özelliğiyle birlikte kullanmamız için özel türlerimize faydalı davranışlar ekleyebilecek bir dizi özellik sağlamıştır. Bu özellikler ve davranışları Ekleme C'de listelenmiştir. Bu özellikleri özel davranışlarla nasıl uygulayacağınızı ve kendi özelliklerinizi nasıl oluşturacağınızı Bölüm 10'da ele alacağız. derive'ın dışında da birçok nitelik vardır; daha fazla bilgi için, Rust Reference'ın “Nitelikler” bölümüne bakın.

area fonksiyonumuz çok spesifiktir: sadece dikdörtgenlerin alanını hesaplar. Bu davranışı Rectangle yapımıza daha yakından bağlamak faydalı olacaktır, çünkü başka hiçbir türle çalışmayacaktır. area fonksiyonunu Rectangle türümüzde tanımlı bir area metoduna çevirerek bu kodu nasıl yeniden düzenlemeye devam edebileceğimizebakalım.

Method Syntax

Methods are similar to functions: we declare them with the fn keyword and a name, they can have parameters and a return value, and they contain some code that’s run when the method is called from somewhere else. Unlike functions, methods are defined within the context of a struct (or an enum or a trait object, which we cover in Chapters 6 and 17, respectively), and their first parameter is always self, which represents the instance of the struct the method is being called on.

Defining Methods

Let’s change the area function that has a Rectangle instance as a parameter and instead make an area method defined on the Rectangle struct, as shown in Listing 5-13.

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

Listing 5-13: Defining an area method on the Rectangle struct

To define the function within the context of Rectangle, we start an impl (implementation) block for Rectangle. Everything within this impl block will be associated with the Rectangle type. Then we move the area function within the impl curly brackets and change the first (and in this case, only) parameter to be self in the signature and everywhere within the body. In main, where we called the area function and passed rect1 as an argument, we can instead use method syntax to call the area method on our Rectangle instance. The method syntax goes after an instance: we add a dot followed by the method name, parentheses, and any arguments.

In the signature for area, we use &self instead of rectangle: &Rectangle. The &self is actually short for self: &Self. Within an impl block, the type Self is an alias for the type that the impl block is for. Methods must have a parameter named self of type Self for their first parameter, so Rust lets you abbreviate this with only the name self in the first parameter spot. Note that we still need to use the & in front of the self shorthand to indicate this method borrows the Self instance, just as we did in rectangle: &Rectangle. Methods can take ownership of self, borrow self immutably as we’ve done here, or borrow self mutably, just as they can any other parameter.

We’ve chosen &self here for the same reason we used &Rectangle in the function version: we don’t want to take ownership, and we just want to read the data in the struct, not write to it. If we wanted to change the instance that we’ve called the method on as part of what the method does, we’d use &mut self as the first parameter. Having a method that takes ownership of the instance by using just self as the first parameter is rare; this technique is usually used when the method transforms self into something else and you want to prevent the caller from using the original instance after the transformation.

The main reason for using methods instead of functions, in addition to providing method syntax and not having to repeat the type of self in every method’s signature, is for organization. We’ve put all the things we can do with an instance of a type in one impl block rather than making future users of our code search for capabilities of Rectangle in various places in the library we provide.

Note that we can choose to give a method the same name as one of the struct’s fields. For example, we can define a method on Rectangle also named width:

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

Here, we’re choosing to make the width method return true if the value in the instance’s width field is greater than 0, and false if the value is 0: we can use a field within a method of the same name for any purpose. In main, when we follow rect1.width with parentheses, Rust knows we mean the method width. When we don’t use parentheses, Rust knows we mean the field width.

Often, but not always, when we give methods with the same name as a field we want it to only return the value in the field and do nothing else. Methods like this are called getters, and Rust does not implement them automatically for struct fields as some other languages do. Getters are useful because you can make the field private but the method public and thus enable read-only access to that field as part of the type’s public API. We will be discussing what public and private are and how to designate a field or method as public or private in Chapter 7.

Where’s the -> Operator?

In C and C++, two different operators are used for calling methods: you use . if you’re calling a method on the object directly and -> if you’re calling the method on a pointer to the object and need to dereference the pointer first. In other words, if object is a pointer, object->something() is similar to (*object).something().

Rust doesn’t have an equivalent to the -> operator; instead, Rust has a feature called automatic referencing and dereferencing. Calling methods is one of the few places in Rust that has this behavior.

Here’s how it works: when you call a method with object.something(), Rust automatically adds in &, &mut, or * so object matches the signature of the method. In other words, the following are the same:


#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}

The first one looks much cleaner. This automatic referencing behavior works because methods have a clear receiver—the type of self. Given the receiver and name of a method, Rust can figure out definitively whether the method is reading (&self), mutating (&mut self), or consuming (self). The fact that Rust makes borrowing implicit for method receivers is a big part of making ownership ergonomic in practice.

Methods with More Parameters

Let’s practice using methods by implementing a second method on the Rectangle struct. This time, we want an instance of Rectangle to take another instance of Rectangle and return true if the second Rectangle can fit completely within self (the first Rectangle); otherwise it should return false. That is, once we’ve defined the can_hold method, we want to be able to write the program shown in Listing 5-14.

Filename: src/main.rs

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-14: Using the as-yet-unwritten can_hold method

And the expected output would look like the following, because both dimensions of rect2 are smaller than the dimensions of rect1 but rect3 is wider than rect1:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

We know we want to define a method, so it will be within the impl Rectangle block. The method name will be can_hold, and it will take an immutable borrow of another Rectangle as a parameter. We can tell what the type of the parameter will be by looking at the code that calls the method: rect1.can_hold(&rect2) passes in &rect2, which is an immutable borrow to rect2, an instance of Rectangle. This makes sense because we only need to read rect2 (rather than write, which would mean we’d need a mutable borrow), and we want main to retain ownership of rect2 so we can use it again after calling the can_hold method. The return value of can_hold will be a Boolean, and the implementation will check whether the width and height of self are both greater than the width and height of the other Rectangle, respectively. Let’s add the new can_hold method to the impl block from Listing 5-13, shown in Listing 5-15.

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-15: Implementing the can_hold method on Rectangle that takes another Rectangle instance as a parameter

When we run this code with the main function in Listing 5-14, we’ll get our desired output. Methods can take multiple parameters that we add to the signature after the self parameter, and those parameters work just like parameters in functions.

Associated Functions

All functions defined within an impl block are called associated functions because they’re associated with the type named after the impl. We can define associated functions that don’t have self as their first parameter (and thus are not methods) because they don’t need an instance of the type to work with. We’ve already used one function like this: the String::from function that’s defined on the String type.

Associated functions that aren’t methods are often used for constructors that will return a new instance of the struct. These are often called new, but new isn’t a special name and isn’t built into the language. For example, we could choose to provide an associated function named square that would have one dimension parameter and use that as both width and height, thus making it easier to create a square Rectangle rather than having to specify the same value twice:

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}

The Self keywords in the return type and in the body of the function are aliases for the type that appears after the impl keyword, which in this case is Rectangle.

To call this associated function, we use the :: syntax with the struct name; let sq = Rectangle::square(3); is an example. This function is namespaced by the struct: the :: syntax is used for both associated functions and namespaces created by modules. We’ll discuss modules in Chapter 7.

Multiple impl Blocks

Each struct is allowed to have multiple impl blocks. For example, Listing 5-15 is equivalent to the code shown in Listing 5-16, which has each method in its own impl block.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-16: Rewriting Listing 5-15 using multiple impl blocks

There’s no reason to separate these methods into multiple impl blocks here, but this is valid syntax. We’ll see a case in which multiple impl blocks are useful in Chapter 10, where we discuss generic types and traits.

Summary

Structs let you create custom types that are meaningful for your domain. By using structs, you can keep associated pieces of data connected to each other and name each piece to make your code clear. In impl blocks, you can define functions that are associated with your type, and methods are a kind of associated function that let you specify the behavior that instances of your structs have.

But structs aren’t the only way you can create custom types: let’s turn to Rust’s enum feature to add another tool to your toolbox.

Numaralandırılmışlar ve Model Eşleme

Bu bölümde numaralandırılmış yapılara, bir diğer deyişle numaralandırışmışlara göz atacağız. Numaralandırılmışlar, olası varyantları numaralandırarak bir türü tanımlamanıza olanak tanır. İlk olarak, bir numaralandırmanın verilerle birlikte anlamı nasıl kodlayabileceğini göstermek için bir numaralandırma tanımlayacağız ve kullanacağız. Ardından, bir değerin bir şey ya da hiçbir şey olabileceğini ifade eden Option adlı tanımla yararlı bir numaralandırmayı keşfedeceğiz. Ardından, eşleşme ifadesindeki kalıp eşleştirmenin, bir numaralandırmanın farklı değerleri için farklı kod çalıştırmayı nasıl kolaylaştırdığına bakacağız. Son olarak, if let yapısının kodunuzdaki numaralandırmaları işlemek için kullanılabilen başka bir kullanışlı ve özlü deyim olduğunu ele alacağız.

Numaralandırılmış Yapı Tanımlamak

Yapıların size width ve height üyeleri olan bir Rectangle gibi ilgili üyeleri ve verileri gruplandırmanın bir yolunu verdiği yerde, numaralandırmalar size bir değerin olası bir değer kümesinden biri olduğunu söylemenin bir yolunu verir. Örneğin, Rectangle'ın Circle ve Triangle'ı da içeren olası şekillerden biri olduğunu söylemek isteyebiliriz. Bunu yapmak için Rust, bu olasılıkları bir enum olarak kodlamamıza izin verir.

Kodda ifade etmek isteyebileceğimiz bir duruma bakalım ve bu durumda numaralandırmaların neden yapılardan daha yararlı ve daha uygun olduğunu görelim. IP adresleriyle çalışmamız gerektiğini farz edin. Şu anda IP adresleri için iki ana standart kullanılmaktadır: V4 ve V6. Programımızın karşılaşacağı bir IP adresi için tek olasılık bunlar olduğundan, tüm olası değişkenleri sıralayabiliriz, bu da numaralandırmanın adını aldığı yerdir.

Herhangi bir IP adresi, V4 veya V6 adresi olabilir, ancak ikisi aynı anda olamaz. IP adreslerinin bu özelliği, enum veri yapısını uygun hale getirir, çünkü bir enum değeri onun türevlerinden yalnızca biri olabilir. Hem V4 hem de V6 adresleri hala temelde IP adresleridir, bu nedenle kod herhangi bir IP adresi için geçerli olan durumları işlerken aynı tür olarak ele alınmalıdır.

Bu kavramı bir IpAddrKind numaralandırması tanımlayarak ve bir IP adresinin olabileceği olası türleri V4 ve V6 olarak listeleyerek kodda ifade edebiliriz. Bunlar, numaralandırmanın varyantlarıdır:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

IpAddrKind artık kodumuzun başka bir yerinde kullanabileceğimiz özel bir veri türüdür.

enum Değerleri

IpAddrKind'in iki varyantının her birinin örneklerini şu şekilde oluşturabiliriz:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Numaralandırmanın türevlerinin, tanımlayıcısının altında ad alanlı olduğuna ve ikisini ayırmak için çift iki nokta üst üste (:) kullandığımıza dikkat edin. Bu kullanışlıdır çünkü artık her iki IpAddrKind::V4 ve IpAddrKind::V6 değeri aynı türdedir: IpAddrKind. Daha sonra, örneğin, herhangi bir IpAddrKind alan bir fonksiyon tanımlayabiliriz:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Ve bu fonksiyonu her iki değişkenle de çağırabiliriz:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Numaralandırma kullanmanın daha da fazla avantajı vardır. IP adresi türümüz hakkında daha fazla düşünürsek, şu anda gerçek IP adresi verilerini saklamanın bir yolu yok; sadece ne tür olduğunu biliyoruz. Bölüm 5'te yapılar hakkında yeni öğrendiğinize göre, bu sorunu Liste 6-1'de gösterildiği gibi yapılarla çözmeye cazip gelebilirsiniz.

fn main() {
    enum IpAddrKind {
        V4,
        V6,
    }

    struct IpAddr {
        kind: IpAddrKind,
        address: String,
    }

    let home = IpAddr {
        kind: IpAddrKind::V4,
        address: String::from("127.0.0.1"),
    };

    let loopback = IpAddr {
        kind: IpAddrKind::V6,
        address: String::from("::1"),
    };
}

Liste 6-1: Bir IP adresinin verilerinin ve IpAddrKind değişkeninin struct kullanılarak saklanması

Burada, iki üyesi olan bir IpAddr yapısı tanımladık: IpAddrKind türünde bir tür üyesi (daha önce tanımladığımız numaralandırma) ve String türünde bir adres üyesi. Bu yapının iki örneğine sahibiz. Birincisi home'dır ve 127.0.0.1 ilişkili adres verileriyle kendi türünde IpAddrKind::V4 değerine sahiptir. İkinci örnek geri döngüdür. Tür değeri V6 olarak IpAddrKind'in diğer türevine sahiptir ve onunla ilişkili ::1 adresine sahiptir. Tür ve adres değerlerini bir araya toplamak için bir yapı kullandık, bu yüzden şimdi değişken değerle ilişkilendirildi.

Bununla birlikte, aynı kavramı yalnızca bir numaralandırma kullanarak temsil etmek daha özlüdür: bir yapı içindeki bir numaralandırma yerine, verileri doğrudan her bir numaralandırma değişkenine koyabiliriz. IpAddr enum'un bu yeni tanımı, hem V4 hem de V6 varyantlarının ilişkili String değerlerine sahip olacağını söylüyor:

fn main() {
    enum IpAddr {
        V4(String),
        V6(String),
    }

    let home = IpAddr::V4(String::from("127.0.0.1"));

    let loopback = IpAddr::V6(String::from("::1"));
}

enum'un her türevine doğrudan veri ekliyoruz, bu nedenle ekstra bir yapıya gerek yok. Burada ayrıca numaralandırmaların nasıl çalıştığına dair başka bir ayrıntıyı görmek daha kolaydır: tanımladığımız her bir numaralandırma değişkeninin adı aynı zamanda numaralandırmanın bir örneğini oluşturan bir fonksiyon haline gelir. Diğer bir deyişle, IpAddr::V4(), bir String bağımsız değişkeni alan ve IpAddr türünün bir örneğini döndüren bir fonksiyon çağrısıdır. enum'u tanımlamanın bir sonucu olarak bu yapıcı fonksiyonu otomatik olarak tanımlarız.

Bir yapı yerine bir numaralandırma kullanmanın başka bir avantajı daha vardır: her değişken, farklı türde ve miktarda ilişkili veriye sahip olabilir. V4 tip IP adresleri her zaman 0 ile 255 arasında değerlere sahip dört sayısal bileşene sahip olacaktır:

fn main() {
    enum IpAddr {
        V4(u8, u8, u8, u8),
        V6(String),
    }

    let home = IpAddr::V4(127, 0, 0, 1);

    let loopback = IpAddr::V6(String::from("::1"));
}

V4 ve V6 IP adreslerini depolamak için veri yapılarını tanımlamanın birkaç farklı yolunu gösterdik. Ancak, ortaya çıktığı gibi, IP adreslerini saklamak ve hangi tür olduklarını kodlamak istemek o kadar yaygın ki, standart kütüphanenin kullanabileceğimiz bir tanımı var! Standart kütüphanenin IpAddr'yi nasıl tanımladığına bakalım: tanımladığımız ve kullandığımız tam enum ve varyantlara sahiptir, ancak adres verilerini varyantların içine, her varyant için farklı tanımlanmış iki farklı yapı şeklinde gömer:


#![allow(unused)]
fn main() {
struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}
}

Bu kod, herhangi bir türde veriyi bir numaralandırma değişkeninin içine koyabileceğinizi gösterir: örneğin dizgiler, sayısal türler veya yapılar. Hatta başka bir numaralandırma ekleyebilirsiniz! Ayrıca, standart kütüphane türleri genellikle bulabileceklerinizden çok daha karmaşık değildir.

Standart kütüphanenin IpAddr için bir tanım içermesine rağmen, standart kütüphanenin tanımını kapsamımıza almadığımız için kendi tanımımızı çakışmadan oluşturup kullanabileceğimize dikkat edin. Bölüm 7'de türleri kapsama almak hakkında daha fazla konuşacağız.

Liste 6-2'deki başka bir numaralandırma örneğine bakalım: bu, türevlerinde gömülü çok çeşitli türlere sahiptir.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}

Liste 6-2: Varyantlarının her biri farklı miktar ve türde değerleri saklayan bir Message numaralandırması

Bu numaralandırmanın farklı türlerde dört çeşidi vardır:

  • Quit onunla ilişkili hiçbir veriye sahip değil.
  • Move bir yapının yaptığı gibi alanları adlandırmıştır.
  • Write tek bir String'i dahil eder.
  • ChangeColor üç tane i32 değerini dahil eder.

Liste 6-2'dekiler gibi değişkenlerle bir numaralandırma tanımlamak, farklı türde yapı tanımları tanımlamaya benzer, ancak numaralandırmanın struct anahtar sözcüğünü kullanmaması ve tüm değişkenlerin Message türü altında gruplandırılması dışında. Aşağıdaki yapılar, önceki numaralandırma değişkenlerinin sahip olduğu aynı verileri tutabilir:

struct QuitMessage; // unit struct
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct

fn main() {}

Ancak, her biri kendi tipine sahip olan farklı yapıları kullanırsak, bu tür mesajların herhangi birini almak için, tek bir mesaj olan Liste 6-2'de tanımlanan Message enum'u ile yapabileceğimiz kadar kolay bir fonksiyon tanımlayamazdık.

Numaralandırmalar ve yapılar arasında bir benzerlik daha vardır: impl kullanarak yapılar üzerinde yöntemleri tanımlayabildiğimiz gibi, enum'lar üzerinde de yöntemler tanımlayabiliriz. İşte Message enum'umuzda tanımlayabileceğimiz call adında bir metod:

fn main() {
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }

    impl Message {
        fn call(&self) {
            // method body would be defined here
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();
}

Metodun gövdesi, metodu çağırdığımız değeri almak için self'i kullanır. Bu örnekte, m adında Message::Write(String::from("hello"))'yu tutan bir değişken oluşturduk ve m.call() çalıştığında self call metodunun gövdesinde olacak.

Standart kütüphanedeki çok yaygın ve kullanışlı olan başka bir numaralandırmaya bakalım: Option.

Option Numaralandırması ve Null Değerlerine Göre Avantajları

Bu bölüm, standart kütüphane tarafından tanımlanan başka bir numaralandırma olan Option'ın örnek olay incelemesini incelemektedir. Option türü, bir değerin bir şey olabileceği veya hiçbir şey olamayacağı çok yaygın senaryoyu kodlar.

Örneğin, öğeleri içeren bir listenin ilkini talep ederseniz, bir değer alırsınız. Boş bir listenin ilk maddesini talep ederseniz, hiçbir şey alamazsınız. Bu kavramı tür sistemi cinsinden ifade etmek, derleyicinin, ele almanız gereken tüm durumları ele alıp almadığınızı kontrol edebileceği anlamına gelir; bu işlevsellik, diğer programlama dillerinde son derece yaygın olan hataları önleyebilir.

Programlama dili tasarımı genellikle hangi özellikleri eklediğinize göre düşünülür, ancak hariç tuttuğunuz özellikler de önemlidir. Rust, diğer birçok dilde bulunan null özelliğine sahip değildir. null, hiçbir değer olmadığı anlamına gelen bir değerdir. null olan dillerde, değişkenler her zaman iki durumdan birinde olabilir: null veya null değil.

null'un mucidi Tony Hoare, “Null References: The Billion Dollar Mistake,” adlı 2009 sunumunda şunları söylüyor:

Ben buna milyar dolarlık hatam diyorum. O zaman, nesne yönelimli bir dilde referanslar için ilk kapsamlı tip sistemini tasarlıyordum. Amacım, derleyici tarafından otomatik olarak gerçekleştirilen kontrol ile referansların tüm kullanımının kesinlikle güvenli olmasını sağlamaktı. Ancak, uygulanması çok kolay olduğu için boş bir referans koymanın cazibesine karşı koyamadım. Bu, son kırk yılda muhtemelen milyarlarca dolarlık acıya ve hasara neden olan sayısız hataya, güvenlik açığına ve sistem çökmesine neden oldu.

Boş değerlerle ilgili sorun, boş olmayan bir değer olarak boş bir değer kullanmaya çalışırsanız, bir tür hata almanızdır. Bu boş veya boş olmayan özellik yaygın olduğundan, bu tür bir hatayı yapmak son derece kolaydır.

Bununla birlikte, null'un ifade etmeye çalıştığı kavram hala kullanışlıdır: null, şu anda geçersiz olan veya herhangi bir nedenle mevcut olmayan bir değerdir.

Sorun gerçekten konseptte değil, uygulanmasındadır. Bu nedenle, Rust'ın boş değerleri yoktur, ancak var olan veya olmayan bir değer kavramını kodlayabilen bir numaralandırmaya sahiptir. Bu numaralandırma Option<T>'dir ve standart kütüphane tarafından aşağıdaki gibi tanımlanır:


#![allow(unused)]
fn main() {
enum Option<T> {
    None,
    Some(T),
}
}

Option<T> enum'u o kadar kullanışlıdır ki, girişe bile dahil edilmiştir; Bunu açıkça kapsama sokmanız gerekmez. Varyantları da başlangıç bölümüne dahil edilmiştir: Some ve None'ı doğrudan Option:: ön eki olmadan kullanabilirsiniz. Option<T> numaralandırma hala normal bir numaralandırmadır ve Some(T) ve None hala Option<T> türünün varyantlarıdır.

<T> sözdizimi, Rust'ın henüz bahsetmediğimiz bir özelliğidir. Bu genel bir tür parametresidir ve yaygınları Bölüm 10'da daha ayrıntılı olarak ele alacağız. Şimdilik, bilmeniz gereken tek şey <T>'nin Option enum'unun Some varyantının herhangi bir türden tek bir veri parçasını tutabileceği anlamına geldiğidir. Sayı türlerini ve dize türlerini tutmak için Option değerlerini kullanmanın bazı örnekleri:

fn main() {
    let some_number = Some(5);
    let some_char = Some('e');

    let absent_number: Option<i32> = None;
}

some_number türü Option<i32> şeklindedir. some_char türü, farklı bir tür olan Option<char>'dır. Rust, Some varyantı içinde bir değer belirttiğimiz için bu türlerin çıkarımını yapabilir. absent_number için Rust, genel Otpion türüne açıklama eklememizi gerektirir: derleyici, yalnızca None değerine bakarak karşılık gelen Some varyantının tutacağı türü çıkaramaz. Burada, absent_number için Option<i32> türünde olmasını kastettiğimizi Rust'a söylüyoruz.

Bir Some değerine sahip olduğumuzda, bir değerin mevcut olduğunu ve değerin Some içinde tutulduğunu biliriz. None değerine sahip olduğumuzda, bir anlamda null ile aynı anlama gelir: geçerli bir değerimiz yoktur. Öyleyse neden Option<T> seçeneğine sahip olmak boş değere sahip olmaktan daha iyidir?

fn main() {
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;
}

Bu kodu çalıştırırsak şöyle bir hata mesajı alırız:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
 --> src/main.rs:5:17
  |
5 |     let sum = x + y;
  |                 ^ no implementation for `i8 + Option<i8>`
  |
  = help: the trait `Add<Option<i8>>` is not implemented for `i8`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` due to previous error

Aslında bu hata mesajı, farklı türler oldukları için Rust'ın bir i8 ve bir Option<i8> nasıl ekleneceğini anlamadığı anlamına gelir. Rust'ta i8 gibi bir değerimiz olduğunda, derleyici her zaman geçerli bir değere sahip olmamızı sağlayacaktır. Bu değeri kullanmadan önce null değerini kontrol etmek zorunda kalmadan güvenle ilerleyebiliriz. Yalnızca bir Option<i8> olduğunda (veya hangi tür değerle çalışırsak çalışalım), muhtemelen bir değere sahip olmama konusunda endişelenmemiz gerekir ve derleyici, değeri kullanmadan önce bu durumu ele aldığımızdan emin olacaktır.

Başka bir deyişle, onunla T işlemleri gerçekleştirmeden önce Option<T> öğesini T'ye dönüştürmeniz gerekir. Genellikle bu, null ile ilgili en yaygın sorunlardan birini yakalamaya yardımcı olur: bir şeyin gerçekte boş olmadığını varsaymak.

Yanlış bir şekilde boş olmayan bir değer varsayma riskini ortadan kaldırmak, kodunuza daha fazla güvenmenize yardımcı olur. Muhtemelen null olabilecek bir değere sahip olmak için, bu değerin türünü Option<T> yaparak açıkça seçmelisiniz. Ardından, bu değeri kullandığınızda, değer boş olduğunda durumu açıkça ele almanız gerekir. Bir değerin Option<T> olmayan bir türü olduğu her yerde, değerin boş olmadığını güvenle varsayabilirsiniz. Bu, Rust'ın null'ın yaygınlığını sınırlamak ve Rust kodunun güvenliğini artırmak için kasıtlı bir tasarım kararıydı.

Öyleyse, Option<T> türünde bir değeriniz olduğunda, bu değeri kullanabilmeniz için Some varyantından T değerini nasıl alırsınız? Option<T> enum'u, çeşitli durumlarda yararlı olan çok sayıda yönteme sahiptir; dokümantasyonundan kontrol edebilirsiniz. Option<T> üzerindeki metodlara aşina olmak, Rust ile olan yolculuğunuzda son derece yararlı olacaktır.

Genel olarak, bir Option<T> değeri kullanmak için her bir değişkeni işleyecek bir koda sahip olmak istersiniz. Yalnızca bir Some(T) değerine sahip olduğunuzda çalışacak bir kod istiyorsunuz ve bu kodun iç T'yi kullanmasına izin veriliyor. None değeriniz varsa ve bu kodda başka bir kodun çalıştırılmasını istiyorsunuz. bir T değeri mevcuttur. match ifadesi, numaralandırmalarla kullanıldığında tam da bunu yapan bir kontrol akışı yapısıdır: sahip olduğu numaralandırmanın hangi türevine bağlı olarak farklı kod çalıştırır ve bu kod, eşleşen değerin içindeki verileri kullanabilir.

match Kontrol Akışı Yapısı

Rust, bir değeri bir dizi modelle karşılaştırmanıza ve ardından hangi modelin eşleştiğine göre kod yürütmenize olanak tanıyan, match adı verilen son derece güçlü bir kontrol akışı yapısına sahiptir. Modeller değişmez değerlerden, değişken adlarından, joker karakterlerden ve diğer birçok şeyden oluşabilir; Bölüm 18, tüm farklı kalıp türlerini ve ne yaptıklarını kapsar. match'in gücü, modellerin ifade gücünden ve derleyicinin tüm olası vakaların ele alındığını doğrulamasından gelir.

Bir match ifadesini madeni para ayırma makinesi gibi düşünün: madeni paralar, üzerinde çeşitli büyüklükte delikler bulunan bir raydan aşağı kayar ve her madeni para, karşılaştığı ilk delikten girer ve sığar. Aynı şekilde, değerler bir match'te her bir modelden geçer ve ilk modelde değer “uyar”, değer, yürütme sırasında kullanılacak ilgili kod bloğuna düşer.

Madeni paralardan bahsetmişken, onları match'i kullanırken örnek verebiliriz! Bir Birleşik Devletler madeni parasını alan ve sayma makinesine benzer bir şekilde, hangi madeni para olduğunu belirleyen ve burada Liste 6-3'te gösterildiği gibi değerini sent olarak döndüren bir fonksiyon yazabiliriz.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}

Liste 6-3: Bir enum ve bir match ifadesi, model olarak enum türüne sahip

value_in_cents fonksiyonundaki match'i parçalayalım. İlk olarak, eşleşen anahtar kelimeyi ve ardından bu durumda jeton değeri olan bir ifadeyi listeleriz. Bu, if ile kullanılan bir ifadeye çok benziyor, ancak büyük bir fark var: if ile ifadenin bir Boole değeri döndürmesi gerekiyor, ancak burada herhangi bir tür döndürebilir. Bu örnekteki jeton türü, ilk satırda tanımladığımız Coin; enum'dur.

Sıradaki match kolları. Bu kolların iki parçası vardır: bir model ve bir miktar kod. Buradaki ilk kol Coin::Penny değerinde bir modele ve ardından model ile çalıştırılacak kodu ayıran => operatörüne sahiptir. Bu durumda kod sadece 1 değeridir. Her kol bir sonrakinden virgülle ayrılır.

match ifadesi yürütüldüğünde, elde edilen değeri sırayla her bir kolun modeliyle karşılaştırır. Bir model değerle eşleşirse, o kalıpla ilişkili kod yürütülür. Bu model değerle eşleşmezse, yürütme bir madeni para sıralama makinesinde olduğu gibi bir sonraki kola devam eder. İhtiyacımız olduğu kadar çok kola sahip olabiliriz: Liste 6-3'te match'imizde dört kol var.

Her kolla ilişkili kod bir ifadedir ve eşleşen koldaki ifadenin sonuç değeri, tüm match ifadesi için döndürülen değerdir.

Her bir kolun yalnızca bir değer döndürdüğü Liste 6-3'te olduğu gibi, match kolu kodu kısaysa genellikle süslü parantezler kullanmayız. Bir match kolunda birden çok kod satırı çalıştırmak istiyorsanız, süslü parantezleri kullanmanız gerekir ve kolu takip eden virgül isteğe bağlıdır. Örneğin, aşağıdaki kod “Lucky penny!” yazdırır. Metod bir Coin::Penny ile her çağrıldığında, ancak bloğun son değeri olan 1'i döndürür:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}

Değerlere Bağlanan Modeller

match kollarının bir başka kullanışlı özelliği de değerlerin modelle eşleşen kısımlarına bağlanabilmeleridir. enum değişkenlerinden değerleri bu şekilde çıkarabiliriz.

Örnek olarak, enum değişkenlerimizden birini veriyi içinde tutacak şekilde değiştirelim. 1999'dan 2008'e kadar Amerika Birleşik Devletleri, bir taraftaki 50 eyaletin her biri için farklı tasarımlara sahip paralar bastı. Başka hiçbir madeni paranın bu tarz bir devlet tasarımı yoktur, bu nedenle yalnızca çeyrekler buna sahiptir. Bu bilgiyi, Quarter değişkenini, burada Liste 6-4'te yaptığımız, içinde saklanan bir UsState değerini içerecek şekilde değiştirerek enum'a ekleyebiliriz.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {}

Liste 6-4: Quarter değişkeninin de bir UsState değerine sahip olduğu bir Coin enum'u

Bir arkadaşın 50 eyalet parasının tamamını toplamaya çalıştığını düşünelim. Bozuk paramızı madeni para türüne göre sıralarken, her üç aylık dönemle ilişkili eyaletin adını da söyleyeceğiz, böylece arkadaşımızın sahip olmadığı bir şey varsa, koleksiyonlarına ekleyebilirler.

Bu kodun match ifadesinde, Coin::Quarter varyantının değerleriyle eşleşen modele state adlı bir değişken ekleriz. Coin::Quarter eşleştiğinde, state değişkeni o çeyreğin durumunun değerine bağlanır. Daha sonra bu kolun kodundaki state'i şu şekilde kullanabiliriz:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

fn main() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

value_in_cents(Coin::Quarter(UsState::Alaska)) olarak çağıracak olsaydık, coin Coin::Quarter(UsState::Alaska) olurdu. Bu değeri match kollarının her biri ile karşılaştırdığımızda, Coin::Quarter(state) değerine ulaşana kadar hiçbiri eşleşmez. Bu noktada, state için değer UsState::Alaska olacaktır. Daha sonra bu atamayı println!'de kullanabiliriz.

Option<T>'la Eşleştirme

Önceki bölümde, Option<T> kullanırken Some durumundan iç T değerini almak istedik; Option<T> ile Coin enum'da yaptığımız gibi match'i kullanarak da işleyebiliriz! Madeni paraları karşılaştırmak yerine, Option<T>'nin türevlerini karşılaştıracağız, ancak match ifadesinin çalışma şekli aynı kalıyor.

Diyelim ki Option<i32> alan ve içinde bir değer varsa bu değere 1 ekleyen bir fonksiyon yazmak istiyoruz. İçinde bir değer yoksa, fonksiyon None değerini döndürmeli ve herhangi bir işlem yapmaya çalışmamalıdır.

Bu fonksiyonun yazılması, match sayesinde çok kolaydır ve Liste 6-5'e benzeyecektir.

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Liste 6-5: Option<i32> için match ifadesi kullanan bir fonksiyon

Şimdi plus_one'ın ilk süreklemesini daha detaylı inceleyelim. plus_one(five dediğimizde, plus_one gövdesindeki x değişkeni Some(5) değerine sahip olacaktır. Daha sonra bunu her bir match koluyla karşılaştırırız.

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Some(5) değeri None ile eşleşmediğinden dolayı sonraki kolla devam ediyoruz:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Some(5) Some(i) ile eşleşiyor mu? Neden evet olduğuna bakalım! Aynı varyanta sahibiz. i'ye Some içindeki değer atanır, bu yüzden 5 değerini alır. match kolundaki kod daha sonra çalıştırılır, bu yüzden i'nin değerine 1 ekliyoruz ve içindeki toplam 6 ile yeni bir Some değeri oluşturuyoruz.

Şimdi, x'in None olduğu Liste 6-5'teki ikinci plus_one çağrısını ele alalım. match'e giriyoruz ve ilk kolla karşılaştırıyoruz.

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Eşleşiyor! Eklenecek bir değer yoktur, bu nedenle program durur ve => öğesinin sağ tarafındaki None değerini döndürür. İlk kol eşleştiği için diğer kollar karşılaştırılmaz.

match ve enum'u birleştirmek çoğu durumda yararlıdır. Bu modeli birçok Rust kodunda göreceksiniz: enum eşleştirmek, içindeki verilere bir değişken bağlamak ve ardından buna dayalı olarak kod yürütmek. İlk başta biraz zor ama alıştıktan sonra tüm dillerde olmasını dileyeceksiniz. Rustseverlerin favorisidir.

match'ler Kapsamlıdır

match'in tartışmamız gereken başka bir yönü daha var: kollardaki modeller tüm olasılıkları kapsamalıdır. Bir hata içeren ve derlenmeyecek olan plus_one fonksiyonumuzun bu sürümünü düşünün:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

None durumunu ele almadık yani eğer None match'e argüman olarak verilirse bir sorun ortaya çıkacaktır. Şansımıza ki, bu sorun Rust'ın nasıl çözeceğini bildiği bir sorundur. Eğer kodu çalıştırırsak, Rust bize bu hatayı verecektir:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
   --> src/main.rs:3:15
    |
3   |         match x {
    |               ^ pattern `None` not covered
    |
    = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
    = note: the matched value is of type `Option<i32>`

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` due to previous error

Rust, olası her durumu ele almadığımızı ve hatta hangi modeli unuttuğumuzu biliyor! Rust'taki match'ler kapsamlıdır: Kodun geçerli olması için son tüm olasılıkları ele almalıyız. Özellikle Option<T> durumunda; Rust, None durumunu açıkça ele almayı unutmamızı engellediğinde, null'a sahip olabileceğimiz bir değere sahip olduğumuzu varsaymaktan bizi korur, böylece daha önce tartışılan milyar dolarlık hatayı imkansız hale getirir.

Tümünü Yakalama Modelleri ve _ Yer Tutucusu

enum'u kullanarak, belirli birkaç değer için özel eylemler de yapabiliriz, ancak diğer tüm değerler için bir varsayılan eylem gerçekleştirir. Bir zar atarken 3 atarsanız, oyuncunuzun hareket etmediği, bunun yerine yeni bir süslü şapka aldığı bir oyun uyguladığımızı hayal edin. 7 atarsanız, oyuncunuz süslü bir şapka kaybeder. Diğer tüm değerler için, oyuncunuz oyun tahtasında bu sayıda yeri hareket ettirir:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        other => move_player(other),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn move_player(num_spaces: u8) {}
}

İlk iki kol için modeller, 3 ve 7 değişmez değerleridir. Diğer tüm olası değerleri kapsayan son kol için, model, diğer olarak adlandırmayı seçtiğimiz değişkendir. Diğer kol için çalışan kod, değişkeni move_player fonksiyonuna geçirerek kullanır.

Bu kod, bir u8'in sahip olabileceği tüm olası değerleri listelememiş olsak bile derlenir, çünkü son model özel olarak listelenmemiş tüm değerlerle eşleşecektir. Bu tümünü yakalama modeli, match'in kapsamlı olması gerekliliğini karşılar. Modeller sırayla değerlendirildiği için tümünü yakalama kolunu en sona koymamız gerektiğini unutmayın. Her şeyi yakalama kolunu daha erken koyarsak, diğer kollar asla çalışmaz, bu yüzden hepsini yakalamadan sonra kolları eklersek Rust bizi uyarır!

Rust'ta ayrıca hepsini yakalamak istediğimizde ancak tümünü yakalama modelindeki değeri kullanmak istemediğimizde kullanabileceğimiz bir model vardır: _ herhangi bir değerle eşleşen ve o değere bağlanmayan özel bir modeldir. Bu, Rust'a değeri kullanmayacağımızı söyler, bu nedenle Rust bizi kullanılmayan bir değişken hakkında uyarmaz.

Oyunun kurallarını değiştirelim: şimdi, 3 veya 7'den başka bir şey atarsanız, tekrar atmanız gerekir. Artık tümünü yakalama değerini kullanmamız gerekmiyor, bu nedenle kodumuzu other adlı değişken yerine _ kullanacak şekilde değiştirebiliriz:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => reroll(),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn reroll() {}
}

Bu örnek ayrıca, son koldaki diğer tüm değerleri açıkça yok saydığımız için ayrıntılı olma gereksinimini de karşılamaktadır; hiçbir şeyi unutmadık.

Son olarak, oyunun kurallarını bir kez daha değiştireceğiz, böylece 3 veya 7'den başka bir şey atarsanız, başla bir şey ortaya çıkmaz. Bunu, _ ile gelen kod olarak birim değeri kullanarak ifade edebiliriz:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
}

Burada, Rust'a açıkça daha önceki bir koldaki bir modelle eşleşmeyen başka bir değer kullanmayacağımızı ve bu durumda herhangi bir kod çalıştırmak istemediğimizi söylüyoruz.

Modeller ve eşleştirme hakkında Bölüm 18'de ele alacağımız daha çok şey var. Şimdilik, match ifadesinin biraz kullanışsız olduğu durumlarda faydalı olabilecek if let söz dizimine geçeceğiz.

if let ile Kontrol Akışı

if let söz dizimi, if ve let sözcüklerini bir modelle eşleşen değerleri işlemek ve gerisini yok saymak için daha az ayrıntılı bir şekilde birleştirmenize olanak tanır. Liste 6-6'daki, config_max değişkenindeki bir Option<u8> değeriyle eşleşen, ancak yalnızca değer Some değişkeniyse kodu yürütmek isteyen programı düşünün. ode if the value is the Some variant.

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {}", max),
        _ => (),
    }
}

Liste 6-6: Değer yalnızca Some olduğunda kodu çalıştırmayı önemseyen bir eşleşme

Eğer değer Some ise, değeri modeldeki max değişkenine atayarak Some varyantındaki değeri yazdırırız. None değeriyle hiçbir şey yapmak istemiyoruz. match ifadesi için, yalnızca bir değişkeni işledikten sonra _ => () eklemeliyiz, bu da eklemesi can sıkıcı bir koddur.

Bunun yerine, if let yapısını kullanarak bunu daha kısa bir şekilde yazabiliriz. Aşağıdaki kod, Liste 6-6'daki match yapısıyla aynı şekilde davranır:

fn main() {
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {}", max);
    }
}

if let söz dizimi, eşittir işaretiyle ayrılmış bir model ve ifade alır. İfadenin match'e verildiği ve modelin ilk kolu olduğu bir match ile aynı şekilde çalışır. Bu durumda, model Some(max) şeklindedir ve max, Some içindeki değere atanır. Daha sonra, match yapısında max'ı kullandığımız gibi, if let bloğunun gövdesinde de maxı kullanabiliriz. Değer modelle eşleşmezse if let bloğundaki kod çalışmaz.

if let kullanmak, daha az yazma, daha az girinti ve daha az ilintili kod anlamına gelir. Ancak, match'in zorunlu kıldığı kapsamlı denetimi kaybedersiniz. match ve if let arasında seçim yapmak, kendi özel durumunuzda ne yaptığınıza ve kısalık kazanmanın kapsamlı kontrolü kaybetmek için uygun bir değiş tokuş olup olmadığına bağlıdır.

Başka bir deyişle, if let ifadesini, değer bir model eşleştiğinde kodu çalıştıran ve ardından diğer tüm değerleri yok sayan bir match için 'sözdizimsel tatlılık' olarak düşünebilirsiniz.

Bir if let'e else ekleyebiliriz. else ile gelen kod bloğu, if let ve else'e eş değer olan match ifadesindeki _ durumu ile kullanılacak kod bloğu ile aynıdır. Quarter varyantının da bir UsState değerine sahip olduğu Liste 6-4'teki Coin numralandırılmış yapı tanımını hatırlayın. Çeyreklerin durumunu açıklarken gördüğümüz tüm çeyrek olmayan paraları saymak istersek, bunu şöyle bir eşleşme ifadesi ile yapabilirdik:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {:?}!", state),
        _ => count += 1,
    }
}

Veya şu şekildeki bir if let ve else ifadesini kullanabiliriz:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {:?}!", state);
    } else {
        count += 1;
    }
}

Programınızın bir match kullanarak ifade edemeyecek kadar ayrıntılı bir mantığının olduğu bir durumunuz varsa, if let'in de Rust araç kutunuzda olduğunu unutmayın.

Özet

Şimdi, bir dizi numaralandırılmış değerden biri olabilecek özel türler oluşturmak için numaralandırmaların nasıl kullanılacağını ele aldık. Standart kütüphanenin sunmuş olduğu Option<T> türünün, hataları önlemek için tür sistemini kullanmanıza nasıl yardımcı olduğunu gösterdik. Numaralandırılmış yapı değerlerinin içinde veriler olduğunda, kaç modeli ele almanız gerektiğine bağlı olarak bu değerleri çıkarmak ve kullanmak için match veya if let kullanabilirsiniz.

Rust programlarınız artık yapıları ve numaralandırmaları kullanarak etki alanınızdaki kavramları ifade edebilir. API'nizde kullanılacak özel türler oluşturmak, tür güvenliğini sağlar: derleyici, fonksiyonlarınızın yalnızca her işlevin beklediği türden değerleri almasını sağlar.

Kullanıcılarınıza iyi organize edilmiş, kullanımı kolay ve yalnızca kullanıcılarınızın ihtiyaç duyacaklarını tam olarak ortaya koyan bir API sağlamak için şimdi Rust'ın modüllerine dönelim.

Büyüyen Projeleri Paketler, Kasalar ve Modüllerle Yönetme

Büyük programlar yazarken, kodunuzu düzenlemek giderek daha önemli hale gelecektir. İlgili fonksiyonları gruplandırarak ve kodu farklı özelliklerle ayırarak, belirli bir özelliği uygulayan kodu nerede bulacağınızı ve bir özelliğin nasıl çalıştığını değiştirmek için nereye gideceğinizi netleştireceksiniz.

Şimdiye kadar yazdığımız programlar tek bir dosyada tek bir modülde olmuştur. Bir proje büyüdükçe, kodu birden çok modüle ve ardından birden çok dosyaya bölerek düzenlemeniz gerekir. Bir paket, birden çok ikili kasa ve isteğe bağlı olarak bir kütüphane kasası içerebilir. Bir paket büyüdükçe, parçaları dış bağımlılıklar haline gelen ayrı kasalara çıkarabilirsiniz. Bu bölüm tüm bu teknikleri kapsar. Birlikte gelişen birbiriyle ilişkili bir dizi paketten oluşan çok büyük projeler için Cargo, Bölüm 14'teki “Cargo'nun Çalışma Alanları” bölümünde ele alacağımız çalışma alanları sağlar.

Ayrıca, kodu daha yüksek bir düzeyde yeniden kullanmanıza olanak tanıyan kapsülleme uygulama ayrıntılarını da tartışacağız: Bir işlemi uyguladıktan sonra, diğer kod, uygulamanın nasıl çalıştığını bilmek zorunda kalmadan ortak arabirimi aracılığıyla kodunuzu arayabilir. Kodu yazma şekliniz, diğer kodun kullanması için hangi bölümlerin genel olduğunu ve hangi bölümlerin değiştirme hakkını saklı tuttuğunuz özel uygulama ayrıntıları olduğunu tanımlar. Bu, kafanızda tutmanız gereken ayrıntı miktarını sınırlamanın başka bir yoludur.

İlgili bir kavram kapsamdır: Kodun yazıldığı iç içe bağlam, “kapsam dahilinde” olarak tanımlanan bir dizi isme sahiptir. Kod okurken, yazarken ve derlerken, programcılar ve derleyiciler, belirli bir noktadaki belirli bir adın bir değişkene, fonksiyona, yapıya, numaralandırılmış yapıya, modüle, sabite veya başka bir öğeye atıfta bulunup bulunmadığını ve bu öğenin ne anlama geldiğini bilmelidir. Kapsamlar oluşturabilir ve hangi adların kapsam içinde veya dışında olduğunu değiştirebilirsiniz. Aynı kapsamda aynı ada sahip iki öğeniz olamaz; ad çakışmalarını çözmek için araçlar mevcuttur.

Rust, hangi ayrıntıların açığa çıktığı, hangi ayrıntıların özel olduğu ve programlarınızdaki her kapsamda hangi adların bulunduğu dahil olmak üzere kodunuzun organizasyonunu yönetmenize olanak tanıyan bir dizi özelliğe sahiptir. Bazen toplu olarak modül sistemi olarak adlandırılan bu özellikler şunları içerir:

  • Paketler: Kasaları oluşturmanıza, test etmenize ve paylaşmanıza olanak tanıyan bir Cargo özelliği
  • Kasalar: Bir kütüphane ve yürütülebilir dosya oluşturan bir modül ağacı
  • Modüller: Yolların organizasyonunu, kapsamını ve gizliliğini kontrol etmeye izin
  • Yollar: Yapı, fonksiyon veya modül gibi bir öğeyi adlandırmanın bir yolu

Bu bölümde, tüm bu özellikleri ele alacağız, nasıl etkileşime girdiklerini tartışacağız ve kapsamı yönetmek için bunların nasıl kullanılacağını açıklayacağız.

Bölüm sonu canavarını kestiğinizde, modül sistemi hakkında sağlam bir anlayışa sahip olmalısınız ve bir usta gibi kapsamlarla çalışabilmelisiniz!

Paketler ve Kasalar

Modül sisteminin ele alacağımız ilk kısımları paketler ve kasalardır.

Kasa, Rust derleyicisinin bir seferde dikkate aldığı en küçük kod miktarıdır. cargo yerine rustc çalıştırsanız ve tek bir kaynak kod dosyası iletseniz bile (1. Bölüm'ün “Bir Rust Programı Yazma ve Çalıştırma” bölümünde yaptığımız gibi), derleyici bu dosyayı bir kasa olarak kabul eder. Kasalar modüller içerebilir ve modüller, sonraki bölümlerde göreceğimiz gibi, kasa ile derlenen diğer dosyalarda tanımlanabilir.

Bir kasa iki biçimde olabilir: ikili kasa veya kütüphane kasası. İkili kasalar, bir komut satırı programı veya bir sunucu gibi çalıştırabileceğiniz bir yürütülebilir dosyaya derleyebileceğiniz programlardır. Her birinin, yürütülebilir dosya çalıştığında ne olacağını tanımlayan main adlı fonksiyonu olmalıdır. Şimdiye kadar yarattığımız tüm kasalar ikili kasalardı.

Kütüphane kasalarının main fonksiyonu yoktur ve yürütülebilir bir dosyaya derlenmezler. Bunun yerine, birden çok projeyle paylaşılması amaçlanan fonksiyonları tanımlarlar. Örneğin, Bölüm 2'de kullandığımız rand kasası, sözde rastgele sayılar üreten işlevsellik sağlar. Rustseverler çoğu zaman “kasa” derken, kütüphane kasası demek isterler ve “kasa” sözcüğünü genel programlama kavramı olan “kütüphane” ile birbirinin yerine kullanırlar.

Kasa kökü, Rust derleyicisinin başlattığı ve kasanızın kök modülünü oluşturan bir kaynak dosyadır (“Kapsam ve Gizliliği Kontrol Etmek için Modüller Tanımlamak” bölümünde modülleri ayrıntılı olarak açıklayacağız).

Paket, bir dizi işlevsellik sağlayan bir veya daha fazla kasadan oluşan bir pakettir. Bir paket, bu kasaların nasıl oluşturulacağını açıklayan bir Cargo.toml dosyası içerir. Cargo aslında kodunuzu oluşturmak için kullandığınız komut satırı aracı için ikili kasayı içeren bir pakettir. Cargo paketi ayrıca ikili kasanın bağlı olduğu bir kütüphane kasası içerir. Diğer projeler, Cargo komut satırı aracının kullandığı mantığı kullanmak için Cargo kütüphane kasasını kullanabilir.

Bir paket, istediğiniz kadar ikili kasa içerebilir, ancak en fazla yalnızca bir kütüphane kasası olabilir. Bir paket, ister kitaplık ister ikili kasa olsun, en az bir kasa içermelidir.

Bir paket oluşturduğumuzda neler olduğunu gözden geçirelim. İlk olarak, cargo new komutunu giriyoruz:

$ cargo new my-project
     Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

cargo new'i çalıştırdıktan sonra, cargo'nun ne oluşturduğunu görmek için ls kullanırız. Proje dizininde bize bir paket veren bir Cargo.toml dosyası var. Ayrıca main.rs'i içeren bir src dizini vardır. Metin düzenleyicinizde Cargo.toml'yi açın ve burada src/main.rs'den bahsedilmediğini unutmayın. Cargo, src/main.rs öğesinin, paketle aynı ada sahip bir ikili kasanın kasa kökü olduğu kuralına uyar. Benzer şekilde, Cargo, paket dizini src/lib.rs içeriyorsa, paketin paketle aynı ada sahip bir kütüphane kasası içerdiğini ve src/lib.rs'nin bunun kasa kökü olduğunu bilir. Cargo, kütüphaneyi veya ikili dosyayı oluşturmak için sandık kök dosyalarını rustc'ye iletir.

Burada, yalnızca src/main.rs içeren bir paketimiz var, yani yalnızca my-project adında bir ikili sandık içeriyor. Bir paket src/main.rs ve src/lib.rs içeriyorsa, iki kasası vardır: ikisi de paketle aynı ada sahip ikili dosya ve kütüphane. Bir paket, dosyaları src/bin dizinine yerleştirerek birden çok ikili kasaya sahip olabilir: her dosya ayrı bir ikili kasa olacaktır.

Kapsam ve Gizliliği Kontrol Etmek İçin Modüllerin Tanımlanması

Bu bölümde, modüller ve modül sisteminin diğer bölümlerinden, yani öğeleri adlandırmanıza izin veren yollardan bahsedeceğiz; kapsam içine bir yol getiren use anahtar sözcüğü; ve öğeleri herkese açık hale getirmek için pub anahtar sözcüğü. Ayrıca as anahtar sözcüğünü, harici paketleri ve glob operatörünü tartışacağız.

İlk olarak, kodunuzu düzenlerken kolay referans için bir kurallar listesiyle başlayacağız. Ardından, kuralların her birini ayrıntılı olarak açıklayacağız.

Modüllerin Ana Hatları

Burada modüllerin, yolların, use anahtar sözcüğünün ve pub anahtar sözcüğünün derleyicide nasıl çalıştığı ve çoğu geliştiricinin kodlarını nasıl düzenlediği hakkında hızlı bir referans sağlıyoruz. Bu bölüm boyunca bu kuralların her birinin örneklerini inceleyeceğiz, ancak şu an, modüllerin nasıl çalıştığını hatırlatmak için harika bir zamandır.

  • Sandık kökünden başlamak: Bir kasayı derlerken, derleyici ilk olarak kasa kök dosyasına bakar (genellikle bir kütüphane kasası için src/lib.rs veya bir ikili kasa için src/main.rs).
  • Modüller tanımlamak: Sandık kök dosyasında yeni modüller tanımlayabilirsiniz; diyelim ki mod garden ile bir “garden” modülü tanımladınız; Derleyici, modülün kodunu şu yerlerde arayacaktır:
    • Noktalı virgül yerine süslü parantez içinde mod garden'ı doğrudan takip eden satır içi
    • src/garden.rs dosyasında
    • src/garden/mod.rs dosyasında
  • Alt modül tanımlamak: Sandık kökü dışındaki herhangi bir dosyada alt modüller bildirebilirsiniz. Örneğin, mod vegetables tanımlayabilirsiniz; src/garden.rs'de olacak şekilde. Derleyici, aşağıdaki yerlerde ana modül için adlandırılan dizinde alt modülün kodunu arayacaktır:
    • Satır içi, noktalı virgül yerine süslü parantezler içinde; mod vegetables'ın hemen ardından
    • src/garden/vegetables.rs dosyasında
    • src/garden/vegetables/mod.rs dosyasında
  • Modüllerde kodlama yolları: Bir modül kasanızın bir parçası olduğunda, gizlilik kurallarının izin verdiği sürece, kodun yolunu kullanarak aynı kasadaki herhangi bir yerden o modüldeki koda başvurabilirsiniz. Örneğin, vegetables modülündeki bir Asparagus türü crate::garden::vegetables::Asparagus'ta bulunur.
  • Özel vs genel: Bir modül içindeki kod, varsayılan olarak üst modüllerinden özeldir. Bir modülü herkese açık hale getirmek için mod yerine pub mod ile bildirin. Bir genel modül içindeki öğeleri de herkese açık hale getirmek için, bildirimlerinden önce pub'ı kullanın.
  • use anahtar sözcüğü: Bir kapsam içinde, use anahtar sözcüğü, uzun yolların tekrarını azaltmak için öğelere kısayollar oluşturur. crate::garden::vegetables::Asparagus ile ilgili olabilecek herhangi bir kapsamda, use crate::garden::vegetables::Asparagus kullanarak bir kısayol oluşturabilirsiniz; ve o andan itibaren bu türden kapsamda faydalanmak için sadece Asparagus yazmanız yeterlidir.

Burada, bu kuralları gösteren backyard adında bir ikili sandık oluşturuyoruz. Kasanın backyard olarak da adlandırılan dizini şu dosyaları ve dizinleri içerir:

backyard
├── Cargo.lock
├── Cargo.toml
└── src
    ├── garden
    │   └── vegetables.rs
    ├── garden.rs
    └── main.rs

Bu durumda sandık kök dosyası src/main.rs şeklindedir ve şunları içerir:

Dosya adı: src/main.rs

use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
    let plant = Asparagus {};
    println!("I'm growing {:?}!", plant);
}

pub mod garden; satırı, derleyiciye src/garden.rs içinde bulduğu kodu eklemesini söyler;

Dosya adı: src/garden.rs

pub mod vegetables;

Burada pub mod vegetables, src/garden/vegetables.rs içindeki kodun da dahil olduğu anlamına gelir. Bu kod:

#[derive(Debug)]
pub struct Asparagus {}

Şimdi bu kuralların ayrıntılarına girelim ve bunları uygulamada gösterelim!

İlgili Kodu Modüllerde Gruplama

Modüller, okunabilirlik ve kolay yeniden kullanım için kodu bir kasa içinde düzenlememize izin verir. Modüller ayrıca, bir modül içindeki kod varsayılan olarak özel olduğundan, öğelerin gizliliğini kontrol etmemizi sağlar. Özel öğeler, harici kullanım için mevcut olmayan dahili uygulama ayrıntılarıdır. Modülleri ve içlerindeki öğeleri herkese açık hale getirmeyi seçebiliriz, bu da onları harici kodun kullanmasına ve bunlara bağımlı olmasına izin verecek şekilde ortaya çıkarır.

Örnek olarak bir restoranın fonksiyonelliğini sağlayan bir kütüphane kasası yazalım. Fonksiyonların imzalarını tanımlayacağız, ancak bir restoranın süreklenmesinden ziyade kodun organizasyonuna konsantre olmak için fonksiyon içlerini boş bırakacağız.

Restoran endüstrisinde, bir restoranın bazı bölümleri evin önü, diğerleri ise evin arkası olarak adlandırılır. Evin önü müşterilerin bulunduğu yerdir; bu, ev sahiplerinin müşterileri oturduğu, sunucuların siparişleri ve ödemeleri aldığı ve barmenlerin içecek verdiği yerleri kapsar. Evin arkası, mutfakta şeflerin ve aşçıların çalıştığı, bulaşık makinelerinin temizlik yaptığı ve yöneticilerin idari işleri yaptığı yerdir.

Kasamızı bu şekilde yapılandırmak için işlevlerini iç içe modüller halinde düzenleyebiliriz. cargo new --lib restaurant komutunu kullanarak restaurant adında yeni kütüphane oluşturun, daha sonra bazı modülleri ve fonksiyon imzalarını tanımlamak için Liste 7-1'deki kodu src/lib.rs içine girin. İşte evin ön cephesi:

Dosya adı: src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

Liste 7-1: Fonksiyonları içeren ve diğer modülleri içeren bir front_of_house modülü

mod anahtar sözcüğünü ve ardından modülün adını (bu durumda, front_of_house) takip eden bir modül tanımlarız. Modülün gövdesi süslü parantezlerin içine girer. Modüllerin içine, diğer modülleri de yerleştirebiliriz, bu durumda modüllerin barındırılması ve sunulmasında olduğu gibi. Modüller ayrıca yapılar, numaralandırmalar, sabitler, özellikler ve -Liste 7-1'de olduğu gibi- fonksiyonlar gibi diğer öğeler için tanımları da içerebilir.

Modülleri kullanarak ilgili tanımları birlikte gruplayabilir ve neden ilişkili olduklarını adlandırabiliriz. Bu kodu kullanan programcılar, tüm tanımları okumak zorunda kalmadan gruplara göre kodda gezinebilir, bu da kendileriyle ilgili tanımları bulmayı kolaylaştırır. Bu koda yeni fonksiyonlar ekleyen programcılar, programı düzenli tutmak için kodu nereye yerleştireceklerini bilirler.

Daha önce, src/main.rs ve src/lib.rs'nin kasa kökleri olarak adlandırıldığından bahsetmiştik. Adlarının nedeni, bu iki dosyadan herhangi birinin içeriğinin, kasanın modül yapısının kökünde, modül ağacı olarak bilinen crate adlı bir modül oluşturmasıdır.

Liste 7-2, Liste 7-1'deki yapı için modül ağacını gösterir.

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

Liste 7-2: Liste 7-1'deki kod için modül ağacı

Bu ağaç, bazı modüllerin birbirinin içine nasıl yuvalandığını gösterir; örneğin, front_of_house içinde hosting'i barındırır. Ağaç ayrıca bazı modüllerin birbiriyle kardeş olduğunu, yani aynı modülde tanımlandıklarını gösterir; hosting ve serving, front_of_house içinde tanımlanan kardeşlerdir. A modülü B modülünün içindeyse, A modülünün B modülünün çocuğu olduğunu ve B modülünün A modülünün ebeveyni olduğunu söyleriz. Tüm modül ağacının crate adlı örtük modül altında köklendiğine dikkat edin.

Modül ağacı size bilgisayarınızdaki dosya sisteminin dizin ağacını hatırlatabilir; bu çok uygun bir karşılaştırma! Tıpkı bir dosya sistemindeki dizinler gibi, kodunuzu düzenlemek için modülleri kullanırsınız. Ve tıpkı bir dizindeki dosyalar gibi, modüllerimizi bulmanın da kolay bir yoluna ihtiyacımız vardır.

Modül Ağacındaki Bir Öğeye Başvuru Yolları

Bir modül ağacında bir öğeyi nerede bulacağını Rust'a göstermek için, bir dosya sisteminde gezinirken bir yol kullandığımız gibi bir yol kullanırız. Bir fonksiyonu çağırmak için yolunu bilmemiz gerekir.

Bir yol iki şekilde olabilir:

  • Mutlak yol, sandık kökünden başlayan tam yoldur; harici bir kasadan gelen kod için, mutlak yol kasa adıyla başlar ve mevcut kasadan gelen kod için değişmez kasayla başlar.
  • Göreli yol, geçerli modülden başlar ve geçerli modülde self, super veya bir tanımlayıcı kullanır.

Hem mutlak hem de göreli yolları, çift iki nokta (::) üst üste ile ayrılmış bir veya daha fazla tanımlayıcı izler.

Liste 7-1'e dönersek, add_to_waitlist fonksiyonunu çağırmak istediğimizi varsayalım. Bu, şunu sormakla aynıdır: add_to_waitlist fonksiyonunun yolu nedir? Liste 7-3, bazı modüller ve fonksiyonlar kaldırılmış olarak Liste 7-1'i içerir. Kasa kökünde tanımlanan yeni bir eat_at_restaurant fonksiyonundan sonra add_to_waitlist fonksiyonunu çağırmanın iki yolunu göstereceğiz. eat_at_restaurant fonksiyonu, kütüphane kasamızın genel API'sinin bir parçasıdır, bu yüzden onu pub anahtar kelimesiyle işaretliyoruz. pub Anahtar Sözcüğüyle Yolları Gösterme” bölümünde, pub hakkında daha fazla ayrıntıya gireceğiz. Bu örneğin henüz derlenmeyeceğini unutmayın; nedenini birazdan açıklayacağız.

Dosya adı: src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Liste 7-3: Mutlak ve göreli yolları kullanarak add_to_waitlist fonksiyonunu çağırma

eat_at_restaurant'ta add_to_waitlist fonksiyonunu ilk çağırdığımızda, mutlak bir yol kullanırız. add_to_waitlist fonksiyonu, eat_at_restaurant ile aynı kasada tanımlanır; bu, mutlak bir yol başlatmak için crate anahtar sözcüğünü kullanabileceğimiz anlamına gelir. Ardından, add_to_waitlist'e gidene kadar ardışık modüllerin her birini dahil ederiz. Aynı yapıya sahip bir dosya sistemi hayal edebilirsiniz: add_to_waitlist programını çalıştırmak için /front_of_house/hosting/add_to_waitlist yolunu belirtirdik; kasa kökünden başlamak için crate adını kullanmak, kabuğunuzdaki dosya sistemi kökünden başlamak için / kullanmaya benzer.

eat_at_restaurant'ta add_to_waitlist'i ikinci kez çağırdığımızda, göreceli bir yol kullanırız. Yol, modül ağacının eat_at_restaurant ile aynı düzeyinde tanımlanan modülün adı olan front_of_house ile başlar. Burada dosya sistemi eşdeğeri front_of_house/hosting/add_to_waitlist yolunu kullanıyor olacaktır. Bir modül adıyla başlamak, yolun göreceli olduğu anlamına gelir.

Göreceli mi yoksa mutlak yol mu kullanacağınızı seçmek, projenize göre vereceğiniz bir karardır ve öğe tanım kodunu öğeyi kullanan koddan ayrı mı yoksa onunla birlikte mi taşıma olasılığınızın daha yüksek olduğuna bağlıdır. Örneğin, front_of_house modülünü ve eat_at_restaurant fonksiyonunu customer_experience adlı bir modüle taşırsak, mutlak yolu add_to_waitlist'e güncellememiz gerekir, ancak göreli yol yine de geçerli olur. Ancak, eat_at_restaurant fonksiyonunu dining adlı bir modüle ayrı olarak taşırsak, add_to_waitlist çağrısının mutlak yolu aynı kalır, ancak göreli yolun güncellenmesi gerekir. Genel olarak tercihimiz mutlak yollar belirtmektir, çünkü kod tanımlarını ve öğe çağrılarını birbirinden bağımsız olarak taşımak isteyeceğimiz daha olasıdır.

Liste 7-3'ü derlemeye çalışalım ve neden henüz derlenmediğini öğrenelim! Aldığımız hata Liste 7-4'te gösterilmektedir.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
 --> src/lib.rs:9:28
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                            ^^^^^^^ private module
  |
note: the module `hosting` is defined here
 --> src/lib.rs:2:5
  |
2 |     mod hosting {
  |     ^^^^^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:12:21
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                     ^^^^^^^ private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

Liste 7-4: Liste 7-3'te kodun oluşturulmasından kaynaklanan derleyici hataları

Hata mesajları, modül hosting'in gizli olduğunu söylüyor. Başka bir deyişle, hosting modülü ve add_to_waitlist fonksiyonu için doğru yollara sahibiz, ancak gizli bölümlere erişimi olmadığı için Rust bunları kullanmamıza izin vermiyor. Rust'ta tüm öğeler (fonksiyonlar, yöntemler, yapılar, numaralandırmalar, modüller ve sabitler) varsayılan olarak üst modüllere gizlidir. Bir fonksiyon veya yapı gibi bir öğeyi gizli yapmak istiyorsanız, onu bir modüle koyarsınız.

Bir üst modüldeki öğeler, alt modüllerdeki gizli öğeleri kullanamaz, ancak alt modüllerdeki öğeler, üst modüllerindeki öğeleri kullanabilir. Bunun nedeni, alt modüllerin uygulama ayrıntılarını sarması ve gizlemesidir, ancak alt modüller tanımlandıkları bağlamı görebilir. Metaforumuza devam etmek için, gizlilik kurallarını bir restoranın arka ofisi gibi düşünün: orada olup bitenler restoran müşterilerine özeldir, ancak ofis yöneticileri işlettikleri restoranda her şeyi görebilir ve yapabilir.

Rust, modül sisteminin bu şekilde çalışmasını seçti, böylece dahili uygulama ayrıntılarını gizlemek varsayılandır. Bu şekilde, dış kodu bozmadan iç kodun hangi kısımlarını değiştirebileceğinizi bilirsiniz. Ancak Rust, bir öğeyi herkese açık hale getirmek için pub anahtar sözcüğünü kullanarak alt modüllerin kodunun iç kısımlarını dış üst modüllere gösterme seçeneği sunar.

pub Anahtar Kelimesiyle Yolları Gösterme

hosting modülünün gizli olduğunu söyleyen Liste 7-4'teki hataya dönelim. Ana modüldeki eat_at_restaurant fonksiyonunun alt modüldeki add_to_waitlist fonksiyonuna erişmesini istiyoruz, bu nedenle hosting modülünü Liste 7-5'te gösterildiği gibi pub anahtar sözcüğüyle işaretliyoruz.

Dosya adı: src/lib.rs

mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Liste 7-5: eat_at_restaurant'tan kullanmak için hosting modülünü pub olarak bildirme

Ne yazık ki, Liste 7-5'teki kod, Liste 7-6'da gösterildiği gibi hala bir hatayla sonuçlanıyor.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
 --> src/lib.rs:9:37
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                                     ^^^^^^^^^^^^^^^ private function
  |
note: the function `add_to_waitlist` is defined here
 --> src/lib.rs:3:9
  |
3 |         fn add_to_waitlist() {}
  |         ^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:12:30
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
3  |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

Liste 7-6: Liste 7-5'te kodun oluşturulmasından kaynaklanan derleyici hataları

Ne oldu? mod hosting'in önüne pub anahtar sözcüğünü eklemek, modülü herkese açık hale getirir. Bu değişiklikle front_of_house'a erişebilirsek, hosting'e de erişebiliriz. Ancak hosting içeriği hala gizlidir; modülü herkese açık hale getirmek, içeriğini herkese açık hale getirmez. Bir modüldeki pub anahtar sözcüğü, iç koduna erişmesine değil, yalnızca ata modüllerindeki kodun ona başvurmasına izin verir. Modüller kapsayıcı olduğundan, yalnızca modülü herkese açık hale getirerek yapabileceğimiz pek bir şey yoktur; daha ileri gitmemiz ve modül içindeki bir veya daha fazla öğeyi de herkese açık hale getirmeyi seçmemiz gerekiyor.

Liste 7-6'daki hatalar, add_to_waitlist fonksiyonunun gizli olduğunu söylüyor. Gizlilik kuralları yapılar, numaralandırmalar, fonksiyonlar ve yöntemler ile modüller için geçerlidir.

Ayrıca Liste 7-7'deki gibi tanımından önce pub anahtar sözcüğünü ekleyerek add_to_waitlist fonksiyonunu genel yapalım.

Dosya adı: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Liste 7-7: mod hosting ve fn add_to_waitlist'e pub eklemek, fonksiyonu eat_at_restaurant'tan çağırabilmemizi sağlar

Kod derlenecek! pub anahtar sözcüğünü eklemenin neden bu yolları add_to_waitlist'te gizlilik kurallarına göre kullanmamıza izin verdiğini görmek için mutlak ve göreli yollara bakalım.

Mutlak yolda, kasamızın modül ağacının kökü olan crate ile başlıyoruz. front_of_house modülü kasa kökünde tanımlanır. front_of_house herkese açık olmasa da, eat_at_restaurant fonksiyonu front_of_house ile aynı modülde tanımlandığından (yani, eat_at_restaurant ve front_of_house kardeştir), eat_at_restaurant'tan front_of_house'a başvurabiliriz. Sonraki, pub ile işaretlenmiş barındırma modülüdür. Barındırma ana modülüne erişebiliriz, böylece barındırmaya erişebiliriz. Son olarak, add_to_waitlist fonksiyonu pub ile işaretlenmiştir ve üst modülüne erişebiliriz, böylece bu fonksiyon çağrısı çalışır!

Göreceli yolda mantık, ilk adım dışında mutlak yolla aynıdır: crate kökünden başlamak yerine, yol front_of_house'dan başlar. front_of_house modülü, eat_at_restaurant ile aynı modül içinde tanımlanır, bu nedenle, eat_at_restaurant'ın tanımlandığı modülden başlayan göreli yol çalışır. Ardından, hosting ve add_to_waitlist pub ile işaretlendiğinden, yolun geri kalanı çalışır ve bu fonksiyon çağrısı da geçerlidir ve çalışır!

Diğer projelerin kodunuzu kullanabilmesi için kütüphane kasanızı paylaşmayı planlıyorsanız, genel API'niz, kasanızın kullanıcılarıyla, kodunuzla nasıl etkileşim kurabileceklerini belirleyen sözleşmenizdir. İnsanların kasanıza bağımlı olmasını kolaylaştırmak için genel API'nizde yapılan değişiklikleri yönetme konusunda birçok husus vardır. Bu düşünceler bu kitabın kapsamı dışındadır; Bu konuyla ilgileniyorsanız, bkz. Rust API Yönergeleri.

İkili Program ve Kitaplık İçeren Paketler için En İyi Uygulamalar

Bir paketin hem bir src/main.rs ikili kasa kökü hem de bir src/lib.rs kütüphane kasa kökü içerebileceğinden ve her iki kasanın da varsayılan olarak paket adına sahip olacağından bahsetmiştik. Tipik olarak, hem kütüphane hem de ikili kasa içeren bu modele sahip paketler, ikili kasada kütüphane kasasıyla kod çağıran bir yürütülebilir dosyayı başlatmak için yeterli koda sahip olacaktır. Bu, kütüphane kasasının kodu paylaşılabildiğinden, diğer projelerin paketin sağladığı en fazla işlevsellikten faydalanmasını sağlar. Modül ağacı src/lib.rs içinde tanımlanmalıdır. Ardından, paketin adıyla yolları başlatarak ikili sandıkta herhangi bir genel öğe kullanılabilir. İkili kasa, tıpkı tamamen harici bir kasanın kütüphane kasasını kullanması gibi kütüphane kasasının bir kullanıcısı olur: yalnızca genel API'yi kullanabilir. Bu, iyi bir API tasarlamanıza yardımcı olur; sadece yazar değil, aynı zamanda bir müşterisiniz!

Bölüm 12'de hem ikili kasa hem de kitaplık kasası içeren bir komut satırı programıyla bu organizasyonel uygulamayı göstereceğiz.

Göreli Yolları super ile Başlatma

Yolun başlangıcında super kullanarak, geçerli modül veya kasa kökü yerine ana modülde başlayan göreli yollar oluşturabiliriz. Bu, .. söz dizimi ile bir dosya sistemi yolunu başlatmak gibidir. Bu, üst modülde olduğunu bildiğimiz bir öğeye başvurmamızı sağlar; bu, modül üst öğeyle yakından ilişkili olduğunda modül ağacının yeniden düzenlenmesini kolaylaştırabilir, ancak üst öğe bir gün modül ağacında başka bir yere taşınabilir.

Bir şefin yanlış bir siparişi düzelttiği ve bunu müşteriye kişisel olarak sunduğu durumu modelleyen Liste 7-8'deki kodu göz önünde bulundurun. back_of_house modülünde tanımlanan fix_incorrect_order fonksiyonu, super ile başlayan deliver_order yolunu belirterek üst modülde tanımlanan deliver_order fonksiyonunu çağırır:

Dosya adı: src/lib.rs

fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order();
    }

    fn cook_order() {}
}

Liste 7-8: super ile başlayan göreli bir yol kullanarak bir fonksiyonu çağırma

fix_incorrect_order fonksiyonu back_of_house modülündedir, bu nedenle super'i back_of_house'un üst modülüne gitmek için kullanabiliriz, bu durumda kasa, köktür. Oradan deliver_order arar ve buluruz. Başarılı! Kasanın modül ağacını yeniden düzenlemeye karar vermemiz durumunda, back_of_house modülünün ve deliver_order fonksiyonunun birbiriyle aynı ilişkide kalacağını ve birlikte hareket edeceğini düşünüyoruz. Bu nedenle, gelecekte bu kod farklı bir modüle taşınırsa kodu güncellemek için daha az yerimiz olacağı için super kullandık.

Yapıları ve Numaralandırmaları Herkese Açık Yapma

Yapıları ve numaralandırılmış yapıları public olarak atamak için pub'ı da kullanabiliriz, ancak pub'ın yapılar ve numaralandırmalarla kullanımına ilişkin birkaç ayrıntı daha vardır. Bir struct tanımından önce pub kullanırsak, struct'ı herkese açık yaparız, ancak struct'ın alanları yine gizli olur. Her bir alanı duruma göre kamuya açık hale getirebilir veya açıklamayabiliriz. Liste 7-9'da, genel bir toast üyesi, ancak özel bir seasonal_fruit üyesi olan bir genel back_of_house::Breakfast yapısı tanımladık. Bu, müşterinin yemekle birlikte gelen ekmeğin türünü seçebildiği, ancak şefin mevsime ve stokta bulunanlara göre yemeğe hangi meyvenin eşlik edeceğine karar verdiği bir restorandaki durumu modellemektedir. Mevcut meyve stoğu hızla değişir, bu nedenle müşteriler meyveyi seçemez ve hatta hangi meyveyi alacaklarını göremezler.

Dosya adı: src/lib.rs

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal
    // meal.seasonal_fruit = String::from("blueberries");
}

Liste 7-9: Bazı ortak üyeler ve bazı gizli üyeler içeren bir yapı

back_of_house::Breakfast yapısındaki toast alanı genel olduğundan, eat_at_restaurant'ta nokta gösterimini kullanarak toast alanına yazabilir ve okuyabiliriz. seasonal_fruit gizli olduğu için eat_at_restaurant'ta seasonal_fruit üyesini kullanamadığımıza dikkat edin. Hangi hatayı aldığınızı görmek için seasonal_fruit üye değerini değiştirerek satırın yorumunu kaldırmayı deneyin!

Ayrıca, back_of_house::Breakfast'ın gizli bir alanı olduğundan, yapının bir Breakfast örneği oluşturan ortak bir ilişkili fonksiyon sağlaması gerektiğini unutmayın (buraya summer adını verdik). Eğer Breakfast'ın böyle bir fonksiyonu olmasaydı, eat_at_restaurant'ta özel seasonal_fruit üyesinin değerini ayarlayamadığımız için, eat_at_restaurant'ta bir Breakfast örneği oluşturamazdık.

Buna karşılık, bir numaralandırmayı herkese açık yaparsak, tüm varyantları genel olur. Liste 7-10'da gösterildiği gibi, pub'a yalnızca enum anahtar sözcüğünden önce ihtiyacımız var.

Dosya adı: src/lib.rs

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

Liste 7-10: Bir numaralandırmanın genel olarak atanması, tüm türevlerini herkese açık hale getirir

Appetizer enum'u public yaptığımız için, eat_at_restaurant'ta Soup ve Salad çeşitlerini kullanabiliriz.

Değişkenleri herkese açık olmadığı sürece, numaralandırmalar pek kullanışlı değildir; her durumda tüm numaralandırma değişkenlerine pub ile açıklama eklemek can sıkıcı olurdu, bu nedenle numaralandırma değişkenleri için varsayılan değer herkese açık olmaktır. Yapılar genellikle alanları herkese açık olmadan yararlıdır, bu nedenle yapı alanları, pub ile açıklama yapılmadığı sürece varsayılan olarak her şeyin gizli olduğu genel kuralını izler.

pub ile ilgili ele almadığımız bir durum daha var ve bu bizim son modül sistemi özelliğimiz: use anahtar sözcüğü. İlk önce use'ı tek başına ele alacağız ve ardından pub ve use'ın nasıl birleştirileceğini göstereceğiz.

Bringing Paths into Scope with the use Keyword

Having to write out the paths to call functions can feel inconvenient and repetitive. In Listing 7-7, whether we chose the absolute or relative path to the add_to_waitlist function, every time we wanted to call add_to_waitlist we had to specify front_of_house and hosting too. Fortunately, there’s a way to simplify this process: we can create a shortcut to a path with the use keyword once, and then use the shorter name everywhere else in the scope.

In Listing 7-11, we bring the crate::front_of_house::hosting module into the scope of the eat_at_restaurant function so we only have to specify hosting::add_to_waitlist to call the add_to_waitlist function in eat_at_restaurant.

Filename: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

Listing 7-11: Bringing a module into scope with use

Adding use and a path in a scope is similar to creating a symbolic link in the filesystem. By adding use crate::front_of_house::hosting in the crate root, hosting is now a valid name in that scope, just as though the hosting module had been defined in the crate root. Paths brought into scope with use also check privacy, like any other paths.

Note that use only creates the shortcut for the particular scope in which the use occurs. Listing 7-12 moves the eat_at_restaurant function into a new child module named customer, which is then a different scope than the use statement, so the function body won’t compile:

Filename: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

mod customer {
    pub fn eat_at_restaurant() {
        hosting::add_to_waitlist();
    }
}

Listing 7-12: A use statement only applies in the scope it’s in

The compiler error shows that the shortcut no longer applies within the customer module:

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
  --> src/lib.rs:11:9
   |
11 |         hosting::add_to_waitlist();
   |         ^^^^^^^ use of undeclared crate or module `hosting`

warning: unused import: `crate::front_of_house::hosting`
 --> src/lib.rs:7:5
  |
7 | use crate::front_of_house::hosting;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` due to previous error; 1 warning emitted

Notice there’s also a warning that the use is no longer used in its scope! To fix this problem, move the use within the customer module too, or reference the shortcut in the parent module with super::hosting within the child customer module.

Creating Idiomatic use Paths

In Listing 7-11, you might have wondered why we specified use crate::front_of_house::hosting and then called hosting::add_to_waitlist in eat_at_restaurant rather than specifying the use path all the way out to the add_to_waitlist function to achieve the same result, as in Listing 7-13.

Filename: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
}

Listing 7-13: Bringing the add_to_waitlist function into scope with use, which is unidiomatic

Although both Listing 7-11 and 7-13 accomplish the same task, Listing 7-11 is the idiomatic way to bring a function into scope with use. Bringing the function’s parent module into scope with use means we have to specify the parent module when calling the function. Specifying the parent module when calling the function makes it clear that the function isn’t locally defined while still minimizing repetition of the full path. The code in Listing 7-13 is unclear as to where add_to_waitlist is defined.

On the other hand, when bringing in structs, enums, and other items with use, it’s idiomatic to specify the full path. Listing 7-14 shows the idiomatic way to bring the standard library’s HashMap struct into the scope of a binary crate.

Filename: src/main.rs

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

Listing 7-14: Bringing HashMap into scope in an idiomatic way

There’s no strong reason behind this idiom: it’s just the convention that has emerged, and folks have gotten used to reading and writing Rust code this way.

The exception to this idiom is if we’re bringing two items with the same name into scope with use statements, because Rust doesn’t allow that. Listing 7-15 shows how to bring two Result types into scope that have the same name but different parent modules and how to refer to them.

Filename: src/lib.rs

use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
    Ok(())
}

fn function2() -> io::Result<()> {
    // --snip--
    Ok(())
}

Listing 7-15: Bringing two types with the same name into the same scope requires using their parent modules.

As you can see, using the parent modules distinguishes the two Result types. If instead we specified use std::fmt::Result and use std::io::Result, we’d have two Result types in the same scope and Rust wouldn’t know which one we meant when we used Result.

Providing New Names with the as Keyword

There’s another solution to the problem of bringing two types of the same name into the same scope with use: after the path, we can specify as and a new local name, or alias, for the type. Listing 7-16 shows another way to write the code in Listing 7-15 by renaming one of the two Result types using as.

Filename: src/lib.rs

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
    Ok(())
}

fn function2() -> IoResult<()> {
    // --snip--
    Ok(())
}

Listing 7-16: Renaming a type when it’s brought into scope with the as keyword

In the second use statement, we chose the new name IoResult for the std::io::Result type, which won’t conflict with the Result from std::fmt that we’ve also brought into scope. Listing 7-15 and Listing 7-16 are considered idiomatic, so the choice is up to you!

Re-exporting Names with pub use

When we bring a name into scope with the use keyword, the name available in the new scope is private. To enable the code that calls our code to refer to that name as if it had been defined in that code’s scope, we can combine pub and use. This technique is called re-exporting because we’re bringing an item into scope but also making that item available for others to bring into their scope.

Listing 7-17 shows the code in Listing 7-11 with use in the root module changed to pub use.

Filename: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

Listing 7-17: Making a name available for any code to use from a new scope with pub use

Before this change, external code would have to call the add_to_waitlist function by using the path restaurant::front_of_house::hosting::add_to_waitlist(). Now that this pub use has re-exported the hosting module from the root module, external code can now use the path restaurant::hosting::add_to_waitlist() instead.

Re-exporting is useful when the internal structure of your code is different from how programmers calling your code would think about the domain. For example, in this restaurant metaphor, the people running the restaurant think about “front of house” and “back of house.” But customers visiting a restaurant probably won’t think about the parts of the restaurant in those terms. With pub use, we can write our code with one structure but expose a different structure. Doing so makes our library well organized for programmers working on the library and programmers calling the library. We’ll look at another example of pub use and how it affects your crate’s documentation in the “Exporting a Convenient Public API with pub use section of Chapter 14.

Using External Packages

In Chapter 2, we programmed a guessing game project that used an external package called rand to get random numbers. To use rand in our project, we added this line to Cargo.toml:

Filename: Cargo.toml

rand = "0.8.3"

Adding rand as a dependency in Cargo.toml tells Cargo to download the rand package and any dependencies from crates.io and make rand available to our project.

Then, to bring rand definitions into the scope of our package, we added a use line starting with the name of the crate, rand, and listed the items we wanted to bring into scope. Recall that in the “Generating a Random Number” section in Chapter 2, we brought the Rng trait into scope and called the rand::thread_rng function:

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Members of the Rust community have made many packages available at crates.io, and pulling any of them into your package involves these same steps: listing them in your package’s Cargo.toml file and using use to bring items from their crates into scope.

Note that the standard std library is also a crate that’s external to our package. Because the standard library is shipped with the Rust language, we don’t need to change Cargo.toml to include std. But we do need to refer to it with use to bring items from there into our package’s scope. For example, with HashMap we would use this line:


#![allow(unused)]
fn main() {
use std::collections::HashMap;
}

This is an absolute path starting with std, the name of the standard library crate.

Using Nested Paths to Clean Up Large use Lists

If we’re using multiple items defined in the same crate or same module, listing each item on its own line can take up a lot of vertical space in our files. For example, these two use statements we had in the Guessing Game in Listing 2-4 bring items from std into scope:

Filename: src/main.rs

use rand::Rng;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Instead, we can use nested paths to bring the same items into scope in one line. We do this by specifying the common part of the path, followed by two colons, and then curly brackets around a list of the parts of the paths that differ, as shown in Listing 7-18.

Filename: src/main.rs

use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Listing 7-18: Specifying a nested path to bring multiple items with the same prefix into scope

In bigger programs, bringing many items into scope from the same crate or module using nested paths can reduce the number of separate use statements needed by a lot!

We can use a nested path at any level in a path, which is useful when combining two use statements that share a subpath. For example, Listing 7-19 shows two use statements: one that brings std::io into scope and one that brings std::io::Write into scope.

Filename: src/lib.rs

use std::io;
use std::io::Write;

Listing 7-19: Two use statements where one is a subpath of the other

The common part of these two paths is std::io, and that’s the complete first path. To merge these two paths into one use statement, we can use self in the nested path, as shown in Listing 7-20.

Filename: src/lib.rs

use std::io::{self, Write};

Listing 7-20: Combining the paths in Listing 7-19 into one use statement

This line brings std::io and std::io::Write into scope.

The Glob Operator

If we want to bring all public items defined in a path into scope, we can specify that path followed by the * glob operator:


#![allow(unused)]
fn main() {
use std::collections::*;
}

This use statement brings all public items defined in std::collections into the current scope. Be careful when using the glob operator! Glob can make it harder to tell what names are in scope and where a name used in your program was defined.

The glob operator is often used when testing to bring everything under test into the tests module; we’ll talk about that in the “How to Write Tests” section in Chapter 11. The glob operator is also sometimes used as part of the prelude pattern: see the standard library documentation for more information on that pattern.

Modülleri Farklı Dosyalara Ayırma

Şimdiye kadar, bu bölümdeki tüm örnekler tek bir dosyada birden fazla modül tanımladı. Modüller büyüdüğünde, kodda gezinmeyi kolaylaştırmak için tanımlarını ayrı bir dosyaya taşımak isteyebilirsiniz.

Örneğin, birden fazla restoran modülüne sahip olan Liste 7-17'deki koddan başlayalım. Tüm modülleri kasa kök dosyasında tanımlamak yerine, modülleri dosyalara çıkaracağız. Bu durumda, kasa kök dosyası src/lib.rs'dir, ancak bu prosedür, kasa kök dosyası src/main.rs olan ikili kasalarla da çalışır.

Öncelikle front_of_house modülünü kendi dosyasına çıkaracağız. Yalnızca mod front_of_house'u bırakarak front_of_house modülü için süslü parantezlerin içindeki kodu kaldırın; böylece src/lib.rs Liste 7-21'de gösterilen kodu içerir. Liste 7-22'de src/front_of_house.rs dosyasını oluşturana kadar bunun derlenmeyeceğini unutmayın.

Dosya adı: src/lib.rs

mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

Liste 7-21: Gövdesi src/front_of_house.rs içinde olacak bir front_of_house modülünün tanımlanması

Ardından, köşeli parantez içindeki kodu Liste 7-22'de gösterildiği gibi src/front_of_house.rs adlı yeni bir dosyaya yerleştirin. Derleyici bu dosyaya bakması gerektiğini bilir çünkü kasa kökündeki modül bildirimi front_of_house adıyla karşılaştı.

Dosya adı: src/front_of_house.rs

pub mod hosting {
    pub fn add_to_waitlist() {}
}

Liste 7-22: src/front_of_house.rs'deki front_of_house modülünün içindeki tanımlar

Modül ağacınızda yalnızca bir mod bildirimi kullanarak bir dosya yüklemeniz gerektiğini unutmayın. Derleyici dosyanın projenin bir parçası olduğunu öğrendiğinde (ve mod deyimini nereye koyduğunuzdan dolayı kodun modül ağacında nerede olduğunu bildiğinde), projenizdeki diğer dosyalar bir yol kullanarak yüklenen dosyanın koduna başvurmalıdır. “Modül Ağacındaki Bir Öğeye Başvuru Yolları”]paths bölümünde anlatıldığı gibi, beyan edildiği yerde. Başka bir deyişle mod, diğer programlama dillerinde görmüş olabileceğiniz bir “include” işlemi değildir.

Ardından, hosting modülünü kendi dosyasına çıkaracağız. hosting, kök modülün değil, front_of_house'ın bir alt modülü olduğundan, süreç biraz farklı olacaktır. hosting dosyasını modül ağacındaki ataları için adlandırılacak yeni bir dizine yerleştireceğiz, bu durumda src/front_of_house/ olacaktır.

hosting'i taşımak için, src/front_of_house.rs dosyasını yalnızca hosting modülünün bildirimini içerecek şekilde değiştiriyoruz:

Dosya adı: src/front_of_house.rs

pub mod hosting;

Ardından bir src/front_of_house dizini ve hosting modülünde yapılan tanımları içerecek bir hosting.rs dosyası oluşturuyoruz:

Dosya adı: src/front_of_house/hosting.rs

pub fn add_to_waitlist() {}

Bunun yerine src dizinine hosting.rs'i koyarsak, derleyici hosting.rs kodunun kasa kökünde bildirilen bir hosting modülünde olmasını ve front_of_house modülünün alt öğesi olarak bildirilmemesini bekler. Derleyicinin hangi dosyaların hangi modüllerin kodunu kontrol edeceğine ilişkin kuralları, dizinlerin ve dosyaların modül ağacıyla daha yakından eşleştiği anlamına gelir.

Alternatif Dosya Yolları

Şimdiye kadar Rust derleyicisinin kullandığı en deyimsel dosya yollarını ele aldık, ancak Rust ayrıca daha eski bir dosya yolu stilini de destekliyor. Kasa kökünde bildirilen front_of_house adlı bir modül için, derleyici modülün kodunu şurada arayacaktır:

  • src/front_of_house.rs (şu ana kadar ele aldığımız yol)
  • src/front_of_house/mod.rs (eski stil, hala desteklenen yol)

front_of_house alt modülü olan hosting adlı bir modül için, derleyici modülün kodunu şurada arayacaktır:

  • src/front_of_house/hosting.rs (şu ana kadar ele aldığımız yol)
  • src/front_of_house/hosting/mod.rs (eski stil, hala desteklenen yol),

Aynı modül için her iki stili de kullanırsanız derleyici hatası alırsınız. Aynı projede farklı modüller için her iki stili de kullanmaya izin verilir, ancak projenizde gezinen kişiler için kafa karıştırıcı olabilir.

mod.rs adlı dosyaları kullanan stilin ana dezavantajı, projenizin mod.rs adlı birçok dosyayla sonuçlanabilmesidir; bu, onları aynı anda kod editörünüzde açtığınızda kafa karıştırıcı olmaya sebebiyet verebilir.

Her modülün kodunu ayrı bir dosyaya taşıdık ve modül ağacı aynı kaldı. eat_at_restaurant içindeki fonksiyon çağrıları, tanımlar farklı dosyalarda bulunsa bile herhangi bir değişiklik yapılmadan çalışacaktır. Bu teknik, modülleri boyut olarak büyüdükçe yeni dosyalara taşımanıza olanak tanır.

src/lib.rs içindeki pub use crate::front_of_house::hosting ifade yapısının da değişmediğini ve kullanımın kasanın parçası olarak hangi dosyaların derlendiği üzerinde herhangi bir etkisi olmadığını unutmayın. mod anahtar sözcüğü, modülleri bildirir ve Rust, o modüle giren kod için modülle aynı ada sahip bir dosyaya bakar.

Özet

Rust, bir paketi birden çok kasaya ve bir kasayı modüllere ayırmanıza olanak tanır, böylece bir modülde tanımlanan öğelere başka bir modülden başvurabilirsiniz. Bunu, mutlak veya göreli yollar belirterek yapabilirsiniz. Bu yollar, bir use ifade yapısı ile kapsama alınabilir, böylece o kapsamdaki öğenin birden çok kullanımı için daha kısa bir yol kullanabilirsiniz. Modül kodu varsayılan olarak gizlidir, ancak pub anahtar sözcüğünü ekleyerek tanımları herkese açık hale getirebilirsiniz.

Bir sonraki bölümde, düzenli bir şekilde organize edilmiş kodunuzda kullanabileceğiniz standart kütüphanedeki bazı koleksiyon veri yapılarına bakacağız.

Yaygın Koleksiyonlar

Rust'ın standart kütüphanesi, koleksiyon adı verilen bir dizi çok kullanışlı veri yapısını içerir. Diğer veri türlerinin çoğu belirli bir değeri temsil eder, ancak koleksiyonlar birden çok değer içerebilir. Yerleşik dizi ve tanımlama grubu türlerinin aksine, bu koleksiyonların işaret ettiği veriler öbek üzerinde depolanır; bu, veri miktarının derleme zamanında bilinmesine gerek olmadığı ve program çalışırken büyüyüp küçülebileceği anlamına gelir. Her toplama türünün farklı yetenekleri ve maliyetleri vardır ve mevcut durumunuza uygun olanı seçmek zamanla geliştireceğiniz bir beceridir. Bu bölümde, Rust programlarında çok sık kullanılan üç koleksiyonu tartışacağız:

  • Bir vektör değişken sayıda aynı tür değerleri yan yana saklamanıza izin verir.
  • Bir dizgi karakter koleksiyonudur. String türünden önceden de bahsetmiştik ama artık daha derine ineceğiz.
  • Bir kilit koleksiyonu bir değeri belirli bir anahtarla ilişkilendirmenizi sağlar. map diye tanımlanan veri yapısının daha özel bir süreklemesidir (uygulamasıdır).

Standart kütüphane tarafından sunulan diğer tür koleksiyonlar hakkında bilgi almak için dokümantasyona göz atabilirsiniz.

Vektörlerin, dizgilerin ve kilit koleksiyonlarının nasıl oluşturulacağını ve güncelleneceğini ve ayrıca her birini neyin özel kıldığını tartışacağız.

Vektörlerle Değer Listelerini Saklama

Bakacağımız ilk koleksiyon türü, vektör olarak da bilinen Vec<T>'dir. Vektörler, tüm değerleri bellekte yan yana koyan tek bir veri yapısında birden fazla değeri saklamanıza izin verir. Vektörler yalnızca aynı türdeki değerleri saklayabilir. Bir dosyadaki metin satırları veya bir alış veriş sepetindeki ürünlerin fiyatları gibi bir öğe listeniz olduğunda kullanışlıdırlar.

Yeni bir Vektör Oluşturma

Yeni bir boş vektör oluşturmak için Liste 8-1'de gösterildiği gibi Vec::new fonksiyonunu çağırıyoruz.

fn main() {
    let v: Vec<i32> = Vec::new();
}

Liste 8-1: i32 türündeki değerleri tutmak için yeni, boş bir vektör oluşturma

Buraya bir tür ek açıklaması eklediğimize dikkat edin. Bu vektöre herhangi bir değer eklemediğimiz için, Rust ne tür öğeler depolamak istediğimizi bilmiyor. Bu önemli bir noktadır. Vektörler yaygınlar kullanılarak uygulanır; Bölüm 10'da yaygınları kendi türlerinizle nasıl kullanacağınızı ele alacağız. Şimdilik, standart kütüphane tarafından sağlanan Vec<T> türünün herhangi bir türü tutabileceğini bilin. Belirli bir türü tutmak için bir vektör oluşturduğumuzda, türü köşeli parantezler içinde belirtebiliriz. Liste 8-1'de, Rust'a v içindeki Vec<T>'nin i32 tipinde elemanlar tutacağını söyledik.

Daha sık olarak, ilk değerlerle bir Vec<T> oluşturursunuz ve Rust saklamak istediğiniz değerin türünü çıkarır, bu nedenle bu tür ek açıklamasını yapmanız nadiren gerekir. Rust, verdiğiniz değerleri tutan yeni bir vektör oluşturacak olan vec! makrosunu uygun bir şekilde sağlar. Liste 8-2, 1, 2 ve 3 değerlerini tutan yeni bir Vec<i32> oluşturur. Tam sayı türü i32'dir çünkü bu, Bölüm 3'ün “Veri Türleri” bölümünde tartıştığımız gibi varsayılan tam sayı türüdür.

fn main() {
    let v = vec![1, 2, 3];
}

Liste 8-2: Değerler içeren yeni bir vektör oluşturma

i32 değerlerini verdiğimiz için, Rust v türünün Vec<i32> olduğu sonucunu çıkarabilir ve tür ek açıklamasına gerek kalmaz. Sonraki başlıkta bir vektörün nasıl değiştirileceğine bakacağız.

Bir Vektörü Güncelleme

Bir vektör oluşturmak ve daha sonra ona eleman eklemek için push metodunu kullanabiliriz, Liste 8-3'te gösterildiği gibi.

fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}

Listing 8-3: Using the push method to add values to a vector

Herhangi bir değişkende olduğu gibi, değerini değiştirebilmek istiyorsak, şunu yapmamız gerekir: Bölüm 3'te tartışıldığı gibi mut anahtar sözcüğünü kullanarak değiştirilebilir hale getirmelisiniz.

Sayılar içine yerleştirdiğimiz tüm öğeler i32 türündedir ve Rust bunu verilerden çıkarır, yani Vec<i32> ek açıklamasına ihtiyacımız yoktur.

Vektörlerin Elemanlarını Okuma

Bir vektörde saklanan bir değere başvurmanın iki yolu vardır: indeksleme veya get metodunu kullanma. Aşağıdaki örneklerde, daha fazla netlik için bu fonksiyonlardan döndürülen değerlerin türlerini açıkladık.

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let third: &i32 = &v[2];
    println!("The third element is {}", third);

    let third: Option<&i32> = v.get(2);
    match third  {
        Some(third) => println!("The third element is {}", third),
        None => println!("There is no third element."),
    }
}

Liste 8-4: Bir vektördeki bir öğeye erişmek için indekslemeyi veya get metodunu kullanma

Burada birkaç ayrıntıya dikkat edin. Üçüncü elemanı elde etmek için 2 indeks değerini kullanırız çünkü vektörler sıfırdan başlayarak sayıya göre indekslenir ve [] kullanmak bize indeks değerindeki elemana bir referans verir. get metodunu argüman olarak geçirilen indeksle kullandığımızda, match ile kullanabileceğimiz bir Option<&T> elde ederiz.

Rust'ın bir öğeye başvurmak için bu iki yolu sağlamasının nedeni, mevcut öğelerin aralığı dışında bir indeks değeri kullanmaya çalıştığınızda programın nasıl davranacağını seçebilmenizdir. Örnek olarak, beş elemanlı bir vektörümüz olduğunda ve ardından Liste 8-5'te gösterildiği gibi her bir teknikle 100 indeksindeki bir elemana erişmeye çalıştığımızda ne olacağını görelim.

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}

Liste 8-5: Beş element içeren bir vektörde indeks 100'deki elemana erişmeye çalışmak

Bu kodu çalıştırdığımızda ilk [] metodu var olmayan bir elemente referans verdiği için programın paniğe kapılmasına neden olacaktır. Bu yöntem en iyi şekilde, vektörün sonundaki bir öğeye erişme girişimi olduğunda programınızın çökmesini istediğinizde kullanılır.

get metodu vektörün dışında bir indeks iletildiğinde panik yapmadan None döndürür. Normal koşullar altında, vektör aralığının dışındaki bir öğeye erişim ara sıra gerçekleşebiliyorsa, bu metodu kullanırsınız. Kodunuz, Bölüm 6'da tartışıldığı gibi, Some(&element) veya None'a sahip olmayı işlemek için bir mantığa sahip olacaktır. Örneğin, dizin bir sayı giren bir kişiden geliyor olabilir. Yanlışlıkla çok büyük bir sayı girerlerse ve program None değeri alırsa, kullanıcıya geçerli vektörde kaç öğe olduğunu söyleyebilir ve onlara geçerli bir değer girmeleri için bir şans daha verebilirsiniz. Bu, bir yazım hatası nedeniyle programı çökertmekten daha kullanıcı dostu olurdu!

Programın geçerli bir referansı olduğunda, ödünç alma denetleyicisi, bu referansın ve vektörün içeriğine yönelik diğer referansların geçerli kalmasını sağlamak için mülkiyet ve ödünç alma kurallarını (Bölüm 4'te ele alınmıştır) uygular. Aynı kapsamda değiştirilebilir ve değişmez referanslara sahip olamayacağınızı belirten kuralı hatırlayın. Bu kural, bir vektördeki ilk öğeye değişmez bir referans tuttuğumuz ve sona bir öğe eklemeye çalıştığımız Liste 8-6'da geçerlidir. Fonksiyonda daha sonra bu öğeye başvurmaya çalışırsak, bu program çalışmayacaktır:

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {}", first);
}

Liste 8-6: Bir öğeye referans tutarken bir vektöre öğe eklemeye çalışmak

Bu kodu derlemek şu hataya neden olur:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 | 
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 | 
8 |     println!("The first element is: {}", first);
  |                                          ----- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` due to previous error

Liste 8-6'daki kod çalışması gerekiyormuş gibi görünebilir: ilk elemana yapılan bir referans vektörün sonundaki değişiklikleri neden önemsesin? Bu hata vektörlerin çalışma şeklinden kaynaklanmaktadır: vektörler değerleri bellekte yan yana koyduğu için, vektörün sonuna yeni bir eleman eklemek, vektörün şu anda depolandığı yerde tüm elemanları yan yana koymak için yeterli yer yoksa, yeni bellek ayırmayı ve eski elemanları yeni alana kopyalamayı gerektirebilir. Bu durumda, ilk elemanın referansı ayrılmış belleğe işaret ediyor olacaktır. Ödünç alma kuralları programların bu duruma düşmesini engeller.

Not: Vec<T> türünün sürekleme ayrıntıları hakkında daha fazla bilgi için bkz. “The Rustonomicon”.

Bir Vektördeki Değerler Üzerinde Yineleme

Bir vektördeki her bir öğeye sırayla erişmek için, her seferinde bir öğeye erişmek üzere indisleri kullanmak yerine tüm öğeler arasında yineleme yaparız. Liste 8-7, i32 değerlerinden oluşan bir vektördeki her bir öğeye değişmez referanslar almak ve bunları yazdırmak için bir for döngüsünün nasıl kullanılacağını gösterir.

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{}", i);
    }
}

Liste 8-7: Bir for döngüsü kullanarak öğeler üzerinde yineleme yaparak bir vektördeki her bir öğeyi yazdırma

Ayrıca, tüm öğelerde değişiklik yapmak için değişebilir bir vektördeki her bir öğeye yönelik değişebilir referanslar üzerinde yineleme yapabiliriz. Liste 8-8'deki for döngüsü her öğeye 50 ekleyecektir.

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}

Liste 8-8: Bir vektördeki öğelere yönelik değiştirilebilir referanslar üzerinde yineleme

Değiştirilebilir referansın ifade ettiği değeri değiştirmek için, += operatörünü kullanmadan önce i içindeki değere ulaşmak için * referansı alma operatörünü kullanmamız gerekir. Referansı alma operatörü hakkında daha fazla bilgiyi Bölüm 15'teki “Referansı Alma Operatörü ile Değere Bakan İşaretçiyi Takip Etme” kısmında bulacağız.

İster değişmez ister değişebilir olsun, bir vektör üzerinde yineleme yapmak, ödünç denetleyicisinin kuralları nedeniyle güvenlidir. Liste 8-7 ve Liste 8-8'deki for döngüsü gövdelerine öğe eklemeye veya çıkarmaya çalışırsak, Liste 8-6'daki kodla aldığımıza benzer bir derleyici hatası alırız. for döngüsünün tuttuğu vektör referansı tüm vektörün aynı anda değiştirilmesini engeller.

Birden Fazla Türü Saklamak için enum Kullanma

Vektörler yalnızca aynı türden değerleri depolayabilir. Bu elverişsiz olabilir; farklı türlerdeki öğelerin bir listesini saklamaya ihtiyaç duyan kullanım durumları kesinlikle vardır. Neyse ki, bir enum'un varyantları aynı enum tipi altında tanımlanır, bu nedenle farklı tiplerdeki öğeleri temsil etmek için tek bir tipe ihtiyaç duyduğumuzda, bir enum tanımlayabilir ve kullanabiliriz!

Örneğin, satırdaki sütunlardan bazılarının tam sayılar, bazılarının kayan noktalı sayılar ve bazılarının da dizgiler içerdiği bir elektronik tablodaki bir satırdan değerler almak istediğimizi varsayalım. Varyantları farklı değer türlerini tutacak bir enum tanımlayabiliriz ve tüm enum varyantları aynı tür olarak kabul edilir. Daha sonra bu enum'u tutmak için bir vektör oluşturabiliriz ve böylece sonuçta farklı türleri tutabiliriz. Bunu Liste 8-9'da gösterdik.

fn main() {
    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];
}

Liste 8-9: Farklı türlerdeki değerleri tek bir vektörde saklamak için enum tanımlama

Rust'ın derleme zamanında vektörde hangi türlerin olacağını bilmesi gerekir, böylece her bir öğeyi depolamak için yığın üzerinde tam olarak ne kadar bellek gerekeceğini bilir. Ayrıca bu vektörde hangi türlere izin verildiği konusunda da açık olmalıyız. Rust bir vektörün herhangi bir türü tutmasına izin verseydi, türlerden birinin veya daha fazlasının vektörün elemanları üzerinde gerçekleştirilen işlemlerde hatalara neden olma ihtimali olurdu. Bir enum ve bir match ifadesi kullanmak, Bölüm 6'da tartışıldığı gibi, Rust'ın derleme zamanında olası her durumun ele alınmasını sağlayacağı anlamına gelir.

Bir programın çalışma zamanında bir vektörde depolamak için alacağı kapsamlı tür kümesini bilmiyorsanız, enum işe yaramayacaktır. Bunun yerine, Bölüm 17'de ele alacağımız bir trait tanımını kullanabilirsiniz.

Vektörleri kullanmanın en yaygın yollarından bazılarını tartıştığımıza göre, standart kütüphane tarafından Vec<T> üzerinde tanımlanan birçok yararlı yöntem için API dokümantasyonunu gözden geçirdiğinizden emin olun. Örneğin, push'a ek olarak, pop yöntemi son elemanı kaldırır ve döndürür.

Bir Vektörü Düşürmek Elemanlarını Düşürür

struct'lar gibi, bir vektör de kapsam dışına çıktığında, Liste 8-10'da açıklandığı gibi serbest bırakılır.

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // do stuff with v
    } // <- v goes out of scope and is freed here
}

Liste 8-10: Vektörün ve elemanlarının nereye bırakıldığını gösterme

Vektör bırakıldığında, tüm içeriği de bırakılır, yani tuttuğu tam sayılar temizlenir. Ödünç alma denetleyicisi, bir vektörün içeriğine yapılan referansların yalnızca vektörün kendisi geçerli olduğu sürece kullanılmasını sağlar.

Bir sonraki koleksiyon türüne geçelim: String!

UTF-8 Kodlu Metni Dizgilerde Saklama

Dizgilerden Bölüm 4'te bahsetmiştik, ancak şimdi onlara daha derinlemesine bakacağız. Yeni Rustseverler genellikle üç nedenden dolayı dizgilere takılırlar: Rust'ın olası hataları açığa çıkarma eğilimi, dizgilerin birçok programcının düşündüğünden daha karmaşık bir veri yapısı olması ve UTF-8. Bu faktörler, diğer programlama dillerinden geldiğinizde zor görünebilecek bir şekilde birleşir.

Dizgileri koleksiyonlar bağlamında ele alıyoruz çünkü dizgiler bir bayt koleksiyonu ve bu baytlar metin olarak yorumlandığında yararlı işlevler sağlayan bazı metodlar olarak uygulanmaktadır. Bu bölümde, String üzerinde her koleksiyon türünün sahip olduğu oluşturma, güncelleme ve okuma gibi işlemlerden bahsedeceğiz. Ayrıca, String'in diğer koleksiyonlardan farklı olduğu yönleri, yani bir String'de indekslemenin, insanların ve bilgisayarların String verilerini yorumlama biçimleri arasındaki farklar nedeniyle nasıl karmaşıklaştığını tartışacağız.

Dizgi Nedir?

İlk olarak dizgi terimi ile ne kastettiğimizi tanımlayacağız. Rust'ın çekirdek dilinde yalnızca bir dizgi tipi vardır, bu da genellikle ödünç alınmış &str biçiminde görülen dizgi dilimi str'dir. Bölüm 4'te, başka bir yerde saklanan bazı UTF-8 kodlu dizgi verilerine referans olan dizgi dilimlerinden bahsetmiştik. Örneğin dizgi değişmezleri, programın ikili dosyasında saklanır ve bu nedenle dizgi dilimleridir.

Çekirdek dile kodlanmak yerine Rust'ın standart kütüphanesi tarafından sağlanan String türü, büyütülebilir, değiştirilebilir, sahipli, UTF-8 kodlu bir dize türüdür. Rustseverler Rust'ta “dizgilerden” bahsettiklerinde, String ya da dizgi dilimi &str tiplerinden birine atıfta bulunuyor olabilirler, sadece bu tiplerden birine değil. 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 olarak kullanılır ve hem String hem de string dilimleri UTF-8 kodludur.

Yeni Bir String Oluşturma

Vec<T> ile kullanılabilen işlemlerin çoğu String ile de kullanılabilir, çünkü String aslında bazı ekstra garantilere, kısıtlamalara ve yeteneklere sahip bir bayt vektörü etrafında bir sarmalayıcı olarak uygulanmaktadır. Vec<T> ve String ile aynı şekilde çalışan bir fonksiyon örneği, Liste 8-11'de gösterilen bir örnek oluşturmak için new fonksiyonudur.

fn main() {
    let mut s = String::new();
}

Liste 8-11: Yeni, boş bir String oluşturma

Bu satır, daha sonra içine veri yükleyebileceğimiz s adında yeni bir boş dizgi oluşturur. Genellikle, dizgiyi başlatmak istediğimiz bazı başlangıç verilerimiz olacaktır. Bunun için, dizgi değişmezlerinin yaptığı gibi Display tanımını sürekleyen herhangi bir türde kullanılabilen to_string metodunu kullanırız.

Liste 8-12'de iki örnek gösterilmektedir.

fn main() {
    let data = "initial contents";

    let s = data.to_string();

    // the method also works on a literal directly:
    let s = "initial contents".to_string();
}

Liste 8-12: Bir dize değişmezinden bir String oluşturmak için to_string metodunu kullanma

String::from fonksiyonunu bir dizgi değişmezinden String oluşturmak için de kullanabiliriz. Liste 8-13'teki kod, Liste 8-12'deki to_string kullanan koda eş değerdir.

fn main() {
    let s = String::from("initial contents");
}

Liste 8-13: Bir dizgi değişmezinden String oluşturmak için String::from fonksiyonunu kullanma

Dizgiler pek çok şey için kullanıldığından, dizgiler için pek çok farklı genel API kullanabiliriz ve bu da bize pek çok seçenek sunar. Bazıları gereksiz görünebilir, ancak hepsinin önemli amacı vardır! Bu durumda, String::from ve to_string aynı şeyi yapar, bu nedenle hangisini seçeceğiniz bir stil ve okunabilirlik meselesidir.

Dizgilerin UTF-8 kodlu olduğunu unutmayın, bu nedenle Liste 8-14'te gösterildiği gibi uygun şekilde kodlanmış herhangi bir veriyi bunlara dahil edebiliriz.

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שָׁלוֹם");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}

Liste 8-14: Farklı dillerdeki selamlamaları dizgilerde saklama

Bunların tamamı geçerli String değerleridir.

String'i Güncelleme

Bir String'in boyutu büyüyebilir ve içine daha fazla veri koyarsanız, tıpkı bir Vec<T>'nin içeriği gibi içeriği değişebilir. Ayrıca, String değerlerini birleştirmek için + operatörünü veya format! makrosunu rahatlıkla kullanabilirsiniz.

push_str ve push ile String'e ekleme yapmak

Liste 8-15'te gösterildiği gibi, bir dizgi dilimi eklemek için push_str metodunu kullanarak bir String'i büyütebiliriz.

fn main() {
    let mut s = String::from("foo");
    s.push_str("bar");
}

Liste 8-15: push_str metodunu kullanarak bir String'e bir dizgi dilimi ekleme

Bu iki satırdan sonra, s foobar'ı içerecektir. push_str metodu bir dizgi dilimi alır çünkü parametrenin sahipliğini almak zorunda değilizdir. Örneğin, Liste 8-16'daki kodda, içeriğini s1'e ekledikten sonra s2'yi kullanabilmek istiyoruz.

fn main() {
    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(s2);
    println!("s2 is {}", s2);
}

Liste 8-16: İçeriğini bir String'e ekledikten sonra dizgi dilimini kullanma

Eğer push_str metodu s2'nin sahipliğini alsaydı, değerini son satıra yazdıramazdık. Ancak, bu kod beklediğimiz gibi çalışıyor!

push metodu parametre olarak tek bir karakter alır ve onu String'e ekler. Liste 8-17, push metodunu kullanarak bir String'e “l” harfini ekler.

fn main() {
    let mut s = String::from("lo");
    s.push('l');
}

Liste 8-17: push kullanarak String'e karakter ekleme

Sonuç olarak, s, lol içerecektir.

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

Çoğu zaman, mevcut iki dizgiyi birleştirmek isteyeceksiniz. Bunu yapmanın bir yolu, Liste 8-18'de gösterildiği gibi + operatörünü kullanmaktır.

fn main() {
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
}

Liste 8-18: İki String değerini yeni bir String değeriyle birleştirmek için + operatörünü kullanmak

s3 dizgisi Hello, world! içerecektir. Ekleme işleminden sonra s1'in artık geçerli olmamasının ve s2'ye bir referans kullanmamızın nedeni, + operatörünü kullandığımızda çağrılan metodun imzasıyla ilgilidir. Imzası aşağıdaki gibi görünen add metodunu kullanır:

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

Standart kütüphanede, yaygınlar ve ilişkili türler kullanılarak tanımlanmış eklentiler görürsünüz. Burada, bu metodu String değerleriyle çağırdığımızda olan şey olan somut türlerle değiştirdik. Yaygınları Bölüm 10'da tartışacağız. Bu imza bize + operatörünün zor kısımlarını anlamamız için gereken ipuçlarını verir.

İlk olarak, s2 bir & içerir, yani ilk dizgiye ikinci dizginin bir referansını ekliyoruz. Bunun nedeni add fonksiyonundaki s parametresidir: bir String'e yalnızca bir &str ekleyebiliriz; iki String değerini birbirine ekleyemeyiz. Ama bekleyin - &s2'nin türü, add fonksiyonunun ikinci parametresinde belirtildiği gibi &str değil, &String'dir.

Öyleyse Liste 8-18 neden derleniyor?

add çağrısında &s2'yi kullanabilmemizin nedeni, derleyicinin &String argümanını &str'e zorlayabilmesidir. add metodunu çağırdığımızda, Rust burada &s2'yi &s2[..]'ye dönüştüren bir deref zorlaması kullanır. deref zorlamasını Bölüm 15'te daha derinlemesine tartışacağız. add, s parametresinin sahipliğini almadığından, s2 bu işlemden sonra hala geçerli bir String olacaktır.

İkinci olarak, imzada add'in self'in sahipliğini aldığını görebiliriz, çünkü self'in &'si yoktur. Bu, Liste 8-18'deki s1'in add çağrısına taşınacağı ve bundan sonra artık geçerli olmayacağı anlamına gelir. Dolayısıyla, let s3 = s1 + &s2; ifade yapısı her iki dizgiyi de kopyalayıp yeni bir tane oluşturacak gibi görünse de, bu ifade yapısı aslında s1'in sahipliğini alır, s2'nin içeriğinin bir kopyasını ekler ve ardından sonucun sahipliğini döndürür. Başka bir deyişle, çok sayıda kopya oluşturuyormuş gibi görünür ancak oluşturmaz; uygulama kopyalamadan daha verimlidir.

Birden fazla dizgiyi birleştirmemiz gerekirse, + operatörünün davranışı hantal hale gelir:

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = s1 + "-" + &s2 + "-" + &s3;
}

Bu noktada, s tic-tac-toe olacak. Tüm + ve " karakterleri ile neler olup bittiğini görmek zordur. Daha karmaşık dizgi birleştirmeleri için format! makrosunu kullanabiliriz:

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = format!("{}-{}-{}", s1, s2, s3);
}

Bu kod da aynı şekilde s'yi tic-tac-toe olarak ayarlar. format! makrosu println! gibi çalışır, ancak çıktıyı ekrana yazdırmak yerine, içeriği içeren bir String döndürür. Kodun format! kullanan versiyonunun okunması çok daha kolaydır ve format! makrosu tarafından oluşturulan kod referanslar kullanır, böylece bu çağrı parametrelerinden herhangi birinin sahipliğini almaz.

Dizgilerde İndeksleme

Diğer birçok programlama dilinde, bir dizedeki karakterlere indeksle referans vererek tek tek erişmek geçerli ve yaygın bir işlemdir. Ancak, Rust'ta indeksleme söz dizimini kullanarak bir String'in parçalarına erişmeye çalışırsanız, bir hata alırsınız. Liste 8-19'daki geçersiz kodu düşünün.

fn main() {
    let s1 = String::from("hello");
    let h = s1[0];
}

Liste 8-19: İndeksleme söz dizimini String ile kullanmaya çalışmak

Bu kod aşağıdaki hataya sebebiyet verecektir:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `String` cannot be indexed by `{integer}`
 --> src/main.rs:3:13
  |
3 |     let h = s1[0];
  |             ^^^^^ `String` cannot be indexed by `{integer}`
  |
  = help: the trait `Index<{integer}>` is not implemented for `String`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` due to previous error

Hata ve not hikayeyi anlatıyor: Rust dizgileri indekslemeyi desteklemez. Ama neden desteklemiyor? Bu soruyu yanıtlamak için, Rust'ın dizgileri bellekte nasıl sakladığını tartışmamız gerekir.

Dahili Temsil

String, Vec<u8> kullanan bir sarmalayıcıdır. Liste 8-14'teki düzgün kodlanmış UTF-8 örnek dizgilerimizden bazılarına bakalım.

İlk olarak, buna bakalım:

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שָׁלוֹם");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}

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

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שָׁלוֹם");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}

Dizginin ne kadar uzunlukta olduğu sorulduğunda 12 diyebilirsiniz. Aslında Rust'ın cevabı 24'tür: UTF-8'de “Здравствуйте” yi kodlamak için gereken bayt sayısı budur, çünkü bu dizgideki her Unicode skaler değeri 2 bayt depolama alanı alır. Bu nedenle, dizginin baytlarındaki bir indeks her zaman geçerli bir Unicode skaler değeriyle ilişkili olmayacaktır.

Kanıt için bu geçersiz Rust kodunu düşünün:

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

answer'ın ilk harf olan З olmayacağını zaten biliyorsunuz. UTF-8'de kodlandığında, З'nin ilk baytı 208 ve ikincisi 151'dir, bu nedenle cevabın aslında 208 olması gerekir, ancak 208 tek başına geçerli bir karakter değildir. Bu dizginin ilk harfini soran bir kullanıcı muhtemelen 208 sonucunu almak istemeyecektir; ancak Rust'ın 0 bayt indeksinde sahip olduğu tek veri budur. Kullanıcılar, dizgi yalnızca Latin harfleri içerse bile genellikle bayt değerinin döndürülmesini istemezler: &"hello"[0] bayt değerini döndüren geçerli bir kod olsaydı, h değil 104 döndürürdü.

O halde cevap, beklenmedik bir değer döndürmekten ve hemen keşfedilemeyecek hatalara neden olmaktan kaçınmak için Rust'ın bu kodu hiç derlememesi ve geliştirme sürecinin başlarında yanlış anlamaları önlemesidir.

Baytlar ve Skaler Değerler ve Grapheme Kümeleri! Amanın!

UTF-8 ile ilgili bir başka nokta da, Rust'ın bakış açısından dizgilere bakmanın aslında üç ilgili yolu olduğudur: baytlar, skaler değerler ve grapheme kümeleri (harf olarak adlandırdığımız şeye en yakın şey).

Devanagari alfabesiyle yazılmış Hintçe “नमस्ते” kelimesine bakarsak, bu kelime aşağıdaki gibi görünen u8 değerlerinden oluşan bir vektör olarak saklanır:

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

Bu 18 bayttır ve bilgisayarlar bu verileri nihai olarak bu şekilde depolar. Bunlara Unicode skaler değerleri olarak bakarsak, ki Rust'ın char türü budur, bu baytlar şöyle görünür:

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

Burada altı char değeri vardır, ancak dördüncü ve altıncı harfler harf değildir: bunlar kendi başlarına bir anlam ifade etmeyen aksan işaretleridir. Son olarak, bunlara grapheme kümeleri olarak bakarsak, bir kişinin Hintçe kelimeyi oluşturan dört harf olarak adlandıracağı şeyi elde ederiz:

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

Rust, bilgisayarların depoladığı ham dizgi verilerini yorumlamak için farklı yollar sağlar, böylece veriler hangi insan dilinde olursa olsun her program ihtiyaç duyduğu yorumu seçebilir.

Rust'ın bir karakteri elde etmek için bir String içinde indeksleme yapmamıza izin vermemesinin son bir nedeni, indeksleme işlemlerinin her zaman sabit zaman (O(1)) almasının beklenmesidir. Ancak bir String ile bu performansı garanti etmek mümkün değildir, çünkü Rust'ın kaç tane geçerli karakter olduğunu belirlemek için başlangıçtan indekse kadar içerik boyunca yürümesi gerekir.

String'i Dilimleme

Bir dizeye indeksleme yapmak genellikle kötü bir fikirdir çünkü dizeye indeksleme işleminin dönüş türünün ne olması gerektiği açık değildir: bayt değeri, karakter, grapheme kümesi veya dizgi dilimi. Bu nedenle, dizgi dilimleri oluşturmak için gerçekten indis kullanmanız gerekiyorsa, Rust sizden daha spesifik olmanızı bekler.

Tek bir sayı ile [] kullanarak indeksleme yapmak yerine, belirli baytları içeren bir dizgi dilimi oluşturmak için [] ile bir aralığı kullanabilirsiniz:


#![allow(unused)]
fn main() {
let hello = "Здравствуйте";

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

Burada s, dizginin ilk 4 baytını içeren bir &str olacaktır. Daha önce, bu karakterlerin her birinin 2 bayt olduğundan bahsetmiştik, bu da s'nin Зд olacağı anlamına gelir.

Eğer bir karakterin baytlarının sadece bir kısmını &hello[0..1] gibi bir şeyle dilimlemeye çalışsaydık, Rust çalışma zamanında bir vektörde geçersiz bir indekse erişildiğinde olduğu gibi paniğe kapılırdı:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/collections`
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/main.rs:4:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Dizgi dilimleri oluşturmak için aralıkları dikkatli kullanmalısınız, çünkü yanlış kullanım programınızı çökertebilir.

String Üzerinde Yineleme Yöntemleri

Dizgi parçaları üzerinde işlem yapmanın en iyi yolu, karakter mi yoksa bayt mı istediğinizi açıkça belirtmektir. Tek tek Unicode skaler değerleri için chars metodunu kullanabilirsiniz. “Зд” üzerinde chars metodu çağrıldığında char türünde iki değer ayrılır ve döndürülür; her bir öğeye erişmek için sonuç üzerinde yineleme yapabilirsiniz:


#![allow(unused)]
fn main() {
for c in "Зд".chars() {
    println!("{}", c);
}
}

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

З
д

Alternatif olarak, bytes yöntemi, kullanımınız için uygun olabilecek her ham baytı döndürür:


#![allow(unused)]
fn main() {
for b in "Зд".bytes() {
    println!("{}", b);
}
}

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

208
151
208
180

Ancak geçerli Unicode skaler değerlerinin 1 bayttan fazla olabileceğini unutmayın.

Devanagari alfabesinde olduğu gibi dizelerden grapheme kümeleri elde etmek karmaşıktır, bu nedenle bu fonksiyon direkt standart kütüphane tarafından sağlanmamaktadır.

İhtiyacınız olan işlevsellik buysa crates.io'da işe yarayabilecek kasalar mevcuttur.

String'ler O Kadar da Basit Değildir

Özetlemek gerekirse, dizgiler karmaşıktır. Farklı programlama dilleri, bu karmaşıklığın programcıya nasıl sunulacağı konusunda farklı seçimler yapar. Rust, String verilerinin doğru işlenmesini tüm Rust programları için varsayılan davranış haline getirmeyi seçmiştir, bu da programcıların UTF-8 verilerini önceden ele almak için daha fazla düşünmesi gerektiği anlamına gelir. Bu değiş tokuş, dizelerin karmaşıklığını diğer programlama dillerinde göründüğünden daha fazla ortaya çıkarır, ancak geliştirme yaşam döngünüzün ilerleyen aşamalarında ASCII olmayan karakterleri içeren hataları ele almak zorunda kalmanızı önler.

İyi haber şu ki, standart kütüphane bu karmaşık durumların doğru şekilde ele alınmasına yardımcı olmak için String ve &str türlerinden oluşturulmuş çok sayıda işlevsellik sunar. Bir dizgi içinde arama yapmak için contains ve bir dizginin parçalarını başka bir dizgiyle değiştirmek için replace gibi yararlı metodlara ulaşmak için ilgili dokümantasyonlara göz attığınızdan emin olun.

Daha az karışık bir şeye geçelim: anahtar-kilit koleksiyonları!

Anahtar-Kilit Koleksiyonlarında İlişkili Değerlerle Anahtarları Saklama

Ortak koleksiyonlarımızın sonuncusu anahtar-kilit koleksiyonudur. HashMap<K, V> türü, bu anahtarları ve değerleri belleğe nasıl yerleştireceğini belirleyen bir karma fonksiyonu kullanarak K türündeki anahtarların V türündeki değerlerle eşlenmesini depolar. Birçok programlama dili bu tür bir veri yapısını destekler, ancak genellikle hash, map, object, hash table, dictionary veya associative array gibi farklı isimler kullanırlar.

Anahtar-kilit koleksiyonları, vektörlerde olduğu gibi bir indeks kullanarak değil, herhangi bir türde olabilen bir anahtar kullanarak verileri aramak istediğinizde kullanışlıdır. Örneğin, bir oyunda, her anahtarın bir takımın adı ve değerlerin her takımın puanı olduğu bir anahtar-kilit koleksiyonunda her takımın puanını takip edebilirsiniz. Bir takım adı verildiğinde, skorunu alabilirsiniz.

Bu bölümün devamında a-k.k diyerek bahsedeceğimiz şey anahtar-kilit koleksiyonu olacaktır.

Bu bölümde a-k.k'nin temel API'sinin üzerinden geçeceğiz, ancak standart kütüphane tarafından HashMap<K, V> üzerinde tanımlanan fonksiyonlarda çok daha fazla güzellik gizlidir. Her zaman olduğu gibi, daha fazla bilgi için standart kütüphane dokümantasyonunu kontrol edin.

Yeni Bir A-K.K Oluşturma

Boş bir a-k.k oluşturmanın bir yolu new kullanmak ve insert ile eleman eklemektir. Liste 8-20'de, isimleri Blue ve Yellow olan iki takımın skorlarını takip ediyoruz. Blue takım 10 puanla başlar ve Yellow takım 50 puanla başlar.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
}

Liste 8-20: Yeni bir a-k.k oluşturma ve bazı anahtar ve değerleri ekleme

Öncelikle standart kütüphanenin koleksiyonlar bölümündeki HashMap'i kullanmamız gerektiğini unutmayın. Üç yaygın koleksiyonumuz arasında bu en az kullanılanıdır, bu nedenle başlangıçta otomatik olarak kapsama alınan özelliklere dahil edilmemiştir. HashMap standart kütüphaneden de daha az destek alır; örneğin bunları oluşturmak için yerleşik bir makro yoktur.

Tıpkı vektörler gibi, a-k.k da verilerini yığın üzerinde saklar. Bu HashMap'in String türünde anahtarları ve i32 türünde değerleri vardır. Vektörler gibi, a-k.k da homojendir: tüm anahtarlar birbiriyle aynı türde olmalıdır ve tüm değerler aynı türde olmalıdır.

A.K-K'da Değerlere Erişme

Liste 8-21'de gösterildiği gibi, get metoduna anahtarı sağlayarak a.k-k'dan dönüş değerini alabiliriz.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    let team_name = String::from("Blue");
    let score = scores.get(&team_name);
}

Liste 8-21: A-k.k'da saklanan Blue takımının skoruna erişim

Burada, skor Blue takımla ilişkilendirilen değere sahip olacak ve sonuç 10 olacaktır. get metodu Option<&V> döndürür; a-k.k'da o anahtar için değer yoksa get, None döndürür. Bu program, skorlarda anahtar için bir giriş yoksa skoru sıfıra ayarlamak için unwrap_or öğesini çağırarak Option'ı işler.

Bir a-k.k'daki her bir anahtar/değer çifti üzerinde, vektörlerde yaptığımıza benzer şekilde, bir for döngüsü kullanarak yineleme yapabiliriz:

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    for (key, value) in &scores {
        println!("{}: {}", key, value);
    }
}

Bu kod, her bir çifti rastgele bir sırayla yazdıracaktır:

Yellow: 50
Blue: 10

A-K.K'lar ve Sahiplik

Copy tanımını uygulayan i32 gibi türler için değerler a-k.k'a kopyalanır. String gibi sahip olunan değerler için, değerler taşınır ve a-k.k, Liste 8-22'de gösterildiği gibi bu değerlerin sahibi olur.

fn main() {
    use std::collections::HashMap;

    let field_name = String::from("Favorite color");
    let field_value = String::from("Blue");

    let mut map = HashMap::new();
    map.insert(field_name, field_value);
    // field_name and field_value are invalid at this point, try using them and
    // see what compiler error you get!
}

Liste 8-22: Eklendikten sonra anahtarların ve değerlerin a-k.k'a ait olduğunu gösterme

field_name ve field_value değişkenlerini, insert çağrısı ile a-k.k'a taşındıktan sonra kullanamıyoruz.

Değerlere yapılan referansları a-k.k'a eklersek, değerler hash haritasına taşınmaz. Referansların işaret ettiği değerler en azından a-k.k geçerli olduğu sürece geçerli olmalıdır. Bölüm 10'daki “Referansları Yaşam Süreleriyle Doğrulama” bölümünde bu konular hakkında daha fazla konuşacağız.

A-K.K'unu Güncelleme

Anahtar ve değer çiftlerinin sayısı artırılabilir olsa da, her benzersiz anahtar aynı anda kendisiyle ilişkilendirilmiş yalnızca bir değere sahip olabilir (ancak bunun tersi geçerli değildir: örneğin, hem Blue takım hem de Yellow takım skorlar a-k.k'da depolanan 10 değerine sahip olabilir).

Bir anahtar-kilit eşlemedeki verileri değiştirmek istediğinizde, bir anahtarın zaten atanmış bir değere sahip olduğu durumu nasıl ele alacağınıza karar vermeniz gerekir. Eski değeri tamamen göz ardı ederek eski değeri yeni değerle değiştirebilirsiniz. Eski değeri tutup yeni değeri yok sayabilir, yalnızca anahtarın zaten bir değeri yoksa yeni değeri ekleyebilirsiniz. Ya da eski değer ile yeni değeri birleştirebilirsiniz. Şimdi bunların her birinin nasıl yapılacağına bakalım!

Bir Değerin Üzerine Yazma

Bir a-k.k'a bir anahtar ve bir değer eklersek ve daha sonra aynı anahtarı farklı bir değerle eklersek, bu anahtarla ilişkili değer değiştirilecektir. Liste 8-23'teki kod iki kez insert çağrısı yapsa da, Blue takımın anahtarının değerini iki kez eklediğimiz için a-k.k yalnızca bir anahtar/değer çifti içerecektir.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Blue"), 25);

    println!("{:?}", scores);
}

Liste 8-23: Belirli bir anahtarla saklanan bir değeri değiştirme

Bu kod {"Blue": 25} yazdıracaktır. 10'un orijinal değerinin üzerine yazılmıştır.

Yalnızca Bir Anahtar Mevcut Değilse Anahtar ve Değer Ekleme

Belirli bir anahtarın a-k.k'da bir değerle zaten var olup olmadığını kontrol etmek ve ardından aşağıdaki eylemleri gerçekleştirmek yaygındır: anahtar a-k.k'da varsa, mevcut değer olduğu gibi kalmalıdır. Anahtar mevcut değilse, onu ve değerini eklersiniz.

A-k.k'ları bunun için kontrol etmek istediğiniz anahtarı parametre olarak alan entry adında özel bir API'ye sahiptir. entry metodunun geri dönüş değeri, var olabilecek veya olmayabilecek bir değeri temsil eden Entry adlı bir enum'dur. Diyelim ki Yellow takımın anahtarının kendisiyle ilişkili bir değeri olup olmadığını kontrol etmek istiyoruz. Eğer yoksa, 50 değerini eklemek istiyoruz ve aynı şeyi Blue takım için de yapmak istiyoruz. entry API'sini kullanarak yazacağımız kod Liste 8-24'e benzeyecektir:

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    scores.entry(String::from("Yellow")).or_insert(50);
    scores.entry(String::from("Blue")).or_insert(50);

    println!("{:?}", scores);
}

Liste 8-24: Yalnızca anahtarın halihazırda bir değeri yoksa eklemek için entry metodunu kullanma

Entry üzerindeki or_insert metodu, ilgili Entry anahtarı mevcutsa bu anahtarın değerine değiştirilebilir bir referans döndürmek için tanımlanmıştır; mevcut değilse, parametreyi bu anahtarın yeni değeri olarak ekler ve yeni değere değiştirilebilir bir referans döndürür. Bu teknik, mantığı kendimiz yazmaktan çok daha temizdir ve ayrıca ödünç denetleyicisi ile daha iyi çalışır.

Liste 8-24'teki kod çalıştırıldığında {"Yellow": 50, "Blue": 10} çıktısını verecektir. entry'e yapılan ilk çağrı Yellow takımın anahtarına 50 değerini ekleyecektir çünkü Yellow takımın zaten bir değeri yoktur. İkinci entry çağrısı a-k.k'unu değiştirmeyecektir çünkü Blue takım zaten 10 değerine sahiptir.

Eski Değere Dayalı Olarak Bir Değeri Güncelleme

A-K.K'lar için bir başka yaygın kullanım durumu da bir anahtarın değerini aramak ve ardından eski değere göre güncellemektir. Örneğin, Liste 8-25, bir metinde her bir kelimenin kaç kez geçtiğini sayan kodu göstermektedir. Kelimeleri anahtar olarak içeren bir a-k.k kullanırız ve o kelimeyi kaç kez gördüğümüzü takip etmek için değeri artırırız. Eğer bir kelimeyi ilk kez görüyorsak, önce 0 değerini ekleriz.

fn main() {
    use std::collections::HashMap;

    let text = "hello world wonderful world";

    let mut map = HashMap::new();

    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{:?}", map);
}

Liste 8-25: Kelimeleri ve sayıları saklayan bir a-k.k kullanarak kelimelerin oluşumlarını sayma

This code will print {"world": 2, "hello": 1, "wonderful": 1}. You might see the same key/value pairs printed in a different order: recall from the section that iterating over a hash map happens in an arbitrary order.

Bu kod {"world": 2, "hello": 1, "wonderful": 1} yazdıracaktır. Aynı anahtar/değer çiftlerinin farklı bir sırada yazdırıldığını görebilirsiniz: “A-K.K'daki Değerlere Erişim” bölümünden bir a-k.k üzerinde yinelemenin rastgele bir sırada gerçekleştiğini hatırlayın.

split_whitespace metodu, metin içindeki değerin boşluklarla ayrılmış alt dilimleri üzerinde bir yineleyici döndürür. or_insert metodu, belirtilen anahtar için değere değiştirilebilir bir referans (&mut V) döndürür. Burada bu değişebilir referansı count değişkeninde saklarız, bu nedenle bu değere atama yapmak için önce yıldız işaretini (*) kullanarak count referansını kaldırmamız gerekir. Değiştirilebilir referans for döngüsünün sonunda kapsam dışına çıkar, bu nedenle tüm bu değişiklikler güvenlidir ve ödünç alma kuralları tarafından izin verilir.

Şifreleme Fonksiyonları

Varsayılan olarak HashMap, anahtar-kilit tablolarını içeren Hizmet Reddi (DoS) saldırılarına karşı direnç sağlayabilen SipHash adlı bir şifreleme fonksiyonu kullanır1. Bu, mevcut en hızlı şifreleme algoritması değildir, ancak performanstaki düşüşle birlikte gelen daha iyi güvenlik için yapılan takas buna değecektir. Kodunuzun profilini çıkarırsanız ve varsayılan şifreleme fonksiyonunun amaçlarınız için çok yavaş olduğunu fark ederseniz, farklı bir şifreleyici belirterek başka bir fonksiyona geçebilirsiniz. Bir şifreleyici, BuildHasher tanımını uygulayan bir türdür. Özellikler ve bunların nasıl uygulanacağı hakkında Bölüm 10'da konuşacağız. Kendi şifreleyicinizi sıfırdan yazmak zorunda değilsiniz; crates.io, birçok yaygın hashing algoritmasını uygulayan şifreleyiciler sağlayan, diğer Rust kullanıcıları tarafından paylaşılan kütüphanelere sahiptir.

Özet

Vektörler, dizgiler ve anahtar-kilit koleksiyonları, verileri depolamanız, erişmeniz ve değiştirmeniz gerektiğinde programlarda gerekli olan büyük miktarda işlevsellik sağlayacaktır.

İşte şimdi çözmeniz gereken bazı alıştırmalar:

  • Bir tam sayı listesi verildiğinde, bir vektör kullanın ve listenin medyanını (sıralandığında orta konumdaki değer) ve modunu (en sık ortaya çıkan değer; bir a-k.k burada yardımcı olacaktır) döndürün.
  • Dizgileri Domuz Latincesine dönüştürün. Her kelimenin ilk ünsüzü kelimenin sonuna taşınır ve “ay” eklenir, böylece “first” “irst-fay olur. Sesli harfle başlayan kelimelerin sonuna “hay” eklenir (“apple”, “apple-hay” olur). UTF-8 kodlamasıyla ilgili ayrıntıları aklınızda bulundurun!
  • Bir a-k.k ve vektör kullanarak, bir kullanıcının bir şirketteki bir departmana çalışan isimleri eklemesine olanak tanıyan bir metin arayüzü oluşturun. Örneğin, “Sally'i Mühendisliğe Ekle” veya “Amir'i Satış Danışmanlığına Ekle”. Ardından kullanıcının bir departmandaki tüm kişilerin veya şirketteki tüm kişilerin alfabetik olarak sıralanmış bir listesini almasına izin verin.

Standart kütüphane API dokümantasyonları vektörlerin, dizgilerin ve a-k.k'ların bu alıştırmalar için yararlı olacak metodlarını açıklamaktadır!

İşlemlerin başarısız olabileceği daha karmaşık programlara giriyoruz, bu nedenle hata işlemeyi tartışmak için mükemmel bir zaman. Bunu daha sonra yapacağız!

Hata Yönetimi

Hatalar, yazılımlar için hayatın bir gerçeğidir, bu nedenle Rust, bir şeylerin yanlış gittiği durumları ele almak için bir dizi özelliğe sahiptir. Çoğu durumda, Rust, bir hata olasılığını kabul etmenizi ve kodunuz derlenmeden önce bazı işlemler yapmanızı gerektirir. Bu gereksinim, kodunuzu üretime dağıtmadan önce hataları keşfetmenizi ve bunları uygun şekilde işlemenizi sağlayarak programınızı daha sağlam hale getirir!

Rust, hataları iki ana kategoriye ayırır: kurtarılabilir ve kurtarılamaz hatalar. Dosya bulunamadı hatası gibi kurtarılabilir bir hata için, büyük olasılıkla sorunu kullanıcıya bildirmek ve işlemi yeniden denemek istiyoruz. Kurtarılamaz hatalar her zaman bir dizinin sonunun ötesindeki bir konuma erişmeye çalışmak gibi hataların belirtileridir ve bu nedenle programı hemen durdurmak istiyoruz.

Çoğu dil, bu iki tür hatayı ayırt etmez ve istisnalar gibi mekanizmalar kullanarak her ikisini de aynı şekilde ele alır. Rust'ın istisnaları yoktur. Bunun yerine, kurtarılabilir hatalar ve panik durumu için Result<T, E> türüne sahiptir! Program kurtarılamaz bir hatayla karşılaştığında yürütmeyi durduran şey burada panic! makrosudur. Bu bölüm öncelikle panic!'i çağırmayı kapsar ve sonrasında Result<T, E> değerlerini döndürmekten bahseder. Ek olarak, bir hatadan kurtulmaya veya yürütmeyi durdurmaya karar verirken göz önünde bulundurulması gereken noktaları keşfedeceğiz.

panic! İle Kurtarılamayan Hatalar

Bazen kodunuzda kötü şeyler olur ve bu konuda yapabileceğiniz hiçbir şey yoktur. Bu durumlarda Rust'ta panic! makrosu kullanılır. Pratikte paniğe yol açmanın iki yolu vardır: kodumuzun paniklemesine neden olan bir eylemde bulunmak (sondan sonraki bir diziye erişmek gibi) veya açıkça panic! makrosunu çağırmaktır. Her iki durumda da programımızda paniğe neden oluyoruz. Varsayılan olarak, bu panikler bir hata mesajı yazdırır, yığıtı temizler ve çıkar. Bir ortam değişkeni aracılığıyla, panik meydana geldiğinde panik kaynağını bulmayı kolaylaştırmak için Rust'ın çağrı yığınını görüntülemesini de sağlayabilirsiniz.

Paniğe Tepki Olarak Yığıtı Çözme veya Durdurma

Varsayılan olarak, bir panik meydana geldiğinde program çözülmeye başlar, bu da Rust'ın yığıtı geri aldığı ve karşılaştığı her fonksiyondan gelen verileri temizlediği anlamına gelir. Ancak, bu geri dönüş ve temizlik çok iştir. Bu nedenle Rust, programı temizlemeden sonlandıran hemen iptal etme alternatifini seçmenize izin verir.

Programın kullandığı belleğin işletim sistemi tarafından temizlenmesi gerekecektir. Projenizde elde edilen ikili dosyayı mümkün olduğu kadar küçük yapmanız gerekiyorsa, Cargo.toml dosyanızdaki uygun [profile] bölümlerine panic = 'abort' ekleyerek gevşemeden panik durumunda iptal etmeye geçebilirsiniz. Örneğin, yayın modund paniği iptal etmek istiyorsanız şunu ekleyin:

[profile.release]
panic = 'abort'

Basit bir programda panic!'i çağırmaya çalışalım:

Dosya adı: src/main.rs

fn main() {
    panic!("crash and burn");
}

Programı çalıştırdığınızda, şöyle bir şey göreceksiniz:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

panic! çağrısı, son iki satırda bulunan hata mesajına neden olur. İlk satır panik mesajımızı ve kaynak kodumuzda paniğin meydana geldiği yeri gösterir: src/main.rs:2:5 src/main.rs dosyamızın ikinci satırı, beşinci karakteri olduğunu gösterir.

Bu durumda belirtilen satır kodumuzun bir parçasıdır ve o satıra gidersek panic! makro çağrısını görürüz. Diğer durumlarda, panic! çağrısı, kodumuzun çağırdığı kodda olabilir ve hata mesajı tarafından bildirilen dosya adı ve satır numarası, panic! makrosunun çağrıldığı yeri gösterecektir. Kodumuzun soruna neden olan kısmını bulmak için panic! çağrısının geldiği fonksiyonların geri izini kullanabiliriz. Geri izlemeleri ileride daha ayrıntılı olarak tartışacağız.

panic! Geri İzlemesini Kullanma

Kodumuzun doğrudan makroyu çağırması yerine kodumuzdaki bir hata nedeniyle bir kütüphaneden bir panic! çağrısı geldiğinde nasıl bir şey olduğunu görmek için başka bir örneğe bakalım. Liste 9-1, geçerli dizin aralığının ötesinde bir vektördeki bir dizine erişmeye çalışan bazı kodlara sahiptir.

Dosya adı: src/main.rs

fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

Liste 9-1: Bir vektörün büyüklüğünden fazla bir indekse erişmeye çalışmak, panic! çağrısına neden olacaktır.

Burada, vektörümüzün 100. elemanına erişmeye çalışıyoruz (bu, indeksleme sıfırdan başladığı için indeks 99'dadır), ancak vektörün sadece 3 elemanı vardır. Bu durumda Rust paniğe kapılır. [] öğesinin kullanılması bir öğe döndürmesi gerekir, ancak geçersiz bir dizini iletirseniz, Rust'ın buraya döndürebileceği hiçbir öğe yoktur, bu da olması gerekendir.

C'de, bir veri yapısının sonunun ötesini okumaya çalışmak tanımsız davranıştır. Bellek o yapıya ait olmasa bile, veri yapısındaki o öğeye karşılık gelen bellekteki konumda ne varsa alabilirsiniz. Buna arabellek aşırı okuma denir ve bir saldırgan dizini, veri yapısından sonra depolanmasına izin verilmemesi gereken verileri okuyacak şekilde değiştirebiliyorsa, g üvenlik açıklarına yol açabilir.

Programınızı bu tür bir güvenlik açığından korumak için, var olmayan bir dizindeki bir öğeyi okumaya çalışırsanız, Rust yürütmeyi durdurur ve devam etmeyi reddeder. Deneyelim ve görelim:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Bu hata, dizin 99'a erişmeye çalıştığımız main.rs'in 4. satırına işaret ediyor. Sonraki not satırı bize, RUST_BACKTRACE ortam değişkenini, hataya neden olan şeyin tam olarak geri izini almak için ayarlayabileceğimizi söylüyor. Geri izleme, bu noktaya gelmek için çağrılan tüm fonksiyonların bir listesidir. Rust'ta geriye dönük izlemeler, diğer dillerde olduğu gibi çalışır: Geri izlemeyi okumanın anahtarı, en baştan başlamak ve yazdığınız dosyaları görene kadar okumaktır. Sorunun ortaya çıktığı yer orasıdır. Bu noktanın üzerindeki satırlar, kodunuzun çağırdığı koddur; Aşağıdaki satırlar, kodunuzu çağıran koddur. Bu öncesi ve sonrası satırları, temel Rust kodunu, standart kütüphane kodunu veya kullandığınız kasaları içerebilir. RUST_BACKTRACE ortam değişkenini 0 dışında herhangi bir değere ayarlayarak geri izlemeye almayı deneyelim. Liste 9-2, göreceğinize benzer bir çıktı gösteriyor.

$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483
   1: core::panicking::panic_fmt
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:85
   2: core::panicking::panic_bounds_check
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:62
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:255
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:15
   5: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/vec.rs:1982
   6: panic::main
             at ./src/main.rs:4
   7: core::ops::function::FnOnce::call_once
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ops/function.rs:227
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Liste 9-2: RUST_BACKTRACE ortam değişkeni ayarlandığında görüntülenen panic! çağrısı tarafından oluşturulan geri izleme

Çok fazla çıktı var! Gördüğünüz tam çıktı, işletim sisteminize ve Rust sürümünüze bağlı olarak farklı olabilir. Bu bilgilerle geriye dönük izler almak için hata ayıklama sembollerinin etkinleştirilmesi gerekir. Hata ayıklama sembolleri, burada olduğu gibi --release bayrağı olmadan cargo build veya cargo run kullanılırken varsayılan olarak etkindir.

Liste 9-2'deki çıktıda, geri izlemenin 6. satırı, projemizde soruna neden olan satırı işaret ediyor: src/main.rs dosyasının 4. satırı. Eğer programımızın paniğe kapılmasını istemiyorsak, ilk satırın gösterdiği ve yazdığımız bir dosyadan bahseden yerden araştırmamıza başlamalıyız. Kasten panik yaratacak kod yazdığımız Liste 9-1'de, paniği düzeltmenin yolu, vektör indekslerinin aralığının ötesinde bir öğe talep etmemektir. Kodunuz paniklediğinde, paniğe neden olmak için kodun hangi değerlerle hangi eylemi gerçekleştirdiğini ve bunun yerine kodun ne yapması gerektiğini bulmanız gerekir.

panic!'e geri döneceğiz ve hataları yönetmek için panic!'i ne zaman kullanıp kullanmamamız gerektiğini panic!'lemek ya da panic!'lememek” bölümünde daha sonra anlatacağız. Sonraki bölümde, Result tanımını kullanarak nasıl hatadan dönülebileceğine bakacağız.

Result İle Kurtarılabilir Hatalar

Çoğu hata, programın tamamen durdurulmasını gerektirecek kadar ciddi değildir. Bazen, bir fonksiyon başarısız olduğunda, kolayca yorumlayabileceğiniz ve yanıt verebileceğiniz bir nedenden dolayı başarısız olur. Örneğin, bir dosyayı açmaya çalışırsanız ve dosya mevcut olmadığı için bu işlem başarısız olursa, işlemi sonlandırmak yerine dosyayı oluşturmak isteyebilirsiniz.

Bölüm 2'deki Result Türü ile Potansiyel Başarısızlığı Ele Alma” kısmından, Result enum'unun aşağıdaki gibi Ok ve Err olmak üzere iki değişken olarak tanımlandığını hatırlayın:


#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

T ve E yaygın tür parametreleridir: yaygın türleri Bölüm 10'da daha ayrıntılı olarak tartışacağız. Şu anda bilmeniz gereken şey, T'nin Ok değişkeni içinde bir başarı durumunda döndürülecek değerin türünü temsil ettiği ve E'nin Err değişkeni içinde bir başarısızlık durumunda döndürülecek hatanın türünü temsil ettiğidir. Result bu yaygın tür parametrelerine sahip olduğu için, döndürmek istediğimiz başarılı değer ile hata değerinin farklı olabileceği birçok farklı durumda Result türünü ve üzerinde tanımlı fonksiyonları kullanabiliriz.

Fonksiyon başarısız olabileceği için Result değeri döndüren bir fonksiyon çağıralım. Liste 9-3'te bir dosya açmaya çalışıyoruz.

Dosya adı: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}

Liste 9-3: Dosya açmak

File::open öğesinin dönüş türü Result<T, E>'dir. Yaygın parametre T, File::open tarafından başarı değerinin türü olan std::fs::File ile tanımlanmıştır, bu da bir dosya tanıtıcısıdır. Hata değerinde kullanılan E'nin türü std::io::Error'dır. Bu dönüş türü, File::open çağrısının başarılı olabileceği ve okuyabileceğimiz veya yazabileceğimiz bir dosya tanıtıcısı döndürebileceği anlamına gelir. Fonksiyon çağrısı başarısız da olabilir: örneğin, dosya mevcut olmayabilir veya dosyaya erişim iznimiz olmayabilir. File::open fonksiyonunun bize başarılı ya da başarısız olduğunu söyleyecek ve aynı zamanda bize dosya tanıtıcısı ya da hata bilgisi verecek bir yolu olmalıdır. Bu bilgi tam olarak Result enum'unun anlattığı şeydir.

File::open fonksiyonunun başarılı olduğu durumda, greeting_file_result değişkenindeki değer, bir dosya tanıtıcısı içeren Ok tanımı olacaktır. Başarısız olduğu durumda, greeting_file_result değişkenindeki değer, meydana gelen hata türü hakkında daha fazla bilgi içeren Err tanımı olacaktır.

File::open'ın döndürdüğü değere bağlı olarak farklı eylemler gerçekleştirmek için Liste 9-3'teki koda ekleme yapmamız gerekir. Liste 9-4, Bölüm 6'da tartıştığımız temel bir araç olan match ifadesini kullanarak Result'u ele almanın bir yolunu göstermektedir.

Dosya adı: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

Liste 9-4: Döndürülebilecek Result değişkenlerini işlemek için match ifadesi kullanma

Option enum'u gibi Result enum'u ve varyantlarının da prelude tarafından kapsama alındığına dikkat edin, bu nedenle match kollarında Ok ve Err varyantlarından önce Result:: belirtmemize gerek yoktur.

Result, Ok olduğunda, bu kod Ok değişkeninden iç dosya değerini döndürür ve daha sonra bu dosya tanıtıcısı değerini greeting_file değişkenine atar. match'ten sonra, dosya tanıtıcısını okuma veya yazma için kullanabiliriz.

match'in diğer kolu, File::open'dan Err değeri aldığımız durumu ele alır. Bu örnekte, panic! makrosunu çağırmayı seçtik. Geçerli dizinimizde hello.txt adında bir dosya yoksa ve bu kodu çalıştırırsak, panic! makrosundan aşağıdaki çıktıyı görürüz:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`
thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Her zamanki gibi, bu çıktı bize tam olarak neyin yanlış gittiğini söyler.

Farklı Hatalarda Eşleştirme

Liste 9-4'teki kod, File::open'ın neden başarısız olduğuna bakmadan panic! yapacaktır. Ancak, farklı başarısızlık nedenleri için farklı eylemler gerçekleştirmek istiyoruz: File::open dosya mevcut olmadığı için başarısız olduysa, dosyayı oluşturmak ve yeni dosyanın tanıtıcısını döndürmek istiyoruz. File::open başka bir nedenle başarısız olduysa - örneğin, dosyayı açma iznimiz olmadığı için - kodun yine de Liste 9-4'te olduğu gibi panic! yapmasını istiyoruz. Bunun için Liste 9-5'te gösterilen bir iç match ifadesi ekleriz.

Dosya adı: src/main.rs

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            }
            other_error => {
                panic!("Problem opening the file: {:?}", other_error);
            }
        }
    };
}

Liste 9-5: Farklı türdeki hataları farklı şekillerde ele alma

File::open'ın Err değişkeni içinde döndürdüğü değerin türü, standart kütüphane tarafından sağlanan bir yapı olan io::Error'dır. Bu struct, bir io::ErrorKind değeri elde etmek için çağırabileceğimiz bir kind metoduna sahiptir. io::ErrorKind enum'u standart kütüphane tarafından sağlanır ve bir io işleminden kaynaklanabilecek farklı hata türlerini temsil eden varyantlara sahiptir. Kullanmak istediğimiz değişken, açmaya çalıştığımız dosyanın henüz mevcut olmadığını gösteren ErrorKind::NotFound'dur. Bu yüzden greeting_file_result ile eşleşiyoruz, ancak error.kind() ile de bir iç match'imiz var.

İç match'te kontrol etmek istediğimiz koşul, error.kind() tarafından döndürülen değerin ErrorKind enum'unun NotFound varyantı olup olmadığıdır. Eğer varsa, dosyayı File::create ile oluşturmaya çalışırız. Ancak, File::create de başarısız olabileceğinden, iç match ifadesinde ikinci bir kola ihtiyacımız var. Dosya oluşturulamadığında, farklı bir hata mesajı yazdırılır. Dış match'in ikinci kolu aynı kalır, böylece program eksik dosya hatası dışındaki herhangi bir hatada panik yapar.

Result<T, E> ile match Kullanmanın Alternatifleri

Çok fazla match var! match ifadesi çok kullanışlıdır ancak aynı zamanda çok ilkeldir. Bölüm 13'te, Result<T, E> üzerinde tanımlanan birçok metodla birlikte kullanılan kapanış ifadeleri hakkında bilgi edineceksiniz. Bu metodlar, kodunuzdaki Result<T, E> değerlerini işlerken match kullanmaktan daha özlü olabilir.

Örneğin, Liste 9-5'te gösterilen aynı mantığı bu kez kapanış ifadelerini ve unwrap_or_else metodunu kullanarak yazmanın başka bir yolunu aşağıda bulabilirsiniz:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

Bu kod Liste 9-5 ile aynı davranışa sahip olsa da, herhangi bir match ifadesi içermez ve okunması daha temizdir. Bölüm 13'ü okuduktan sonra bu örneğe geri dönün ve standart kütüphane dokümantasyonundan unwrap_or_else metoduna bakın. Bu metodlardan çok daha fazlası, hatalarla uğraşırken iç içe geçmiş büyük match ifadelerini temizleyebilir.

Panik Hatası Kısayolları: unwrap ve expect

match kullanmak yeterince işe yarar, ancak biraz ayrıntılı olabilir ve amacı her zaman iyi iletmez. Result<T, E> türü, çeşitli ve daha spesifik görevleri yerine getirmek için üzerinde tanımlanmış birçok yardımcı metoda sahiptir. unwrap metodu, tıpkı Liste 9-4'te yazdığımız match ifadesi gibi uygulanan bir kısayol yöntemidir. Result değeri Ok varyantı ise unwrap, Ok içindeki değeri döndürür. Result değeri Err değişkeniyse, unwrap bizim için panic! makrosunu çağırır. İşte unwrap'in iş başında olduğu bir örnek:

Dosya adı: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

Bu kodu hello.txt dosyası olmadan çalıştırırsak, unwrap yönteminin yaptığı panic! çağrısından bir hata mesajı görürüz:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4

Benzer şekilde, expect metodu da panic! hata mesajını seçmemizi sağlar. unwrap yerine expect kullanmak ve iyi hata mesajları sağlamak, amacınızı iletebilir ve paniğin kaynağını bulmayı kolaylaştırabilir. expect'in söz dizimi şu şekildedir:

Dosya adı: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

expect'i unwrap ile aynı şekilde kullanırız: dosya tanıtıcısını döndürmek veya panic! makrosunu çağırmak için. expect tarafından panic! çağrısında kullanılan hata mesajı, unwrap'ın kullandığı varsayılan panic! mesajı yerine expect'e aktardığımız parametre olacaktır. İşte böyle görünüyor:

thread 'main' panicked at 'hello.txt should be included in this project: Error
{ repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4

Üretim kalitesindeki kodlarda, çoğu Rustsever unwrap yerine expect'i seçer ve işlemin neden her zaman başarılı olmasının beklendiği hakkında daha fazla bağlam verir. Bu şekilde, varsayımlarınızın yanlış olduğu kanıtlanırsa, hata ayıklamada kullanabileceğiniz daha fazla bilgiye sahip olursunuz.

Hataların Yayılması

Bir fonksiyonun süreklemesi başarısız olabilecek bir şeyi çağırdığında, hatayı fonksiyonun kendi içinde ele almak yerine, hatayı çağıran koda döndürebilirsiniz, böylece işlev ne yapacağına karar verebilir. Bu, hatanın yayılması olarak bilinir ve hatanın nasıl ele alınması gerektiğini belirleyen daha fazla bilgi veya mantığın kodunuzun bağlamında mevcut olandan daha fazla olabileceği çağıran koda daha fazla kontrol sağlar.

Örneğin, Liste 9-6'da bir dosyadan kullanıcı adı okuyan bir fonksiyon gösterilmektedir. Dosya mevcut değilse veya okunamıyorsa, bu fonksiyon; hataları, fonksiyonu çağıran koda döndürür.

Dosya adı: src/main.rs


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}
}

Liste 9-6: match kullanarak arama koduna hata döndüren bir fonksiyon

Bu fonksiyon çok daha kısa bir şekilde yazılabilir, ancak hata işlemeyi keşfetmek için çoğunu manuel olarak yaparak başlayacağız; sonunda daha kısa yolu göstereceğiz. Önce fonksiyonun dönüş tipine bakalım: Result<String, io::Error>. Bu, fonksiyonun Result<T, E> türünde bir değer döndürdüğü anlamına gelir; burada yaygın parametre T, somut String türüyle ve yaygın tür E, somut io::Error tipiyle doldurulmuştur.

Bu fonksiyon sorunsuz bir şekilde başarılı olursa, bu fonksiyonu çağıran kod, bu fonksiyonun dosyadan okuduğu kullanıcı adı olan bir String içeren bir Ok değeri alacaktır. Bu fonksiyon herhangi bir sorunla karşılaşırsa, çağıran kod, sorunların ne olduğu hakkında daha fazla bilgi içeren io::Error tanımını tutan bir Err değeri alır. Bu fonksiyonun geri dönüş türü olarak io::Error'ı seçtik çünkü bu fonksiyonun gövdesinde çağırdığımız ve başarısız olabilecek her iki işlemden (File::open fonksiyonu ve read_to_string metodu) dönen hata değerinin türü budur.

Fonksiyonun gövdesi File::open fonksiyonunu çağırarak başlar. Ardından Result değerini Liste 9-4'teki match'e benzer bir match ile ele alıyoruz. File::open başarılı olursa, file kalıp değişkenindeki dosya tanıtıcısı username_file değişkenindeki değer olur ve fonksiyon devam eder. Err durumunda, panic! çağrısı yapmak yerine, return anahtar sözcüğünü kullanarak fonksiyondan erken döneriz ve File::open'dan gelen hata değerini, şimdi e kalıp değişkeninde, bu fonksiyonun hata değeri olarak çağıran koda geri aktarırız.

Dolayısıyla, username_file içinde bir dosya tanıtıcımız varsa, fonksiyon username değişkeninde yeni bir String oluşturur ve dosyanın içeriğini username içine okumak için username_file içindeki dosya tanıtıcısında read_to_string metodunu çağırır. read_to_string metodu da Result döndürür, çünkü File::open başarılı olsa bile başarısız olabilir. Bu nedenle, Result'u işlemek için başka bir match'e ihtiyacımız var: read_to_string başarılı olursa, fonksiyonumuz başarılı olmuştur ve şimdi bir Ok'a sarılmış username'de bulunan dosyadan kullanıcı adını döndürürüz. read_to_string başarısız olursa, File::open'ın dönüş değerini işleyen match'te hata değerini döndürdüğümüz şekilde hata değerini döndürürüz. Ancak, bu fonksiyondaki son ifade olduğu için açıkça return dememize gerek yoktur.

Bu kodu çağıran kod daha sonra kullanıcı adı içeren bir Ok değeri ya da io::Error içeren bir Err değeri almayı işleyecektir. Bu değerlerle ne yapılacağına çağıran kod karar verir. Çağıran kod bir Err değeri alırsa, panic! çağrısı yapabilir ve programı çökertebilir, varsayılan bir kullanıcı adı kullanabilir veya örneğin kullanıcı adını bir dosyadan başka bir yerden arayabilir. Çağıran kodun gerçekte ne yapmaya çalıştığı hakkında yeterli bilgiye sahip değiliz, bu nedenle uygun şekilde işlemesi için tüm başarı veya hata bilgilerini yukarı doğru yayıyoruz.

Bu hata yayma eğilimi Rust'ta o kadar yaygındır ki, Rust bunu kolaylaştırmak için ? soru işareti operatörünü sağlar.

Hataları Yaymak İçin Bir Kısayol: ? Operatörü

Liste 9-7, Liste 9-6'dakiyle aynı fonksiyona sahip bir read_username_from_file süreklemesini göstermektedir, ancak bu sürekleme ? işlecini kullanmaktadır.

Dosya adı: src/main.rs


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}
}

Liste 9-7: ? operatörünü kullanarak arama koduna hata döndüren bir fonksiyon

Bir Result değerinden sonra yerleştirilen ? işareti, Liste 9-6'da Result değerlerini işlemek için tanımladığımız match ifadeleriyle hemen hemen aynı şekilde çalışacak şekilde tanımlanmıştır. Result'un değeri bir Ok ise, Ok'un içindeki değer bu ifadeden döndürülür ve program devam eder. Değer Err ise, return anahtar sözcüğünü kullanmışız gibi tüm fonksiyondan Err döndürülür, böylece hata değeri çağıran koda yayılır.

Liste 9-6'daki match ifadesinin yaptığı ile ? operatörünün yaptığı arasında bir fark vardır: ? operatörünün çağrıldığı hata değerleri, standart kütüphanedeki From tanımında tanımlanan ve değerleri bir türden diğerine dönüştürmek için kullanılan from fonksiyonundan geçer. ? işleci from fonksiyonunu çağırdığında, alınan hata türü geçerli fonksiyonun dönüş türünde tanımlanan hata türüne dönüştürülür. Bu, bir fonksiyonun birçok farklı nedenden dolayı başarısız olsa bile, bir fonksiyonun başarısız olabileceği tüm yolları temsil etmek için tek bir hata türü döndürdüğünde kullanışlıdır.

Örneğin, Liste 9-7'deki read_username_from_file fonksiyonunu OurError adında tanımladığımız özel bir hata türünü döndürecek şekilde değiştirebiliriz. Bir io::Error'dan bir OurError tanımı oluşturmak için impl From<io::Error> for OurError olarak tanımlarsak, read_username_from_file'ın gövdesindeki ? operatör çağrıları, fonksiyona daha fazla kod eklemeye gerek kalmadan from'u çağıracak ve hata türlerini dönüştürecektir.

Liste 9-7 bağlamında, File::open çağrısının sonundaki ?, Ok içindeki değeri username_file değişkenine döndürecektir. Bir hata oluşursa, ? işleci tüm fonksiyonlardan erken dönecek ve çağıran koda herhangi bir Err değeri verecektir. Aynı şey read_to_string çağrısının sonundaki ? için de geçerlidir.

? işleci, çok sayıda kopyala-yapıştır kodu ortadan kaldırır ve bu fonksiyonun süreklenmesini de daha basit hale getirir. Hatta Liste 9-8'de gösterildiği gibi, ?'den hemen sonra metod çağrılarını zincirleyerek bu kodu daha da kısaltabiliriz.

Dosya adı: src/main.rs


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}
}

Liste 9-8: ? operatöründen sonra zincirleme yöntemi çağrıları

username'de yeni String'in oluşturulmasını fonksiyonun başına taşıdık; sıkıntısız çalışacaktır. Bir username_file değişkeni oluşturmak yerine read_to_string çağrısını doğrudan File::open("hello.txt")? sonucunun üzerine zincirledik. Hala bir ? read_to_string çağrısının sonunda ve hata döndürmek yerine hem File::open hem de read_to_string başarılı olduğunda username'i içeren Ok değerini döndürürüz. İşlevsellik yine Liste 9-6 ve Liste 9-7'deki ile aynıdır; bu sadece yazmanın farklı, daha ergonomik bir yolu.

Liste 9-9, fs::read_to_string kullanarak bunu daha da kısaltmanın bir yolunu gösterir.

Dosya adı: src/main.rs


#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
}

Liste 9-9: Dosyayı açıp okumak yerine fs::read_to_string kullanma

Bir dosyayı bir String'e okumak oldukça yaygın bir işlemdir, bu nedenle standart kütüphane dosyayı açan, yeni bir String oluşturan, dosyanın içeriğini okuyan, içeriği bu String'e atayan ve geri döndüren kullanışlı fs::read_to_string fonksiyonunu sağlar. Elbette, fs::read_to_string kullanmak bize tüm hata işlemlerini açıklama fırsatı vermez, bu yüzden önce uzun yoldan yaptık.

? Operatörünün Kullanılabileceği Yerler

? işleci yalnızca dönüş türü ? işlecinin kullanıldığı değerle uyumlu olan fonksiyonlarda kullanılabilir. Bunun nedeni, ? işlecinin, Liste 9-6'da tanımladığımız match ifadesiyle aynı şekilde, bir değerin fonksiyondan erken dönüşünü gerçekleştirmek üzere tanımlanmış olmasıdır. Liste 9-6'da, match Result değeri kullanıyordu ve erken dönüş kolu Err(e) değeri döndürüyordu. Bu dönüşle uyumlu olması için fonksiyonun dönüş tipi bir Result olmalıdır.

Liste 9-10'da, ? operatörünü ? kullandığımız değerin türüyle uyumsuz bir geri dönüş türüne sahip main fonksiyonunda kullanırsak alacağımız hataya bakalım:

Dosya adı: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}

Liste 9-10: () döndüren main fonksiyonunda ? kullanmaya çalışırsak derlenmeyecektir

Bu kod, başarısız olabilecek bir dosya açar. ? işleci File::open tarafından döndürülen Result değerini takip eder, ancak bu main fonksiyonunun dönüş türü Result değil ()'dir. Bu kodu derlediğimizde aşağıdaki hata mesajını alırız:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:4:36
  |
3 | / fn main() {
4 | |     let f = File::open("hello.txt")?;
  | |                                    ^ cannot use the `?` operator in a function that returns `()`
5 | | }
  | |_- this function should return `Result` or `Option` to accept `?`
  |
  = help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` due to previous error

Bu hata, ? operatörünü yalnızca Result, Option veya FromResidual'ı sürekleyen başka bir tür döndüren bir fonksiyonda kullanabileceğimize işaret eder.

Hatayı düzeltmek için iki seçeneğiniz vardır. Seçeneklerden biri, bunu engelleyen herhangi bir kısıtlama olmadığı sürece fonksiyonunuzun dönüş türünü ? operatörünü kullandığınız değerle uyumlu olacak şekilde değiştirmektir. Diğer teknik ise, Result<T, E>'yi uygun olan şekilde işlemek için match veya Result<T, E> yöntemlerinden birini kullanmaktır.

Hata mesajında ayrıca ? operatörünün Option<T> değerleriyle de kullanılabileceği belirtilmiştir. Result üzerinde ? kullanımında olduğu gibi, Option üzerinde ? kullanımını da yalnızca Option döndüren bir fonksiyonda kullanabilirsiniz. Bir Option<T> üzerinde çağrıldığında ? operatörünün davranışı, bir Result<T, E> üzerinde çağrıldığında gösterdiği davranışa benzer: değer None ise, None o noktada fonksiyondan erken döndürülür. Değer Some ise, Some içindeki değer ifadenin sonuç değeridir ve fonksiyon devam eder. Liste 9-11'de, verilen metindeki ilk satırın son karakterini bulan bir fonksiyon örneği vardır:

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

fn main() {
    assert_eq!(
        last_char_of_first_line("Hello, world\nHow are you today?"),
        Some('d')
    );

    assert_eq!(last_char_of_first_line(""), None);
    assert_eq!(last_char_of_first_line("\nhi"), None);
}

Liste 9-11: Bir Option<T> değerinde ? operatörünü kullanma

Bu fonksiyon Option<char> döndürür, çünkü orada bir karakter olması mümkündür, ancak olmaması da mümkündür. Bu kod, metin dizgisi dilim argümanını alır ve dizedeki satırlar üzerinde bir yineleyici döndüren lines metodunu çağırır. Bu fonksiyon ilk satırı incelemek istediğinden, yineleyiciden ilk değeri almak için yineleyicide next öğesini çağırır. Eğer text boş dizgiyse, next'e yapılan bu çağrı None değerini döndürür, bu durumda durdurmak için ? kullanırız ve last_char_of_first_line'dan None değerini döndürürüz. text boş değilse, next çağrısı metindeki ilk satırın dizgi dilimini içeren Some değerini döndürür.

? dizgi dilimini çıkarır ve karakterlerinin bir yineleyicisini almak için bu dizgi dilimi üzerinde chars'ı çağırabiliriz. Bu ilk satırdaki son karakterle ilgilendiğimizden, yineleyicideki son öğeyi döndürmek için last öğesini çağırırız. Bu bir Option'dur çünkü ilk satırın boş bir dizgi olması mümkündür, örneğin metin boş bir satırla başlıyorsa ancak "\nhi" gibi diğer satırlarda karakterler varsa. Ancak, ilk satırda bir son karakter varsa, Some değişkeninde döndürülür. Ortadaki ? operatörü bize bu mantığı ifade etmek için kısa bir yol sunar ve fonksiyonu tek bir satırda yazmamıza olanak tanır. Option üzerinde ? operatörünü kullanamasaydık, bu mantığı daha fazla metod çağrısı veya bir match ifadesi kullanarak uygulamamız gerekirdi. Kısaca Rust bu ameleliği sizin üzerinizden alır.

Result döndüren bir fonksiyonda bir Result üzerinde ? operatörünü kullanabileceğinizi ve Option döndüren bir fonksiyonda bir Option üzerinde ? operatörünü kullanabileceğinizi, ancak karıştırıp eşleştiremeyeceğinizi unutmayın. ? işleci bir Result'u otomatik olarak Option'a dönüştürmez veya tam tersini yapmaz; bu gibi durumlarda, dönüştürmeyi açıkça yapmak için Result üzerinde Ok metodu veya Option üzerinde ok_or metodu gibi metodları kullanabilirsiniz.

Şimdiye kadar kullandığımız tüm main fonksiyonlar () döndürüyordu. main fonksiyonu özeldir, çünkü çalıştırılabilir programların giriş ve çıkış noktasıdır ve programların beklendiği gibi davranması için dönüş türünün ne olabileceği konusunda kısıtlamalar vardır.

Neyse ki main aynı zamanda bir Result<(), E> de döndürebilir. Liste 9-12, Liste 9-10'daki kodu içerir, ancak main'in dönüş türünü Result<(), Box<dyn Error>> olarak değiştirdik ve sonuna bir dönüş değeri Ok(()) ekledik. Bu kod şimdi derlenecektir:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}

Liste 9-12: main'i Result<(), E> döndürecek şekilde değiştirmek, Result değerlerinde ? operatörünün kullanılmasına izin verir

Box<dyn Error> türü bir tanım nesnesidir ve 17. Bölümdeki “Farklı Türlerde Değerlere İzin Veren Tanım Nesnelerini Kullanma” kısmında bundan bahsedeceğiz. Şimdilik, Box<dyn Error> türünü “hatanın her türlüsü” olarak okuyabilirsiniz. Hata türü Box<dyn Error> olan bir main'de, Result değeri üzerinde ? kullanılmasına izin verilir, çünkü herhangi bir Err değerinin erken döndürülmesine izin verir. Bu main'in gövdesi yalnızca std::io::Error türünde hatalar döndürecek olsa da, Box<dyn Error> belirtilerek, main gövdesine başka hatalar döndüren başka kodlar eklense bile bu imza doğru olmaya devam edecektir.

main, Result<(), E> döndürdüğünde, main Ok(()) döndürürse yürütülebilir dosya 0 değeriyle çıkar ve main Err değeri döndürürse sıfır olmayan bir değerle çıkar. C'de yazılmış çalıştırılabilir dosyalar çıktıklarında tam sayı döndürür: başarıyla çıkan programlar 0 döndürür ve hata veren programlar 0 dışında bir tam sayı döndürür. Rust da bu kuralla uyumlu olmak için çalıştırılabilir dosyadan tam sayı döndürür.

main fonksiyonu, ExitCode döndüren bir fonksiyon raporu içeren std::process::Termination tanımını sürekleyen herhangi bir türü döndürebilir. Kendi türleriniz için Termination tanımını sürekleme hakkında daha fazla bilgi için standart kütüphane dokümantasyonuna bakın.

panic! çağrısı yapmanın veya Result döndürmenin ayrıntılarını tartıştığımıza göre, hangi durumlarda hangisinin kullanılmasının uygun olacağına nasıl karar verileceği konusuna geri dönelim.

panic!'lemek mi, panic!'lememek mi?

Peki ne zaman panik yapacağınıza ve ne zaman Result'a geri döneceğinize nasıl karar vereceksiniz? Kod panik yaptığında, kurtarmanın bir yolu yoktur. Kurtarmanın olası bir yolu olsun ya da olmasın, herhangi bir hata durumu için panic! çağrısı yapabilirsiniz, ancak o zaman çağıran kod adına bir durumun kurtarılamaz olduğuna karar vermiş olursunuz. Bir Result değeri döndürmeyi seçtiğinizde, çağıran koda seçenekler sunarsınız. Çağıran kod, kendi durumuna uygun bir şekilde kurtarma girişiminde bulunmayı seçebilir veya bu durumda bir Err değerinin kurtarılamaz olduğuna karar verebilir, böylece panic! çağrısı yapabilir ve kurtarılabilir hatanızı kurtarılamaz bir hataya dönüştürebilir. Bu nedenle, başarısız olabilecek bir fonksiyon tanımlarken Result döndürmek iyi bir varsayılan seçimdir.

Örnekler, prototip kodu ve testler gibi durumlarda, Result döndürmek yerine panikleyen kod yazmak daha uygundur. Nedenini inceleyelim, ardından derleyicinin başarısızlığın imkansız olduğunu söyleyemediği, ancak insan olarak sizin söyleyebildiğiniz durumları tartışalım. Bölüm, kütüphane kodunda panik yapıp yapmamaya nasıl karar verileceğine ilişkin bazı genel yönergelerle sona erecektir.

Örnekler, Prototip Kod ve Testler

Bir kavramı açıklamak için bir örnek yazarken, sağlam hata işleme kodu da eklemek örneği daha az anlaşılır hale getirebilir. Örneklerde, unwrap gibi panik yaratabilecek bir metoda yapılan çağrının, uygulamanızın hataları nasıl ele almasını istediğinize yönelik bir yer tutucu olduğu anlaşılır; bu da kodunuzun geri kalanının ne yaptığına bağlı olarak farklılık gösterebilir.

Benzer şekilde, unwrap ve expect metodları, hataları nasıl ele alacağınıza karar vermeye hazır olmadan önce prototip oluştururken çok kullanışlıdır. Programınızı daha sağlam hale getirmeye hazır olduğunuzda kodunuzda net işaretler bırakırlar.

Bir testte bir metod çağrısı başarısız olursa, bu yöntem test edilen fonksiyon olmasa bile tüm testin başarısız olmasını istersiniz. panic! bir testin başarısız olarak işaretlenme şekli olduğundan, unwrap veya expect çağrısı tam olarak olması gereken şeydir.

Derleyiciden Daha Fazla Bilgiye Sahip Olduğunuz Durumlar

Ayrıca, Result'un Ok değerine sahip olmasını sağlayan başka bir mantığınız olduğunda unwrap veya expect çağrısı yapmak da uygun olacaktır, ancak bu mantık derleyicinin anlayabileceği bir şey değildir. Hala işlemeniz gereken bir Result değeriniz olacaktır: çağırdığınız işlem, sizin özel durumunuzda mantıksal olarak imkansız olsa bile, genel olarak başarısız olma olasılığına sahiptir. Kodu manuel olarak inceleyerek hiçbir zaman bir Err varyantına sahip olmayacağınızdan emin olabiliyorsanız, unwrap'i çağırmak tamamen kabul edilebilir ve hatta expect metninde hiçbir zaman bir Err varyantına sahip olmayacağınızı düşünmenizin nedenini belgelemek daha iyidir. İşte bir örnek:

fn main() {
    use std::net::IpAddr;

    let home: IpAddr = "127.0.0.1"
        .parse()
        .expect("Hardcoded IP address should be valid");
}

Kodlanmış bir dizeyi ayrıştırarak bir IpAddr örneği oluşturuyoruz. 127.0.0.1'in geçerli bir IP adresi olduğunu görebiliyoruz, bu nedenle burada expect kullanmak kabul edilebilir. Ancak, sabit kodlu, geçerli bir dizeye sahip olmak, parse metodunun dönüş türünü değiştirmez: hala bir Result değeri alırız ve derleyici, bu dizginin her zaman geçerli bir IP adresi olduğunu görecek kadar akıllı olmadığından, Err varyantı bir olasılıkmış gibi Result'u işlememizi sağlar. IP adresi dizgisi programa kodlanmak yerine bir kullanıcıdan gelseydi ve bu nedenle hata olasılığı olsaydı, bunun yerine Result'u kesinlikle daha sağlam bir şekilde ele almak isterdik. Bu IP adresinin sabit kodlu olduğu varsayımından bahsetmek, gelecekte IP adresini başka bir kaynaktan almamız gerekirse, daha iyi hata işleme kodu beklentisini değiştirmemizi sağlayacaktır.

Hata İşleme Yönergeleri

Kodunuzun kötü bir duruma düşme olasılığı olduğunda kodunuzun paniğe kapılması tavsiye edilir. Bu bağlamda kötü durum, geçersiz değerler, çelişkili değerler veya eksik değerlerin kodunuza aktarılması gibi bazı varsayımların, garantilerin, sözleşmelerin veya değişmezlerin ihlal edilmesi ve ayrıca aşağıdakilerden bir veya daha fazlasının gerçekleşmesi durumudur:

  • Kötü durum, kullanıcının yanlış formatta veri girmesi gibi ara sıra meydana gelebilecek bir durumun aksine beklenmedik bir durumdur. Bu noktadan sonra kodunuzun her adımda sorunu kontrol etmek yerine bu kötü durumda olmamaya güvenmesi gerekir. Kullandığınız türlerde bu bilgiyi kodlamanın iyi bir yolu yoktur. Bölüm 17'deki “Durumları ve Davranışları Türler Olarak Kodlama” kısmında ne demek istediğimizi bir örnekle açıklayacağız.

  • Birisi kodunuzu çağırır ve mantıklı olmayan değerler girerse, yapabiliyorsanız bir hata döndürmek en iyisidir, böylece kütüphane kullanıcısı bu durumda ne yapmak istediğine karar verebilir. Ancak, devam etmenin güvensiz veya zararlı olabileceği durumlarda, en iyi seçim panic! çağrısı yapmak ve kütüphanenizi kullanan kişiyi kodlarındaki hata konusunda uyarmak olabilir, böylece geliştirme sırasında düzeltebilirler. Benzer şekilde, kontrolünüz dışında olan harici bir kodu çağırıyorsanız ve bu kod düzeltme imkanınızın olmadığı geçersiz bir durum döndürüyorsa panic! çağrısı yapılmalıdır.

  • Başarısızlık beklendiğinde, panic! çağrısı yapmaktansa bir Result döndürmek daha uygundur. Örnekler arasında, hatalı biçimlendirilmiş verilerin verildiği bir ayrıştırıcı veya bir hız sınırına ulaştığınızı gösteren bir durum döndüren bir HTTP isteği yer alır. Bu durumlarda, Result döndürmek, başarısızlığın, çağıran kodun nasıl ele alacağına karar vermesi gereken beklenen bir olasılık olduğunu gösterir.

Kodunuz, geçersiz değerler kullanılarak çağrıldığında kullanıcıyı riske atabilecek bir işlem gerçekleştirdiğinde, kodunuz önce değerlerin geçerli olduğunu doğrulamalı ve değerler geçerli değilse paniklemelidir. Bu çoğunlukla güvenlik nedenleriyle yapılır: geçersiz veriler üzerinde işlem yapmaya çalışmak kodunuzu güvenlik açıklarına maruz bırakabilir. Sınır dışı bir bellek erişimi denediğinizde standart kütüphanenin panic! çağrısı yapmasının ana nedeni budur: mevcut veri yapısına ait olmayan belleğe erişmeye çalışmak yaygın bir güvenlik sorunudur. Fonksiyonların genellikle sözleşmeleri vardır: davranışları yalnızca girdilerin belirli gereksinimleri karşılaması durumunda garanti edilir. Sözleşme ihlal edildiğinde paniklemek mantıklıdır çünkü bir sözleşme ihlali her zaman çağıran tarafında bir hata olduğunu gösterir ve çağıran kodun açıkça ele almasını istediğiniz bir hata türü değildir. Aslında, çağıran kodun hatayı telafi etmesinin makul bir yolu yoktur; çağıran programcıların kodu düzeltmesi gerekir. Bir fonksiyon için sözleşmeler, özellikle de bir ihlal paniğe neden olacaksa, işlevin API belgelerinde açıklanmalıdır.

Ancak, tüm fonksiyonlarınızda çok sayıda hata kontrolü olması ayrıntılı ve can sıkıcı olacaktır. Neyse ki, Rust'ın tür sistemini (ve dolayısıyla derleyici tarafından yapılan tür kontrolünü) kontrollerin çoğunu sizin için yapmak için kullanabilirsiniz. Fonksiyonunuz parametre olarak belirli bir türe sahipse, derleyicinin zaten geçerli bir değere sahip olduğunuzdan emin olduğunu bilerek kodunuzun mantığına devam edebilirsiniz. Örneğin, bir Option yerine bir türünüz varsa, programınız hiçbir şey yerine bir şey olmasını bekler. Bu durumda kodunuz Some ve None varyantları için iki durumla uğraşmak zorunda kalmaz: kesinlikle bir değere sahip olmak için yalnızca bir durum olacaktır. Fonksiyonunuza hiçbir şey iletmemeye çalışan kod derlenmez bile, bu nedenle fonksiyonunuzun çalışma zamanında bu durumu kontrol etmesi gerekmez. Başka bir örnek de u32 gibi işaretsiz bir tam sayı türü kullanmaktır, bu da parametrenin asla negatif olmamasını sağlar.

Doğrulama için Özel Tipler Oluşturma

Geçerli bir değere sahip olduğumuzdan emin olmak için Rust'ın tür sistemini kullanma fikrini bir adım daha ileri götürelim ve doğrulama için özel bir tür oluşturmaya bakalım. Bölüm 2'de kodumuzun kullanıcıdan 1 ile 100 arasında bir sayı tahmin etmesini istediği tahmin oyununu hatırlayın. Gizli sayımızla karşılaştırmadan önce kullanıcının tahmininin bu sayılar arasında olduğunu doğrulamadık; yalnızca tahminin pozitif olduğunu doğruladık. Bu durumda, sonuçlar çok vahim değildi: “Too high” veya "Too low" çıktılarımız yine de doğru olacaktı. Ancak, kullanıcıyı geçerli tahminlere yönlendirmek ve bir kullanıcı aralık dışında bir sayı tahmin ettiğinde, bunun yerine örneğin harfler yazdığında farklı bir davranışa sahip olmak yararlı bir geliştirme olacaktır.

Bunu yapmanın bir yolu, potansiyel olarak negatif sayılara izin vermek için tahmini yalnızca bir u32 yerine bir i32 olarak ayrıştırmak ve ardından sayının aralıkta olup olmadığına dair bir kontrol eklemek olabilir:

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        // --snip--

        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: i32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        if guess < 1 || guess > 100 {
            println!("The secret number will be between 1 and 100.");
            continue;
        }

        match guess.cmp(&secret_number) {
            // --snip--
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

if ifadesi, değerimizin aralık dışında olup olmadığını kontrol eder, kullanıcıya sorun hakkında bilgi verir ve döngünün bir sonraki yinelemesini başlatmak ve başka bir tahmin istemek için continue çağrısı yapar. if ifadesinden sonra, tahminin 1 ile 100 arasında olduğunu bilerek tahmin ile gizli sayı arasındaki karşılaştırmalara devam edebiliriz.

Ancak, bu ideal bir çözüm değildir: programın yalnızca 1 ile 100 arasındaki değerler üzerinde çalışması kesinlikle kritikse ve bu gereksinime sahip birçok fonksiyonu varsa, her işlevde bunun gibi bir kontrol yapmak sıkıcı olacaktır (ve performansı etkileyebilir).

Bunun yerine, yeni bir tür oluşturabilir ve doğrulamaları her yerde tekrarlamak yerine türün bir örneğini oluşturmak için doğrulamaları bir fonksiyona koyabiliriz. Bu şekilde, fonksiyonların imzalarında yeni türü kullanmaları ve aldıkları değerleri güvenle kullanmaları güvenli olur. Liste 9-13, yalnızca yeni fonksiyon 1 ile 100 arasında bir değer alırsa Guess'in bir örneğini oluşturacak bir Guess türü tanımlamanın bir yolunu göstermektedir.


#![allow(unused)]
fn main() {
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }

    pub fn value(&self) -> i32 {
        self.value
    }
}
}

Liste 9-13: Yalnızca 1 ile 100 arasındaki değerlerle devam edecek bir Guess türü

İlk olarak, bir i32 tutan value adlı bir üyeye sahip Guess adlı bir yapı tanımlarız. Burası sayının saklanacağı yerdir.

Ardından, Guess değerlerinin örneklerini oluşturan Guess üzerinde new adında ilişkili bir fonksiyon uyguluyoruz. new fonksiyonu, i32 türünde value adında bir parametreye sahip olacak ve bir Guess değeri döndürecek şekilde tanımlanır. new fonksiyonunun gövdesindeki kod, 1 ile 100 arasında olduğundan emin olmak için değeri test eder. Eğer value bu testi geçemezse, çağıran kodu yazan programcıyı düzeltmesi gereken bir hata olduğu konusunda uyaracak bir panic! çağrısı yaparız, çünkü bu aralığın dışında bir değere sahip bir Guess oluşturmak Guess::new'in dayandığı sözleşmeyi ihlal edecektir. Guess::new'in paniğe kapılabileceği koşullar kamuya açık API dokümantasyonunda tartışılmalıdır; Bölüm 14'te oluşturacağınız API dokümantasyonunda panic olasılığını belirten dokümantasyon kurallarını ele alacağız. Eğer value testi geçerse, value alanı value parametresine ayarlanmış yeni bir Guess yaratırız ve Guess'i döndürürüz.

Ardından, self öğesini ödünç alan, başka parametresi olmayan ve bir i32 döndüren value adlı bir metod yazarız. Bu tür yöntemlere bazen getter adı verilir, çünkü amacı alanlarından bazı verileri almak ve döndürmektir. Guess yapısının value üyesi gizli olduğu için burada yaygın metod gereklidir. Değer üyesinin gizli olması önemlidir, böylece Guess yapısını kullanan kodun değeri doğrudan ayarlamasına izin verilmez: modül dışındaki kod, bir Guess tanımı oluşturmak için Guess::new fonksiyonunu kullanmalıdır, böylece Guess'in Guess::new fonksiyonundaki koşullar tarafından kontrol edilmemiş bir değere sahip olmasının hiçbir yolu yoktur.

Parametresi olan veya yalnızca 1 ile 100 arasındaki sayıları döndüren bir fonksiyon, imzasında bir i32 yerine bir Guess aldığını veya döndürdüğünü bildirebilir ve gövdesinde herhangi bir ek kontrol yapması gerekmez.

Özet

Rust'ın hata işleme özellikleri, daha sağlam kod yazmanıza yardımcı olmak için tasarlanmıştır. panic! makrosu, programınızın üstesinden gelemeyeceği bir durumda olduğunu bildirir ve geçersiz veya yanlış değerlerle devam etmeye çalışmak yerine sürece durmasını söylemenizi sağlar. Result enum'u, işlemlerin kodunuzun kurtarabileceği bir şekilde başarısız olabileceğini belirtmek için Rust'ın tür sistemini kullanır. Result'u, kodunuzu çağıran koda olası başarı veya başarısızlığı da ele alması gerektiğini söylemek için kullanabilirsiniz. panic! ve Result'u uygun durumlarda kullanmak, kaçınılmaz sorunlar karşısında kodunuzu daha güvenilir hale getirecektir.

Standart kütüphanenin Option ve Result enum'ları ile yaygınları nasıl kullandığını gördüğünüze göre, yaygınların nasıl çalıştığından ve bunları kodunuzda nasıl kullanabileceğinizden bahsedeceğiz.

Yaygın Türler, Tanımlar ve Ömürler

Her programlama dili, kavramların tekrarını etkin bir şekilde ele almak için araçlara sahiptir. Rust'ta bu araç yaygınlardır. Kodu derlerken ve çalıştırırken yerlerinde ne olacağını bilmeden yaygınların davranışını veya diğer yaygınlarla nasıl ilişkili olduklarını ifade edebiliriz.

Fonksiyonlar, i32 veya String gibi somut bir tür yerine bazı yaygın türdeki parametreleri alabilir; aynı şekilde, bir işlevin aynı kodu birden çok somut değerde çalıştırmak için bilinmeyen değerlere sahip parametreleri alması gibi. Aslında, Bölüm 6'da Option<T>, Bölüm 8'de Vec<T> ve HashMap<K, V> ve Bölüm 9'da Result<T, E> ile yaygınları zaten kullandık. Bu bölümde, yaygınları kullanarak kendi türlerinizi, fonksiyonlarınızı ve metodlarınızı nasıl tanımlayacabileceğinizi keşfedeceksiniz!

İlk olarak, kod tekrarını azaltmak için bir fonksiyonun nasıl çıkarılacağını inceleyeceğiz. Daha sonra, yalnızca parametrelerinin türlerinde farklılık gösteren iki fonksiyondan yaygın bir fonksiyon yapmak için aynı tekniği kullanacağız. Ayrıca struct ve enum tanımlarında yaygın türlerin nasıl kullanılacağını açıklayacağız.

Ardından, davranışı yaygın bir şekilde tanımlamak için tanımları nasıl kullanacağınızı öğreneceksiniz. Yaygın bir türü, herhangi bir türün aksine, yalnızca belirli bir davranışı olan türleri kabul edecek şekilde sınırlamak için tanımları genel türlerle birleştirebilirsiniz.

Son olarak, derleyiciye referansların birbirleriyle nasıl ilişkili olduğu hakkında bilgi veren çeşitli yaygın türleri olan yaşam sürelerini tartışacağız. Ömürler, derleyiciye ödünç alınan değerler hakkında yeterli bilgi vermemize izin verir, böylece referansların bizim yardımımız olmadan yapabileceğinden daha fazla durumda geçerli olmasını sağlayabilir.

Bir Fonksiyonu Ayıklayarak Fazlalığı Atmak

Yaygınlar, kod çoğaltmasını kaldırmak için belirli türleri birden çok türü temsil eden bir yer tutucuyla değiştirmemize olanak tanır. Yaygınların söz dizimine dalmadan önce, belirli değerleri birden çok değeri temsil eden bir yer tutucuyla değiştiren bir fonksiyonu çıkararak, yaygın türleri içermeyen bir şekilde çoğaltmanın nasıl ayıklanabileceğine bakalım. Ardından, yaygın bir fonksiyonu ayıklamak için aynı tekniği uygulayacağız! Bir fonksiyondan çıkarabileceğiniz fazlalık kodu nasıl tanıyacağınıza bakarak, yaygınları kullanabilen fazlalık kodu tanımaya başlayacaksınız.

Liste 10-1'deki listedeki en büyük sayıyı bulan kısa programla başlıyoruz.

Dosya adı: src/main.rs

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let mut largest = number_list[0];

    for number in number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {}", largest);
    assert_eq!(largest, 100);
}

Liste 10-1: Sayı listesindeki en büyük sayıyı bulma

number_list değişkeninde bir tam sayı listesi saklarız ve listedeki ilk sayıyı largest adlı bir değişkene atarız. Daha sonra listedeki tüm sayılar arasında yineleniriz ve mevcut sayı largest'ta saklanan sayıdan büyükse, o değişkendeki sayıyı değiştiririz. Ancak, mevcut sayı o ana kadar görülen en büyük sayıdan küçük veya ona eşitse, değişken değişmez ve kod listedeki bir sonraki sayıya geçer. Listedeki tüm sayıları göz önünde bulundurduktan sonra, largest, bu durumda 100 olan en büyük sayıyı tutmalıdır.

Şimdi iki farklı sayı listesindeki en büyük sayıyı bulmakla görevlendirildiğimize göre, bunu yapmak için Liste 10-1'deki kodu çoğaltmayı seçebilir ve Liste 10-2'de gösterildiği gibi programdaki iki farklı yerde aynı mantığı kullanabiliriz.

Dosya adı: src/main.rs

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let mut largest = number_list[0];

    for number in number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {}", largest);

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let mut largest = number_list[0];

    for number in number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {}", largest);
}

Liste 10-2: İki sayı listesindeki en büyük sayıyı bulmak için kullanılabilecek kod

Bu kod çalışsa da, kodu kopyalamak sıkıcı ve hataya açık. Ayrıca kodu değiştirmek istediğimizde birden çok yeri de güncellemeyi unutmamalıyız.

Bu tekrarı ortadan kaldırmak için, bir parametrede geçirilen herhangi bir tam sayı listesinde çalışan bir fonksiyon tanımlayarak bir soyutlama oluşturacağız. Bu çözüm, kodumuzu daha net hale getirir ve bir listedeki en büyük sayıyı bulma kavramını soyut olarak ifade etmemizi sağlar.

Liste 10-3'te, en büyük sayıyı bulan kodu largest adlı bir fonksiyona çıkarıyoruz. Ardından, Liste 10-2'deki iki listedeki en büyük sayıyı bulmak için bu fonksiyonu çağırırız. Bu fonksiyonu, gelecekte diğer i32 değerleri listesinde de kullanabiliriz.

Dosya adı: src/main.rs

fn largest(list: &[i32]) -> i32 {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);
    assert_eq!(result, 100);

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let result = largest(&number_list);
    println!("The largest number is {}", result);
    assert_eq!(result, 6000);
}

Liste 10-3: İki listedeki en büyük sayıyı bulmak için kullanılabilecek soyut kod

largest fonksiyonunun, fonksiyona aktarabileceğimiz herhangi bir somut i32 değer dilimini temsil eden list adında bir parametresi vardır. Sonuç olarak, fonksiyonu çağırdığımızda kod, ilettiğimiz belirli değerler üzerinde çalışır. Şimdilik for döngüsünün söz dizimini dert etmeyin. Burada bir i32 referansına herhangi bir atıfta bulunmuyoruz; for döngüsünün aldığı her &i32'yi modelle eşleştiriyor ve yok ediyoruz, böylece item döngü gövdesi içinde olduğu sürece türü i32 olacaktır. Model eşleştirmeyi Bölüm 18'de daha ayrıntılı olarak ele alacağız.

Özetle, kodu Liste 10-2'den Liste 10-3'e değiştirmek için attığımız adımlar şunlardır:

  1. Yinelenen kodu tanımlamak.
  2. Yinelenen kodu fonksiyonun gövdesine çıkarmak ve bu kodun girdilerini ve dönüş değerlerini fonksiyon tanımında belirtmek.
  3. Fonksiyonu çağırmak için yinelenen kodun iki örneğini de güncellemek.

Daha sonra, kod tekrarını azaltmak için aynı adımları yaygınlarla birlikte kullanacağız.

Fonksiyon gövdesinin belirli değerler yerine soyut bir liste üzerinde çalışabilmesi gibi, yaygınlar da kodun soyut türler üzerinde çalışmasına izin verir.

Örneğin, iki fonksiyonumuz olduğunu varsayalım: biri i32 değerleri dilimindeki en büyük öğeyi bulan ve diğeri bir char değerleri dilimindeki en büyük öğeyi bulan. Bu tekrarı nasıl ortadan kaldıracağız?

Hadi bulalım!

Yaygın Veri Türleri

Fonksiyon imzaları veya yapılar gibi öğeler için tanımlar oluşturmak için yaygınları kullanırız ve bunları daha sonra birçok farklı somut veri türüyle kullanabiliriz. İlk olarak yaygınları kullanarak fonksiyonları, yapıları, enum'ları ve metodları nasıl tanımlayacağımıza bakalım. Daha sonra yaygınların kod performansını nasıl etkilediğini tartışacağız.

Fonksiyon Tanımlarında

Yaygın kullanan bir fonksiyon tanımlarken, yaygınları fonksiyonun imzasına, genellikle parametrelerin ve dönüş değerinin veri tiplerini belirttiğimiz yere yerleştiririz. Bunu yapmak kodumuzu daha esnek hale getirir ve kod tekrarını önlerken fonksiyonumuzu çağıranlara daha fazla işlevsellik sağlar.

largest fonksiyonumuzla devam edersek, Liste 10-4'te her ikisi de bir dilimdeki en büyük değeri bulan iki fonksiyon gösterilmektedir. Daha sonra bunları yaygın kullanan tek bir fonksiyonda birleştireceğiz.

Dosya adı: src/main.rs

fn largest_i32(list: &[i32]) -> i32 {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> char {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest_i32(&number_list);
    println!("The largest number is {}", result);
    assert_eq!(result, 100);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {}", result);
    assert_eq!(result, 'y');
}

Liste 10-4: Yalnızca adlarında ve imzalarındaki türlerde farklılık gösteren iki fonksiyon

largest_i32 fonksiyonu, bir dilimdeki en büyük i32'yi bulan Liste 10-3'te çıkardığımız fonksiyondur. largest_char fonksiyonu bir dilimdeki en büyük char değerini bulur. Fonksiyon gövdeleri aynı koda sahiptir, bu nedenle tek bir fonksiyona yaygın tür parametresi ekleyerek yinelemeyi ortadan kaldıralım.

Yeni bir tek fonksiyonda türleri parametrelendirmek için, tıpkı bir fonksiyonun değer parametreleri için yaptığımız gibi tür parametresini adlandırmamız gerekir. Tür parametresi adı olarak herhangi bir tanımlayıcı kullanabilirsiniz. Ancak biz T kullanacağız çünkü Rust'ta parametre adları genellikle sadece bir harf olmak üzere kısadır ve Rust'ın tür adlandırma kuralı CamelCase'dir. “tür, type” kelimesinin kısaltması olan T, çoğu Rust programcısının varsayılan tercihidir.

Fonksiyonun gövdesinde bir parametre kullandığımızda, parametre adını imzada bildirmemiz gerekir, böylece derleyici bu adın ne anlama geldiğini bilir. Benzer şekilde, bir fonksiyon imzasında bir tür parametre adı kullandığımızda, kullanmadan önce tür parametre adını bildirmemiz gerekir. Yaygın largest fonksiyonunu tanımlamak için, tür adı bildirimlerini fonksiyonun adı ile parametre listesi arasına köşeli parantezler (<>) içinde yerleştirin, aşağıdaki gibi:

fn largest<T>(list: &[T]) -> T {

Bu tanımı şu şekilde okuyabiliriz: largest fonksiyonu bazı T türleri üzerinde yaygındır. Bu fonksiyonun list adında bir parametresi vardır ve bu parametre T türünde bir değer dilimidir. largest fonksiyonu aynı T türünde bir değer döndürecektir.

Liste 10-5, imzasında yaygın veri tipini kullanan birleşik largest fonksiyon tanımını gösterir. Liste ayrıca, fonksiyonu i32 değerlerinden oluşan bir dilim ya da char değerleriyle nasıl çağırabileceğimizi de gösterir. Bu kodun henüz derlenmeyeceğini unutmayın, ancak bu bölümün ilerleyen kısımlarında bunu düzelteceğiz.

Dosya adı: src/main.rs

fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

Liste 10-5: Yaygın tür parametreleri kullanan largest fonksiyonu; bu henüz derlenmiyor

Kodu şimdi derlemeye çalışırsak, şu hatayı alırız:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- T
  |            |
  |            T
  |
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
  |             ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error

Notta bir tanım olan std::cmp::PartialOrd'dan bahsedilmektedir. Tanımlar hakkında bir sonraki bölümde konuşacağız. Şimdilik, bu hatanın largest'in gövdesinin T'nin olabileceği tüm olası türler için çalışmayacağını belirttiğini bilin. Gövdede T türündeki değerleri karşılaştırmak istediğimiz için, yalnızca değerleri sıralanabilen türleri kullanabiliriz. Karşılaştırmaları etkinleştirmek için, standart kütüphanede türler üzerinde uygulayabileceğiniz std::cmp::PartialOrd tanımı vardır (bu tanım hakkında daha fazla bilgi için Ekleme C'ye bakın). Yaygın bir türün belirli bir tanıma sahip olduğunu nasıl belirteceğinizi “Parametre Olarak Tanımlar” bölümünde öğreneceksiniz. Bu kodu düzeltmeden önce (“Tanım Sınırları ile Fonksiyonu Düzeltme” bölümünde), yaygın tür parametrelerini kullanmanın diğer yollarını inceleyelim.

Struct Tanımlarında

Ayrıca, <> söz dizimini kullanarak bir veya daha fazla alanda yaygın tür parametresi kullanmak için struct'ları tanımlayabiliriz. Liste 10-6, herhangi bir türdeki x ve y koordinat değerlerini tutmak için bir Point<T> struct'ı tanımlar.

Dosya adı: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

Liste 10-6: T türünde x ve y değerlerini tutan bir Point<T yapısı

Yapı tanımlarında yaygın türlerin kullanımı için söz dizimi, fonksiyon tanımlarında kullanılan söz dizimine benzer. İlk olarak, struct adından hemen sonra köşeli parantezler içinde tür parametresinin adını bildiririz. Ardından, struct tanımında somut veri türlerini belirteceğimiz yerde yaygın türü kullanırız.

Point<T>'yi tanımlamak için yalnızca bir yaygın tür kullandığımızdan, bu tanımın Point<T> yapısının bazı T türleri üzerinde yaygın olduğunu ve x ve y üyelerinin her ikisinin de, bu tür ne olursa olsun, aynı tür olduğunu söylediğine dikkat edin. Liste 10-7'de olduğu gibi, farklı türlerde değerlere sahip bir Point<T> tanımı oluşturursak, kodumuz derlenmeyecektir.

Dosya adı: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}

Liste 10-7: Her ikisi de aynı genel veri türü T'ye sahip olduğundan, x ve y üyeleri aynı türde olmalıdır.

Bu örnekte, x'e 5 tam sayı değerini atadığımızda, derleyiciye T yaygın türünün bu Point<T> tanımı için bir tam sayı olacağını bildiririz. Daha sonra, x ile aynı türe sahip olacak şekilde tanımladığımız y için 4.0 değerini belirttiğimizde, aşağıdaki gibi tür uyuşmazlığı hatası alırız:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
 --> src/main.rs:7:38
  |
7 |     let wont_work = Point { x: 5, y: 4.0 };
  |                                      ^^^ expected integer, found floating-point number

For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` due to previous error

x ve y'nin her ikisinin de yaygın olduğu ancak farklı türlere sahip olabileceği bir Point yapısını tanımlamak için birden fazla yaygın tür parametresi kullanabiliriz. Örneğin, Liste 10-8'de, Point tanımını T ve U türleri üzerinde yaygın olacak şekilde değiştiririz; burada x T tipinde ve y U tipindedir.

Dosya adı: src/main.rs

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

Liste 10-8: İki tür üzerinde yaygın bir Point<T, U>, böylece x ve y farklı türlerin değerleri olabilir

Artık gösterilen tüm Point tanımlarına izin verilmektedir! Bir tanımda istediğiniz kadar yaygın tür parametresi kullanabilirsiniz, ancak birkaç taneden fazla kullanmak kodunuzun okunmasını zorlaştırır. Kodunuzda çok sayıda yaygın türe ihtiyaç duyuyorsanız, bu kodunuzun daha küçük parçalar halinde yeniden yapılandırılması gerektiğini gösterebilir.

enum Tanımlarında

Yapılarda yaptığımız gibi, yaygın veri türlerini varyantlarında tutmak için enum'ları tanımlayabiliriz. Standart kütüphanenin sağladığı ve Bölüm 6'da kullandığımız Option<T> enum'una bir kez daha göz atalım:


#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}
}

Bu tanım şimdi size daha anlamlı gelecektir. Gördüğünüz gibi Option<T> enum'u T türü üzerinde yaygındır ve iki çeşidi vardır: T türünde bir değer tutan Some ve herhangi bir değer tutmayan None varyantı. Option<T> enum'unu kullanarak, isteğe bağlı bir değerin soyut kavramını ifade edebiliriz ve Option<T> yaygın olduğu için, isteğe bağlı değerin türü ne olursa olsun bu soyutlamayı kullanabiliriz.

enum'lar birden fazla yaygın tür de kullanabilir. Bölüm 9'da kullandığımız Result enum tanımı buna bir örnektir:


#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

Result enum'u, T ve E olmak üzere iki tür üzerinde yaygındır ve iki çeşidi vardır: T türünde bir değer tutan Ok ve E türünde bir değer tutan Err. Bu tanım, başarılı (T türünde bir değer döndüren) veya başarısız (E türünde bir hata döndüren) olabilecek bir işlemimiz olan her yerde Result enum'unu kullanmayı kolaylaştırır. Aslında, Liste 9-3'te bir dosyayı açmak için kullandığımız şey buydu; dosya başarıyla açıldığında T, std::fs::File türüyle atandı ve dosyanın açılmasında sorun olduğunda E, std::io::Error türüyle atandı.

Kodunuzda, yalnızca tuttukları değerlerin türlerinde farklılık gösteren birden fazla struct veya enum tanımının bulunduğu durumları fark ettiğinizde, bunun yerine yaygın türleri kullanarak yinelemeyi önleyebilirsiniz.

Metod Tanımlarında

Yapılar ve enum'lar üzerinde metodlar uygulayabilir (Bölüm 5'te yaptığımız gibi) ve tanımlarında yaygın türleri kullanabiliriz. Liste 10-9, Liste 10-6'da tanımladığımız Point<T> yapısını ve üzerinde uygulanan x adlı bir metodu göstermektedir.

Dosya adı: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

Liste 10-9: T türündeki x üyesine bir başvuru döndürecek olan Point<T> yapısında x adlı metodun süreklenmesi

Burada, x üyesindeki verilere bir referans döndüren Point<T> üzerinde x adında bir metod tanımladık.

T'yi impl'den hemen sonra bildirmemiz gerektiğine dikkat edin, böylece T'yi Point<T> türünde metodlar tanımladığımızı belirtmek için kullanabiliriz. T'yi impl'den sonra yaygın bir tür olarak bildirerek, Rust, Point'teki köşeli parantez içindeki türün somut bir tür yerine yaygın bir tür olduğunu belirleyebilir. Bu yaygın parametre için struct tanımında bildirilen yaygın parametreden farklı bir isim seçebilirdik, ancak aynı ismi kullanmak gelenekseldir. Yaygın türü bildiren bir impl içinde yazılan metodlar, yaygın türün yerine hangi somut tür geçerse geçsin, türün herhangi bir tanımı üzerinde tanımlanacaktır.

Tür üzerinde metod tanımlarken yaygın türler üzerinde kısıtlamalar da belirtebiliriz. Örneğin, herhangi bir yaygın türe sahip Point<T> tanımları yerine yalnızca Point<f32> tanımları üzerinde metodlar uygulayabiliriz. Liste 10-10'da somut f32 türünü kullanıyoruz, yani impl'den sonra herhangi bir tür bildirmiyoruz.

Dosya adı: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

Liste 10-10: Yaygın tür parametresi T için yalnızca belirli bir somut türe sahip bir yapıya tanımlanan bir impl bloğu

Bu kod, Point<f32> türünün bir distance_from_origin metoduna sahip olacağı anlamına gelir; T'nin f32 türünde olmadığı diğer Point<T> örneklerinde bu metod tanımlı olmayacaktır. Metod, noktamızın (0.0, 0.0) koordinatlarındaki noktadan ne kadar uzakta olduğunu ölçer ve yalnızca kayan nokta türleri için kullanılabilen matematiksel işlemleri kullanır.

Bir struct tanımındaki yaygın tür parametreleri her zaman aynı struct'ın metod imzalarında kullandıklarınızla aynı değildir. Liste 10-11, örneği daha açık hale getirmek için Point struct'ı için X1 ve Y1 yaygın türlerini ve mixup metod imzası için X2 Y2'yi kullanır. Metod, kendi Point'inden (X1 türünde) alınan x değeri ve aktarılan Point'ten (Y2 türünde) alınan y değeriyle yeni bir Point tanımı oluşturur.

Dosya adı: src/main.rs

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

Liste 10-11: Yapısının tanımından farklı yaygın türleri kullanan bir metod

main'de, x için bir i32 (değeri 5) ve y için bir f64 (değeri 10,4) olan bir Point tanımladık. p2 değişkeni, x için bir dizgi dilimine ("Hello" değeriyle) ve y için bir char değerine (c değeriyle) sahip bir Point struct'tır. p1 üzerinde p2 argümanıyla mixup çağrıldığında, x p1'den geldiği için x için bir i32'ye sahip olan p3 elde edilir. p3 değişkeninde y için bir char olacaktır, çünkü y p2'den gelmiştir. println! makro çağrısı p3.x = 5, p3.y = c yazdıracaktır.

Bu örneğin amacı, bazı yaygın parametrelerin impl ile bildirildiği ve bazılarının metod tanımıyla bildirildiği bir durumu göstermektir. Burada, X1 ve Y1 yaygın parametreleri impl'den sonra bildirilir, çünkü bunlar struct tanımıyla birlikte tanımlanmıştır. X2 ve Y2 yaygın parametreleri fn mixup'tan sonra bildirilir, çünkü bunlar yalnızca metodla ilgilidir.

Yaygınları Kullanan Kodun Performansı

Yaygın tür parametrelerini kullanırken bir çalışma zamanı maliyeti olup olmadığını merak ediyor olabilirsiniz. İyi haber şu ki, yaygın türleri kullanmak çalışmanızı somut tiplere göre daha yavaş hale getirmeyecektir.

Rust bunu, derleme zamanında yaygınları kullanarak kodun monomorfizasyonunu gerçekleştirerek başarır. Monomorfizasyon, derlendiğinde kullanılan somut tiplerin içini doldurarak genel kodu belirli bir koda dönüştürme işlemidir. Bu süreçte derleyici, Liste 10-5'teki yaygın fonksiyonu oluşturmak için kullandığımız adımların tersini yapar: derleyici, genel kodun çağrıldığı tüm yerlere bakar ve genel kodun çağrıldığı somut türler için kod oluşturur.

Standart kütüphanenin yaygın Option<T> enum'unu kullanarak bunun nasıl çalıştığına bakalım:


#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}

Rust bu kodu derlediğinde, monomorflaştırma gerçekleştirir. Bu işlem sırasında, derleyici Option<T> tanımlarında kullanılan değerleri okur ve iki tür Option<T> tanımlar: biri i32 ve diğeri f64. Bu nedenle, Option<T>'nin yaygın tanımını Option_i32 ve Option_f64 olarak genişletir, böylece genel tanımı özel olanlarla değiştirir.

Kodun monomorfize edilmiş versiyonu aşağıdaki gibi görünür:

Dosya adı: src/main.rs

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

Yaygın Option<T>, derleyici tarafından oluşturulan özel tanımlarla değiştirilir. Rust, yaygın kodu her örnekte türü belirten koda derlediğinden, yaygınları kullanmak için çalışma zamanı maliyeti ödemeyiz. Kod çalıştığında, her bir tanımı elle çoğaltmış olsaydık nasıl çalışacaksa öyle çalışır. Monomorfizasyon süreci, Rust'ın yaygınlarını çalışma zamanında son derece verimli hale getirir.

Tanımlar: Paylaşılan Davranışı Tanımlama

Bir tanım, belirli bir türün sahip olduğu ve diğer türlerle paylaşabileceği işlevselliği tanımlar. Paylaşılan davranışı soyut bir şekilde tanımlamak için tanımları kullanabiliriz. Genel bir türün belirli davranışlara sahip herhangi bir tür olabileceğini belirtmek için tanım sınırlarını kullanabiliriz.

Not: Tanımlar, bazı farklılıklara rağmen diğer dillerde genellikle arayüz olarak adlandırılan bir özelliğe benzer.

Tanım Tanımlama

Bir türün davranışı, o tür üzerinde çağırabileceğimiz metodlardan oluşur. Tüm türler üzerinde aynı metodları çağırabiliyorsak, farklı tipler aynı davranışı paylaşır. Tanım tanımları, bir amacı gerçekleştirmek için gerekli olan bir dizi davranışı tanımlamak üzere metod imzalarını bir araya getirmenin bir yoludur.

Örneğin, çeşitli tür ve miktarlarda metin tutan birden fazla yapımız olduğunu varsayalım: belirli bir konumda dosyalanmış bir haberi tutan bir NewsArticle yapısı ve yeni bir tweet mi, retweet mi yoksa başka bir tweet'e yanıt mı olduğunu gösteren meta verilerle birlikte en fazla 280 karaktere sahip olabilen bir Tweet.

Bir NewsArticle veya Tweet örneğinde saklanabilecek verilerin özetlerini görüntüleyebilen aggregator adlı bir medya toplayıcı kütüphane kasası yapmak istiyoruz. Bunu yapmak için, her türden bir özete ihtiyacımız var ve bir örnek üzerinde bir summarize yöntemi çağırarak bu özeti talep edeceğiz. Liste 10-12, bu davranışı ifade eden bir genel Summary tanımının tanımlanmasını göstermektedir.

Dosya adı: src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String;
}

Liste 10-12: summarize metoduyla sağlanan davranıştan oluşan bir Summary tanımı

Burada, trait anahtar sözcüğünü ve ardından bu durumda Summary olan trait'in adını kullanarak bir trait bildiriyoruz. Ayrıca, birkaç örnekte göreceğimiz gibi, bu kasaya bağlı kasaların da bu tanımı kullanabilmesi için tanımı pub olarak bildirdik. Süslü parantezlerin içinde, bu tanımı uygulayan türlerin davranışlarını tanımlayan metod imzalarını bildiriyoruz; bu durumda fn summarize(&self) -> String olacaktır.

Metod imzasından sonra, süslü parantezler içinde bir sürekleme sağlamak yerine noktalı virgül kullanırız. Bu tanımı uygulayan her tür, metodun gövdesi için kendi özel davranışını sağlamalıdır. Derleyici, Summary tanımına sahip herhangi bir türün summarize metodunun tam olarak bu imza ile tanımlanmasını zorunlu kılacaktır.

Bir tanımın gövdesinde birden fazla metod olabilir: metod imzaları her satırda bir tane listelenir ve her satır noktalı virgülle biter.

Tür Üzerinde Tanım Uygulama

Summary tanımının metodlarının istenen imzalarını tanımladığımıza göre, bunu medya toplayıcımızdaki türlere uygulayabiliriz. Liste 10-13, Summary tanımının NewsArticle yapısı üzerinde, summarize'ın dönüş değerini oluşturmak için başlığı, yazarı ve konumu kullanan bir süreklemesini göstermektedir. Tweet yapısı için, tweet içeriğinin zaten 280 karakterle sınırlı olduğunu varsayarak, summarize özelliğini kullanıcı adı ve ardından tweet metninin tamamı olarak tanımlarız.

Dosya adı: src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

Liste 10-13: NewsArticle ve Tweet türlerine Summary tanımının süreklenmesi

Bir tür üzerinde bir tanım süreklemek, normal metodları süreklemeye benzer. Aradaki fark, impl'den sonra süreklemek istediğimiz özellik adını koymamız, ardından for anahtar sözcüğünü kullanmamız ve ardından tanımı uygulamak istediğimiz türün adını belirtmemizdir. impl bloğunun içine, tanım tanımının tanımladığı metod imzalarını koyarız. Her imzadan sonra noktalı virgül eklemek yerine, süslü parantezler kullanırız ve metod gövdesini, tanımın metodlarının belirli bir tür için sahip olmasını istediğimiz belirli davranışla doldururuz.

Artık kütüphane NewsArticle ve Tweet üzerinde Summary tanımını süreklediğine göre, kasa kullanıcıları NewsArticle ve Tweet örnekleri üzerindeki tanım metodlarını normal metodları çağırdığımız şekilde çağırabilir. Tek fark, kullanıcının türlerin yanı sıra tanımı da kapsam içine alması gerektiğidir. İşte ikili bir kasanın aggregator kütüphane kasamızı nasıl kullanabileceğine dair bir örnek:

use aggregator::{Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

Bu kod 1 new tweet: horse_ebooks: of course, as you probably already know, people yazdırır.

aggregator kasamıza bağımlı olan diğer kasalar da Summary tanımını kendi türlerinde süreklemek için kapsam içine alabilir. Unutulmaması gereken bir kısıtlama, bir özelliği bir tür üzerinde yalnızca tanım veya türden en az birinin kasamız için yerel olması durumunda uygulayabileceğimizdir. Örneğin, Tweet türü aggregator kasamız için yerel olduğundan, Display gibi standart kütüphane özelliklerini Tweet gibi gizli bir tür üzerinde kasa işlevselliğimizin bir parçası olarak sürekleyebiliriz. Ayrıca Summary tanımını Vec<T> üzerinde de sürekleyebiliriz, çünkü Summary tanımı kasamız için yereldir.

Ancak harici tanımları harici türler üzerinde uygulayamayız. Örneğin, Display tanımını Vec<T> üzerinde; kasamızda sürekleyemeyiz, çünkü Display ve Vec<T> standart kütüphanede tanımlanmıştır ve kasamız için yerel değildir. Bu kısıtlama, tutarlılık adı verilen bir özelliğin ve daha spesifik olarak, ana tür mevcut olmadığı için bu şekilde adlandırılan yetim kuralının bir parçasıdır. Bu kural, başkalarının kodunun sizin kodunuzu bozamamasını ve bunun tersinin de geçerli olmamasını sağlar. Bu kural olmasaydı, iki kasa aynı tür için aynı tanımı sürekleyebilirdi ve Rust hangi süreklemeyi kullanacağını bilemezdi.

Varsayılan Süreklemeler

Bazen, her türdeki tüm metodlar için sürekleme gerektirmek yerine, bir tanımdaki metodlardan bazıları veya tümü için varsayılan davranışa sahip olmak yararlıdır. Daha sonra, tanımı belirli bir tür üzerinde süreklerken, her bir metodun varsayılan davranışını koruyabilir veya geçersiz kılabiliriz.

Liste 10-14'te, Liste 10-12'de yaptığımız gibi yalnızca metod imzasını tanımlamak yerine Summary tanımının summarize metodu için varsayılan bir dizgi belirtiyoruz.

Dosya adı: src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

Liste 10-14: Summarize metodunun varsayılan süreklemesiyle bir Summary tanımının yazılması

NewsArticle örneklerini özetlemek üzere varsayılan bir sürekleme kullanmak istediğimiz için, impl Summary for NewsArticle {} ile boş bir impl bloğu belirtiriz.

Artık NewsArticle üzerinde summarize metodunu doğrudan tanımlamıyor olsak da, varsayılan bir sürekleme sağladık ve NewsArticle'ın Summary tanımını süreklediğini belirttik. Sonuç olarak, bir NewsArticle örneği üzerinde summarize metodunu aşağıdaki gibi çağırabiliriz:

use chapter10::{self, NewsArticle, Summary};

fn main() {
    let article = NewsArticle {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA, USA"),
        author: String::from("Iceburgh"),
        content: String::from(
            "The Pittsburgh Penguins once again are the best \
             hockey team in the NHL.",
        ),
    };

    println!("New article available! {}", article.summarize());
}

Bu kod New article available! (Read more...) çıktısını verir.

Varsayılan bir sürekleme oluşturmak, Liste 10-13'teki Tweet'deki Summary süreklemesinde herhangi bir değişiklik yapmamızı gerektirmez. Bunun nedeni, varsayılan bir süreklemeyi geçersiz kılma söz diziminin, varsayılan bir süreklemeye sahip olmayan bir tanım metodunu sürekleme söz dizimiyle aynı olmasıdır.

Varsayılan süreklemeler, diğer metodların varsayılan bir süreklemesi olmasa bile aynı tanımdaki diğer metodları çağırabilir. Bu şekilde, bir özellik çok sayıda yararlı işlevsellik sağlayabilir ve uygulayıcıların bunun yalnızca küçük bir bölümünü belirtmesini gerektirebilir. Örneğin, Summary tanımını, süreklenmesi gerekli olan bir summarize_author metoduna sahip olacak şekilde tanımlayabilir ve ardından summarize_author metodunu çağıran varsayılan bir süreklemeye sahip bir summarize metodu tanımlayabiliriz:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

Summary'in bu sürümünü kullanmak için, özelliği bir türe süreklediğimizde yalnızca summary_author'u tanımlamamız gerekir:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

summarize_author'ı tanımladıktan sonra, Tweet yapısının örnekleri üzerinde summarize'ı çağırabiliriz ve summarize'ın varsayılan süreklemesi, sağladığımız summarize_author tanımını çağıracaktır. summarize_author tanımını süreklediğimiz için, Summary tanımı bize daha fazla kod yazmamızı gerektirmeden summarize metodunun davranışını vermiştir.

use chapter10::{self, Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

Bu kod 1 new tweet: (Read more from @horse_ebooks...) çıktısını verecektir.

Aynı metodun geçersiz kılınan bir süreklemesinden varsayılan süreklemeyi çağırmanın mümkün olmadığını unutmayın.

Parametre olarak Tanımlar

Artık tanımları nasıl tanımlayacağınızı ve sürekleyeceğinizi bildiğinize göre, birçok farklı türü kabul eden fonksiyonları tanımlamak için tanımları nasıl kullanacağımızı keşfedebiliriz. Liste 10-13'te NewsArticle ve Tweet türleri üzerinde süreklediğimiz Summary tanımını, Summary tanımını sürekleyen bir türden olan item parametresi üzerinde summarize metodunu çağıran notify fonksiyonunu tanımlamak için kullanacağız. Bunu yapmak için, aşağıdaki gibi impl Trait söz dizimini kullanırız:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Öğe parametresi için somut bir tür yerine, impl anahtar sözcüğünü ve tanım adını belirtiriz. Bu parametre, belirtilen tanımı uygulayan herhangi bir türü kabul eder. notify'ın gövdesinde, item üzerinde Summary tanımından gelen summarize gibi herhangi bir metodu çağırabiliriz. notify'ı çağırabilir ve NewsArticle veya Tweet'in herhangi bir örneğini aktarabiliriz. Fonksiyonu String veya i32 gibi başka bir türle çağıran kod derlenmez çünkü bu türler Summary tanımını uygulamaz.

Tanıma Bağlılık Söz Dizimi

impl Trait söz dizimi basit durumlar için çalışır, ancak aslında tanıma bağlılık olarak bilinen daha uzun bir form için söz dizimi tatlılığıdır; şöyle görünür:

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

Bu uzun form önceki bölümdeki örneğe eş değerdir ancak daha ayrıntılıdır. Tanıma bağlılık, iki nokta üst üste işaretinden sonra ve köşeli parantezler içinde yaygın tür parametresinin bildirimiyle birlikte yerleştiririz.

impl Trait söz dizimi kullanışlıdır ve basit durumlarda daha özlü bir kod sağlarken, tanıma bağlılık söz dizimi karmaşık durumlarda daha farklı bir şekilde kendini ifade edebilir. Örneğin, Summary öğesini uygulayan iki parametreye sahip olabiliriz. Bunu impl Trait sö zdizimi ile yapmak şu şekilde görünür:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

Bu fonksiyonun item1 ve item2'nin farklı türlere sahip olmasına izin vermesini istiyorsak (her iki tür de Summary'yi süreklediği sürece) impl Trait kullanmak uygundur. Ancak her iki parametrenin de aynı türde olmasını istiyorsak, aşağıdaki gibi, tanıma bağlılığı kullanmalıyız:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

item1 ve item2 parametrelerinin türü olarak belirtilen T yaygın türü; fonksiyonu, item1 ve item2 için argüman olarak aktarılan değerin somut türünün aynı olması gerektiği şekilde kısıtlar.

+ Söz Dizimi ile Birden Fazla Tanıma Bağlılığın Belirtilmesi

Birden fazla tanıma bağlılığı da belirleyebiliriz. Diyelim ki notify'ın öğe üzerinde özetlemenin yanı sıra görüntüleme biçimlendirmesini de kullanmasını istedik: notify tanımında öğenin hem Display hem de Summary'i süreklemesi gerektiğini belirtiriz. Bunu + söz dizimini kullanarak da yapabiliriz:

pub fn notify(item: &(impl Summary + Display)) {

+ söz dizimi, yaygın türlerdeki tanıma bağlılıkta da geçerlidir:

pub fn notify<T: Summary + Display>(item: &T) {

Belirtilen iki tanıma bağlılık ile, notify öğesinin gövdesi summarize'ı çağırabilir ve item'ı biçimlendirmek için {} kullanabilir.

where ile Daha Net Tanıma Bağlılık

Çok fazla tanıma bağlılık kullanmanın dezavantajları vardır. Her yaygın kendine özgü tanıma bağlılığa sahiptir, bu nedenle birden fazla yaygın tür parametresi olan fonksiyonlar, fonksiyonun adı ve parametre listesi arasında çok sayıda tanıma bağlılık bilgisi içerebilir ve bu da fonksiyon imzasının okunmasını zorlaştırır. Bu nedenle Rust, fonksiyon imzasından sonra where içinde tanıma bağlılığı belirtmek için alternatif bir söz dizime sahiptir.

Yani bunu yazmak yerine:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

where kullanabiliriz:

fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{

Bu fonksiyonun imzası daha az karmaşıktır: fonksiyon adı, parametre listesi ve dönüş türü birbirine yakındır, çok sayıda tamıma bağlılığı olmayan bir fonksiyona benzer.

Tanımları Sürekleyen Dönüş Türleri

Burada gösterildiği gibi, bir tanımı sürekleyen bir türden bir değer döndürmek için return konumunda impl Trait söz dizimini de kullanabiliriz:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

Dönüş türü için impl Summary kullanarak, returns_summarizable fonksiyonunun somut türü belirtmeden Summary tanımını sürekleyen bir türü döndürdüğünü belirtiyoruz. Bu durumda, returns_summarizable Tweet döndürür, ancak bu fonksiyonu çağıran kodun bunu bilmesine gerek yoktur.

Bir geri dönüş türünü yalnızca uyguladığı özelliğe göre belirtme yeteneği, özellikle Bölüm 13'te ele aldığımız kapanış ifadeleri ve yineleyiciler bağlamında kullanışlıdır. Kapanış ifadeleri ve yineleyiciler yalnızca derleyicinin bildiği türler veya belirtilmesi çok uzun olan türler oluşturur. impl Trait söz dizimi, çok uzun bir tür yazmanıza gerek kalmadan bir fonksiyonun Iterator tanımını sürekleyen bir tür döndürdüğünü kısaca belirtmenizi sağlar.

Ancak, impl Trait'i yalnızca tek bir tür döndürüyorsanız kullanabilirsiniz. Örneğin, dönüş türü impl Summary olarak belirtilen bir NewsArticle veya Tweet döndüren bu kod çalışmaz:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from(
                "Penguins win the Stanley Cup Championship!",
            ),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from(
                "The Pittsburgh Penguins once again are the best \
                 hockey team in the NHL.",
            ),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            retweet: false,
        }
    }
}

Derleyicide impl Trait söz diziminin nasıl süreklendiğine ilişkin kısıtlamalar nedeniyle NewsArticle veya Tweet döndürülmesine izin verilmez. Bu davranışa sahip bir fonksiyonun nasıl yazılacağını Bölüm 17'deki “Farklı Türlerde Değerlere İzin Veren Tanım Nesnelerini Kullanma” kısmında ele alacağız.

Tanıma Bağlılık ile largest Fonksiyonunu Düzeltme

Artık yaygın tür parametresinin bağlılıklarını kullanarak istediğiniz davranışı nasıl belirleyeceğinizi bildiğinize göre, yaygın tür parametresi kullanan largest fonksiyonunun tanımını düzeltmek için Liste 10-5'e dönelim! Bu kodu en son çalıştırmayı denediğimizde bu hatayı almıştık:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- T
  |            |
  |            T
  |
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
  |             ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error

largest'in gövdesinde, daha büyük (>) operatörünü kullanarak T türündeki iki değeri karşılaştırmak istedik. Bu operatör standart kütüphane özelliği std::cmp::PartialOrd üzerinde varsayılan bir metod olarak tanımlandığından, T'ye tanıma bağlılıktan dolayı PartialOrd'u süreklememiz gerekir, böylece largest fonksiyonu karşılaştırabileceğimiz herhangi bir türden dilimler üzerinde çalışabilir. PartialOrd'u kapsam içine almamıza gerek yok çünkü o zaten kapsama otomatik olarak dahildir... Yani, largest'in imzasını aşağıdaki gibi değiştirin:

fn largest<T: PartialOrd>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

Bu sefer kodu derlediğimizde farklı hatalar alıyoruz:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0508]: cannot move out of type `[T]`, a non-copy slice
 --> src/main.rs:2:23
  |
2 |     let mut largest = list[0];
  |                       ^^^^^^^
  |                       |
  |                       cannot move out of here
  |                       move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
  |                       help: consider borrowing here: `&list[0]`

error[E0507]: cannot move out of a shared reference
 --> src/main.rs:4:18
  |
4 |     for &item in list {
  |         -----    ^^^^
  |         ||
  |         |data moved here
  |         |move occurs because `item` has type `T`, which does not implement the `Copy` trait
  |         help: consider removing the `&`: `item`

Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `chapter10` due to 2 previous errors

Buranın en önemli noktası, kopyalanmamış bir dilim olan [T] türünün dışına çıkamaz (cannot move out of type [T], a non-copy slice) hatasıdır. largest fonksiyonunun yaygın olmayan versiyonlarında, yalnızca en büyük i32 veya char'ı bulmaya çalışıyorduk. Bkz. “Sadece Yığıt Kullanan Veriler: Kopyalama” konusuna bakın, i32 ve char gibi bilinen bir boyuta sahip türler yığıtta saklanabilir, böylece Copy tanımı süreklenebilir. Ancak, largest fonksiyonunu genelleştirdiğimizde, list parametresinin içinde Copy tanımını süreklemeyen türler olması mümkün hale geldi. Sonuç olarak, değeri list[0]'ten largest değişkenine taşıyamayız, bu da bu hataya neden olur.

Bu kodu yalnızca Copy tanımını sürekleyen türlerle birlikte çağırmak için, T'nin tanıma bağlılık listesine Copy'i ekleyebiliriz! Liste 10-15, fonksiyona aktardığımız dilimdeki değerlerin türleri i32 ve char gibi PartialOrd ve Copy tanımlarını süreklediği sürece derlenecek yaygın largest fonksiyonunun tam kodunu gösterir.

Dosya adı: src/main.rs

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

Liste 10-15: PartialOrd ve Copy tanımlarını sürekleyen herhangi bir yaygın tür üzerinde çalışan largest fonksiyonunun düzgün çalışan versiyonu

largest fonksiyonunu Copy tanımını sürekleyen türlerle sınırlamak istemiyorsak, T'nin Copy yerine Clone tanımına sahip olduğunu belirtebiliriz. Böylece, largest fonksiyonunun sahibi olmasını istediğimizde dilimdeki her değeri klonlayabiliriz. Clone fonksiyonunu kullanmak, String gibi yığın verisine sahip türler söz konusu olduğunda potansiyel olarak daha fazla yığın tahsisi yapacağımız anlamına gelir ve büyük miktarda veriyle çalışıyorsak yığın tahsisleri yavaş olabilir.

Ayrıca, fonksiyonun dilimdeki bir T değerine bir referans döndürmesini sağlayarak largest'ı sürekleyebiliriz. Dönüş türünü T yerine &T olarak değiştirirsek, böylece fonksiyonun gövdesini bir referans döndürecek şekilde değiştirirsek, Clone veya Copy tanıma bağlılıklarına ihtiyacımız olmaz ve yığın tahsisatlarından kaçınabiliriz. Bu alternatif çözümleri kendi başınıza yazmayı deneyin! Yaşam süreleri ile ilgili hatalara takılırsanız, okumaya devam edin: “Yaşam Süreleri ile Referansları Doğrulama” bölümü durumu açıklayacaktır, ancak bu meydan okumaları çözmek için yaşam süreleri gerekli değildir.

Metodları Koşullu Olarak Süreklemek için Tanıma Bağlılığı Kullanma

Yaygın tür parametreleri kullanan bir impl bloğu ile bağlı bir tanım kullanarak, belirtilen tanımları sürekleyen türler için metodları koşullu olarak sürekleyebiliriz. Örneğin, Liste 10-16'daki Pair<T> türü her zaman yeni bir Pair<T> örneği döndürmek için new fonksiyonunu çağırır (Bölüm 5'teki “Metodları Tanımlama” bölümünden Self'in impl bloğunun türü için bir tür takma ad olduğunu hatırlayın, bu durumda Pair<T>'dir). Ancak bir sonraki impl bloğunda, Pair<T> yalnızca iç tipi T, karşılaştırmayı sağlayan PartialOrd tanımını ve yazdırmayı sağlayan Display tanımını süreklerse cmp_display metodunu sürekleyebilir.

Dosya adı: src/lib.rs

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

Liste 10-16: Tanıma bağlılığa bağlı olarak metodları yaygın bir tür üzerinde koşullu , olarak süreklemek

Ayrıca, başka bir tanımı sürekleyen herhangi bir tür için bir tanımı koşullu olarak sürekleyebiliriz. Bir tanımın, tanıma bağlılığı karşılayan herhangi bir tür üzerindeki süreklemelerine kapsamlı sürekleme denir ve Rust standart kütüphanesinde yaygın olarak kullanılır. Örneğin, standart kütüphane Display tanımını sürekleyen herhangi bir tür üzerinde ToString tanımını sürekler. Standart kütüphanedeki impl bloğu bu koda benzer:

impl<T: Display> ToString for T {
    // --snip--
}

Standart kütüphane bu kapsamlı süreklemeye sahip olduğundan, Display tanımını sürekleyen herhangi bir tür üzerinde ToString tanımı tarafından tanımlanan to_string metodunu çağırabiliriz. Örneğin, tam sayılar Display tanımını süreklediği için tam sayıları karşılık gelen String değerlerine dönüştürebiliriz:


#![allow(unused)]
fn main() {
let s = 3.to_string();
}

Kapsamlı süreklemeler, tanımın dokümantasyonunun “Implementors” bölümünde görünür.

Tanımlar ve tanıma bağlılık, yinelemeyi azaltmak için yaygın tür parametrelerini kullanan kod yazmamıza ve aynı zamanda derleyiciye yaygın türün belirli bir davranışa sahip olmasını istediğimizi belirtmemize olanak tanır. Derleyici daha sonra kodumuzla birlikte kullanılan tüm somut tiplerin doğru davranışı sağlayıp sağlamadığını kontrol etmek için tanıma bağlılık bilgisini kullanabilir. Dinamik olarak yazılan dillerde, metodu tanımlamayan bir tür üzerinde bir metod çağırırsak çalışma zamanında bir hata alırız. Ancak Rust bu hataları derleme zamanına taşır, böylece kodumuz daha çalışmadan önce sorunları düzeltmek zorunda kalırız. Ayrıca, derleme zamanında zaten kontrol ettiğimiz için çalışma zamanında davranışı kontrol eden kod yazmak zorunda kalmayız. Bunu yapmak, yaygınların esnekliğinden vazgeçmek zorunda kalmadan performansı artırır.

Validating References with Lifetimes

Lifetimes are another kind of generic that we’ve already been using. Rather than ensuring that a type has the behavior we want, lifetimes ensure that references are valid as long as we need them to be.

One detail we didn’t discuss in the “References and Borrowing” section in Chapter 4 is that every reference in Rust has a lifetime, which is the scope for which that reference is valid. Most of the time, lifetimes are implicit and inferred, just like most of the time, types are inferred. We only must annotate types when multiple types are possible. In a similar way, we must annotate lifetimes when the lifetimes of references could be related in a few different ways. Rust requires us to annotate the relationships using generic lifetime parameters to ensure the actual references used at runtime will definitely be valid.

Annotating lifetimes is not even a concept most other programming languages have, so this is going to feel unfamiliar. Although we won’t cover lifetimes in their entirety in this chapter, we’ll discuss common ways you might encounter lifetime syntax so you can get comfortable with the concept.

Preventing Dangling References with Lifetimes

The main aim of lifetimes is to prevent dangling references, which cause a program to reference data other than the data it’s intended to reference. Consider the program in Listing 10-17, which has an outer scope and an inner scope.

fn main() {
    {
        let r;

        {
            let x = 5;
            r = &x;
        }

        println!("r: {}", r);
    }
}

Listing 10-17: An attempt to use a reference whose value has gone out of scope

Note: The examples in Listings 10-17, 10-18, and 10-24 declare variables without giving them an initial value, so the variable name exists in the outer scope. At first glance, this might appear to be in conflict with Rust’s having no null values. However, if we try to use a variable before giving it a value, we’ll get a compile-time error, which shows that Rust indeed does not allow null values.

The outer scope declares a variable named r with no initial value, and the inner scope declares a variable named x with the initial value of 5. Inside the inner scope, we attempt to set the value of r as a reference to x. Then the inner scope ends, and we attempt to print the value in r. This code won’t compile because the value r is referring to has gone out of scope before we try to use it. Here is the error message:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
  --> src/main.rs:7:17
   |
7  |             r = &x;
   |                 ^^ borrowed value does not live long enough
8  |         }
   |         - `x` dropped here while still borrowed
9  | 
10 |         println!("r: {}", r);
   |                           - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

The variable x doesn’t “live long enough.” The reason is that x will be out of scope when the inner scope ends on line 7. But r is still valid for the outer scope; because its scope is larger, we say that it “lives longer.” If Rust allowed this code to work, r would be referencing memory that was deallocated when x went out of scope, and anything we tried to do with r wouldn’t work correctly. So how does Rust determine that this code is invalid? It uses a borrow checker.

The Borrow Checker

The Rust compiler has a borrow checker that compares scopes to determine whether all borrows are valid. Listing 10-18 shows the same code as Listing 10-17 but with annotations showing the lifetimes of the variables.

fn main() {
    {
        let r;                // ---------+-- 'a
                              //          |
        {                     //          |
            let x = 5;        // -+-- 'b  |
            r = &x;           //  |       |
        }                     // -+       |
                              //          |
        println!("r: {}", r); //          |
    }                         // ---------+
}

Listing 10-18: Annotations of the lifetimes of r and x, named 'a and 'b, respectively

Here, we’ve annotated the lifetime of r with 'a and the lifetime of x with 'b. As you can see, the inner 'b block is much smaller than the outer 'a lifetime block. At compile time, Rust compares the size of the two lifetimes and sees that r has a lifetime of 'a but that it refers to memory with a lifetime of 'b. The program is rejected because 'b is shorter than 'a: the subject of the reference doesn’t live as long as the reference.

Listing 10-19 fixes the code so it doesn’t have a dangling reference and compiles without any errors.

fn main() {
    {
        let x = 5;            // ----------+-- 'b
                              //           |
        let r = &x;           // --+-- 'a  |
                              //   |       |
        println!("r: {}", r); //   |       |
                              // --+       |
    }                         // ----------+
}

Listing 10-19: A valid reference because the data has a longer lifetime than the reference

Here, x has the lifetime 'b, which in this case is larger than 'a. This means r can reference x because Rust knows that the reference in r will always be valid while x is valid.

Now that you know where the lifetimes of references are and how Rust analyzes lifetimes to ensure references will always be valid, let’s explore generic lifetimes of parameters and return values in the context of functions.

Generic Lifetimes in Functions

We’ll write a function that returns the longer of two string slices. This function will take two string slices and return a single string slice. After we’ve implemented the longest function, the code in Listing 10-20 should print The longest string is abcd.

Filename: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

Listing 10-20: A main function that calls the longest function to find the longer of two string slices

Note that we want the function to take string slices, which are references, rather than strings, because we don’t want the longest function to take ownership of its parameters. Refer to the “String Slices as Parameters” section in Chapter 4 for more discussion about why the parameters we use in Listing 10-20 are the ones we want.

If we try to implement the longest function as shown in Listing 10-21, it won’t compile.

Filename: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Listing 10-21: An implementation of the longest function that returns the longer of two string slices but does not yet compile

Instead, we get the following error that talks about lifetimes:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` due to previous error

The help text reveals that the return type needs a generic lifetime parameter on it because Rust can’t tell whether the reference being returned refers to x or y. Actually, we don’t know either, because the if block in the body of this function returns a reference to x and the else block returns a reference to y!

When we’re defining this function, we don’t know the concrete values that will be passed into this function, so we don’t know whether the if case or the else case will execute. We also don’t know the concrete lifetimes of the references that will be passed in, so we can’t look at the scopes as we did in Listings 10-18 and 10-19 to determine whether the reference we return will always be valid. The borrow checker can’t determine this either, because it doesn’t know how the lifetimes of x and y relate to the lifetime of the return value. To fix this error, we’ll add generic lifetime parameters that define the relationship between the references so the borrow checker can perform its analysis.

Lifetime Annotation Syntax

Lifetime annotations don’t change how long any of the references live. Rather, they describe the relationships of the lifetimes of multiple references to each other without affecting the lifetimes. Just as functions can accept any type when the signature specifies a generic type parameter, functions can accept references with any lifetime by specifying a generic lifetime parameter.

Lifetime annotations have a slightly unusual syntax: the names of lifetime parameters must start with an apostrophe (') and are usually all lowercase and very short, like generic types. Most people use the name 'a for the first lifetime annotation. We place lifetime parameter annotations after the & of a reference, using a space to separate the annotation from the reference’s type.

Here are some examples: a reference to an i32 without a lifetime parameter, a reference to an i32 that has a lifetime parameter named 'a, and a mutable reference to an i32 that also has the lifetime 'a.

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

One lifetime annotation by itself doesn’t have much meaning, because the annotations are meant to tell Rust how generic lifetime parameters of multiple references relate to each other. For example, let’s say we have a function with the parameter first that is a reference to an i32 with lifetime 'a. The function also has another parameter named second that is another reference to an i32 that also has the lifetime 'a. The lifetime annotations indicate that the references first and second must both live as long as that generic lifetime.

Lifetime Annotations in Function Signatures

Now let’s examine lifetime annotations in the context of the longest function. As with generic type parameters, we need to declare generic lifetime parameters inside angle brackets between the function name and the parameter list. We want the signature to express the following constraint: the returned reference will be valid as long as both the parameters are valid. This is the relationship between lifetimes of the parameters and the return value. We’ll name the lifetime 'a and then add it to each reference, as shown in Listing 10-22.

Filename: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Listing 10-22: The longest function definition specifying that all the references in the signature must have the same lifetime 'a

This code should compile and produce the result we want when we use it with the main function in Listing 10-20.

The function signature now tells Rust that for some lifetime 'a, the function takes two parameters, both of which are string slices that live at least as long as lifetime 'a. The function signature also tells Rust that the string slice returned from the function will live at least as long as lifetime 'a. In practice, it means that the lifetime of the reference returned by the longest function is the same as the smaller of the lifetimes of the references passed in. These relationships are what we want Rust to use when analyzing this code.

Remember, when we specify the lifetime parameters in this function signature, we’re not changing the lifetimes of any values passed in or returned. Rather, we’re specifying that the borrow checker should reject any values that don’t adhere to these constraints. Note that the longest function doesn’t need to know exactly how long x and y will live, only that some scope can be substituted for 'a that will satisfy this signature.

When annotating lifetimes in functions, the annotations go in the function signature, not in the function body. The lifetime annotations become part of the contract of the function, much like the types in the signature. Having function signatures contain the lifetime contract means the analysis the Rust compiler does can be simpler. If there’s a problem with the way a function is annotated or the way it is called, the compiler errors can point to the part of our code and the constraints more precisely. If, instead, the Rust compiler made more inferences about what we intended the relationships of the lifetimes to be, the compiler might only be able to point to a use of our code many steps away from the cause of the problem.

When we pass concrete references to longest, the concrete lifetime that is substituted for 'a is the part of the scope of x that overlaps with the scope of y. In other words, the generic lifetime 'a will get the concrete lifetime that is equal to the smaller of the lifetimes of x and y. Because we’ve annotated the returned reference with the same lifetime parameter 'a, the returned reference will also be valid for the length of the smaller of the lifetimes of x and y.

Let’s look at how the lifetime annotations restrict the longest function by passing in references that have different concrete lifetimes. Listing 10-23 is a straightforward example.

Filename: src/main.rs

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Listing 10-23: Using the longest function with references to String values that have different concrete lifetimes

In this example, string1 is valid until the end of the outer scope, string2 is valid until the end of the inner scope, and result references something that is valid until the end of the inner scope. Run this code, and you’ll see that the borrow checker approves; it will compile and print The longest string is long string is long.

Next, let’s try an example that shows that the lifetime of the reference in result must be the smaller lifetime of the two arguments. We’ll move the declaration of the result variable outside the inner scope but leave the assignment of the value to the result variable inside the scope with string2. Then we’ll move the println! that uses result to outside the inner scope, after the inner scope has ended. The code in Listing 10-24 will not compile.

Filename: src/main.rs

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Listing 10-24: Attempting to use result after string2 has gone out of scope

When we try to compile this code, we get this error:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);
  |                                          ------ borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

The error shows that for result to be valid for the println! statement, string2 would need to be valid until the end of the outer scope. Rust knows this because we annotated the lifetimes of the function parameters and return values using the same lifetime parameter 'a.

As humans, we can look at this code and see that string1 is longer than string2 and therefore result will contain a reference to string1. Because string1 has not gone out of scope yet, a reference to string1 will still be valid for the println! statement. However, the compiler can’t see that the reference is valid in this case. We’ve told Rust that the lifetime of the reference returned by the longest function is the same as the smaller of the lifetimes of the references passed in. Therefore, the borrow checker disallows the code in Listing 10-24 as possibly having an invalid reference.

Try designing more experiments that vary the values and lifetimes of the references passed in to the longest function and how the returned reference is used. Make hypotheses about whether or not your experiments will pass the borrow checker before you compile; then check to see if you’re right!

Thinking in Terms of Lifetimes

The way in which you need to specify lifetime parameters depends on what your function is doing. For example, if we changed the implementation of the longest function to always return the first parameter rather than the longest string slice, we wouldn’t need to specify a lifetime on the y parameter. The following code will compile:

Filename: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "efghijklmnopqrstuvwxyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

We’ve specified a lifetime parameter 'a for the parameter x and the return type, but not for the parameter y, because the lifetime of y does not have any relationship with the lifetime of x or the return value.

When returning a reference from a function, the lifetime parameter for the return type needs to match the lifetime parameter for one of the parameters. If the reference returned does not refer to one of the parameters, it must refer to a value created within this function. However, this would be a dangling reference because the value will go out of scope at the end of the function. Consider this attempted implementation of the longest function that won’t compile:

Filename: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

Here, even though we’ve specified a lifetime parameter 'a for the return type, this implementation will fail to compile because the return value lifetime is not related to the lifetime of the parameters at all. Here is the error message we get:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return reference to local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function

For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` due to previous error

The problem is that result goes out of scope and gets cleaned up at the end of the longest function. We’re also trying to return a reference to result from the function. There is no way we can specify lifetime parameters that would change the dangling reference, and Rust won’t let us create a dangling reference. In this case, the best fix would be to return an owned data type rather than a reference so the calling function is then responsible for cleaning up the value.

Ultimately, lifetime syntax is about connecting the lifetimes of various parameters and return values of functions. Once they’re connected, Rust has enough information to allow memory-safe operations and disallow operations that would create dangling pointers or otherwise violate memory safety.

Lifetime Annotations in Struct Definitions

So far, the structs we've defined all hold owned types. We can define structs to hold references, but in that case we would need to add a lifetime annotation on every reference in the struct’s definition. Listing 10-25 has a struct named ImportantExcerpt that holds a string slice.

Filename: src/main.rs

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Listing 10-25: A struct that holds a reference, so its definition needs a lifetime annotation

This struct has one field, part, that holds a string slice, which is a reference. As with generic data types, we declare the name of the generic lifetime parameter inside angle brackets after the name of the struct so we can use the lifetime parameter in the body of the struct definition. This annotation means an instance of ImportantExcerpt can’t outlive the reference it holds in its part field.

The main function here creates an instance of the ImportantExcerpt struct that holds a reference to the first sentence of the String owned by the variable novel. The data in novel exists before the ImportantExcerpt instance is created. In addition, novel doesn’t go out of scope until after the ImportantExcerpt goes out of scope, so the reference in the ImportantExcerpt instance is valid.

Lifetime Elision

You’ve learned that every reference has a lifetime and that you need to specify lifetime parameters for functions or structs that use references. However, in Chapter 4 we had a function in Listing 4-9, shown again in Listing 10-26, that compiled without lifetime annotations.

Filename: src/lib.rs

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // first_word works on slices of `String`s
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word works on slices of string literals
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Listing 10-26: A function we defined in Listing 4-9 that compiled without lifetime annotations, even though the parameter and return type are references

The reason this function compiles without lifetime annotations is historical: in early versions (pre-1.0) of Rust, this code wouldn’t have compiled because every reference needed an explicit lifetime. At that time, the function signature would have been written like this:

fn first_word<'a>(s: &'a str) -> &'a str {

After writing a lot of Rust code, the Rust team found that Rust programmers were entering the same lifetime annotations over and over in particular situations. These situations were predictable and followed a few deterministic patterns. The developers programmed these patterns into the compiler’s code so the borrow checker could infer the lifetimes in these situations and wouldn’t need explicit annotations.

This piece of Rust history is relevant because it’s possible that more deterministic patterns will emerge and be added to the compiler. In the future, even fewer lifetime annotations might be required.

The patterns programmed into Rust’s analysis of references are called the lifetime elision rules. These aren’t rules for programmers to follow; they’re a set of particular cases that the compiler will consider, and if your code fits these cases, you don’t need to write the lifetimes explicitly.

The elision rules don’t provide full inference. If Rust deterministically applies the rules but there is still ambiguity as to what lifetimes the references have, the compiler won’t guess what the lifetime of the remaining references should be. Instead of guessing, the compiler will give you an error that you can resolve by adding the lifetime annotations.

Lifetimes on function or method parameters are called input lifetimes, and lifetimes on return values are called output lifetimes.

The compiler uses three rules to figure out the lifetimes of the references when there aren’t explicit annotations. The first rule applies to input lifetimes, and the second and third rules apply to output lifetimes. If the compiler gets to the end of the three rules and there are still references for which it can’t figure out lifetimes, the compiler will stop with an error. These rules apply to fn definitions as well as impl blocks.

The first rule is that the compiler assigns a lifetime parameter to each parameter that’s a reference. In other words, a function with one parameter gets one lifetime parameter: fn foo<'a>(x: &'a i32); a function with two parameters gets two separate lifetime parameters: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); and so on.

The second rule is that, if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters: fn foo<'a>(x: &'a i32) -> &'a i32.

The third rule is that, if there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters. This third rule makes methods much nicer to read and write because fewer symbols are necessary.

Let’s pretend we’re the compiler. We’ll apply these rules to figure out the lifetimes of the references in the signature of the first_word function in Listing 10-26. The signature starts without any lifetimes associated with the references:

fn first_word(s: &str) -> &str {

Then the compiler applies the first rule, which specifies that each parameter gets its own lifetime. We’ll call it 'a as usual, so now the signature is this:

fn first_word<'a>(s: &'a str) -> &str {

The second rule applies because there is exactly one input lifetime. The second rule specifies that the lifetime of the one input parameter gets assigned to the output lifetime, so the signature is now this:

fn first_word<'a>(s: &'a str) -> &'a str {

Now all the references in this function signature have lifetimes, and the compiler can continue its analysis without needing the programmer to annotate the lifetimes in this function signature.

Let’s look at another example, this time using the longest function that had no lifetime parameters when we started working with it in Listing 10-21:

fn longest(x: &str, y: &str) -> &str {

Let’s apply the first rule: each parameter gets its own lifetime. This time we have two parameters instead of one, so we have two lifetimes:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

You can see that the second rule doesn’t apply because there is more than one input lifetime. The third rule doesn’t apply either, because longest is a function rather than a method, so none of the parameters are self. After working through all three rules, we still haven’t figured out what the return type’s lifetime is. This is why we got an error trying to compile the code in Listing 10-21: the compiler worked through the lifetime elision rules but still couldn’t figure out all the lifetimes of the references in the signature.

Because the third rule really only applies in method signatures, we’ll look at lifetimes in that context next to see why the third rule means we don’t have to annotate lifetimes in method signatures very often.

Lifetime Annotations in Method Definitions

When we implement methods on a struct with lifetimes, we use the same syntax as that of generic type parameters shown in Listing 10-11. Where we declare and use the lifetime parameters depends on whether they’re related to the struct fields or the method parameters and return values.

Lifetime names for struct fields always need to be declared after the impl keyword and then used after the struct’s name, because those lifetimes are part of the struct’s type.

In method signatures inside the impl block, references might be tied to the lifetime of references in the struct’s fields, or they might be independent. In addition, the lifetime elision rules often make it so that lifetime annotations aren’t necessary in method signatures. Let’s look at some examples using the struct named ImportantExcerpt that we defined in Listing 10-25.

First, we’ll use a method named level whose only parameter is a reference to self and whose return value is an i32, which is not a reference to anything:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

The lifetime parameter declaration after impl and its use after the type name are required, but we’re not required to annotate the lifetime of the reference to self because of the first elision rule.

Here is an example where the third lifetime elision rule applies:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

There are two input lifetimes, so Rust applies the first lifetime elision rule and gives both &self and announcement their own lifetimes. Then, because one of the parameters is &self, the return type gets the lifetime of &self, and all lifetimes have been accounted for.

The Static Lifetime

One special lifetime we need to discuss is 'static, which denotes that the affected reference can live for the entire duration of the program. All string literals have the 'static lifetime, which we can annotate as follows:


#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}

The text of this string is stored directly in the program’s binary, which is always available. Therefore, the lifetime of all string literals is 'static.

You might see suggestions to use the 'static lifetime in error messages. But before specifying 'static as the lifetime for a reference, think about whether the reference you have actually lives the entire lifetime of your program or not, and whether you want it to. Most of the time, an error message suggesting the 'static lifetime results from attempting to create a dangling reference or a mismatch of the available lifetimes. In such cases, the solution is fixing those problems, not specifying the 'static lifetime.

Generic Type Parameters, Trait Bounds, and Lifetimes Together

Let’s briefly look at the syntax of specifying generic type parameters, trait bounds, and lifetimes all in one function!

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest_with_an_announcement(
        string1.as_str(),
        string2,
        "Today is someone's birthday!",
    );
    println!("The longest string is {}", result);
}

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

This is the longest function from Listing 10-22 that returns the longer of two string slices. But now it has an extra parameter named ann of the generic type T, which can be filled in by any type that implements the Display trait as specified by the where clause. This extra parameter will be printed using {}, which is why the Display trait bound is necessary. Because lifetimes are a type of generic, the declarations of the lifetime parameter 'a and the generic type parameter T go in the same list inside the angle brackets after the function name.

Summary

We covered a lot in this chapter! Now that you know about generic type parameters, traits and trait bounds, and generic lifetime parameters, you’re ready to write code without repetition that works in many different situations. Generic type parameters let you apply the code to different types. Traits and trait bounds ensure that even though the types are generic, they’ll have the behavior the code needs. You learned how to use lifetime annotations to ensure that this flexible code won’t have any dangling references. And all of this analysis happens at compile time, which doesn’t affect runtime performance!

Believe it or not, there is much more to learn on the topics we discussed in this chapter: Chapter 17 discusses trait objects, which are another way to use traits. There are also more complex scenarios involving lifetime annotations that you will only need in very advanced scenarios; for those, you should read the Rust Reference. But next, you’ll learn how to write tests in Rust so you can make sure your code is working the way it should.

Otomatize Testler Yazma

Edsger W. Dijkstra'nın 1972 tarihli “The Humble Programmer” adlı makalesinde, “Program testi, hataların varlığını göstermek için çok etkili bir yol olabilir, ancak onların yokluğunu göstermek için umutsuzca yetersizdir” dedi. Bu, elimizden geldiğince test etmeye çalışmamamız gerektiği anlamına gelmez!

Programlarımızdaki doğruluk, kodumuzun yapmayı amaçladığımız şeyi ne ölçüde yaptığıdır. Rust, programların doğruluğu konusunda yüksek derecede endişe ile tasarlanmıştır, ancak doğruluğu karmaşıktır ve kanıtlanması kolay değildir. Rust'ın tür sistemi bu yükün büyük bir kısmını omuzlar, ancak bu tür sistemi her şeyi yakalayamaz. Bu nedenle Rust, otomatize yazılım testlerini yazma desteğini dahili olarak içermektedir.

Kendisine iletilen sayıya 2 ekleyen bir add_two fonksiyonu yazdığımızı varsayalım. Bu fonksiyonun yapısı, parametre olarak bir tam sayı kabul eder ve sonuç olarak bir tam sayı döndürür. Bu fonksiyonu süreklediğimizde ve derlediğimizde; Rust, örneğin, bu fonksiyona bir String değeri veya geçersiz bir referans geçirmediğimizden emin olmak için şimdiye kadar öğrendiğiniz tüm tür kontrollerini ve ödünç alma kontrollerini yapar. Ancak Rust, bu fonksiyonun tam olarak neyi amaçladığımızı anlayamaz ve bunu kontrol edemez; bu, örneğin parametre artı 10 veya parametre eksi 50 yerine parametre artı 2'yi döndürür! Testlerin gerektiği yer burasıdır.

Örneğin, add_two fonksiyonuna 3'ü ilettiğimizde, döndürülen değerin 5 olduğunu iddia eden testler yazabiliriz.

Testler oluşturmak karmaşık bir beceridir: iyi testlerin nasıl yazılacağına dair her ayrıntıyı bir bölümde ele alamasak da, Rust'ın test tesislerinin mekaniğini tartışacağız. Testlerinizi yazarken kullanabileceğiniz ek açıklamalar ve makrolar, testlerinizi çalıştırmak için sağlanan varsayılan davranış ve seçenekler ve testlerin birim testleri ve entegrasyon testleri halinde nasıl organize edilebileceği hakkında konuşacağız.

How to Write Tests

Tests are Rust functions that verify that the non-test code is functioning in the expected manner. The bodies of test functions typically perform these three actions:

  1. Set up any needed data or state.
  2. Run the code you want to test.
  3. Assert the results are what you expect.

Let’s look at the features Rust provides specifically for writing tests that take these actions, which include the test attribute, a few macros, and the should_panic attribute.

The Anatomy of a Test Function

At its simplest, a test in Rust is a function that’s annotated with the test attribute. Attributes are metadata about pieces of Rust code; one example is the derive attribute we used with structs in Chapter 5. To change a function into a test function, add #[test] on the line before fn. When you run your tests with the cargo test command, Rust builds a test runner binary that runs the annotated functions and reports on whether each test function passes or fails.

Whenever we make a new library project with Cargo, a test module with a test function in it is automatically generated for us. This module gives you a template for writing your tests so you don’t have to look up the exact structure and syntax every time you start a new project. You can add as many additional test functions and as many test modules as you want!

We’ll explore some aspects of how tests work by experimenting with the template test before we actually test any code. Then we’ll write some real-world tests that call some code that we’ve written and assert that its behavior is correct.

Let’s create a new library project called adder that will add two numbers:

$ cargo new adder --lib
     Created library `adder` project
$ cd adder

The contents of the src/lib.rs file in your adder library should look like Listing 11-1.

Filename: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

Listing 11-1: The test module and function generated automatically by cargo new

For now, let’s ignore the top two lines and focus on the function. Note the #[test] annotation: this attribute indicates this is a test function, so the test runner knows to treat this function as a test. We might also have non-test functions in the tests module to help set up common scenarios or perform common operations, so we always need to indicate which functions are tests.

The example function body uses the assert_eq! macro to assert that result, which contains the result of adding 2 and 2, equals 4. This assertion serves as an example of the format for a typical test. Let’s run it to see that this test passes.

The cargo test command runs all tests in our project, as shown in Listing 11-2.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.57s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Listing 11-2: The output from running the automatically generated test

Cargo compiled and ran the test. We see the line running 1 test. The next line shows the name of the generated test function, called it_works, and that the result of running that test is ok. The overall summary test result: ok. means that all the tests passed, and the portion that reads 1 passed; 0 failed totals the number of tests that passed or failed.

It's possible to mark a test as ignored so it doesn't run in a particular instance; we'll cover that in the “Ignoring Some Tests Unless Specifically Requested” section later in this chapter. Because we haven't done that here, the summary shows 0 ignored. We can also pass an argument to the cargo test command to run only tests whose name matches a string; this is called filtering and we'll cover that in the “Running a Subset of Tests by Name” section. We also haven’t filtered the tests being run, so the end of the summary shows 0 filtered out.

The 0 measured statistic is for benchmark tests that measure performance. Benchmark tests are, as of this writing, only available in nightly Rust. See the documentation about benchmark tests to learn more.

The next part of the test output starting at Doc-tests adder is for the results of any documentation tests. We don’t have any documentation tests yet, but Rust can compile any code examples that appear in our API documentation. This feature helps keep your docs and your code in sync! We’ll discuss how to write documentation tests in the “Documentation Comments as Tests” section of Chapter 14. For now, we’ll ignore the Doc-tests output.

Let’s start to customize the test to our own needs. First change the name of the it_works function to a different name, such as exploration, like so:

Filename: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }
}

Then run cargo test again. The output now shows exploration instead of it_works:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.59s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::exploration ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Now we'll add another test, but this time we’ll make a test that fails! Tests fail when something in the test function panics. Each test is run in a new thread, and when the main thread sees that a test thread has died, the test is marked as failed. In Chapter 9, we talked about how the simplest way to panic is to call the panic! macro. Enter the new test as a function named another, so your src/lib.rs file looks like Listing 11-3.

Filename: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

Listing 11-3: Adding a second test that will fail because we call the panic! macro

Run the tests again using cargo test. The output should look like Listing 11-4, which shows that our exploration test passed and another failed.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.72s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok

failures:

---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

Listing 11-4: Test results when one test passes and one test fails

Instead of ok, the line test tests::another shows FAILED. Two new sections appear between the individual results and the summary: the first displays the detailed reason for each test failure. In this case, we get the details that another failed because it panicked at 'Make this test fail' on line 10 in the src/lib.rs file. The next section lists just the names of all the failing tests, which is useful when there are lots of tests and lots of detailed failing test output. We can use the name of a failing test to run just that test to more easily debug it; we’ll talk more about ways to run tests in the “Controlling How Tests Are Run” section.

The summary line displays at the end: overall, our test result is FAILED. We had one test pass and one test fail.

Now that you’ve seen what the test results look like in different scenarios, let’s look at some macros other than panic! that are useful in tests.

Checking Results with the assert! Macro

The assert! macro, provided by the standard library, is useful when you want to ensure that some condition in a test evaluates to true. We give the assert! macro an argument that evaluates to a Boolean. If the value is true, nothing happens and the test passes. If the value is false, the assert! macro calls panic! to cause the test to fail. Using the assert! macro helps us check that our code is functioning in the way we intend.

In Chapter 5, Listing 5-15, we used a Rectangle struct and a can_hold method, which are repeated here in Listing 11-5. Let’s put this code in the src/lib.rs file, then write some tests for it using the assert! macro.

Filename: src/lib.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Listing 11-5: Using the Rectangle struct and its can_hold method from Chapter 5

The can_hold method returns a Boolean, which means it’s a perfect use case for the assert! macro. In Listing 11-6, we write a test that exercises the can_hold method by creating a Rectangle instance that has a width of 8 and a height of 7 and asserting that it can hold another Rectangle instance that has a width of 5 and a height of 1.

Filename: src/lib.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }
}

Listing 11-6: A test for can_hold that checks whether a larger rectangle can indeed hold a smaller rectangle

Note that we’ve added a new line inside the tests module: use super::*;. The tests module is a regular module that follows the usual visibility rules we covered in Chapter 7 in the “Paths for Referring to an Item in the Module Tree” section. Because the tests module is an inner module, we need to bring the code under test in the outer module into the scope of the inner module. We use a glob here so anything we define in the outer module is available to this tests module.

We’ve named our test larger_can_hold_smaller, and we’ve created the two Rectangle instances that we need. Then we called the assert! macro and passed it the result of calling larger.can_hold(&smaller). This expression is supposed to return true, so our test should pass. Let’s find out!

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests (target/debug/deps/rectangle-6584c4561e48942e)

running 1 test
test tests::larger_can_hold_smaller ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

It does pass! Let’s add another test, this time asserting that a smaller rectangle cannot hold a larger rectangle:

Filename: src/lib.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        // --snip--
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

Because the correct result of the can_hold function in this case is false, we need to negate that result before we pass it to the assert! macro. As a result, our test will pass if can_hold returns false:

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Two tests that pass! Now let’s see what happens to our test results when we introduce a bug in our code. We’ll change the implementation of the can_hold method by replacing the greater-than sign with a less-than sign when it compares the widths:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

// --snip--
impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width < other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

Running the tests now produces the following:

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok

failures:

---- tests::larger_can_hold_smaller stdout ----
thread 'main' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:28:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::larger_can_hold_smaller

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

Our tests caught the bug! Because larger.width is 8 and smaller.width is 5, the comparison of the widths in can_hold now returns false: 8 is not less than 5.

Testing Equality with the assert_eq! and assert_ne! Macros

A common way to verify functionality is to test for equality between the result of the code under test and the value you expect the code to return. You could do this using the assert! macro and passing it an expression using the == operator. However, this is such a common test that the standard library provides a pair of macros—assert_eq! and assert_ne!—to perform this test more conveniently. These macros compare two arguments for equality or inequality, respectively. They’ll also print the two values if the assertion fails, which makes it easier to see why the test failed; conversely, the assert! macro only indicates that it got a false value for the == expression, without printing the values that led to the false value.

In Listing 11-7, we write a function named add_two that adds 2 to its parameter, then we test this function using the assert_eq! macro.

Filename: src/lib.rs

pub fn add_two(a: i32) -> i32 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds_two() {
        assert_eq!(4, add_two(2));
    }
}

Listing 11-7: Testing the function add_two using the assert_eq! macro

Let’s check that it passes!

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

We pass 4 as the argument to assert_eq!, which is equal to the result of calling add_two(2). The line for this test is test tests::it_adds_two ... ok, and the ok text indicates that our test passed!

Let’s introduce a bug into our code to see what assert_eq! looks like when it fails. Change the implementation of the add_two function to instead add 3:

pub fn add_two(a: i32) -> i32 {
    a + 3
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds_two() {
        assert_eq!(4, add_two(2));
    }
}

Run the tests again:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... FAILED

failures:

---- tests::it_adds_two stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `4`,
 right: `5`', src/lib.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_adds_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

Our test caught the bug! The it_adds_two test failed, and the message tells us that the assertion that fails was assertion failed: `(left == right)` and what the left and right values are. This message helps us start debugging: the left argument was 4 but the right argument, where we had add_two(2), was 5. You can imagine that this would be especially helpful when we have a lot of tests going on.

Note that in some languages and test frameworks, the parameters to equality assertion functions are called expected and actual, and the order in which we specify the arguments matters. However, in Rust, they’re called left and right, and the order in which we specify the value we expect and the value the code produces doesn’t matter. We could write the assertion in this test as assert_eq!(add_two(2), 4), which would result in the same failure message that displays assertion failed: `(left == right)`.

The assert_ne! macro will pass if the two values we give it are not equal and fail if they’re equal. This macro is most useful for cases when we’re not sure what a value will be, but we know what the value definitely shouldn’t be. For example, if we’re testing a function that is guaranteed to change its input in some way, but the way in which the input is changed depends on the day of the week that we run our tests, the best thing to assert might be that the output of the function is not equal to the input.

Under the surface, the assert_eq! and assert_ne! macros use the operators == and !=, respectively. When the assertions fail, these macros print their arguments using debug formatting, which means the values being compared must implement the PartialEq and Debug traits. All primitive types and most of the standard library types implement these traits. For structs and enums that you define yourself, you’ll need to implement PartialEq to assert equality of those types. You’ll also need to implement Debug to print the values when the assertion fails. Because both traits are derivable traits, as mentioned in Listing 5-12 in Chapter 5, this is usually as straightforward as adding the #[derive(PartialEq, Debug)] annotation to your struct or enum definition. See Appendix C, “Derivable Traits,” for more details about these and other derivable traits.

Adding Custom Failure Messages

You can also add a custom message to be printed with the failure message as optional arguments to the assert!, assert_eq!, and assert_ne! macros. Any arguments specified after the required arguments are passed along to the format! macro (discussed in Chapter 8 in the “Concatenation with the + Operator or the format! Macro” section), so you can pass a format string that contains {} placeholders and values to go in those placeholders. Custom messages are useful for documenting what an assertion means; when a test fails, you’ll have a better idea of what the problem is with the code.

For example, let’s say we have a function that greets people by name and we want to test that the name we pass into the function appears in the output:

Filename: src/lib.rs

pub fn greeting(name: &str) -> String {
    format!("Hello {}!", name)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

The requirements for this program haven’t been agreed upon yet, and we’re pretty sure the Hello text at the beginning of the greeting will change. We decided we don’t want to have to update the test when the requirements change, so instead of checking for exact equality to the value returned from the greeting function, we’ll just assert that the output contains the text of the input parameter.

Now let’s introduce a bug into this code by changing greeting to exclude name to see what the default test failure looks like:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

Running this test produces the following:

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished test [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'assertion failed: result.contains(\"Carol\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

This result just indicates that the assertion failed and which line the assertion is on. A more useful failure message would print the value from the greeting function. Let’s add a custom failure message composed of a format string with a placeholder filled in with the actual value we got from the greeting function:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"),
            "Greeting did not contain name, value was `{}`",
            result
        );
    }
}

Now when we run the test, we’ll get a more informative error message:

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished test [unoptimized + debuginfo] target(s) in 0.93s
     Running unittests (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

We can see the value we actually got in the test output, which would help us debug what happened instead of what we were expecting to happen.

Checking for Panics with should_panic

In addition to checking return values, it’s important to check that our code handles error conditions as we expect. For example, consider the Guess type that we created in Chapter 9, Listing 9-13. Other code that uses Guess depends on the guarantee that Guess instances will contain only values between 1 and 100. We can write a test that ensures that attempting to create a Guess instance with a value outside that range panics.

We do this by adding the attribute should_panic to our test function. The test passes if the code inside the function panics; the test fails if the code inside the function doesn’t panic.

Listing 11-8 shows a test that checks that the error conditions of Guess::new happen when we expect them to.

Filename: src/lib.rs

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Listing 11-8: Testing that a condition will cause a panic!

We place the #[should_panic] attribute after the #[test] attribute and before the test function it applies to. Let’s look at the result when this test passes:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests guessing_game

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Looks good! Now let’s introduce a bug in our code by removing the condition that the new function will panic if the value is greater than 100:

pub struct Guess {
    value: i32,
}

// --snip--
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

When we run the test in Listing 11-8, it will fail:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----
note: test did not panic as expected

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

We don’t get a very helpful message in this case, but when we look at the test function, we see that it’s annotated with #[should_panic]. The failure we got means that the code in the test function did not cause a panic.

Tests that use should_panic can be imprecise. A should_panic test would pass even if the test panics for a different reason from the one we were expecting. To make should_panic tests more precise, we can add an optional expected parameter to the should_panic attribute. The test harness will make sure that the failure message contains the provided text. For example, consider the modified code for Guess in Listing 11-9 where the new function panics with different messages depending on whether the value is too small or too large.

Filename: src/lib.rs

pub struct Guess {
    value: i32,
}

// --snip--
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {}.",
                value
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "Guess value must be less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Listing 11-9: Testing for a panic! with a particular panic message

This test will pass because the value we put in the should_panic attribute’s expected parameter is a substring of the message that the Guess::new function panics with. We could have specified the entire panic message that we expect, which in this case would be Guess value must be less than or equal to 100, got 200. What you choose to specify depends on how much of the panic message is unique or dynamic and how precise you want your test to be. In this case, a substring of the panic message is enough to ensure that the code in the test function executes the else if value > 100 case.

To see what happens when a should_panic test with an expected message fails, let’s again introduce a bug into our code by swapping the bodies of the if value < 1 and the else if value > 100 blocks:

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be less than or equal to 100, got {}.",
                value
            );
        } else if value > 100 {
            panic!(
                "Guess value must be greater than or equal to 1, got {}.",
                value
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "Guess value must be less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

This time when we run the should_panic test, it will fail:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----
thread 'main' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:13:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
      panic message: `"Guess value must be greater than or equal to 1, got 200."`,
 expected substring: `"Guess value must be less than or equal to 100"`

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

The failure message indicates that this test did indeed panic as we expected, but the panic message did not include the expected string 'Guess value must be less than or equal to 100'. The panic message that we did get in this case was Guess value must be greater than or equal to 1, got 200. Now we can start figuring out where our bug is!

Using Result<T, E> in Tests

Our tests so far all panic when they fail. We can also write tests that use Result<T, E>! Here’s the test from Listing 11-1, rewritten to use Result<T, E> and return an Err instead of panicking:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

The it_works function now has the Result<(), String> return type. In the body of the function, rather than calling the assert_eq! macro, we return Ok(()) when the test passes and an Err with a String inside when the test fails.

Writing tests so they return a Result<T, E> enables you to use the question mark operator in the body of tests, which can be a convenient way to write tests that should fail if any operation within them returns an Err variant.

You can’t use the #[should_panic] annotation on tests that use Result<T, E>. To assert that an operation returns an Err variant, don’t use the question mark operator on the Result<T, E> value. Instead, use assert!(value.is_err()).

Now that you know several ways to write tests, let’s look at what is happening when we run our tests and explore the different options we can use with cargo test.

Testlerin Nasıl Çalıştırıldığını Kontrol Etme

Tıpkı cargo run'un kodunuzu derlemesi ve ardından ortaya çıkan ikili dosyayı çalıştırması gibi, cargo test de kodunuzu test modunda derler ve ortaya çıkan test ikili dosyasını çalıştırır. cargo test tarafından üretilen ikilinin varsayılan davranışı, tüm testleri paralel olarak çalıştırmak ve test çalıştırmaları sırasında üretilen çıktıyı yakalamak, çıktının görüntülenmesini önlemek ve test sonuçlarıyla ilgili çıktıyı okumayı kolaylaştırmaktır. Bununla birlikte, bu varsayılan davranışı değiştirmek için komut satırı seçenekleri belirleyebilirsiniz.

Bazı komut satırı seçenekleri cargo test'e, bazıları ise elde edilen test ikilisine gider. Bu iki tür argümanı ayırmak için, cargo test'e giden argümanları ve ardından ayırıcıyı -- ve ardından test ikilisine gidenleri listelersiniz. cargo test --help komutunu çalıştırdığınızda cargo test ile kullanabileceğiniz seçenekler görüntülenir ve cargo test -- --help komutunu çalıştırdığınızda ayırıcıdan sonra kullanabileceğiniz seçenekler görüntülenir.

Testleri Paralel veya Ardışık Olarak Çalıştırma

Birden fazla test çalıştırdığınızda, varsayılan olarak iş parçacıkları kullanılarak paralel olarak çalışırlar, yani daha hızlı çalışırlar ve daha hızlı geri bildirim alırsınız. Testler aynı anda çalıştığından, testlerinizin birbirlerine veya geçerli çalışma dizini veya ortam değişkenleri gibi paylaşılan bir ortam da dahil olmak üzere herhangi bir paylaşılan duruma bağlı olmadığından emin olmalısınız.

Örneğin, testlerinizin her birinin diskte test-output.txt adında bir dosya oluşturan ve bu dosyaya bazı veriler yazan bir kod çalıştırdığını varsayalım. Ardından her test bu dosyadaki verileri okur ve dosyanın her testte farklı olan belirli bir değer içerdiğini iddia eder. Testler aynı anda çalıştığından, bir testin dosyayı yazması ve okuması arasında geçen sürede bir test dosyanın üzerine yazabilir. Bu durumda ikinci test, kod hatalı olduğu için değil, testler paralel olarak çalışırken birbirine karıştığı için başarısız olacaktır. Bir çözüm, her testin farklı bir dosyaya yazdığından emin olmaktır; başka bir çözüm ise testleri teker teker çalıştırmaktır.

Testleri paralel olarak çalıştırmak istemiyorsanız veya kullanılan iş parçacığı sayısı üzerinde daha ayrıntılı kontrol istiyorsanız, --test-threads bayrağını ve kullanmak istediğiniz iş parçacığı sayısını test ikilisine gönderebilirsiniz.

Aşağıdaki örneğe bir göz atın:

$ cargo test -- --test-threads=1

Test iş parçacığı sayısını 1 olarak ayarladık ve programa herhangi bir paralellik kullanmamasını söyledik. Testleri tek bir iş parçacığı kullanarak çalıştırmak, paralel olarak çalıştırmaktan daha uzun sürecektir, ancak testler durumu paylaşırlarsa birbirlerini etkilemeyeceklerdir.

Fonksiyon Çıktısını Gösterme

Varsayılan olarak, bir test geçerse, Rust'ın test kütüphanesi standart çıktıya yazdırılan her şeyi yakalar. Örneğin, bir testte println! komutunu çağırırsak ve test geçerse, uçbirimde println! çıktısını görmeyiz; yalnızca testin geçtiğini gösteren satırı görürüz. Bir test başarısız olursa, başarısız mesajının geri kalanıyla birlikte standart çıktıya ne yazdırıldıysa onu görürüz.

Örnek olarak, Liste 11-10'da parametresinin değerini yazdıran ve 10 döndüren saçma bir fonksiyonun yanı sıra başarılı olan bir test ve başarısız olan bir test vardır.

Dosya adı: src/lib.rs

fn prints_and_returns_10(a: i32) -> i32 {
    println!("I got the value {}", a);
    10
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn this_test_will_pass() {
        let value = prints_and_returns_10(4);
        assert_eq!(10, value);
    }

    #[test]
    fn this_test_will_fail() {
        let value = prints_and_returns_10(8);
        assert_eq!(5, value);
    }
}

Liste 11-10: println! çağrısı yapan bir fonksiyon için testler

Bu testleri cargo test ile çalıştırdığımızda aşağıdaki çıktıyı göreceğiz:

$ cargo test
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished test [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:19:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

Bu çıktının hiçbir yerinde I got the value 4 ifadesini görmediğimize dikkat edin; bu, geçen test çalıştırıldığında yazdırılan değerdir. Bu çıktı yakalanmıştır. Başarısız olan testin çıktısı, I got the value 8, test özeti çıktısının test başarısızlığının nedenini de gösteren bölümünde görünür.

Geçen testler için de yazdırılan değerleri görmek istiyorsak, Rust'a --show-output ile başarılı testlerin çıktısını da en sonunda göstermesini söyleyebiliriz.

$ cargo test -- --show-output

Liste 11-10'daki testleri --show-output bayrağı ile tekrar çalıştırdığımızda aşağıdaki çıktıyı görüyoruz:

$ cargo test -- --show-output
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished test [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

successes:

---- tests::this_test_will_pass stdout ----
I got the value 4


successes:
    tests::this_test_will_pass

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:19:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

Bir Test Alt Kümesini Ada Göre Çalıştırma

Bazen tam bir test paketi çalıştırmak uzun zaman alabilir. Belirli bir alandaki kod üzerinde çalışıyorsanız, yalnızca o kodla ilgili testleri çalıştırmak isteyebilirsiniz. Çalıştırmak istediğiniz test(ler)in adını veya adlarını cargo test'e argüman olarak ileterek hangi testlerin çalıştırılacağını seçebilirsiniz.

Testlerin bir alt kümesinin nasıl çalıştırılacağını göstermek için, önce Liste 11-11'de gösterildiği gibi add_two fonksiyonumuz için üç test oluşturacağız ve hangilerinin çalıştırılacağını seçeceğiz.

Dosya adı: src/lib.rs

pub fn add_two(a: i32) -> i32 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_two_and_two() {
        assert_eq!(4, add_two(2));
    }

    #[test]
    fn add_three_and_two() {
        assert_eq!(5, add_two(3));
    }

    #[test]
    fn one_hundred() {
        assert_eq!(102, add_two(100));
    }
}

Liste 11-11: Üç farklı isimle üç test

Testleri daha önce gördüğümüz gibi herhangi bir argüman geçmeden çalıştırırsak, tüm testler paralel olarak çalışacaktır:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Tekdüze Testleri Çalıştırma

Yalnızca bu testi çalıştırmak için herhangi bir test fonksiyonunun adını cargo test'e geçirebiliriz:

$ cargo test one_hundred
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.69s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::one_hundred ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

Sadece one_hundred isimli test çalıştı; diğer iki test bu isimle eşleşmedi. Test çıktısı, sonunda filtrelenen 2'yi görüntüleyerek çalışmayan daha fazla testimiz olduğunu bilmemizi sağlar.

Bu şekilde birden fazla testin adını belirtemeyiz; yalnızca cargo test'e verilen ilk değer kullanılır. Ancak birden fazla test çalıştırmanın bir yolu var.

Birden Fazla Test Çalıştırmak için Filtreleme

Bir test adının bir kısmını belirtebiliriz ve adı bu değerle eşleşen herhangi bir test çalıştırılır. Örneğin,testlerimizden ikisinin adı add içerdiğinden, bu ikisini cargo test add komutuyla çalıştırabiliriz:

$ cargo test add
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

Bu komut, adında add olan tüm testleri çalıştırır ve one_hundred adlı testi filtreler. Ayrıca, bir testin göründüğü modülün testin adının bir parçası haline geldiğini unutmayın, bu nedenle modülün adına göre filtreleme yaparak bir modüldeki tüm testleri çalıştırabiliriz.

Özel Olarak İstenmedikçe Bazı Testleri Yok Sayma

Bazen belirli birkaç testin yürütülmesi çok zaman alıcı olabilir, bu nedenle cargo test'in çoğu çalıştırması sırasında bunları hariç tutmak isteyebilirsiniz. Çalıştırmak istediğiniz tüm testleri argüman olarak listelemek yerine, zaman alan testleri burada gösterildiği gibi dışlamak için ignore niteliğini kullanarak açıklama ekleyebilirsiniz:

Dosya adı: src/lib.rs

#[test]
fn it_works() {
    assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn expensive_test() {
    // code that takes an hour to run
}

#[test]'ten sonra, hariç tutmak istediğimiz teste #[ignore] satırını ekliyoruz. Testlerimizi çalıştırdığımızda it_works çalışıyor, ancak expensive_test çalışmıyor:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test expensive_test ... ignored
test it_works ... ok

test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

expensive_test fonksiyonu ignored olarak listeleniyor. Yalnızca göz ardı edilen testleri çalıştırmak istiyorsak, cargo test -- --ignored'u kullanabiliriz:

$ cargo test -- --ignored
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test expensive_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Hangi testlerin çalışacağını kontrol ederek, cargo test sonuçlarınızın hızlı olmasını sağlayabilirsiniz. Yok sayılan testlerin sonuçlarını kontrol etmenin mantıklı olduğu bir noktaya geldiğinizde ve sonuçları beklemek için zamanınız olduğunda, bunun yerine cargo test -- --ignored komutunu çalıştırabilirsiniz. Yok sayılsın ya da sayılmasın tüm testleri çalıştırmak istiyorsanız, cargo test -- --include-ignored komutunu çalıştırabilirsiniz.

Test Organizasyonu

Bölümün başında da belirtildiği gibi, test karmaşık bir disiplindir ve farklı kişiler farklı terminoloji ve organizasyon kullanmaktadır. Rust topluluğu testleri iki ana kategoride ele alır: birim testleri ve entegrasyon testleri. Birim testleri küçük ve daha odaklıdır, her seferinde tek bir modülü izole olarak test eder ve özel arayüzleri test edebilir. Entegrasyon testleri kütüphanenizin tamamen dışındadır ve kodunuzu diğer harici kodlarla aynı şekilde kullanır, yalnızca genel arayüzü kullanır ve potansiyel olarak test başına birden fazla modülü test eder.

Her iki tür testin de yazılması, kütüphanenizin parçalarının ayrı ayrı ve birlikte beklediğiniz şeyi yaptığından emin olmak için önemlidir.

Birim Testleri

Birim testlerinin amacı, kodun nerede beklendiği gibi çalışıp çalışmadığını hızlı bir şekilde belirlemek için her bir kod birimini kodun geri kalanından ayrı olarak test etmektir. Birim testlerini, test ettikleri kodun bulunduğu her dosyanın src dizinine koyarsınız. Alışılagelmiş yöntem, her dosyada test işlevlerini içerecek tests adında bir modül oluşturmak ve modüle cfg(test) ile açıklama eklemektir.

Testler Modülü ve #[cfg(test)]

tests modülündeki #[cfg(test)] ek açıklaması, Rust'a test kodunu yalnızca cargo test'i çalıştırdığınızda derlemesini ve çalıştırmasını söyler, cargo build'i çalıştırdığınızda değil. Bu, yalnızca kütüphaneyi derlemek istediğinizde derleme süresinden tasarruf sağlar ve testler dahil edilmediği için sonuçta derlenen yapıda yer tasarrufu sağlar. Entegrasyon testleri farklı bir dizine gittiği için #[cfg(test)] ek açıklamasına ihtiyaç duymadıklarını göreceksiniz. Ancak, birim testleri kodla aynı dosyalara girdiği için, derlenen sonuca dahil edilmemeleri gerektiğini belirtmek için #[cfg(test)] kullanacaksınız.

Bu bölümün ilk kısmında yeni adder projesini oluşturduğumuzda, Cargo'nun bu kodu bizim için oluşturduğunu hatırlayın:

Dosya adı: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

Bu kod otomatik olarak oluşturulan test modülüdür. cfg niteliği yapılandırma anlamına gelir ve Rust'a aşağıdaki öğenin yalnızca belirli bir yapılandırma seçeneği verildiğinde dahil edilmesi gerektiğini söyler. Bu durumda, yapılandırma seçeneği, testlerin derlenmesi ve çalıştırılması için Rust tarafından sağlanan test'tir. cfg niteliğini kullanarak, cargo test kodumuzu yalnızca testleri cargo test ile aktif olarak çalıştırırsak derler. Bu, #[test] ile açıklanan fonksiyonlara ek olarak, bu modül içinde olabilecek tüm yardımcı fonksiyonları içerir.

Özel Fonksiyonları Test Etme

Test topluluğu içinde gizli fonksiyonların doğrudan test edilip edilmemesi gerektiği konusunda tartışmalar vardır ve diğer diller gizli fonksiyonları test etmeyi zorlaştırır veya imkansız hale getirir. Hangi test ideolojisine bağlı olursanız olun, Rust'ın gizlilik kuralları gizli fonksiyonları test etmenize izin verir. Liste 11-12'deki kodu gizli fonksiyon internal_adder ile birlikte düşünün.

Dosya adı: src/lib.rs

pub fn add_two(a: i32) -> i32 {
    internal_adder(a, 2)
}

fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn internal() {
        assert_eq!(4, internal_adder(2, 2));
    }
}

Liste 11-12: Özel bir fonksiyonu test etme

internal_adder fonksiyonunun pub olarak işaretlenmediğine dikkat edin. Testler sadece Rust kodudur ve test modülü sadece başka bir modüldür. “Modül Ağacında Bir Öğeye Başvurma Yolları” bölümünde tartıştığımız gibi, alt modüllerdeki öğeler ata modüllerindeki öğeleri kullanabilir. Bu testte, test modülünün ebeveyninin tüm öğelerini use super::* ile kapsama alırız ve ardından test internal_adder'ı çağırabilir. Gizli fonksiyonların test edilmemesi gerektiğini düşünüyorsanız, Rust'ta sizi bunu yapmaya zorlayacak hiçbir şey yoktur.

Entegrasyon Testleri

Rust'ta entegrasyon testleri kütüphanenizin tamamen dışındadır. Kütüphanenizi diğer kodlarla aynı şekilde kullanırlar, yani yalnızca kütüphanenizin genel API'sinin bir parçası olan fonksiyonları çağırabilirler. Amaçları, kütüphanenizin birçok parçasının birlikte doğru çalışıp çalışmadığını test etmektir. Kendi başlarına doğru çalışan kod birimleri entegre edildiğinde sorun yaşayabilir, bu nedenle entegre kodun test kapsamı da önemlidir. Entegrasyon testleri oluşturmak için öncelikle bir test dizinine ihtiyacınız vardır.

tests Dizini

tests dizinindeki her dosya ayrı bir kasadır, bu nedenle kütüphanemizi her test kasasının kapsamına getirmemiz gerekir. Bu nedenle, birim testlerinde ihtiyaç duymadığımız use adder'ı kodun en üstüne ekliyoruz.

tests/integration_test.rs içindeki herhangi bir koda #[cfg(test)] ile açıklama eklememize gerek yok. Cargo, tests dizinini özel olarak ele alır ve bu dizindeki dosyaları yalnızca cargo test'i çalıştırdığımızda derler. Şimdi cargo test'i çalıştırın:

Dosya adı: tests/integration_test.rs

use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

Liste 11-13: adder kasasındaki bir fonksiyonun entegrasyon testi

Çıktının üç bölümü birim testlerini, entegrasyon testini ve dokümantasyon testlerini içerir. Birim testleri için ilk bölüm gördüğümüzle aynıdır: her birim testi için bir satır (Liste 11-12'de eklediğimiz internal adlı bir satır) ve ardından birim testleri için bir özet satırı.

Entegrasyon testleri bölümü Running tests/integration_test.rs satırıyla başlar. Ardından, bu entegrasyon testindeki her bir test fonksiyonu için bir satır ve Doc-tests adder bölümü başlamadan hemen önce entegrasyon testinin sonuçları için bir özet satırı vardır.

Her entegrasyon testi dosyasının kendi bölümü vardır, bu nedenle tests dizinine daha fazla dosya eklersek, daha fazla entegrasyon testi bölümü olacaktır.

Test fonksiyonunun adını cargo test'e argüman olarak belirterek belirli bir entegrasyon testi fonksiyonunu çalıştırabiliriz. Belirli bir entegrasyon testi dosyasındaki tüm testleri çalıştırmak için, cargo test'in --test argümanını ve ardından dosyanın adını kullanın:

$ cargo test --test integration_test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.64s
     Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Bu komut yalnızca tests/integration_test.rs dosyasındaki testleri çalıştırır.

Entegrasyon Testlerinde Alt Modüller

Daha fazla entegrasyon testi ekledikçe, bunları düzenlemeye yardımcı olmak için tests dizininde daha fazla dosya oluşturmak isteyebilirsiniz; örneğin, test fonksiyonlarını test ettikleri işlevselliğe göre gruplayabilirsiniz. Daha önce de belirtildiği gibi, tests dizinindeki her dosya kendi içinde ayrı kasa olarak derlenir, bu da son kullanıcıların kasanızı kullanma şeklini daha yakından taklit etmek için ayrı kapsamlar oluşturmak için kullanışlıdır. Ancak bu, Bölüm 7'de kodu modüllere ve dosyalara nasıl ayıracağınızı öğrendiğiniz gibi, tests dizinindeki dosyaların src'deki dosyalarla aynı davranışı paylaşmadığı anlamına gelir.

tests dizini dosyalarının farklı davranışı en çok, birden fazla entegrasyon test dosyasında kullanılacak bir dizi yardımcı fonksiyonunuz olduğunda ve bunları ortak bir modüle çıkarmak için Bölüm 7'deki “Modülleri Farklı Dosyalara Ayırma” bölümündeki adımları izlemeye çalıştığınızda fark edilir. Örneğin, tests/common.rs dosyasını oluşturur ve içine setup adında bir fonksiyon yerleştirirsek, setup dosyasına test dosyalarındaki test fonksiyonlarından çağırmak istediğimiz bazı kodlar ekleyebiliriz:

Dosya adı: tests/common.rs

pub fn setup() {
    // setup code specific to your library's tests would go here
}

Testleri tekrar çalıştırdığımızda, common.rs dosyası için test çıktısında yeni bir bölüm göreceğiz, ancak bu dosya herhangi bir test fonksiyonu içermiyor ve setup fonksiyonunu herhangi bir yerden çağırmadık:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.89s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Test sonuçlarında yaygın olarak running 0 tests'in görüntülenmesi istediğimiz şey değildi. Sadece bazı kodları diğer entegrasyon test dosyalarıyla paylaşmak istedik.

common'ın test çıktısında görünmesini önlemek için tests/common.rs oluşturmak yerine tests/common/mod.rs oluşturacağız. Bu, Rust'ın da anladığı alternatif bir adlandırma kuralıdır. Dosyayı bu şekilde adlandırmak, Rust'a common modülünü bir entegrasyon test dosyası olarak ele almamasını söyler. setup fonksiyonu kodunu tests/common/mod.rs dosyasına taşıdığımızda ve tests/common.rs dosyasını sildiğimizde, test çıktısındaki bölüm artık görünmeyecektir. tests dizininin alt dizinlerinde yer alan dosyalar ayrı kasalar olarak derlenmez veya test çıktısında bölümlere sahip olmaz.

tests/common/mod.rs dosyasını oluşturduktan sonra, bunu entegrasyon test dosyalarından herhangi birinde modül olarak kullanabiliriz. Aşağıda, tests/integration_test.rs dosyasındaki it_adds_two testinden setup fonksiyonunun çağrılmasına bir örnek verilmiştir:

Dosya adı: tests/integration_test.rs

use adder;

mod common;

#[test]
fn it_adds_two() {
    common::setup();
    assert_eq!(4, adder::add_two(2));
}

mod common; bildiriminin Liste 7-21'de gösterdiğimiz modül bildirimiyle aynı olduğuna dikkat edin. Daha sonra test fonksiyonunda common::setup() fonksiyonunu çağırabiliriz.

İkili Kasalar için Entegrasyon Testleri

Projemiz yalnızca bir src/main.rs dosyası içeren ve bir src/lib.rs dosyasına sahip olmayan ikili bir kasaysa, tests dizininde entegrasyon testleri oluşturamaz ve src/main.rs dosyasında tanımlanan fonksiyonları bir use ifade yapısı ile kapsama alamayız. Yalnızca kütüphane kasaları diğer kasaların kullanabileceği fonksiyonları açığa çıkarır; ikili kasalar kendi başlarına çalıştırılmak üzere tasarlanmıştır.

Bu, bir ikili dosya sağlayan Rust projelerinin src/lib.rs dosyasında bulunan mantığı çağıran basit bir src/main.rs dosyasına sahip olmasının nedenlerinden biridir. Bu yapıyı kullanarak entegrasyon testleri, önemli fonksiyonları kullanılabilir hale getirmek için kütüphane kasasını kullanarak test edebilir. Önemli işlevsellik çalışırsa, src/main.rs dosyasındaki az miktarda kod da çalışacaktır ve bu az miktarda kodun test edilmesi gerekmez.

Özet

Rust'ın test özellikleri, siz değişiklik yapsanız bile kodun beklediğiniz gibi çalışmaya devam etmesini sağlamak için kodun nasıl çalışması gerektiğini belirtmenin bir yolunu sunar. Birim testleri, bir kütüphanenin farklı bölümlerini ayrı ayrı çalıştırır ve özel uygulama ayrıntılarını test edebilir. Entegrasyon testleri, kütüphanenin birçok parçasının birlikte doğru çalışıp çalışmadığını kontrol eder ve kodu harici kodun kullanacağı şekilde test etmek için kütüphanenin genel API'sini kullanır. Rust'ın tür sistemi ve sahiplik kuralları bazı hata türlerini önlemeye yardımcı olsa da, kodunuzun nasıl davranması beklendiğiyle ilgili mantık hatalarını azaltmak için testler hala önemlidir.

Bu bölümde ve önceki bölümlerde öğrendiğiniz bilgileri bir proje üzerinde çalışmak için birleştirelim!

Bir G/Ç Projesi: Bir Komut Satırı Programı Yazmak

Bu bölüm, şimdiye kadar öğrendiğiniz birçok becerinin bir özeti ve birkaç standart kütüphane özelliğinin daha keşfidir. Şu anda sahip olduğunuz bazı Rust konseptlerini süreklemek için dosya ve komut satırı giriş/çıkış ile etkileşime giren bir komut satırı aracı oluşturacağız.

Rust'ın hızı, güvenliği, tek ikili yürütülebilir çıktısı ve platformlar arası desteği, onu komut satırı araçları oluşturmak için ideal bir dil haline getiriyor, bu nedenle projemiz için klasik komut satırı arama aracı grep'in (globally search a regular expression and print) kendi sürümümüzü yapacağız. En basit kullanım durumunda, grep, belirtilen bir dize için belirtilen bir dosyayı arar. Bunu yapmak için grep, argümanları olarak bir dosya adı ve bir dizgi alır. Sonra dosyayı okur, o dosyada dizgi argümanını içeren satırları bulur ve bu satırları yazdırır.

Bu arada, komut satırı aracımızın diğer birçok komut satırı aracının kullandığı üçbirim özelliklerini kullanmasını nasıl sağlayacağımızı göstereceğiz. Kullanıcının aracımızın davranışını yapılandırmasına izin vermek için bir ortam değişkeninin değerini okuyacağız. Ayrıca hata mesajlarını standart çıktı (stdout) yerine standart hata konsolu akışına (stderr) yazdıracağız, böylece kullanıcı ekranda hata mesajlarını görmeye devam ederken başarılı çıktıyı bir dosyaya yönlendirebilir.

Bir Rust topluluğu üyesi olan Andrew Gallant, ripgrep adlı tam özellikli, çok hızlı bir grep sürümü oluşturmuştur. Karşılaştırıldığında, bizim versiyonumuz oldukça basit olacak, ancak bu bölüm size ripgrep gibi gerçek dünyadaki bir projeyi anlamak için ihtiyaç duyduğunuz bazı arka plan bilgilerini verecektir.

  • Kodu organize etmek (Bölüm 7'de modüllerle ilgili öğrendiklerinizi kullanarak)
  • Vektörleri ve dizgileri kullanmak (Bölüm 8'deki koleksiyonlar)
  • Hataları işlemek (Bölüm 9)
  • Uygun yerlerde tanımları ve ömürlükleri kullanmak (Bölüm 10)
  • Testler yazmak (Bölüm 11)

Ayrıca Bölüm 13 ve 17'de ayrıntılı olarak ele alınacak olan kapanışları, yineleyicileri ve tanım nesnelerini kısaca tanıtacağız.

Komut Satırı Argümanlarını Kabul Etme

Her zaman olduğu gibi cargo new ile yeni bir proje oluşturalım. Sisteminizde mevcut olabilecek grep aracıyla çakışmaması için projemizi minigrep olarak adlandıracağız

$ cargo new minigrep
     Created binary (application) `minigrep` project
$ cd minigrep

İlk görev, minigrep'in iki komut satırı argümanını kabul etmesini sağlamaktır: dosya adı ve aranacak bir dize. Yani, programımızı cargo run'la, aranacak bir dize ve aranacak bir dosyanın yolu ile çalıştırabilmek istiyoruz, şunun gibi:

$ cargo run searchstring example-filename.txt

Şu anda, cargo new tarafından oluşturulan program, ona verdiğimiz argümanları işleyemiyor. crates.io'daki bazı mevcut kütüphaneler, komut satırı argümanlarını kabul eden bir program yazmaya yardımcı olabilir, ancak bu kavramı yeni öğrendiğiniz için, hadi bunu kendimiz sürekleyelim.

Argüman Değerlerini Okumak

minigrep'in kendisine ilettiğimiz komut satırı argümanlarının değerlerini okumasını sağlamak için, Rust'ın standart kütüphanesinde bulunan std::env::args fonksiyonuna ihtiyacımız olacak. Bu fonksiyon, minigrep'e iletilen komut satırı argümanlarının bir yineleyicisini döndürür. Bölüm 13'te yineleyicileri tam olarak ele alacağız. Şimdilik, yineleyiciler hakkında yalnızca iki ayrıntıyı bilmeniz gerekir: yineleyiciler bir dizi değer üretir ve bir yineleyicide onu bir koleksiyona dönüştürmek için collect metodunu çağırabiliriz, örneğin: yineleyicinin ürettiği tüm öğeleri içeren vektör.

Liste 12-1'deki kod, minigrep programınızın kendisine iletilen herhangi bir komut satırı argümanını okumasını ve ardından değerleri bir vektörde toplamasını sağlar.

Dosya adı: src/main.rs

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
}

Liste 12-1: Komut satırı argümanlarını bir vektörde toplama ve yazdırma

İlk olarak, args fonksiyonunu kullanabilmemiz için std::env modülünü use ifade yapısı ile kapsama alıyoruz. std::env::args fonksiyonunun iki modül düzeyinde tutuluyor olduğuna dikkat edin. Bölüm 7'de tartıştığımız gibi, istenen fonksiyonun birden fazla modülde iç içe olduğu durumlarda, ana modülü fonksiyon yerine kapsama almak gelenekseldir. Bunu yaparak, std::env'deki diğer fonksiyonları kolayca kullanabiliriz. Ayrıca use std::env::args eklemekten ve ardından fonksiyonu yalnızca args ile çağırmaktan daha az belirsizdir, çünkü arg'ler kolayca geçerli modülde tanımlanan bir fonksiyonla karıştırılabilir.

args Fonksiyonu ve Geçersiz Unicode

Herhangi bir argüman geçersiz Unicode içeriyorsa std::env::args öğesinin panikleyeceğini unutmayın. Programınızın geçersiz Unicode içeren argümanları kabul etmesi gerekiyorsa, bunun yerine std::env::args_os kullanın. Bu fonksiyon, String değerleri yerine OsString değerleri üreten bir yineleyici döndürür. Basitlik için burada std::env::args kullanmayı seçtik, çünkü OsString değerleri platforma göre farklılık gösterir ve onlarla çalışmak String değerlerinden daha karmaşıktır.

main'in ilk satırında, env::args'ı çağırırız ve yineleyiciyi yineleyici tarafından üretilen tüm değerleri içeren bir vektöre dönüştürmek için collect kullanırız. Pek çok türde koleksiyon oluşturmak için collect fonksiyonunu kullanabiliriz, bu nedenle bir dizi vektörü istediğimizi belirtmek için açıkça arg türüne açıklama ekleriz. Rust'ta türlere çok nadiren açıklama eklememiz gerekse de, collect, genellikle açıklama eklemeniz gereken bir fonksiyondur çünkü Rust, istediğiniz koleksiyon türünü çıkaramaz.

Son olarak, vektörü hata ayıklama biçimlendiricisini (:?) kullanarak yazdırırız. Kodu önce bağımsız değişken olmadan ve ardından iki bağımsız değişkenle çalıştırmayı deneyelim:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/minigrep`
["target/debug/minigrep"]
$ cargo run needle haystack
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 1.57s
     Running `target/debug/minigrep needle haystack`
["target/debug/minigrep", "needle", "haystack"]

Vektördeki ilk değerin, ikili dosyamızın adı olan "target/debug/minigrep" olduğuna dikkat edin. Bu, programların yürütülürken çağrıldıkları adı kullanmasına izin vererek, C'deki argüman listesinin davranışıyla eşleşir. Mesajlarda yazdırmak veya programı çağırmak için hangi komut satırı diğer adının kullanıldığına bağlı olarak programın davranışını değiştirmek istemeniz durumunda, program adına erişiminiz olması genellikle uygundur. Ancak bu bölümün amaçları doğrultusunda, onu görmezden geleceğiz ve yalnızca ihtiyacımız olan iki argümanı kaydedeceğiz.

Değişkenlerde Argüman Değerlerini Tutma

Program şu anda komut satırı argümanları olarak belirtilen değerlere erişebilir. Şimdi değerleri programın geri kalanında kullanabilmemiz için iki argümanın değerlerini değişkenlere kaydetmemiz gerekiyor. Bunu Liste 12-2'de yapıyoruz.

Dosya adı: src/main.rs

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let filename = &args[2];

    println!("Searching for {}", query);
    println!("In file {}", filename);
}

Liste 12-2: Sorgu argümanını ve dosya adı argümanını tutmak için değişkenler oluşturma

Vektörü yazdırdığımızda gördüğümüz gibi, programın adı args[0]'daki vektördeki ilk değeri alır, dolayısıyla argümanları indeks 1'den başlatıyoruz. minigrep'in aldığı ilk argüman, aradığımız dizgidir, bu yüzden değişken sorgusundaki ilk argümana bir referans koyduk. İkinci argüman dosya adı olacaktır, bu yüzden dosya adı değişkenine ikinci argümana bir referans koyduk.

Kodun istediğimiz gibi çalıştığını kanıtlamak için bu değişkenlerin değerlerini geçici olarak yazdırırız. Bu programı test ve sample.txt argümanları ile tekrar çalıştıralım:

$ cargo run test sample.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt

Harika, program çalışıyor! İhtiyacımız olan argümanların değerleri doğru değişkenlere kaydediliyor. Daha sonra, örneğin kullanıcının hiçbir argüman sağlamaması gibi bazı olası hatalı durumlarla başa çıkmak için bazı hata işleme ekleyeceğiz, şimdilik bu durumu görmezden geleceğiz ve bunun yerine dosya okuma yetenekleri eklemeye çalışacağız.

Dosya Okumak

Şimdi, filename argümanında belirtilen dosyayı okumak için işlevsellik ekleyeceğiz. İlk olarak, bunu test etmek için örnek bir dosyaya ihtiyacımız var: Birkaç satırda metin içeren ve bazı tekrarlanan kelimeler içeren bir dosya kullanacağız. Liste 12-3'te işe yarayacak bir Emily Dickinson şiiri var! Projenizin kök dizininde poem.txt adlı bir dosya oluşturun ve “Ben Hiçkimse'yim! Sen kimsin?"

Dosya adı: poem.txt

I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

Liste 12-3: Emily Dickinson'ın bir şiiri iyi bir test örneği yapıyor

Metin yerindeyken, src/main.rs dosyasını düzenleyin ve okunacak dosya için kod ekleyin.

Dosya adı: src/main.rs

use std::env;
use std::fs;

fn main() {
    // --snip--
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let filename = &args[2];

    println!("Searching for {}", query);
    println!("In file {}", filename);

    let contents = fs::read_to_string(filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

Liste 12-4: İkinci argüman tarafından belirtilen dosyanın içeriğini okuma

İlk olarak, standart kitaplığın ilgili bir bölümünü bir use ifadesi ile getiriyoruz: dosyaları işlemek için std::fs'ye ihtiyacımız var.

main'de, fs::read_to_string ifadesi filename argümanını alır, dosyayı açar, ve dosyanın içeriğini tutan Result<String>'i döndürür.

Bundan sonra, geçici olarak bir println! ifadesi koyacağız ki dosya okunduktan sonra içindekileri okuyabilelim.

Hadi şimdi bu kodu herhangi bir ilk komut satırı argümanıyla (çünkü henüz dosya arama kısmını süreklemedik) ve poem.txt dosyasını ikinci bir argüman olarak kullanarak çalıştıralım.

$ cargo run the poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

Güzel! Bu kod dosyanın içeriğini okur ve sonra içeriğini yazar. Ama kod bazı sorunlara sahip. main fonksiyonunun birden fazla işlevi var. Genel olarak tek bir fikre dayalı fonksiyonlar daha kolay karşılanır ve sürdürülür. Bir diğer problem olaraktan, biz henüz herhangi bir hatayı işlemiyoruz. Program küçük yani bu sıkıntılar büyük bir program değil ama program büyüdükçe bu tarz sıkıntıları temizce çözmek zorlaşacaktır. Bu tarz sıkıntıları kodunuz büyümeden çözmek iyi bir pratik olacaktır. Bunu sonra yapacağız.

Modülerliği ve Hata İşlemeyi Geliştirmek için Yeniden Düzenleme

Programımızı iyileştirmek için, programın yapısı ve olası hataları nasıl ele aldığı ile ilgili dört sorunu çözeceğiz. İlk olarak, ma'n fonksiyonumuz artık iki görevi yerine getiriyor: argümanları ayrıştırıyor ve dosyaları okuyor. Programımız büyüdükçe, ana fonksiyonun yerine getirdiği ayrı görevlerin sayısı artacaktır. Bir fonksiyon sorumluluk kazandıkça, hakkında mantık yürütmek daha zor, test etmek daha zor ve parçalarından birini bozmadan değiştirmek daha zor hale gelir. Her fonksiyonun tek bir görevden sorumlu olması için işlevleri ayırmak en iyisidir.

Bu konu aynı zamanda ikinci sorunla da bağlantılıdır: sorgu ve dosya adı programımız için yapılandırma değişkenleri olsa da, içerik gibi değişkenler programın mantığını gerçekleştirmek için kullanılır. main ne kadar uzun olursa, o kadar çok değişkeni kapsama almamız gerekecektir; ne kadar çok değişkeni kapsama alırsak, her birinin amacını takip etmek o kadar zor olacaktır. Amaçlarını netleştirmek için yapılandırma değişkenlerini tek bir yapıda gruplamak en iyisidir.

Üçüncü sorun, dosyayı okuma başarısız olduğunda bir hata mesajı yazdırmak için expect kullandık, ancak hata mesajı sadece Something went wrong reading the file yazıyor. Bir dosyayı okumak çeşitli şekillerde başarısız olabilir: örneğin, dosya eksik olabilir veya dosyayı açmak için iznimiz olmayabilir. Şu anda, durum ne olursa olsun, her şey için aynı hata mesajını yazdırırız ve bu da kullanıcıya hiçbir bilgi vermeyiz.

Dördüncüsü, farklı hataları işlemek için tekrar tekrar expect kullanıyoruz ve kullanıcı programımızı yeterli argüman belirtmeden çalıştırırsa, Rust'tan sorunu açıkça açıklamayan bir index out of bounds hatası alacaktır. Tüm hata işleme kodunun tek bir yerde olması en iyisidir, böylece gelecekteki bakımcılar hata işleme mantığının değişmesi gerektiğinde koda başvurmak için tek bir yere sahip olurlar. Tüm hata işleme kodunun tek bir yerde olması, son kullanıcılarımız için anlamlı olacak mesajları yazdırmamızı da sağlayacaktır.

Projemizi yeniden düzenleyerek bu dört sorunu ele alalım.

İkili Projeler için Endişelerin Ayrılması

Birden fazla görevin sorumluluğunun ana fonksiyona verilmesine ilişkin organizasyonel sorun, birçok ikili projede ortaktır. Sonuç olarak, Rust topluluğu, ana program büyümeye başladığında ikili bir programın ayrı endişelerini bölmek için yönergeler geliştirmiştir. Bu süreç aşağıdaki adımlardan oluşur:

  • Programınızı main.rs ve lib.rs olarak ayırın ve programınızın ana mantığını lib.rs'e taşıyın.
  • Komut satırı ayrıştırma mantığınız küçük olduğu sürece main.rs içinde kalabilir.
  • Komut satırı ayrıştırma mantığı karmaşıklaşmaya başladığında, main.rs'den çıkarın ve lib.rs'e taşıyın.

Bu işlemlerden sonra main fonksiyonunda kalanlar aşağıdakilerle sınırlı olmalıdır:

  • Komut satırı ayrıştırma mantığını argüman değerleriyle çağırmak
  • Diğer yapılandırmaları ayarlama
  • lib.rs içinde bir run fonksiyonu çağırma
  • run bir hata döndürürse hatayı işleme

Bu model endişeleri ayırmakla ilgilidir: main.rs programı çalıştırır ve lib.rs eldeki görevin tüm mantığını ele alır. main fonksiyonunu doğrudan test edemeyeceğiniz için, bu yapı programınızın tüm mantığını lib.rs'deki fonksiyonlara taşıyarak test etmenizi sağlar. main.rs'de kalan kod, okunarak doğruluğu teyit edilebilecek kadar küçük olacaktır.

Bu süreci takip ederek programımızı yeniden düzenleyelim.

Argüman Ayrıştırıcısını Çıkarma

Komut satırı ayrıştırma mantığını src/lib.rs'ye taşımaya hazırlanmak için argümanları ayrıştırma işlevini main'in çağıracağı bir fonksiyona çıkaracağız. Liste 12-5, şimdilik src/main.rs içinde tanımlayacağımız yeni parse_config fonksiyonunu çağıran main'in yeni başlangıcını göstermektedir.

Dosya adı: src/main.rs

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let (query, filename) = parse_config(&args);

    // --snip--

    println!("Searching for {}", query);
    println!("In file {}", filename);

    let contents = fs::read_to_string(filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let filename = &args[2];

    (query, filename)
}

Liste 12-5: parse_config fonksiyonunun main'den çıkarılması

Komut satırı argümanlarını hala bir vektörde topluyoruz, ancak 1. indeksteki argüman değerini sorgu değişkenine ve 2. indeksteki argüman değerini main içindeki dosya adı değişkenine atamak yerine, tüm vektörü parse_config'e aktarıyoruz. parse_config daha sonra hangi argümanın hangi değişkene gideceğini belirleyen mantığı tutar ve değerleri main'e geri aktarır. query ve filename değişkenlerini hala main içinde oluşturuyoruz, ancak main artık komut satırı argümanlarının ve değişkenlerin nasıl karşılık geldiğini belirleme sorumluluğuna sahip değil.

Bu yeniden çalışma küçük programımız için aşırı gibi görünebilir, ancak küçük, artan adımlarla yeniden düzenliyoruz. Bu değişikliği yaptıktan sonra, argüman ayrıştırmanın hala çalıştığını doğrulamak için programı tekrar çalıştırın. İlerlemenizi sık sık kontrol etmek, ortaya çıktıklarında sorunların nedenini belirlemeye yardımcı olmak için iyidir.

Yapılandırma Değerlerini Gruplama

parse_config'i daha da geliştirmek için küçük bir adım daha atabiliriz. Şu anda bir tuple döndürüyoruz, ancak hemen ardından bu tuple'ı tekrar ayrı parçalara ayırıyoruz. Bu, belki de henüz doğru soyutlamaya sahip olmadığımızın bir işaretidir.

İyileştirme için yer olduğunu gösteren bir başka gösterge de parse_config'in config kısmıdır, bu da döndürdüğümüz iki değerin ilişkili olduğunu ve her ikisinin de bir yapılandırma değerinin parçası olduğunu ima eder. Şu anda bu anlamı, iki değeri bir tuple olarak gruplamak dışında verinin yapısında aktarmıyoruz; bunun yerine iki değeri struct içine koyacağız ve struct alanlarının her birine anlamlı bir isim vereceğiz. Bunu yapmak, bu kodun gelecekteki bakımcılarının farklı değerlerin birbirleriyle nasıl ilişkili olduğunu ve amaçlarının ne olduğunu anlamalarını kolaylaştıracaktır.

Liste 12-6, parse_config'de yapılan iyileştirmeleri göstermektedir.

Dosya adı: src/main.rs

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    // --snip--

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let filename = args[2].clone();

    Config { query, filename }
}

Liste 12-6: Config yapısının örneğini döndürmek için parse_config öğesini yeniden düzenleme

query ve filename adında alanlara sahip olacak şekilde tanımlanmış Config adında bir yapı ekledik. parse_config'in imzası artık Config değeri döndürdüğünü gösteriyor. Eskiden args içindeki String değerlerine referans veren dizgi dilimleri döndürdüğümüz parse_config gövdesinde, artık Config'i String değerlerini içerecek şekilde tanımlıyoruz. main içindeki args değişkeni argüman değerlerinin sahibidir ve yalnızca parse_config fonksiyonunun bunları ödünç almasına izin verir, bu da Config'in args içindeki değerlerin sahipliğini almaya çalışması durumunda Rust'ın ödünç alma kurallarını ihlal edeceğimiz anlamına gelir.

String verilerini yönetebileceğimiz birkaç yol vardır; biraz verimsiz olsa da en kolay yol, değerler üzerinde clone metodunu çağırmaktır. Bu, Config örneğinin sahip olması için verilerin tam bir kopyasını oluşturacaktır, bu da dizgi verilerine bir referans depolamaktan daha fazla zaman ve bellek gerektirir. Bununla birlikte, verileri klonlamak kodumuzu çok basit hale getirir çünkü referansların yaşam sürelerini yönetmek zorunda değiliz; bu durumda, basitlik kazanmak için biraz performanstan vazgeçmek değerli bir değiş tokuştur.

clone Kullanmanın Getirileri

Birçok Rustsever arasında, çalışma zamanı maliyeti nedeniyle sahiplik sorunlarını çözmek için clone kullanmaktan kaçınma eğilimi vardır. Bölüm 13'te, bu tür durumlarda nasıl daha verimli yöntemler kullanacağınızı öğreneceksiniz. Ancak şimdilik, ilerlemeye devam etmek için birkaç dizgiyi kopyalamanızda bir sakınca yok çünkü bu kopyaları yalnızca bir kez yapacaksınız ve filename ve query dizginiz çok küçük. İlk geçişinizde kodu aşırı optimize etmeye çalışmaktansa biraz verimsiz çalışan bir programa sahip olmak daha iyidir. Rust ile daha deneyimli hale geldikçe, en verimli çözümle başlamak daha kolay olacaktır, ancak şimdilik clone kullanmak tamamen kabul edilebilir.

main'i, parse_config tarafından döndürülen Config örneğini config adlı bir değişkene atayacak şekilde güncelledik ve daha önce ayrı query ve filename değişkenlerini kullanan kodu güncelledik, böylece artık bunun yerine Config yapısındaki alanları kullanıyor.

Artık kodumuz query ve filename'in ilişkili olduğunu ve amaçlarının programın nasıl çalışacağını yapılandırmak olduğunu daha açık bir şekilde ifade ediyor. Bu değerleri kullanan herhangi bir kod, bunları config örneğinde amaçlarına göre adlandırılmış alanlarda bulmayı bilir.

Config için Bir Yapıcı Oluşturma

Şimdiye kadar, komut satırı argümanlarını ayrıştırmaktan sorumlu mantığı main'den çıkardık ve parse_config fonksiyonuna yerleştirdik. Bunu yapmak, query ve filename değerlerinin ilişkili olduğunu ve bu ilişkinin kodumuzda aktarılması gerektiğini görmemize yardımcı oldu. Daha sonra query ve filename'in amaçlarını adlandırmak ve parse_config fonksiyonundan değerlerin adlarını struct alan adları olarak döndürebilmek için bir Config struct'ı ekledik.

Artık parse_config fonksiyonunun amacı bir Config örneği oluşturmak olduğuna göre, parse_config'i düz bir fonksiyondan Config yapısıyla ilişkili new adlı bir fonksiyona dönüştürebiliriz. Bu değişikliği yapmak kodu daha deyimsel hale getirecektir. Standart kütüphanedeki String gibi türlerin örneklerini String::new fonksiyonunu çağırarak oluşturabiliriz. Benzer şekilde, parse_config'i Config ile ilişkili yeni bir fonksiyona dönüştürerek, Config::new'i çağırarak Config'in örneklerini oluşturabileceğiz. Liste 12-7 yapmamız gereken değişiklikleri göstermektedir.

Dosya adı: src/main.rs

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);

    // --snip--
}

// --snip--

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let filename = args[2].clone();

        Config { query, filename }
    }
}

Liste 12-7: parse_config'i Config::new ile değiştirme

main'de parse_config çağrısı yaptığımız yeri Config::new çağrısı yapacak şekilde güncelledik. parse_config'in adını new olarak değiştirdik ve yeni fonksiyonu Config ile ilişkilendiren impl bloğunun içine taşıdık. Çalıştığından emin olmak için bu kodu tekrar derlemeyi deneyin.

Hata İşlemeyi Düzeltme

Şimdi hata işlememizi düzeltmeye çalışacağız. args vektöründeki değerlere indeks 1 veya indeks 2'den erişmeye çalışmanın, vektör üçten az öğe içeriyorsa programın paniklemesine neden olacağını hatırlayın. Programı herhangi bir argüman olmadan çalıştırmayı deneyin; şöyle görünecektir:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

index out of bounds: the len is 1 but the index is 1, programcılara yönelik detaylı bir hata mesajıdır. Son kullanıcılarımızın bunun yerine ne yapmaları gerektiğini anlamalarına yardımcı olmaz. Şimdi bunu düzeltelim.

Hata Mesajını İyileştirme

Liste 12-8'de, yeni fonksiyona 1. ve 2. dizine erişmeden önce dilimin yeterince uzun olduğunu doğrulayacak bir kontrol ekliyoruz. Dilim yeterince uzun değilse, program paniğe kapılır ve daha iyi bir hata mesajı görüntüler.

Dosya adı: src/main.rs

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    // --snip--
    fn new(args: &[String]) -> Config {
        if args.len() < 3 {
            panic!("not enough arguments");
        }
        // --snip--

        let query = args[1].clone();
        let filename = args[2].clone();

        Config { query, filename }
    }
}

Liste 12-8: Bağımsız değişken sayısı için bir kontrol ekleme

Bu kod, Liste 9-13'te yazdığımız Guess::new fonksiyonuna benzer; burada value bağımsız değişkeni geçerli değerler aralığının dışında kaldığında panic! yapar. Bir değer aralığını kontrol etmek yerine, args uzunluğunun en az 3 olduğunu kontrol ediyoruz ve fonksiyonun geri kalanı bu koşulun sağlandığı varsayımı altında çalışabilir. Eğer args üçten az öğeye sahipse, bu koşul doğru olur ve programı hemen sonlandırmak için panic! makrosunu çağırırız.

Bu ekstra birkaç satırlık yeni kodla, hatanın şimdi nasıl göründüğünü görmek için programı herhangi bir argüman olmadan tekrar çalıştıralım:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at 'not enough arguments', src/main.rs:26:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Bu çıktı daha iyi: artık makul bir hata mesajımız var. Ancak, kullanıcılarımıza vermek istemediğimiz gereksiz bilgilere de sahibiz. Belki de Liste 9-13'te kullandığımız tekniği burada kullanmak en iyisi değildir: Bölüm 9'da tartışıldığı gibi, panic! çağrısı bir kullanım probleminden ziyade bir programlama problemi için daha uygundur. Bunun yerine, Bölüm 9'da öğrendiğiniz diğer tekniği kullanacağız— başarı ya da hatayı gösteren Result'u döndürmek.

panic! Yerine new'den Result Döndürme

Bunun yerine, başarılı durumda bir Config örneği içeren ve hata durumunda sorunu açıklayan bir Result değeri döndürebiliriz. Config::new main ile iletişim kurarken, bir sorun olduğunu belirtmek için Result türünü kullanabiliriz.

Daha sonra main'i, panic!'in neden olduğu main iş parçacığı ve RUST_BACKTRACE ile ilgili çevreleyen metin olmadan kullanıcılarımız için Err varyantını daha pratik bir hataya dönüştürmek için değiştirebiliriz.

Liste 12-9, Config::new'in dönüş değerinde ve Result döndürmek için gereken fonksiyonun gövdesinde yapmamız gereken değişiklikleri göstermektedir. Bunun main'i de güncellemeden derlenmeyeceğini unutmayın, ki bunu bir sonraki listede yapacağız.

Dosya adı: src/main.rs

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

Liste 12-9: Config::new'den Result Döndürme

Yeni fonksiyonumuz artık başarı durumunda bir Config örneği ve hata durumunda &'static str içeren bir Result döndürmektedir. Hata değerlerimiz her zaman 'static ömüre sahip dizgi değişmezleri olacaktır.

Yeni fonksiyonun gövdesinde iki değişiklik yaptık: kullanıcı yeterli argüman iletmediğinde panic! çağrısı yapmak yerine, artık bir Err değeri döndürüyoruz ve Config dönüş değerini bir Ok içinde tuttuk. Bu değişiklikler fonksiyonun yeni tür imzasına uygun olmasını sağlar.

Config::new'den Err değeri döndürmek, main fonksiyonun yeni fonksiyondan dönen Result değerini işlemesini ve hata durumunda süreçten daha temiz bir şekilde çıkmasını sağlar.

Config::new Çağrısı ve Hataların İşlenmesi

Hata durumunu ele almak ve kullanıcı dostu bir mesaj yazdırmak için, Liste 12-10'da gösterildiği gibi, Config::new tarafından döndürülen Result'u ele almak üzere main'i güncellememiz gerekir. Ayrıca, komut satırı aracından sıfır olmayan bir hata koduyla çıkma sorumluluğunu panic!'ten alacağız ve elle sürekleyeceğiz. Sıfır olmayan bir çıkış durumu, programımızı çağıran sürece programın bir hata durumuyla çıktığını bildirmek için kullanılan bir kuraldır.

Dosya adı: src/main.rs

use std::env;
use std::fs;
use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

Liste 12-10: Yeni bir Config oluşturma başarısız olursa bir hata koduyla çıkmak

Bu listelemede, henüz ayrıntılı olarak ele almadığımız bir metod kullandık: standart kütüphane tarafından Result<T, E> üzerinde tanımlanan unwrap_or_else. unwrap_or_else'i kullanmak, panik yaratmayan bazı özel hata işleme yöntemleri tanımlamamızı sağlar. Eğer Result Ok değerinde ise, bu metodun davranışı unwrap'a benzer: Ok'un sarmaladığı iç değeri döndürür. Ancak, değer Err değeriyse, bu metod, tanımladığımız ve unwrap_or_else'ye argüman olarak aktardığımız anonim bir fonksiyon olan kapanış ifadesindeki kodu çağırır. Kapanış ifadelerini Bölüm 13'te daha ayrıntılı olarak ele alacağız. Şimdilik, unwrap_or_else'nin Err'nin iç değerini, yani bu durumda Liste 12-9'da eklediğimiz "not enough arguments" statik dizgisini, dikey aktarmalar arasında görünen err argümanı içinde kapanış ifadesini aktaracağını bilmeniz yeterlidir. Daha sonra kapanış ifadesi içindeki kod çalıştığında err değerini kullanabilir.

Standart kütüphaneden process'i kapsam içine almak için yeni bir use satırı ekledik. Hata durumunda çalıştırılacak kapanış ifadesi içindeki kod yalnızca iki satırdır: err değerini yazdırırız ve ardından process::exit'i çağırırız. process::exit fonksiyonu programı hemen durduracak ve çıkış durum kodu olarak aktarılan sayıyı döndürecektir. Bu, Liste 12-8'de kullandığımız panic!-tabanlı işleme benzer, ancak artık tüm ekstra çıktıları almıyoruz. Hadi deneyelim:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments

Harika! Bu çıktı, kullanıcılarımız için çok daha güzel.

main'den Mantığı Çıkarma

Yapılandırma ayrıştırmasını yeniden düzenlemeyi bitirdiğimize göre, şimdi programın mantığına dönelim.

“İkili Projeler için İşlerin Ayrılması” bölümünde belirttiğimiz gibi, yapılandırmayı ayarlamak veya hataları ele almakla ilgili olmayan main'de şu anda bulunan tüm mantığı tutacak run adlı bir fonksiyon çıkaracağız. İşimiz bittiğinde, main kısa ve öz olacak ve inceleme yoluyla doğrulanması kolay olacak ve diğer tüm mantık için testler yazabileceğiz.

Liste 12-11 ayıklanmış çalıştırma fonksiyonunu göstermektedir. Şimdilik sadece fonksiyonu ayıklayarak küçük ve aşamalı bir iyileştirme yapıyoruz. Fonksiyonu src/main.rs içinde tanımlıyoruz.

Dosya adı: src/main.rs

use std::env;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

// --snip--

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

Liste 12-11: Program mantığının geri kalanını içeren run'ın çıkarılması

run fonksiyonu artık dosyanın okunmasından başlayarak main'den kalan tüm mantığı içerir. run fonksiyonu Config örneğini bir argüman olarak alır.

run Fonksiyonundan Hata Döndürme

Kalan program mantığının run fonksiyonuna ayrılmasıyla, Liste 12-9'da Config::new ile yaptığımız gibi hata işlemeyi geliştirebiliriz. Bir şeyler ters gittiğinde programın expect'i çağırarak paniklemesine izin vermek yerine, run fonksiyonu Result<T, E> döndürecektir. Bu, hataları ele alma mantığını main'de kullanıcı dostu bir şekilde daha da birleştirmemizi sağlayacaktır. Liste 12-12, run'ın imzasında ve gövdesinde yapmamız gereken değişiklikleri göstermektedir.

Dosya adı: src/main.rs

use std::env;
use std::fs;
use std::process;
use std::error::Error;

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    run(config);
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    println!("With text:\n{}", contents);

    Ok(())
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

Liste 12-12: Result döndürmek için run'ı değiştirme

Burada üç önemli değişiklik yaptık. İlk olarak, run fonksiyonunun dönüş tipini Result<(), Box<dyn Error>> olarak değiştirdik. Bu fonksiyon daha önce birim tipi olan ()'i döndürüyordu ve bunu Ok durumunda döndürülen değer olarak tutuyoruz.

Hata türü için Box<dyn Error> tanım nesnesini kullandık (ve std::error::Error'ı en üstte bir use deyimiyle kapsama aldık). Tanım nesnelerini Bölüm 17'de ele alacağız. Şimdilik, Box<dyn Error>'un fonksiyonun Error tanımını sürekleyen tür döndüreceği anlamına geldiğini bilin, ancak dönüş değerinin hangi tür olacağını belirtmek zorunda değiliz. Bu bize farklı hata durumlarında farklı türlerde olabilecek hata değerleri döndürme esnekliği sağlar. dyn anahtar sözcüğü “dinamikin” kısaltmasıdır.

İkinci olarak, Bölüm 9'da bahsettiğimiz gibi ? operatörü için expect çağrısını kaldırdık. Bir hatada panic! yerine, ? işleci çağıranın işlemesi için geçerli fonksiyondan hata değerini döndürecektir.

Üçüncü olarak, run fonksiyonu artık başarı durumunda bir Ok değeri döndürmektedir. İmzada run fonksiyonunun başarı tipini () olarak bildirdik, bu da birim tür değerini Ok değerine sarmamız gerektiği anlamına geliyor. Bu Ok(()) söz dizimi ilk başta biraz garip görünebilir, ancak ()'yi bu şekilde kullanmak, run'ı yalnızca yan etkileri için çağırdığımızı belirtmenin deyimsel yoludur; ihtiyacımız olan bir değer döndürmez.

Bu kodu çalıştırdığınızda, derlenecek ancak bir uyarı görüntülenecektir:

$ cargo run the poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
  --> src/main.rs:19:5
   |
19 |     run(config);
   |     ^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

warning: `minigrep` (bin "minigrep") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.71s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

Rust bize kodumuzun Result değerini göz ardı ettiğini ve Result değerinin bir hata oluştuğunu gösterebileceğini söyler. Ancak bir hata olup olmadığını kontrol etmiyoruz ve derleyici bize muhtemelen burada bazı hata işleme kodlarına sahip olmamız gerektiğini hatırlatıyor! Şimdi bu sorunu düzeltelim.

main'de Çalıştırmadan Dönen Hataları İşleme

Hataları kontrol edeceğiz ve bunları Liste 12-10'da Config::new ile kullandığımıza benzer bir teknik kullanarak ele alacağız, ancak küçük bir farkla:

Dosya adı: src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    if let Err(e) = run(config) {
        println!("Application error: {}", e);

        process::exit(1);
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    println!("With text:\n{}", contents);

    Ok(())
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

run'ın bir Err değeri döndürüp döndürmediğini kontrol etmek ve döndürürse process::exit(1)'i çağırmak için unwrap_or_else yerine if let kullanırız. run fonksiyonu, Config::new'in Config örneğini döndürdüğü gibi unwrap etmek istediğimiz bir değer döndürmez. run başarı durumunda () değerini döndürdüğü için, yalnızca bir hatayı tespit etmekle ilgileniyoruz, bu nedenle unwrap_or_else'in yalnızca () değerini döndürmesine gerek yok.

if let ve unwrap_or_else fonksiyonlarının gövdeleri her iki durumda da aynıdır: hatayı yazdırır ve çıkarız.

Kodu Kütüphane Kasasına Bölme

minigrep projemiz şu ana kadar iyi görünüyor! Şimdi src/main.rs dosyasını böleceğiz ve src/lib.rs dosyasına bazı kodlar koyacağız. Bu şekilde kodu test edebilir ve daha az sorumluluğu olan bir src/main.rs dosyasına sahip olabiliriz.

main fonksiyonu olmayan tüm kodları src/main.rs dosyasından src/lib.rs dosyasına taşıyalım:

  • run fonksiyonu tanımı
  • İlişkili use deyimleri
  • Config'in tanımı
  • Config::new fonksiyon tanımı

src/lib.rs dosyasının içeriği Liste 12-13'te gösterilen imzalara sahip olmalıdır (kısa olması için fonksiyonların gövdelerini atladık). Liste 12-14'te src/main.rs'yi değiştirene kadar bunun derlenmeyeceğini unutmayın.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        // --snip--
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    // --snip--
    let contents = fs::read_to_string(config.filename)?;

    println!("With text:\n{}", contents);

    Ok(())
}

Liste 12-13: Config ve runsrc/lib.rs'e taşıma

pub anahtar sözcüğünü bolca kullandık: Config üzerinde, Config'in alanları üzerinde, new metodu üzerinde ve run fonksiyonu üzerinde. Artık test edebileceğimiz herkese açık bir API'ye sahip bir kütüphane kasamız var!

Şimdi src/lib.rs dosyasına taşıdığımız kodu, Liste 12-14'te gösterildiği gibi src/main.rs dosyasındaki ikili kasanın kapsamına almamız gerekiyor.

Dosya adı: src/main.rs

use std::env;
use std::process;

use minigrep::Config;

fn main() {
    // --snip--
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    if let Err(e) = minigrep::run(config) {
        // --snip--
        println!("Application error: {}", e);

        process::exit(1);
    }
}

Liste 12-14: src/main.rs içinde minigrep kütüphanesini kullanma

Config türünü kütüphane kasasından ikili kasanın kapsamına getirmek için use minigrep::Config satırı ekliyoruz ve run fonksiyonunun önüne kasa adımızı ekliyoruz. Artık tüm fonksiyonlar birbirine bağlı olmalı ve çalışmalıdır. Programı cargo run ile çalıştırın ve her şeyin doğru çalıştığından emin olun.

Vay be! Sanki bu seni ve beni çok yormuş gibi duruyor, ancak gelecekte başarılı olmak için bunları yapmalıydık. Artık hataları ele almak çok daha kolay ve kodu daha modüler hale getirdik. Bundan sonraki neredeyse tüm işlerimiz src/lib.rs içinde yapılacak.

Eski kodla zor olan ancak yeni kodla kolay olan bir şeyi yaparak bu yeni modülerlikten yararlanalım: bazı testler yazacağız!

Test Odaklı Geliştirme ile Kütüphanenin İşlevselliğini Geliştirme

Artık kök mantığı src/lib.rs'ye çıkardığımıza ve argüman toplama ve hata işlemeyi src/main.rs'de bıraktığımıza göre, kodumuzun temel işlevselliği için test yazmak çok daha kolay. Fonksiyonları çeşitli argümanlarla doğrudan çağırabilir ve ikili dosyamızı komut satırından çağırmak zorunda kalmadan dönüş değerlerini kontrol edebiliriz.

Bu bölümde, aşağıdaki adımlarla test odaklı geliştirme (TDD) sürecini kullanarak minigrep programına arama mantığını ekleyeceğiz:

  1. Başarısız olan bir test yazın ve beklediğiniz nedenden dolayı başarısız olduğundan emin olmak için çalıştırın.
  2. Yeni testin geçmesi için yeterli kodu yazın veya değiştirin.
  3. Yeni eklediğiniz veya değiştirdiğiniz kodu yeniden düzenleyin ve testlerin geçmeye devam ettiğinden emin olun.
  4. Adım 1'den itibaren tekrarlayın!

TDD, yazılım yazmanın birçok yolundan yalnızca biri olsa da kod tasarımını yönlendirmeye yardımcı olabilir. Testin geçmesini sağlayan kodu yazmadan önce testi yazmak, süreç boyunca yüksek test kapsamının korunmasına yardımcı olur.

Dosya içeriğindeki sorgu dizgisini gerçekten arayacak ve sorguyla eşleşen satırların bir listesini üretecek işlevselliğin uygulanmasını test edeceğiz. Bu işlevselliği search adlı bir fonksiyona ekleyeceğiz.

Başarısız Bir Test Yazma

Artık onlara ihtiyacımız olmadığından, programın davranışını kontrol etmek için kullandığımız println! ifade yapılarını src/lib.rs ve src/main.rs dosyalarından kaldıralım. Daha sonra, src/lib.rs dosyasına, Bölüm 11'de yaptığımız gibi bir test fonksiyonu içeren bir tests modülü ekleyelim. Test fonksiyonu, search fonksiyonunun sahip olmasını istediğimiz davranışı belirtir: bir sorgu ve aranacak metni alır ve metinden yalnızca sorguyu içeren satırları döndürür. Liste 12-15, henüz derlenmeyecek olan bu testi göstermektedir.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Liste 12-15: Keşke yapsaydık dediğimiz search fonksiyonu için başarısız bir test oluşturma

Bu test "duct" dizgisini arar. Aradığımız metin, yalnızca biri "duct" içeren üç satırdır (Açılıştaki çift tırnak işaretinden sonraki ters eğik çizginin Rust'a bu dize değişmezinin içeriğinin başına yeni satır karakteri koymamasını söylediğine dikkat edin). Arama fonksiyonundan dönen değerin sadece beklediğimiz satırı içerdiğini iddia ediyoruz.

Henüz bu testi çalıştırıp başarısız olmasını izleyemiyoruz çünkü test derlenmiyor bile: arama fonksiyonu henüz mevcut değil! TDD ilkelerine uygun olarak, Liste 12-16'da gösterildiği gibi her zaman boş bir vektör döndüren bir arama fonksiyonu tanımı ekleyerek testin derlenmesini ve çalışmasını sağlayacak kadar kod ekleyeceğiz. Ardından test derlenmeli ve başarısız olmalıdır çünkü boş bir vektör "safe, fast, productive" satırını içeren bir vektörle eşleşmez.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    vec![]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Liste 12-16: Testimizin derlenebilmesi için search fonksiyonunun değiştirilmesi

search'ın imzasında açık bir 'a yaşam süresi tanımlamamız ve bu yaşam süresini contents argümanı ve dönüş değeri ile kullanmamız gerektiğine dikkat edin. Bölüm 10'da yaşam süresi parametrelerinin hangi argüman yaşam süresinin geri dönüş değerinin yaşam süresine bağlı olduğunu belirttiğini hatırlayın. Bu durumda, döndürülen vektörün (argüman sorgusu yerine) argüman contents'in dilimlerine referans veren dizgi dilimleri içermesi gerektiğini belirtiriz.

Başka bir deyişle, Rust'a search fonksiyonu tarafından döndürülen verilerin, contents argümanında search fonksiyonuna aktarılan veriler kadar uzun yaşayacağını söylüyoruz. Bu çok önemlidir! Referansın geçerli olabilmesi için bir dilim tarafından referans verilen verinin geçerli olması gerekir; derleyici içerik yerine sorgunun dize dilimlerini oluşturduğumuzu varsayarsa, güvenlik kontrolünü yanlış yapacaktır.

Eğer yaşam süresi ek açıklamalarını unutur ve bu fonksiyonu derlemeye çalışırsak, bu hatayı alırız:

$ cargo build
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
  --> src/lib.rs:28:51
   |
28 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
   |                      ----            ----         ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
   |
28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
   |              ++++         ++                 ++              ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` due to previous error

Rust bu iki argümandan hangisine ihtiyacımız olduğunu bilemez, bu yüzden bunu ona açıkça söylememiz gerekir. contents tüm metnimizi içeren argüman olduğundan ve bu metnin eşleşen kısımlarını döndürmek istediğimizden, contents'in lifetime söz dizimini kullanarak dönüş değerine bağlanması gereken argüman olduğunu biliyoruz.

Diğer programlama dilleri, imzada argümanları dönüş değerlerine bağlamanızı gerektirmez, ancak bu uygulama zamanla daha kolay hale gelecektir. Bu örneği Bölüm 10'daki “Referansları Yaşam Süreleri ile Doğrulama” bölümü ile karşılaştırmak isteyebilirsiniz.

Şimdi testi çalıştıralım:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 0.97s
     Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... FAILED

failures:

---- tests::one_result stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `["safe, fast, productive."]`,
 right: `[]`', src/lib.rs:44:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::one_result

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

Harika, test tam da beklediğimiz gibi başarısız oldu. Hadi testi geçelim!

Testi Geçirmek İçin Kod Yazma

Şu anda, her zaman boş bir vektör döndürdüğümüz için testimiz başarısız oluyor. Bunu düzeltmek ve search'ü uygulamak için programımızın aşağıdaki adımları izlemesi gerekir:

  • İçeriğin her satırını yineleyin.
  • Satırın sorgu dizemizi içerip içermediğini kontrol edin.
  • Eğer içeriyorsa, döndürdüğümüz değerler listesine ekleyin.
  • Eğer içermiyorsa, hiçbir şey yapmayın.
  • Eşleşen sonuçların listesini döndürün.

Satırlar arasında yineleme ile başlayarak her adımda çalışalım.

lines Metodu ile Satırlar Arasında Yineleme

Rust, dizgilerin satır satır yinelenmesini işlemek için uygun bir şekilde satır olarak adlandırılan ve Liste 12-17'de gösterildiği gibi çalışan yararlı bir metoda sahiptir. Bunun henüz derlenmeyeceğini unutmayın.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        // do something with line
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Liste 12-17: contents'teki her satırı yineleme

lines metodu bir yineleyici döndürür. Yineleyiciler hakkında Bölüm 13'te derinlemesine konuşacağız, ancak bir yineleyici kullanmanın bu yolunu, bir koleksiyondaki her bir öğe üzerinde bazı kodlar çalıştırmak için bir yineleyici ile bir for döngüsü kullandığımız Liste 3-5'te gördüğünüzü hatırlayın.

Sorgu için Her Satırı Arama

Ardından, geçerli satırın sorgu dizemizi içerip içermediğini kontrol edeceğiz. Neyse ki, String bunu bizim için yapan contains adında yararlı bir metoda sahiptir! Liste 12-18'de gösterildiği gibi, search fonksiyonuna contains metoduna bir çağrı ekleyin. Bunun henüz derlenmeyeceğini unutmayın.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        if line.contains(query) {
            // do something with line
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Liste 12-18: Satırın query'deki dizgiyi içerip içermediğini görmek için yeni bir işlevsellik ekleme

Şu anda işlevsellik geliştiriyoruz. Derlemek için, fonksiyon imzasında belirttiğimiz gibi gövdeden bir değer döndürmemiz gerekiyor.

Eşleşen Satırları Depolama

Bu fonksiyonu tamamlamak için, döndürmek istediğimiz eşleşen satırları saklamanın bir yoluna ihtiyacımız var. Bunun için, for döngüsünden önce değiştirilebilir bir vektör oluşturabilir ve bir satırı vektörde saklamak için push metodunu çağırabiliriz. for döngüsünden sonra, Liste 12-19'da gösterildiği gibi vektörü döndürürüz.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Liste 12-19: Eşleşen satırları döndürebilmek için depolamak

Şimdi search fonksiyonu yalnızca query'i içeren satırları döndürmeli ve testimiz geçmelidir. Testi çalıştıralım:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 1.22s
     Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Testimiz geçti, bu yüzden işe yaradığını biliyoruz!

Bu noktada, aynı işlevselliği korumak için testlerin geçmesini sağlarken search fonksiyonunun süreklemesini yeniden düzenleme fırsatlarını değerlendirebiliriz. search fonksiyonundaki kod çok da kötü değil, ancak yineleyicilerin bazı yararlı özelliklerinden yararlanmıyor o kadar. Yineleyicileri ayrıntılı olarak inceleyeceğimiz Bölüm 13'te bu örneğe geri döneceğiz ve nasıl geliştirebileceğimize bakacağız.

search Fonksiyonunu run Fonksiyonunda Kullanma

Artık search fonksiyonu çalıştığına ve test edildiğine göre, run fonksiyonumuzdan search'ü çağırmamız gerekiyor. config.query değerini ve run'ın dosyadan okuduğu içeriği search fonksiyonuna aktarmamız gerekiyor. Ardından run, search çağrısından dönen her satırı yazdıracaktır:

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    for line in search(&config.query, &contents) {
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Her satırı search'ten döndürmek ve yazdırmak için hala bir for döngüsü kullanıyoruz.

Artık programımız çalışmalıdır! İlk olarak Emily Dickinson şiirinden tam olarak bir dizgi döndürmesi gereken bir sözcük ile deneyelim, “frog”:

$ cargo run frog poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/minigrep frog poem.txt`
How public, like a frog

Harika! Şimdi “body” gibi birden fazla satırla eşleşecek bir sözcüğü deneyelim:

$ cargo run body poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!

Ve son olarak, “monomorphization” gibi şiirin hiçbir yerinde olmayan bir sözcüğü aradığımızda herhangi bir satır almadığımızdan emin olalım:

$ cargo run monomorphization poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep monomorphization poem.txt`

Mükemmel! Klasik aracın kendi mini versiyonumuzu oluşturduk ve uygulamaların nasıl yapılandırılacağı hakkında çok şey öğrendik. Ayrıca dosya girişi ve çıkışı, yaşam süreleri, test etme ve komut satırı ayrıştırma hakkında da biraz bilgi edindik.

Bu projeyi tamamlamak için, her ikisi de komut satırı programları yazarken yararlı olan ortam değişkenleriyle nasıl çalışılacağını ve standart hataya nasıl yazdırılacağını kısaca göstereceğiz.

Ortam Değişkenleriyle Çalışmak

Ekstra bir özellik ekleyerek minigrep'i geliştireceğiz: kullanıcının bir ortam değişkeni aracılığıyla açabileceği büyük/küçük harfe duyarlı olmayan arama seçeneği. Bu özelliği bir komut satırı seçeneği haline getirebilir ve kullanıcıların her uygulamak istediklerinde girmelerini isteyebilirdik, ancak bunun yerine bir ortam değişkeni yaparak, kullanıcılarımızın ortam değişkenini bir kez ayarlamalarına ve o terminal oturumunda tüm aramalarının büyük/küçük harfe duyarsız olmasına izin veriyoruz.

Büyük/Küçük Harfe Duyarsız search Fonksiyonu için Başarısız Testi Yazma

İlk olarak, ortam değişkeni bir değere sahip olduğunda çağrılacak yeni bir search_case_insensitive fonksiyonu ekliyoruz. TDD sürecini takip etmeye devam edeceğiz, bu nedenle ilk adım yine başarısız bir test yazmaktır. Yeni search_case_insensitive fonksiyonu için yeni bir test ekleyeceğiz ve Liste 12-20'de gösterildiği gibi iki test arasındaki farkları netleştirmek için eski testimizin adını one_result'tan case_sensitive'e değiştireceğiz.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    for line in search(&config.query, &contents) {
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Liste 12-20: Eklemek üzere olduğumuz büyük/küçük harfe duyarlı olmayan fonksiyon için yeni bir başarısız test ekleme

Eski testin contents'ini de düzenlediğimizi unutmayın. Büyük/küçük harfe duyarlı bir şekilde arama yaparken "duct" sorgusuyla eşleşmemesi gereken büyük D harfini kullanarak "Duct tape." metnini içeren yeni bir satır ekledik. Eski testi bu şekilde değiştirmek, halihazırda uyguladığımız büyük/küçük harfe duyarlı arama işlevini yanlışlıkla bozmamamızı sağlamaya yardımcı olur. Bu test şimdi geçmeli ve biz büyük/küçük harfe duyarsız arama üzerinde çalışırken sorunsuzca geçmeye devam etmelidir.

Büyük/küçük harfe duyarlı olmayan search için yeni test, sorgu olarak "rUsT" kullanır. Eklemek üzere olduğumuz search_case_insensitive fonksiyonunda, "rUsT" sorgusu, büyük R ile "Rust:" içeren satırla eşleşmeli ve her ikisi de sorgudan farklı harflere sahip olsa bile "Trust me." satırıyla eşleşmelidir. Bu bizim başarısız testimizdir ve henüz search_case_insensitive fonksiyonunu tanımlamadığımız için derlenemeyecektir. Testin derlenip başarısız olduğunu görmek için Liste 12-16'daki search fonksiyonu için yaptığımıza benzer şekilde, her zaman boş bir vektör döndüren bir sürekleme eklemekten çekinmeyin.

search_case_insensitive Fonksiyonunun Süreklenmesi

Liste 12-21'de gösterilen search_case_insensitive fonksiyonu, search fonksiyonuyla neredeyse aynı olacaktır. Tek fark, sorguyu ve her satırı küçük harfle yazacağız, böylece girdi argümanlarının büyük/küçük harf durumu ne olursa olsun, satırın sorguyu içerip içermediğini kontrol ettiğimizde aynı büyük/küçük harf durumunda olacaklar.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    for line in search(&config.query, &contents) {
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Liste 12-21: Sorguyu ve satırı karşılaştırmadan önce küçük harfle yazmak için search_case_insensitive fonksiyonunu tanımlama

İlk olarak, sorgu dizesini küçük harfle yazarız ve aynı ada sahip gölgeli bir değişkende saklarız. Sorgu üzerinde to_lowercase çağrısı yapmak gereklidir, böylece kullanıcının sorgusu "rust", "RUST", "Rust" veya "rUsT" olsun, sorguyu "rust" olarak ele alacağız ve fonksiyona büyük/küçük harfe duyarsız şekilde yönlendireceğiz. to_lowercase temel Unicode'u işleyecek olsa da, tüm durumlarda %100 doğru çalışmayacaktır. Gerçek bir uygulama yazıyor olsaydık, burada biraz daha fazla durumu işlemek isterdik, ancak bu bölüm Unicode değil, ortam değişkenleri ile ilgilidir, bu yüzden burada bırakacağız.

Sorgunun artık bir dizgi dilimi yerine bir String olduğuna dikkat edin, çünkü to_lowercase çağrısı mevcut verilere başvurmak yerine yeni veriler oluşturur. Örnek olarak, sorgunun "rUsT" olduğunu varsayalım: bu dizgi diliminde kullanabileceğimiz küçük harfli bir u veya t bulunmadığından, "rust" içeren yeni bir String tahsis etmemiz gerekir. Şimdi sorguyu contains metoduna argüman olarak ilettiğimizde, contains'ın imzası bir dizgi dilimi alacak şekilde tanımlandığı için bir ve işareti eklememiz gerekir.

Ardından, tüm karakterleri küçük harfle yazmak için her satıra to_lowercase çağrısı ekliyoruz. Artık satırı ve sorguyu küçük harfe dönüştürdüğümüze göre, sorgunun büyük/küçük harf durumu ne olursa olsun match yapısını kullanarak bulacağız.

Bakalım bu sürekleme testleri geçebilecek mi?

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 1.33s
     Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Harika! Geçti. Şimdi, run fonksiyonundan yeni search_case_insensitive fonksiyonunu çağıralım. İlk olarak, büyük/küçük harfe duyarlı ve duyarsız arama arasında geçiş yapmak için Config yapısına bir yapılandırma seçeneği ekleyeceğiz. Bu alanı eklemek derleyici hatalarına neden olacaktır çünkü bu alanı henüz hiçbir yerde tanımlamıyoruz:

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Bir Boole tutan ignore_case alanını ekledik. Daha sonra, ignore_case alanının değerini kontrol etmek ve bunu Liste 12-22'de gösterildiği gibi search fonksiyonunu mu yoksa search_case_insensitive fonksiyonunu mu çağıracağımıza karar vermek için kullanacak run fonksiyonuna ihtiyacımız olacak. Bu hala derlenmeyecektir.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Liste 12-22: config.ignore_case içindeki değere bağlı olarak search ya da search_case_insensitive çağrısı

Son olarak, ortam değişkenini kontrol etmemiz gerekiyor. Ortam değişkenleriyle çalışma fonksiyonları standart kütüphanedeki env modülündedir, bu yüzden bu modülü src/lib.rs dosyasının üst kısmına getiriyoruz. Ardından, Liste 12-23'te gösterildiği gibi, IGNORE_CASE adlı bir ortam değişkeni için herhangi bir değer ayarlanıp ayarlanmadığını kontrol etmek için env modülündeki var fonksiyonunu kullanacağız.

Dosya adı: src/lib.rs

use std::env;
// --snip--

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            filename,
            ignore_case,
        })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Liste 12-23: IGNORE_CASE` adlı bir ortam değişkeninde herhangi bir değer olup olmadığını kontrol etmek

Burada, yeni bir ignore_case değişkeni oluşturuyoruz. Değeri ayarlamak için env::var fonksiyonunu çağırıyoruz ve ona IGNORE_CASE ortam değişkeninin adını aktarıyoruz. env::var fonksiyonu, ortam değişkeni herhangi bir değere ayarlanmışsa, ortam değişkeninin değerini içeren başarılı Ok değişkeni olacak bir Result döndürür. Ortam değişkeni ayarlanmamışsa Err değişkenini döndürür.

Ortam değişkeninin ayarlanıp ayarlanmadığını kontrol etmek için Result üzerinde is_ok metodunu kullanıyoruz, bu da programın büyük/küçük harfe duyarlı olmayan bir arama yapması gerektiği anlamına geliyor. IGNORE_CASE ortam değişkeni herhangi bir şeye ayarlanmamışsa, is_ok yöntemi false değerini döndürür ve program büyük/küçük harfe duyarlı bir arama gerçekleştirir. Ortam değişkeninin değeriyle henüz ilgilenmiyoruz, sadece ayarlı ya da ayarsız olmasıyla ilgileniyoruz, bu yüzden unwrap, expect ya da Result'ta gördüğümüz diğer metodlardan birini kullanmak yerine is_ok'u kontrol ediyoruz.

ignore_case değişkenindeki değeri Config örneğine aktarırız, böylece run fonksiyonu bu değeri okuyabilir ve Liste 12-22'de yaptığımız gibi search_case_insensitive veya search çağrısı oluşturup oluşturmayacağına karar verebilir.

Hadi deneyelim! İlk olarak, programımızı ortam değişkeni ayarlanmadan; to sorgusuyla çalıştıracağız; bu sorgu, to sözcüğünü tümüyle küçük harfli şekilde içeren herhangi bir satırla eşleşmelidir:

$ cargo run to poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!

Görünüşe göre hala çalışıyor! Şimdi, programı IGNORE_CASE 1 olarak ayarlanmış şekilde, aynı sorgu ile çalıştıralım.

$ IGNORE_CASE=1 cargo run to poem.txt

PowerShell kullanıyorsanız, ortam değişkenini ayarlamanız ve programı ayrı komutlar olarak çalıştırmanız gerekecektir:

PS> $Env:IGNORE_CASE=1; cargo run to poem.txt

Bu, kabuk oturumunuzun geri kalanında IGNORE_CASE'in kalıcı olmasını sağlayacaktır. Remove-Item cmdlet'i ile bu ayar kaldırılabilir:

PS> Remove-Item Env:IGNORE_CASE

“to” içeren, büyük harfli olabilecek satırlar görmeliyiz:

Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

Muhteşem, “To” içeren satırlarımızı da görüyoruz! minigrep programımız artık bir ortam değişkeni tarafından kontrol edilen büyük/küçük harfe duyarlı olmayan arama yapabiliyor. Artık komut satırı argümanları ya da ortam değişkenleri kullanılarak ayarlanan seçenekleri nasıl yöneteceğinizi biliyorsunuz.

Bazı programlar aynı yapılandırma için argümanlara ve ortam değişkenlerine izin verir. Bu durumlarda, programlar birinin ya da diğerinin öncelikli olduğuna karar verir. Kendi başınıza başka bir alıştırma yapmak için, bir komut satırı argümanı veya bir ortam değişkeni aracılığıyla büyük/küçük harf duyarlılığını kontrol etmeyi deneyin. Program biri büyük/küçük harfe duyarlı diğeri büyük/küçük harfi yok sayacak şekilde ayarlanmış olarak çalıştırılırsa komut satırı argümanının mı yoksa ortam değişkeninin mi öncelikli olacağına karar verin.

std::env modülü, ortam değişkenleriyle başa çıkmak için daha birçok yararlı özellik içerir: nelerin mevcut olduğunu görmek için ilgili dokümantasyonuna göz atın.

Standart Çıktı Yerine Standart Hataya Hata Mesajları Yazma

Şu anda çıktımızın tamamını println! makrosunu kullanarak uçbirime yazıyoruz. Çoğu uçbirimde iki tür çıktı vardır: genel bilgiler için standart çıktı (stdout) ve hata mesajları için standart hata (stderr). Bu ayrım, kullanıcıların bir programın başarılı çıktısını bir dosyaya yönlendirmeyi, ancak yine de ekrana hata mesajlarını yazdırmayı seçmelerini sağlar.

prıntln! makrosu yalnızca standart çıktıya yazdırabilir, bu nedenle standart hataya yazdırmak için başka bir şey kullanmamız gerekir.

Hataların Nerede Yazıldığını Kontrol Etme

Öncelikle minigrep tarafından yazdırılan içeriğin şu anda standart çıktıya nasıl yazıldığını gözlemleyelim, bunun yerine standart hataya yazmak istediğimiz herhangi bir hata mesajı dahil. Bunu, kasıtlı olarak bir hataya neden olurken standart çıktı akışını bir dosyaya yeniden yönlendirerek yapacağız. Standart hata akışını yeniden yönlendirmeyeceğiz, bu nedenle standart hataya gönderilen herhangi bir içerik ekranda görüntülenmeye devam edecektir.

Komut satırı programlarının standart hata akışına hata mesajları göndermesi beklenir, böylece standart çıktı akışını bir dosyaya yönlendirsek bile ekranda hata mesajlarını görebiliriz.

Bu davranışı göstermek için programı > ve standart çıktı akışını yönlendirmek istediğimiz output.txt dosya adıyla çalıştıracağız. Hataya neden olacak herhangi bir argüman iletmiyoruz:

$ cargo run > output.txt

> söz dizimi, kabuğa standart çıktının içeriğini ekrana değil de output.txt dosyasına yazmasını söyler. Ekrana yazdırılmasını beklediğimiz hata mesajını görmedik, bu da dosyada bittiği anlamına geliyor. output.txt şunları içerir:

Problem parsing arguments: not enough arguments

Evet, hata mesajımız standart çıktıya yazdırılıyor. Bunun gibi hata mesajlarının standart hataya yazdırılması çok daha kullanışlıdır, bu nedenle dosyada yalnızca başarılı bir çalıştırmadan elde edilen veriler biter. Bunu değiştireceğiz.

Hataları Standart Hataya Yazdırma

Hata mesajlarının nasıl yazdırılacağını değiştirmek için Liste 12-24'teki kodu kullanacağız. Bu bölümde daha önce yaptığımız yeniden düzenleme nedeniyle, hata mesajlarını yazdıran tüm kodlar tek bir fonksiyondadır. Standart kütüphane, standart hata akışına yazdıran eprintln! makrosunu sağlar, bu yüzden hataları yazdırmak için println! dediğimiz iki yeri değiştirelim ve bunun yerine eprintln! kullanalım.

Dosya adı: src/main.rs

use std::env;
use std::process;

use minigrep::Config;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    if let Err(e) = minigrep::run(config) {
        eprintln!("Application error: {}", e);

        process::exit(1);
    }
}

Liste 12-24: eprintln! kullanarak standart çıktı yerine hata mesajlarını standart hataya yazma

Şimdi programı aynı şekilde, herhangi bir argüman olmadan ve standart çıktıyı > ile yeniden yönlendirerek tekrar çalıştıralım:

$ cargo run > output.txt
Problem parsing arguments: not enough arguments

Şimdi ekranda hatayı görüyoruz ve output.txt hiçbir şey içermiyor, bu da komut satırı programlarından beklediğimiz davranış.

Programı, hataya neden olmayan ancak yine de standart çıktıyı bir dosyaya yönlendiren argümanlarla tekrar çalıştıralım, şöyle:

$ cargo run to poem.txt > output.txt

Uçbirimde herhangi bir çıktı görmeyeceğiz ve output.txt sonuçlarımızı içerecektir:

Dosya adı: output.txt

Are you nobody, too?
How dreary to be somebody!

Bu, başarılı çıktı için standart çıktıyı ve uygun olduğu şekilde hata çıktısı için standart hatayı kullandığımızı gösterir.

Özet

Bu bölüm, şimdiye kadar öğrendiğiniz bazı temel kavramları özetledi ve Rust'ta genel *G/Ç/ işlemlerinin nasıl gerçekleştirileceğini ele aldı. Komut satırı bağımsız değişkenlerini, dosyaları, ortam değişkenlerini ve eprintln! makrosunu kullanmaya ve komut satırı uygulamaları yazmaya hazırsınız. Önceki bölümlerdeki kavramlarla birleştiğinde, kodunuz iyi organize edilecek, verileri uygun veri yapılarında etkin bir şekilde depolayacak, hataları güzel bir şekilde ele alacak ve iyi test edilecektir.

Ardından, fonksiyonel dillerden etkilenen bazı Rust özelliklerini keşfedeceğiz: kapanış ifadeleri ve yineleyiciler.

Fonksiyonel Dil Özellikleri: Yineleyiciler ve Kapanış İfadeleri

Rust'ın tasarımı birçok mevcut dil ve teknikten ilham almıştır ve önemli bir etkisi de fonksiyonel programlamadır. Fonksiyonel bir tarzda programlama, genellikle, fonksiyonları argümanlarda ileterek, diğer fonksiyonlardan geri döndürerek, daha sonra yürütülmek üzere değişkenlere atayarak vb. değerler olarak kullanmayı içerir.

Bu bölümde, fonksiyonel programlamanın ne olduğu veya olmadığı konusunu tartışmayacağız, bunun yerine Rust'ın birçok dilde genellikle fonksiyonel olarak adlandırılan özelliklere benzeyen bazı özelliklerini tartışacağız.

Daha özel olarak, ele alacağız:

  • Kapanış ifadeleri, bir değişkende saklayabileceğiniz fonksiyon benzeri bir yapı
  • Yineleyiciler, bir dizi elemanı işlemenin bir yolu
  • Bölüm 12'deki I/O projesini geliştirmek için bu iki özelliğin nasıl kullanılacağı
  • Bu iki özelliğin performansı (Spoiler uyarısı: düşündüğünüzden daha hızlılar!)

Diğer bölümlerde ele aldığımız model eşleştirme ve numaralandırma gibi diğer Rust özellikleri de fonksiyonel stilden etkilenir. Kapanış ifadelerine ve yineleyicilere hakim olmak, deyimsel, hızlı Rust kodu yazmanın önemli bir parçasıdır, bu yüzden bu bölümün tamamını onlara ayıracağız.

Kapanış İfadeleri: Çevrelerini Yakalayabilen Anonim Fonksiyonlar

Rust'ın kapanışları, bir değişkene kaydedebileceğiniz veya diğer fonksiyonlara argüman olarak aktarabileceğiniz anonim fonksiyonlardır. Kapanışları bir yerde oluşturabilir ve daha sonra farklı bir bağlamda değerlendirmek için kapanışları çağırabilirsiniz. Fonksiyonların aksine, kapanışlar tanımlandıkları kapsamdaki değerleri yakalayabilir. Bu kapanış özelliklerinin kodun yeniden kullanımına ve davranış özelleştirmesine nasıl izin verdiğini göstereceğiz.

Kapanışlar ile Ortamı Yakalama

Kapanışların inceleyeceğimiz ilk yönü, kapanışların tanımlandıkları ortamdaki değerleri daha sonra kullanmak üzere yakalayabilmeleridir. İşte senaryo: Bir tişört şirketi, e-posta listesindeki bir kişiye sık sık ücretsiz bir tişört hediye ediyor. E-posta listesindeki kişiler isteğe bağlı olarak profillerine favori renklerini ekleyebilirler. Ücretsiz tişörtü almak için seçilen kişinin profilinde en sevdiği renk varsa, o renk tişörtü alır. Kişi favori rengini belirtmemişse, şirketin şu anda en çok sahip olduğu rengi alır.

Bunu yapmanın birçok yolu vardır. Bu örnek için, Red ve Blue değişkenlerine sahip ShirtColor adlı bir enum kullanacağız. Şirketin envanteri, şu anda stokta bulunan tişörtleri temsil eden bir Vec<ShirtColor> içeren shirts adlı bir alana sahip bir Inventory struct'ı ile temsil edilir. Inventory üzerinde tanımlanan shirt_giveaway metodu, ücretsiz gömlek alacak kişinin isteğe bağlı gömlek rengi tercihini alır ve kişinin alacağı gömlek rengini döndürür. Bu, Liste 13-1'de gösterilmektedir:

Dosya adı: src/main.rs

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}

Liste 13-1: Tişört şirketi çekilişi

main'de tanımlı store'da iki mavi ve bir kırmızı gömlek bulunmaktadır. Ardından, kırmızı gömlek tercihi olan bir kullanıcı ve herhangi bir tercihi olmayan bir kullanıcı için giveaway metodunu çağırmış olsun. Bu kodu çalıştırmak şunları yazdırırır:

$ cargo run
   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue

Yine, bu kod birçok şekilde uygulanabilir, ancak bu yol, bir kapanışları kullanan giveaway metodunun gövdesi dışında, daha önce öğrendiğiniz kavramları kullanır. giveaway metodu kullanıcı tercihi Option<ShirtColor>'ı alır ve üzerinde unwrap_or_else çağrısı yapar. Option<T> üzerindeKİ unwrap_or_else metodu metodu standart kütüphane tarafından tanımlanmıştır. Bir argüman alır: T (Option<T>'nin Some varyantında saklanan aynı tür, bu durumda bir ShirtColor) döndüren herhangi bir argümanı olmayan bir kapanıştır. Option<T>, Some varyantı ise unwrap_or_else, Some içindeki değeri döndürür. Option<T> None varyantıysa, unwrap_or_else kapanışı çağırır ve ka tarafından döndürülen değeri döndürür.

Bu ilginçtir çünkü mevcut Inventory örneğinde self.most_stocked() fonksiyonunu çağıran bir kapanış geçirdik. Standart kütüphanenin, tanımladığımız Inventory veya ShirtColor türleri ya da bu senaryoda kullanmak istediğimiz mantık hakkında hiçbir şey bilmesine gerek yoktu. Kapanış, self Inventory örneğine değişmez bir referans yakaladı ve unwrap_or_else metoduna belirttiğimiz kodla birlikte aktardı. Fonksiyonlar kendi ortamlarını bu şekilde yakalayamazlar.

Kapanış Tür Çıkarsaması ve Ek Açıklama

Fonksiyonlar ve kapanışlar arasında daha fazla fark vardır. Kapanışlar genellikle fn fonksiyonlarında olduğu gibi parametrelerin veya dönüş değerinin türlerini açıklamanızı gerektirmez. Kullanıcılarınıza açık bir arayüzün parçası oldukları için fonksiyonlarda tür ek açıklamaları gereklidir. Bu arayüzü katı bir şekilde tanımlamak, bir fonksiyonun kullandığı ve döndürdüğü değer türleri konusunda herkesin hemfikir olmasını sağlamak açısından önemlidir. Ancak kapanışlar bu şekilde açık bir arayüzde kullanılmazlar: değişkenlerde saklanırlar ve isimlendirilmeden ve kütüphanemizin kullanıcılarına gösterilmeden kullanılırlar.

Kapanışlar tipik olarak kısadır ve herhangi bir rastgele senaryodan ziyade yalnızca dar bir bağlamda önemlidir. Bu sınırlı bağlamlarda derleyici, çoğu değişkenin türünü çıkarabildiği gibi parametrelerin türlerini ve dönüş türünü de çıkarabilir (derleyicinin kapanış türü ek açıklamalarına ihtiyaç duyduğu nadir durumlar da vardır).

Değişkenlerde olduğu gibi, kesinlikle gerekli olandan daha ayrıntılı olma pahasına açıklığı ve netliği artırmak istiyorsak tür ek açıklamaları ekleyebiliriz. Bir kapanış için tür açıklamaları Liste 13-2'de gösterilen tanıma benzeyecektir.

Dosya adı: src/main.rs

use std::thread;
use std::time::Duration;

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num: u32| -> u32 {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!("Today, do {} pushups!", expensive_closure(intensity));
        println!("Next, do {} situps!", expensive_closure(intensity));
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(simulated_user_specified_value, simulated_random_number);
}

Liste 13-2: Kapanıştaki parametre ve dönüş değeri türlerinin isteğe bağlı tür ek açıklamalarının eklenmesi

Tür ek açıklamaları eklendiğinde, kapanışların söz dizimi fonksiyonların söz dizimine daha yakın görünür. Aşağıda, parametresine 1 ekleyen bir fonksiyon ile aynı davranışa sahip bir kapanış tanımının söz diziminin dikey bir karşılaştırması yer almaktadır. İlgili kısımları hizalamak için bazı boşluklar ekledik. Bu, kapanış söz diziminin, boruların kullanımı ve isteğe bağlı söz dizimi miktarı dışında fonksiyon söz dizimine nasıl benzediğini göstermektedir:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

İlk satır bir fonksiyon tanımını, ikinci satır ise tam açıklamalı bir kapanış tanımını göstermektedir. Üçüncü satırda kapanış tanımından tür ek açıklamaları kaldırılır ve dördüncü satırda kapanış gövdesinde yalnızca bir ifade olduğu için isteğe bağlı olan parantezler kaldırılır. Bunların hepsi, çağrıldıklarında aynı davranışı üretecek geçerli tanımlardır. Kapanışların çağrılması add_one_v3 ve add_one_v4'ün derlenebilmesi için gereklidir çünkü türler kullanımlarından çıkarılacaktır.

Kapanış tanımları, parametrelerinin her biri ve dönüş değerleri için çıkarılan bir somut türe sahip olacaktır. Örneğin, Liste 13-3 sadece parametre olarak aldığı değeri döndüren bir kısa kapanış tanımını göstermektedir. Bu kapanış, bu örneğin amaçları dışında çok kullanışlı değildir. Tanıma herhangi bir tür ek açıklaması eklemediğimize dikkat edin: ilk seferinde argüman olarak bir String ve ikinci seferinde bir u32 kullanarak kapanışı iki kez çağırmaya çalışırsak, bir hata alırız.

Dosya adı: src/main.rs

fn main() {
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
}

Liste 13-3: Türleri iki farklı türle çıkarılan bir kapanış çağrılmaya çalışılıyor

Derleyici bu hatayı verir:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |                             ^- help: try using a conversion method: `.to_string()`
  |                             |
  |                             expected struct `String`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` due to previous error

String değeriyle example_closure öğesini ilk kez çağırdığımızda, derleyici x'in türünü ve kapanışın dönüş türünü String olarak çıkarır. Bu türler daha sonra example_closure'daki kapanışa kilitlenir ve aynı kapanış ile farklı bir tür kullanmaya çalışırsak bir tür hatası alırız.

Referansları Yakalama veya Sahipliği Taşıma

Kapanışlar çevrelerinden üç şekilde değer alabilir, bu da doğrudan bir fonksiyonun parametre alabileceği üç yolla eşleşir: değişmez olarak ödünç alma, değişebilir olarak ödünç alma ve sahiplik alma. Kapanış, fonksiyonun gövdesinin yakalanan değerlerle ne yaptığına bağlı olarak bunlardan hangisini kullanacağına karar verecektir.

Liste 13-4, list adlı vektöre değişmez bir ödünç alan bir kapanış tanımlar çünkü değeri yazdırmak için yalnızca değişmez bir ödünç almaya ihtiyaç duyar. Bu örnek aynı zamanda bir değişkenin bir kapanış tanımına bağlanabileceğini ve kapanışın daha sonra değişken adı ve parantezler kullanılarak sanki değişken adı bir fonksiyon adıymış gibi çağrılabileceğini göstermektedir:

Dosya adı: src/main.rs

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    let only_borrows = || println!("From closure: {:?}", list);

    println!("Before calling closure: {:?}", list);
    only_borrows();
    println!("After calling closure: {:?}", list);
}

Liste 13-4: Değişmez bir ödünç almayı yakalayan bir kapanış tanımlama ve çağırma

list, kapanış tanımından önce, kapanış tanımından sonra ancak kapanış çağrılmadan önce ve kapanış çağrıldıktan sonra kod tarafından hala erişilebilir çünkü aynı anda birden fazla değişmez list'i ödünç alabiliriz. Bu kod derlenir, çalıştırılır ve yazdırılır:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]

Ardından, Liste 13-5, kapanış gövdesi liste vektörüne bir eleman eklediği için kapanış tanımını değiştirilebilir bir ödünç almaya ihtiyaç duyacak şekilde değiştirir:

Dosya adı: src/main.rs

fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {:?}", list);
}

Liste 13-5: Değiştirilebilir bir ödünç almayı yakalayan bir kapanış tanımlama ve çağırma

Bu kod derlenir, çalışır ve bunu yazdırır:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]

Tanım ile borrows_mutably kapanışının çağrısı arasında artık bir println! olmadığına dikkat edin: borrows_mutably tanımlandığında, list'e değiştirilebilir bir referans yakalar. Kapanış çağrıldıktan sonra, o noktadan sonra kapanışı tekrar kullanmadığımız için, mutable ödünç alma işlemi sona erer. Kapanış tanımı ve kapanış çağrısı arasında, print'e değişmez bir ödünç almaya izin verilmez çünkü değişebilir bir ödünç alma olduğunda başka ödünç almaya izin verilmez. Nasıl bir hata mesajı alacağınızı görmek için oraya bir println! eklemeyi deneyin!

Kapanışın gövdesi kesinlikle sahipliğe ihtiyaç duymasa bile kapanışı ortamda kullandığı değerlerin sahipliğini almaya zorlamak istiyorsanız, parametre listesinden önce move anahtar sözcüğünü kullanabilirsiniz. Bu teknik çoğunlukla bir kapanışı yeni bir iş parçacığına aktarırken, verileri yeni iş parçacığına ait olacak şekilde taşımak için kullanışlıdır. Eşzamanlılık hakkında konuştuğumuz 16. Bölümde move kapanışları ile ilgili daha fazla örneğimiz olacak.

Yakalanan Değerleri Kapanış ve Fn Tanımlarının Dışına Taşıma

Bir kapanış bir referansı yakaladığında veya bir değeri kapanışa taşıdığında, fonksiyonun gövdesindeki kod da fonksiyonun çağrılmasının bir sonucu olarak referanslara veya değerlere ne olacağını etkiler. Bir kapanış gövdesi, yakalanan bir değeri kapanış dışına taşıyabilir, yakalanan değeri değiştirebilir, yakalanan değeri ne taşıyabilir ne de değiştirebilir veya ortamdan hiçbir şey yakalayamaz. Bir kapanışın ortamdan değerleri yakalama ve işleme şekli, kapanışın hangi özellikleri uyguladığını etkiler. Tanımlar, fonksiyonların ve yapıların ne tür kapanışları kullanabileceklerini belirtme şeklidir.

Kapanışlar otomatik olarak bu Fn tanımlarından birini, ikisini ya da üçünü de eklenebilir bir şekilde sürekleyecektir:

  1. FnOnce en az bir kez çağrılabilen kapanışlar için geçerlidir. Tüm kapanışlar bu tanımı sürekler, çünkü tüm kapanışlar çağrılabilir. Bir kapanış yakalanan değerleri gövdesinin dışına taşırsa, bu kapanış yalnızca FnOnce tanımını sürekler ve diğer Fn tanımlarından hiçbirini süreklemez, çünkü yalnızca bir kez çağrılabilir.
  2. FnMut, yakalanan değerleri gövdelerinin dışına taşımayan ancak yakalanan değerleri mutasyona uğratabilen kapanışlar için geçerlidir. Bu kapanışlar birden fazla kez çağrılabilir.
  3. Fn, yakalanan değerleri gövdelerinin dışına taşımayan ve yakalanan değerleri mutasyona uğratmayan kapamalar için geçerlidir. Bu kapanışlar, ortamlarını değiştirmeden birden fazla kez çağrılabilir; bu da bir closure'ın aynı anda birden fazla kez çağrılması gibi durumlarda önemlidir. Çevrelerinden hiçbir şey yakalamayan kapanışlar Fn'i sürekler.

Liste 13-6'da kullandığımız Option<T> üzerindeki unwrap_or_else metodunun tanımına bakalım:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

T'nin, Option'ın Some varyantındaki değerin türünü temsil eden yaygın tür olduğunu hatırlayın. Bu T türü aynı zamanda unwrap_or_else fonksiyonunun dönüş türüdür: örneğin bir Option<String> üzerinde unwrap_or_else çağrısı yapan kod bir String elde edecektir.

Daha sonra, unwrap_or_else fonksiyonunun ek bir yaygın tür parametresi olduğuna dikkat edin: F. F türü, unwrap_or_else fonksiyonunu çağırırken sağladığımız kapanış olan f adlı parametrenin tipidir.

F yaygın türünde belirtilen tanım bağlılığı FnOnce() -> T'dir, yani F en az bir kez çağrılabilmeli, hiçbir argüman almamalı ve bir T döndürmelidir. Tanım bağlılığında FnOnce kullanılması, unwrap_or_else fonksiyonunun f'yi yalnızca en fazla bir kez çağıracağı kısıtlamasını ifade eder. unwrap_or_else'in gövdesinde; Option Some ise f'nin çağrılmayacağını görebiliriz. Option None ise, f bir kez çağrılacaktır. Tüm kapanışlar FnOnce'ı uyguladığından, unwrap_or_else en farklı kapanış türlerini kabul eder ve olabildiğince esnektir.

Not: Fonksiyonlar da Fn tanımının üçünü de sürekleyebilir. Yapmak istediğimiz şey ortamdan bir değer yakalamayı gerektirmiyorsa, Fn tanımlarından birini sürekleyen bir şeye ihtiyaç duyduğumuz yerde kapanış yerine fonksiyonun adını kullanabiliriz. Örneğin, Option<Vec<T>> değeri üzerinde, değer None ise yeni ve boş bir vektör elde etmek için unwrap_or_else(Vec::new) çağrısı yapabiliriz.

Şimdi bunun nasıl farklılaştığını görmek için dilimler üzerinde tanımlanan standart kütüphane metodu sort_by_key'e bakalım. FnMut'i sürekleyen bir kapanış alır. Kapanış, dikkate alınan dilimdeki geçerli öğeye bir referans olmak üzere bir argüman alır ve sıralanabilen K türünde bir değer döndürür. Bu fonksiyon, bir dilimi her bir öğenin belirli bir özelliğine göre sıralamak istediğinizde kullanışlıdır. Liste 13-7'de, Rectangle örneklerinden oluşan bir listemiz var ve bunları width niteliklerine göre düşükten yükseğe doğru sıralamak için sort_by_key'i kullanıyoruz:

Dosya adı: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle {
            width: 10,
            height: 1,
        },
        Rectangle {
            width: 3,
            height: 5,
        },
        Rectangle {
            width: 7,
            height: 12,
        },
    ];

    list.sort_by_key(|r| r.width);
    println!("{:#?}", list);
}

Liste 13-7: Rectangle örneklerinden oluşan bir listeyi width değerlerine göre sıralamak için sort_by_key'i ve kapanışları kullanma

Bu kod şunları yazdırır:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/rectangles`
[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]

sort_by_key'in FnMut kapanışı alacak şekilde tanımlanmasının nedeni, kapanışı birden çok kez çağırmasıdır: dilimdeki her öğe için bir kez. |r| r.width kapanışı, çevresinden herhangi bir şeyi yakalamaz, mutasyona uğratmaz veya dışarı taşımaz, bu nedenle tanım bağlılığı gereksinimlerini karşılar.

Buna karşılık, Liste 13-8, ortamdan bir değer taşıdığı için yalnızca FnOnce uygulayan bir kapanış örneğini gösterir. Derleyici bu kapanışı sort_by_key ile kullanmamıza izin vermez:

Dosya adı: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle {
            width: 10,
            height: 1,
        },
        Rectangle {
            width: 3,
            height: 5,
        },
        Rectangle {
            width: 7,
            height: 12,
        },
    ];

    let mut sort_operations = vec![];
    let value = String::from("by key called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{:#?}", list);
}

Liste 13-8: sort_by_key ile FnOnce kapanışını kullanma girişimi

Bu, list'i sıralarken sort_by_key'in kaç kez çağrıldığını saymaya çalışmak için uydurulmuş, karmaşık bir yoldur (işe yaramaz). Bu kod bu sayımı, kapanış ortamından bir String olan değeri sort_operations vektörüne iterek yapmaya çalışır. Kapanış değeri yakalar ve ardından değerin sahipliğini sort_operations vektörüne aktararak değeri kapanış dışına taşır. Bu kapanış bir kez çağrılabilir; ikinci kez çağırmaya çalışmak işe yaramaz çünkü değer artık sort_operations'a tekrar itilecek ortamda olmayacaktır! Bu nedenle, bu kapanış yalnızca FnOnce'ı sürekler. Bu kodu derlemeye çalıştığımızda, değerin kapanış dışına taşınamayacağını çünkü kapanıın FnMut'u süreklemesi gerektiğini belirten bir hata alırız:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
  --> src/main.rs:27:30
   |
24 |       let value = String::from("by key called");
   |           ----- captured outer variable
25 | 
26 |       list.sort_by_key(|r| {
   |  ______________________-
27 | |         sort_operations.push(value);
   | |                              ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
28 | |         r.width
29 | |     });
   | |_____- captured by this `FnMut` closure

For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` due to previous error

Hata, değeri ortamın dışına taşıyan kapatma gövdesindeki satıra işaret eder. Bunu düzeltmek için, kapanış gövdesini değerleri ortamın dışına taşımayacak şekilde değiştirmemiz gerekir. sort_by_key'in kaç kez çağrıldığıyla ilgileniyorsak, ortamda bir sayaç tutmak ve kapanış gövdesinde değerini artırmak bunu hesaplamanın daha kolay bir yoludur. Liste 13-9'daki kapanış sort_by_key ile çalışır çünkü sadece num_sort_operations sayacına değiştirilebilir bir referans yakalar ve bu nedenle birden fazla kez çağrılabilir:

Dosya adı: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle {
            width: 10,
            height: 1,
        },
        Rectangle {
            width: 3,
            height: 5,
        },
        Rectangle {
            width: 7,
            height: 12,
        },
    ];

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!("{:#?}, sorted in {num_sort_operations} operations", list);
}

Liste 13-9: sort_by_key ile bir FnMut kapanışının kullanılmasına izin verilir

Fn tanımları, kapanışlardan yararlanan fonksiyonları veya türleri tanımlarken veya kullanırken önemlidir. Bir sonraki bölümde yineleyiciler ele alınmaktadır ve birçok yineleyici yöntemi kapanış argümanları alır. Yineleyicileri keşfederken kapanışlarla ilgili bu ayrıntıları aklınızda tutun!

Yineleyicilerle Bir Öğe Serisini İşleme

Yineleyici deseni, sırayla bir dizi öğe üzerinde bazı görevleri gerçekleştirmenize olanak tanır. Bir yineleyici, her bir öğe üzerinde yineleme mantığından ve dizinin ne zaman bittiğini belirlemekten sorumludur. Yineleyicileri kullandığınızda, bu mantığı kendiniz yeniden uygulamak zorunda kalmazsınız.

Rust'ta yineleyiciler tembeldir, yani siz onu kullanmak için yineleyiciyi tüketen metodları çağırana kadar hiçbir etkileri yoktur. Örneğin, Liste 13-10'daki kod, Vec<T> üzerinde tanımlanan iter metodunu çağırarak v1 vektöründeki öğeler üzerinde bir yineleyici oluşturur. Bu kod kendi başına yararlı bir şey yapmaz.

fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}

Liste 13-10: Yineleyici oluşturmak

Bir yineleyici oluşturduktan sonra, onu çeşitli şekillerde kullanabiliriz. Bölüm 3'teki Liste 3-5'te, öğelerinin her birinde bazı kodlar çalıştırmak için for döngüsü kullanarak bir dizi üzerinde yineleme yaptık. Bu dolaylı olarak bir yineleyici oluşturdu ve sonra üzerinden geçti, ancak şimdiye kadar bunun tam olarak nasıl çalıştığını geçtik.

Liste 13-11'deki örnek, yineleyicinin oluşturulmasını for döngüsündeki yineleyici kullanımından ayırır. Yineleyici v1_iter değişkeninde saklanır ve o sırada herhangi bir yineleme gerçekleşmez. v1_iter içindeki yineleyici kullanılarak for döngüsü çağrıldığında, yineleyicideki her öğe döngünün bir yinelemesinde kullanılır ve her değer yazdırılır.

fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {}", val);
    }
}

Liste 13-11: Bir for döngüsünde yineleyici kullanma

Standart kütüphaneleri tarafından sağlanan yineleyicilere sahip olmayan dillerde, muhtemelen aynı fonksiyonu 0 indeksinde bir değişken başlatarak, bir değer elde etmek için vektörü indekslemek üzere bu değişkeni kullanarak ve vektördeki toplam öğe sayısına ulaşana kadar değişken değerini bir döngü içinde artırarak yazarsınız.

Yineleyiciler tüm bu mantığı sizin için halleder ve potansiyel olarak karıştırabileceğiniz tekrarlayan kodu azaltır. Yineleyiciler, aynı mantığı yalnızca vektörler gibi indeksleyebileceğiniz veri yapılarıyla değil, birçok farklı türde diziyle kullanmanız için size daha fazla esneklik sağlar. Yineleyicilerin bunu nasıl yaptığını inceleyelim.

Iterator Tanımı ve next Metodu

Tüm yineleyiciler, standart kütüphanede tanımlanan Iterator adlı tanımı sürekler. Tanımın tanımı şu şekildedir:


#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}
}

Bu tanımın bazı yeni söz dizimleri kullandığına dikkat edin: Item ve Self::Item türleri bu özellik ile ilişkili bir tür tanımlamaktadır. İlişkili türler hakkında Bölüm 19'da derinlemesine konuşacağız. Şimdilik bilmeniz gereken tek şey, bu kodun Iterator tanımını süreklemenin Item türünü de tanımlamanızı gerektirdiğini ve bu Item türünün bir sonraki metodun dönüş türünde kullanıldığını söylediğidir. Başka bir deyişle, Item türü yineleyiciden döndürülen tür olacaktır.

Iterator tanımı, sürekleyicilerin yalnızca bir metod tanımlamasını gerektirir: Some içine sarılmış olarak her seferinde yineleyicinin bir öğesini döndüren ve yineleme sona erdiğinde None döndüren next metodu.

Yineleyicilerde next metodunu doğrudan çağırabiliriz; Liste 13-12, vektörden oluşturulan yineleyicide next metodunun tekrarlanan çağrılarından hangi değerlerin döndürüldüğünü gösterir.

Dosya adı: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn iterator_demonstration() {
        let v1 = vec![1, 2, 3];

        let mut v1_iter = v1.iter();

        assert_eq!(v1_iter.next(), Some(&1));
        assert_eq!(v1_iter.next(), Some(&2));
        assert_eq!(v1_iter.next(), Some(&3));
        assert_eq!(v1_iter.next(), None);
    }
}

Liste 13-12: Yineleyici üzerinde next metodunun çağrılması

v1_iter'ı değiştirilebilir yapmamız gerektiğine dikkat edin: bir yineleyici üzerinde next metodunu çağırmak, yineleyicinin sıralamada nerede olduğunu takip etmek için kullandığı dahili durumu değiştirir. Başka bir deyişle, bu kod yineleyiciyi tüketir ya da kullanır. Her next çağrısı yineleyiciden bir öğe tüketir. Bir for döngüsü kullandığımızda v1_iter'ı değiştirilebilir yapmamıza gerek yoktu çünkü döngü v1_iter'ın sahipliğini aldı ve onu perde arkasında değiştirilebilir yaptı.

Ayrıca next çağrısından elde ettiğimiz değerlerin vektördeki değerlere değişmez referanslar olduğuna dikkat edin. iter metodu, değişmez referanslar üzerinde bir yineleyici üretir. Eğer v1'in sahipliğini alan ve sahip olunan değerleri döndüren bir yineleyici oluşturmak istiyorsak, iter yerine into_iter'ı çağırabiliriz. Benzer şekilde, değiştirilebilir referanslar üzerinde yineleme yapmak istiyorsak, iter yerine iter_mut metodunu çağırabiliriz.

Yineleyici Kullanan Metodlar

Iterator tanımı, standart kütüphane tarafından sağlanan varsayılan süreklemelere sahip bir dizi farklı metoda sahiptir; Iterator tanımı için standart kütüphane API dokümantasyonuna bakarak bu metodlar hakkında bilgi edinebilirsiniz. Bu metodlardan bazıları tanımlarında next metodunu çağırır, bu nedenle Iterator tanımını süreklerken next metodunu da süreklememiz gerekir.

next metodunu çağıran metodlara tüketim uyarlayıcıları denir, çünkü bunları çağırmak yineleyiciyi kullanır. Bunun bir örneği, yineleyicinin sahipliğini alan ve next metodunu tekrar tekrar çağırarak öğeler arasında yineleme yapan ve böylece yineleyiciyi tüketen sum metodudur. Yineleme sırasında, her öğeyi çalışan bir toplama ekler ve yineleme tamamlandığında toplamı döndürür. Liste 13-13, sum metodunun kullanımını gösteren bir test içerir:

Dosya adı: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn iterator_sum() {
        let v1 = vec![1, 2, 3];

        let v1_iter = v1.iter();

        let total: i32 = v1_iter.sum();

        assert_eq!(total, 6);
    }
}

Liste 13-13: Yineleyicideki tüm öğelerin toplamını almak için sum metodunu çağırma

sum çağrısından sonra v1_iter kullanmamıza izin verilmez çünkü sum, çağırdığımız yineleyicinin sahipliğini alır.

Diğer Yineleyicileri Üreten Metodlar

Iterator tanımı üzerinde tanımlanan ve yineleyici adaptorü olarak bilinen diğer metodlar, yineleyiciyi farklı yineleyici türlerine dönüştürmenize olanak tanır. Karmaşık eylemleri okunabilir bir şekilde gerçekleştirmek için yineleyici uyarlayıcılarına birden fazla çağrıyı zincirleyebilirsiniz. Ancak tüm yineleyiciler tembel olduğundan, yineleyici uyarlayıcılarına yapılan çağrılardan sonuç almak için tüketen uyarlayıcı metodlardan birini çağırmanız gerekir.

Liste 13-14, yeni bir yineleyici üretmek için her öğede çağrılacak bir kapanış alan yineleyici uyarlayıcı yöntemi olan map'i çağırmanın bir örneğini göstermektedir. Buradaki kapanış, vektördeki her öğenin 1 artırıldığı yeni bir yineleyici oluşturur. Ancak, bu kod bir uyarı üretir:

Dosya adı: src/main.rs

fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);
}

Liste 13-14: Yeni bir yineleyici oluşturmak için map yineleyici uyarlayıcısını çağırma

Aldığımız uyarı şudur:

$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

warning: `iterators` (bin "iterators") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

Liste 13-14'teki kod hiçbir şey yapmaz; belirttiğimiz kapanış hiçbir zaman çağrılmaz. Uyarı bize nedenini gösteriyor: yineleyici uyarlayıcıları tembeldir ve burada yineleyiciyi tüketmemiz gerekir.

Bunu düzeltmek ve yineleyiciyi tüketmek için, Bölüm 12'de Liste 12-1'de env::args ile kullandığımız collect metodunu kullanacağız. Bu metod yineleyiciyi tüketir ve elde edilen değerleri bir koleksiyon veri türünde toplar.

Liste 13-15'te, map çağrısından döndürülen yineleyici üzerinde yineleme sonuçlarını bir vektörde topluyoruz. Bu vektör, orijinal vektördeki her bir öğenin 1 ile artırılmış halini içerecektir.

Dosya adı: src/main.rs

fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

    assert_eq!(v2, vec![2, 3, 4]);
}

Liste 13-15: Yeni bir yineleyici oluşturmak için map metodunu çağırmak ve ardından yeni yineleyiciyi tüketmek ve bir vektör oluşturmak için collect metodunu çağırmak

map bir kapanış aldığı için, her bir öğe üzerinde gerçekleştirmek istediğimiz herhangi bir işlemi belirtebiliriz. Bu, kapanışların Iterator tanımını sağladığı yineleme davranışını yeniden kullanırken bazı davranışları özelleştirmenize nasıl izin verdiğinin harika bir örneğidir.

Ortamlarını Yakalayan Kapanışları Kullanma

Yineleyicileri tanıttığımıza göre, filter yineleyici adaptörünü kullanarak çevrelerini yakalayan kapanışların yaygın bir kullanımını gösterebiliriz. Bir yineleyici üzerindeki filter metodu, yineleyicideki her bir öğeyi alan ve bir Boole döndüren bir kapanış alır. Eğer kapanış true döndürürse, değer filter tarafından üretilen yineleyiciye dahil edilecektir. Kapanış false döndürürse, değer elde edilen yineleyiciye dahil edilmez.

Liste 13-16'da, Shoe struct örneklerinden oluşan bir koleksiyon üzerinde yineleme yapmak için ortamından shoe_size değişkenini yakalayan bir kapanış ile filter'ı kullanırız. Yalnızca belirtilen boyutta olan ayakkabıları döndürür.

Dosya adı: src/lib.rs

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe {
                size: 10,
                style: String::from("sneaker"),
            },
            Shoe {
                size: 13,
                style: String::from("sandal"),
            },
            Shoe {
                size: 10,
                style: String::from("boot"),
            },
        ];

        let in_my_size = shoes_in_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}

Liste 13-16: shoe_size öğesini yakalayan bir kapanış ile filter metodunu kullanma

shoes_in_size fonksiyonu, parametre olarak bir ayakkabı vektörüne ve bir shoe_size'a yani ayakkabı boyutuna sahip olur. Yalnızca belirtilen boyuttaki ayakkabıları içeren bir vektör döndürür.

shoes_in_size'ın gövdesinde, vektörün sahipliğini alan bir yineleyici oluşturmak için into_iter'ı çağırıyoruz. Daha sonra bu yineleyiciyi yalnızca kapanışın true döndürdüğü öğeleri içeren yeni bir yineleyiciye uyarlamak için filter'ı çağırıyoruz.

Kapanış, shoe_size parametresini ortamdan alır ve değeri her shoe_size'ı karşılaştırarak yalnızca belirtilendekileri tutar. Son olarak, collect çağrısı, uyarlanmış yineleyici tarafından döndürülen değerleri, fonksiyon tarafından döndürülen bir vektörde toplar.

Test, shoes_in_size öğesini çağırdığımızda, yalnızca belirttiğimiz değerle aynı boyuta sahip ayakkabıları geri aldığımızı gösterir.

G/Ç Projemizi Geliştirmek

Yineleyiciler hakkındaki bu yeni bilgiyle, koddaki yerleri daha açık ve öz hale getirmek için yineleyicileri kullanarak Bölüm 12'deki G/Ç projesini geliştirebiliriz. Şimdi yineleyicilerin Config::new fonksiyonu ve search fonksiyonu uygulamamızı nasıl geliştirebileceğine bakalım.

Yineleyici Kullanarak bir clone'u Kaldırma

Liste 12-6'da, String değerlerinin bir dilimini alan ve dilime indeksleme yapıp değerleri klonlayarak Config yapısının bir örneğini oluşturan ve Config yapısının bu değerlere sahip olmasını sağlayan kodu ekledik. Liste 13-17'de, Config::new fonksiyonunun uygulamasını Liste 12-23'te olduğu gibi yeniden ürettik:

Dosya adı: src/lib.rs

use std::env;
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            filename,
            ignore_case,
        })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Liste 13-17: Liste 12-23'ten Config::new işlevinin çoğaltılması

O zaman, verimsiz clone çağrıları konusunda endişelenmememizi çünkü gelecekte bunları kaldıracağımızı söylemiştik. İşte o zaman şimdi!

Burada clone'a ihtiyacımız vardı çünkü parametre args'ta String öğeleri olan bir dilimimiz var, ancak yeni fonksiyon args'a sahip değil. Bir Config örneğinin sahipliğini döndürmek için Config'in query ve filename alanlarındaki değerleri klonlamamız gerekiyordu, böylece Config örneği kendi değerlerine sahip olabilirdi.

Yineleyiciler hakkındaki yeni bilgilerimizle, yeni fonksiyonu bir dilimi ödünç almak yerine argümanı olarak bir yineleyicinin sahipliğini alacak şekilde değiştirebiliriz. Dilimin uzunluğunu kontrol eden ve belirli konumlara indeksleyen kod yerine yineleyici fonksiyonu kullanacağız. Bu, Config::new fonksiyonunun ne yaptığını netleştirecektir çünkü yineleyici değerlere erişecektir.

Config::new yineleyicinin sahipliğini aldığında ve ödünç alan indeksleme işlemlerini kullanmayı bıraktığında, String değerlerini clone'u çağırmak ve yeni bir tahsisat yapmak yerine yineleyiciden Config'e taşıyabiliriz.

Döndürülen Yineleyiciyi Doğrudan Kullanma

G/Ç projenizin aşağıdaki gibi görünmesi gereken src/main.rs dosyasını açın:

Dosya adı: src/main.rs

use std::env;
use std::process;

use minigrep::Config;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    // --snip--

    if let Err(e) = minigrep::run(config) {
        eprintln!("Application error: {}", e);

        process::exit(1);
    }
}

Liste 12-24'te sahip olduğumuz main fonksiyonun başlangıcını Liste 13-18'deki kodla değiştireceğiz. Bu, biz Config::new'i de güncelleyene kadar derlenmeyecektir.

Dosya adı: src/main.rs

use std::env;
use std::process;

use minigrep::Config;

fn main() {
    let config = Config::new(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    // --snip--

    if let Err(e) = minigrep::run(config) {
        eprintln!("Application error: {}", e);

        process::exit(1);
    }
}

Liste 13-18: env::args dönüş değerinin Config::new öğesine iletilmesi

env::args fonksiyonu bir yineleyici döndürür! Yineleyici değerlerini bir vektörde toplamak ve ardından bir dilimi Config::new'e aktarmak yerine, şimdi env::args'dan dönen yineleyicinin sahipliğini doğrudan Config::new'e aktarıyoruz.

Daha sonra, Config::new'in tanımını güncellememiz gerekiyor. G/Ç projenizin src/lib.rs dosyasında, Config::new'in imzasını Liste 13-19'daki gibi görünecek şekilde değiştirelim. Bu yine de derlenmeyecektir çünkü fonksiyon gövdesini güncellememiz gerekir.

Dosya adı: src/lib.rs

use std::env;
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn new(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        // --snip--
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            filename,
            ignore_case,
        })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Liste 13-19: Bir yineleyici beklemek için Config::new imzasını güncelleme

env::args fonksiyonunun standart kütüphane belgeleri, döndürdüğü yineleyicinin türünün std::env::Args olduğunu ve bu türün Iterator özelliğini uyguladığını ve String değerleri döndürdüğünü gösterir.

Config::new fonksiyonunun imzasını güncelledik, böylece args parametresi &[String] yerine tanım sınırları olan impl Iterator<Item = String> ile yaygın bir türe sahip olacak. Bölüm 10'un “Parametreler Olarak Tanımlar” bölümünde tartıştığımız impl Trait söz diziminin bu kullanımı, args'nin Iterator türünü uygulayan ve String öğeleri döndüren herhangi bir tür olabileceği anlamına gelir.

args'nin sahipliğini aldığımız ve üzerinde yineleme yaparak args'yi mutasyona uğratacağımız için, mut anahtar sözcüğünü args parametresinin belirtimine ekleyerek onu mutasyona uğratılabilir hale getirebiliriz.

İndeksleme Yerine Iterator Tanım Yöntemlerini Kullanma

Sonra, Config::new'in gövdesini düzelteceğiz. args, Iterator tanımını uyguladığı için, bir sonraki yöntemi çağırabileceğimizi biliyoruz! Liste 13-20, next yöntemini kullanmak için Liste 12-23'teki kodu günceller:

Dosya adı: src/lib.rs

use std::env;
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn new(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let filename = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file name"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            filename,
            ignore_case,
        })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Liste 13-20: Yineleyici yöntemlerini kullanmak için Config::new gövdesini değiştirme

env::args'ın dönüş değerindeki ilk değerin programın adı olduğunu unutmayın. Bunu yok saymak ve bir sonraki değere ulaşmak istiyoruz, bu yüzden önce next'i çağırıyoruz ve geri dönüş değeriyle hiçbir şey yapmıyoruz. İkinci olarak, Config'in query alanına koymak istediğimiz değeri almak için next'i çağırıyoruz. next, Some döndürürse, değeri çıkarmak için match kullanırız. None döndürürse, yeterli argüman verilmediği anlamına gelir ve bir Err değeriyle erken döneriz. Aynı şeyi filename değeri için de yaparız.

Yineleyici Bağdaştırıcılar ile Kodu Daha Anlaşılır Hale Getirme

G/Ç projemizdeki search fonksiyonunda da yineleyicilerden yararlanabiliriz; bu fonksiyon burada Liste 12-19'da olduğu gibi Liste 13-21'de yeniden üretilmiştir:

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Liste 13-21: Liste 12-19'dan search fonksiyonunun yazılması

Bu kodu yineleyici bağdaştırıcı metodlarını kullanarak daha kısa bir şekilde yazabiliriz. Bunu yapmak aynı zamanda değiştirilebilir bir ara results vektörüne sahip olmaktan kaçınmamızı sağlar. Fonksiyonel programlama stili, kodu daha anlaşılır hale getirmek için değiştirilebilir durum miktarını en aza indirmeyi tercih eder. Değişken durumu kaldırmak, results vektörüne eşzamanlı erişimi yönetmek zorunda kalmayacağımız için gelecekte yapılacak bir geliştirmeyle aramanın paralel olarak yapılmasını sağlayabilir. Liste 13-22 bu değişikliği göstermektedir:

Dosya adı: src/lib.rs

use std::env;
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn new(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let filename = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file name"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            filename,
            ignore_case,
        })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Liste 13-22: "arama" işlevinin uygulanmasında yineleyici bağdaştırıcı yöntemlerini kullanma

search fonksiyonunun amacının, query'i içeren içerikteki tüm satırları döndürmek olduğunu hatırlayın. Liste 13-16'daki filter örneğine benzer şekilde, bu kod yalnızca line.contains(query) öğesinin true döndürdüğü satırları tutmak için filter kullanır. Daha sonra eşleşen satırları collect ile başka bir vektörde topluyoruz. Çok daha basit! Aynı değişikliği search_case_insensitive fonksiyonunda yineleyici yöntemlerini kullanmak için de yapmaktan çekinmeyin.

Bir sonraki mantıksal soru, kendi kodunuzda hangi stili ve neden seçmeniz gerektiğidir: Liste 13-21'deki orijinal uygulama mı yoksa Liste 13-22'deki yineleyicileri kullanan sürüm mü? Çoğu Rust programcısı yineleyici stilini kullanmayı tercih eder. İlk başta alışmak biraz daha zordur, ancak çeşitli yineleyici uyarlayıcılarını ve ne yaptıklarını bir kez hissettiğinizde, yineleyicileri anlamak daha kolay olabilir. Döngünün çeşitli kısımlarıyla uğraşmak ve yeni vektörler oluşturmak yerine, kod döngünün üst düzey hedefine odaklanır. Bu, sıradan kodların bazılarını soyutlaştırır, böylece yineleyicideki her bir öğenin geçmesi gereken filtreleme koşulu gibi bu koda özgü kavramları görmek daha kolaydır.

Ancak iki uygulama gerçekten eş değer midir? Sezgisel varsayım, daha düşük seviyeli döngünün daha hızlı olacağı yönünde olabilir. Şimdi performans hakkında konuşalım.

Performans Karşılaştırması: Döngüler ve Yineleyiciler

Döngülerin mi yoksa yineleyicilerin mi kullanılacağını belirlemek için, hangi uygulamanın daha hızlı olduğunu bilmeniz gerekir: search fonksiyonunun açık bir for döngüsüne sahip versiyonu ya da yineleyicilere sahip versiyonu.

Sir Arthur Conan Doyle'un The Adventures of Sherlock Holmes'un tüm içeriğini bir String'e yükleyerek ve içerikte kelimeyi arayarak bir kıyaslama yaptık. Burada, for döngüsünü kullanan arama sürümü ve yineleyicileri kullanan sürümle ilgili karşılaştırmanın sonuçları:

test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench:  19,234,900 ns/iter (+/- 657,200)

Yineleyici sürümü biraz daha hızlıydı! Burada kıyaslama kodunu açıklamayacağız, çünkü mesele iki versiyonun eşdeğer olduğunu kanıtlamak değil, bu iki uygulamanın performans açısından nasıl karşılaştırıldığına dair genel bir fikir edinmek.

Daha kapsamlı bir kıyaslama için, içerik olarak çeşitli boyutlarda çeşitli metinler, sorgu olarak farklı kelimeler ve farklı uzunluklarda kelimeler ve her türlü diğer varyasyonları kontrol etmelisiniz.

Mesele şudur: yineleyiciler, üst düzey bir soyutlama olmasına rağmen, alt düzey kodu kendiniz yazmışsınız gibi kabaca aynı koda derlenir. Yineleyiciler, Rust'ın sıfır maliyetli soyutlamalarından biridir; bununla, soyutlamayı kullanmanın ek çalışma zamanı yükü getirmediğini kastediyoruz. Bu, C++'ın orijinal tasarımcısı ve uygulayıcısı olan Bjarne Stroustrup'un “Foundations of C++”'ta (2012) sıfır yükü tanımlamasına benzer:

Genel olarak, C++ uygulamaları sıfır genel gider ilkesine uyar: Kullanmadığınız şey için ödeme yapmazsınız. Ve dahası: Ne kullanırsanız kullanın, kodu daha iyi veremezdiniz.

Başka bir örnek olarak, aşağıdaki kod bir ses kod çözücüsünden alınmıştır. Kod çözme algoritması, önceki örneklerin doğrusal bir fonksiyonuna dayalı olarak gelecekteki değerleri tahmin etmek için doğrusal tahmin matematiksel işlemini kullanır. Bu kod, kapsamdaki üç değişken üzerinde biraz matematik yapmak için bir yineleyici zinciri kullanır: bir arabellek veri dilimi, 12 katsayı dizisi ve qlp_shift'te verilerin kaydırılacağı miktar. Bu örnekte değişkenleri tanımladık ancak onlara herhangi bir değer vermedik; Bu kodun bağlamı dışında pek bir anlamı olmamasına rağmen, yine de Rust'ın yüksek seviyeli fikirleri düşük seviyeli koda nasıl çevirdiğinin kısa ve gerçek bir örneğidir.

let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;

for i in 12..buffer.len() {
    let prediction = coefficients.iter()
                                 .zip(&buffer[i - 12..i])
                                 .map(|(&c, &s)| c * s as i64)
                                 .sum::<i64>() >> qlp_shift;
    let delta = buffer[i];
    buffer[i] = prediction as i32 + delta;
}

prediction değerini hesaplamak için bu kod, katsayılardaki 12 değerin her birini yineler ve katsayı değerlerini arabellekteki önceki 12 değerle eşleştirmek için zip metodunu kullanır. Ardından, her bir çift için değerleri birlikte çarparız, tüm sonuçları toplarız ve toplam qlp_shift bitlerindeki bitleri sağa kaydırırız.

Ses kod çözücüler gibi uygulamalardaki hesaplamalar genellikle performansa en yüksek önceliği verir. Burada, iki bağdaştırıcı kullanarak bir yineleyici oluşturuyoruz ve ardından değeri kullanıyoruz. Bu Rust kodu hangi Assembly koduna derler? Eh, bu yazı itibariyle, elle yazacağınız derlemeye kadar derlenir. Katsayılardaki değerler üzerinde yinelemeye karşılık gelen hiçbir döngü yoktur: Rust, 12 yineleme olduğunu bilir, bu nedenle döngüyü “açar”. Açmak, döngü kontrol kodunun ek yükünü ortadan kaldıran ve bunun yerine döngünün her yinelemesi için tekrarlayan kod üreten bir optimizasyondur.

Tüm katsayılar kayıtlarda saklanır, bu da değerlere erişmenin çok hızlı olduğu anlamına gelir. Çalışma zamanında dizi erişiminde sınır denetimi yoktur. Rust'ın uygulayabildiği tüm bu optimizasyonlar, ortaya çıkan kodu son derece verimli hale getirir. Artık bunu bildiğinize göre, yineleyicileri ve kapanış ifadelerini korkmadan kullanabilirsiniz! Kodun daha yüksek bir seviye gibi görünmesini sağlarlar, ancak bunu yapmak için bir çalışma zamanı performans kapitülasyonlarını vermezler.

Özet

Kapanış ifadeleri ve yineleyiciler, fonksiyonel programlama dili fikirlerinden ilham alan Rust özellikleridir. Rust'ın üst düzey fikirleri düşük düzeyde performansla açıkça ifade etme yeteneğine katkıda bulunurlar. Kapanış ifadelerinin ve yineleyicilerin uygulamaları, çalışma zamanı performansının etkilenmeyeceği şekildedir. Bu, Rust'ın sıfır maliyetli soyutlamalar sağlama hedefinin bir parçasıdır.

G/Ç projemizin dışavurumunu iyileştirdiğimize göre, şimdi projeyi dünyayla paylaşmamıza yardımcı olacak cargo'nun bazı özelliklerine bakalım.

Cargo ve Crates.io Hakkında Daha Fazla Bilgi

Şimdiye kadar; kodumuzu oluşturmak, çalıştırmak ve test etmek için Cargo'nun yalnızca en temel özelliklerini kullandık, ancak Cargo çok daha fazlasına sahiptir. Bu bölümde, aşağıdakileri nasıl yapacağınızı göstermek için diğer gelişmiş özelliklerinden bazılarını tartışacağız:

  • Yayın profilleri aracılığıyla yapınızı özelleştirmek
  • crates.io'da kütüphaneler yayınlamak
  • Çalışma alanlarıyla büyük projeleri organize edin
  • crates.io'dan yürütülebilirleri ve kütüphaneleri yüklemek.
  • Özel komutları kullanarak Cargo'yu genişletmek.

Cargo, bu bölümde ele aldığımız işlevlerden daha fazlasını yapabilir, bu nedenle tüm özelliklerinin tam açıklaması için dokümantasyonuna bakın.

Dağıtım Profilleri ile Yapıları Özelleştirme

Rust'ta dağıtım profilleri, bir programcının kod derlemek için çeşitli seçenekler üzerinde daha fazla kontrole sahip olmasını sağlayan farklı konfigürasyonlara sahip önceden tanımlanmış ve özelleştirilebilir profillerdir. Her profil diğerlerinden bağımsız olarak yapılandırılır.

Cargo'nun iki ana profili vardır: Cargo'nun, cargo build'i çalıştırdığınızda kullandığı dev profili ve cargo build --release'i çalıştırdığınızda kullandığı release profili geliştirme için iyi varsayılanlarla tanımlanır ve dağıtım profili, dağıtım derlemeleri için iyi varsayılanlara sahiptir.

Bu profil adları, yapılarınızın çıktısından tanıdık gelebilir:

$ cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
$ cargo build --release
    Finished release [optimized] target(s) in 0.0s

dev ve release, derleyici tarafından kullanılan farklı tür profillerdir.

Cargo'nun, projenin Cargo.toml dosyasına açıkça herhangi bir [profile.*] bölümü eklemediğinizde uygulanan profillerin her biri için varsayılan ayarları vardır. Özelleştirmek istediğiniz herhangi bir profil için [profil.*] bölümlerini ekleyerek, varsayılan ayarların herhangi bir alt kümesini geçersiz kılarsınız. Örneğin, dev ve release profilleri için opt-level ayarının varsayılan değerleri şunlardır:

Dosya adı: Cargo.toml

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

Optimizasyon düzeyi (opt-level) ayarı, 0 ila 3 aralığında Rust'ın kodunuza uygulayacağı optimizasyonların sayısını kontrol eder. Bu nedenle dev profili için varsayılan tercih düzeyi 0'dır. Kodunuzu yayınlamaya hazır olduğunuzda, derlemeye daha fazla zaman harcamak en iyisidir. Dağıtım modunda yalnızca bir kez derlersiniz, ancak derlenmiş programı birçok kez çalıştırırsınız, bu nedenle dağıtım modu, daha hızlı çalışan kod için daha uzun derleme süresi değiştirir. Bu nedenle, release profili için varsayılan tercih düzeyi 3'tür.

Cargo.toml'da bunun için farklı bir değer ekleyerek bir varsayılan ayarı geçersiz kılabilirsiniz. Örneğin geliştirme profilinde birinci optimizasyon seviyesini kullanmak istiyorsak projemizin Cargo.toml dosyasına şu iki satırı ekleyebiliriz:

Dosya adı: Cargo.toml

[profile.dev]
opt-level = 1

Bu kod, varsayılan 0 ayarını geçersiz kılar. Şimdi, cargo build'i çalıştırdığımızda, Cargo, dev profili için varsayılanları ve ayrıca opt-level özelleştirmemizi kullanacak. opt-level'i 1 olarak ayarladığımız için, Cargo varsayılandan daha fazla optimizasyon uygulayacak, ancak bir release yapısındaki kadar değil.

Her profil için yapılandırma seçeneklerinin ve varsayılanların tam listesi için Cargo'nun dokümantasyonuna bakın.

Crates.io'da Kasa Yayınlama

Biz crates.io'daki paketleri projemizin bağımlılıkları olarak kullandık, ancak siz de kendi paketlerinizi yayınlayarak kodunuzu diğer insanlarla paylaşabilirsiniz. crates.io'daki kasa kaydı, paketlerinizin kaynak kodunu dağıtır, bu nedenle öncelikle açık kaynak kodunu barındırır.

Rust ve Cargo, yayınladığınız paketin insanlar tarafından bulunmasını ve kullanılmasını kolaylaştıran özelliklere sahiptir. Şimdi bu özelliklerden bazılarından bahsedeceğiz ve ardından bir paketin nasıl yayınlanacağını açıklayacağız.

Faydalı Dokümantasyon Yorumları Yapmak

Paketlerinizi doğru bir şekilde belgelendirmek, diğer kullanıcıların bunları nasıl ve ne zaman kullanacaklarını bilmelerine yardımcı olacaktır, bu nedenle belge yazmak için zaman ayırmaya değer. Bölüm 3'te, iki eğik çizgi (//) kullanarak Rust kodunu nasıl yorumlayacağımızı tartıştık. Rust ayrıca, HTML dokümantasyonu oluşturacak, dokümantasyon yorumu olarak bilinen, dokümantasyon için özel bir yorum türüne sahiptir. HTML, kasanızın nasıl süreklendiğinden ziyade kasanızın nasıl kullanılacağını bilmek isteyen programcılara yönelik genel API öğeleri için dokümantasyon yorumlarının içeriğini görüntüler.

Dokümantasyon yorumları iki yerine üç eğik çizgi (///) kullanır ve metni biçimlendirmek için Markdown gösterimini destekler. Dokümantasyon yorumlarını belgeledikleri öğeden hemen önce yerleştirin. Liste 14-1, my_crate adlı kasadaki add_one fonksiyonu için dokümantasyon yorumlarını göstermektedir.

Dosya adı: src/lib.rs

/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}

Listing 14-1: Fonksiyon için dokümantasyon yorumu

Burada, add_one fonksiyonunun ne işe yaradığına dair bir açıklama veriyoruz, Examples başlıklı bir bölüm açıyoruz ve ardından add_one fonksiyonunun nasıl kullanılacağını gösteren bir kod sunuyoruz. Bu dokümantasyon yorumundan, cargo doc çalıştırarak HTML dokümantasyonunu oluşturabiliriz. Bu komut, Rust ile birlikte dağıtılan rustdoc aracını çalıştırır ve oluşturulan HTML belgelerini target/doc dizinine koyar.

Kolaylık sağlamak için, cargo doc --open komutunu çalıştırmak mevcut kasanızın dokümantasyonu için HTML oluşturacak (ayrıca kasanızın tüm bağımlılıkları için dokümantasyon) ve sonucu bir web tarayıcısında açacaktır. add_one fonksiyonuna gidin ve Şekil 14-1'de gösterildiği gibi dokümantasyon yorumlarındaki metnin nasıl oluşturulduğunu göreceksiniz:

`my_crate`'nin `add_one` fonksiyonu için işlenmiş HTML dokümantasyonu

Şekil 14-1: add_one fonksiyonu için HTML dokümantasyonu

Sıkça Kullanılan Bölümler

HTML'de “Examples” başlıklı bir bölüm oluşturmak için Liste 14-1'deki # Examples başlığını kullandık. İşte kasa yazarlarının belgelerinde yaygın olarak kullandıkları diğer bazı bölümler:

  • Panikler: Belgelenen fonksiyonun panik yapabileceği senaryolar. Programlarının paniklemesini istemeyen fonksiyonu çağıranlar, bu durumlarda fonksiyonu çağırmadıklarından emin olmalıdırlar.
  • Hatalar: Fonksiyon Result döndürüyorsa, oluşabilecek hata türlerini ve hangi koşulların bu hataların
    döndürülmesine neden olabileceğini açıklamak, arayanlara yardımcı olabilir, böylece farklı hata türlerini farklı şekillerde ele almak için kod yazabilirler.
  • Güvenlik: Eğer fonksiyon çağrılması güvenli değilse (güvensizliği Bölüm 19'da tartışacağız), fonksiyonun neden güvensiz olduğunu açıklayan ve fonksiyonun çağıranların uymasını beklediği değişmezleri kapsayan bir bölüm olmalıdır.

Çoğu dokümantasyon açıklamasında bu bölümlerin hepsine gerek yoktur, ancak bu, kodunuzun kullanıcıların bilmek isteyeceği yönlerini size hatırlatmak için iyi bir kontrol listesidir.

Test Olarak Dokümantasyon Yorumları

Belge açıklamalarınıza örnek kod blokları eklemek, kütüphanenizin nasıl kullanılacağını göstermeye yardımcı olabilir ve bunu yapmanın ek bir avantajı vardır: cargo test'i çalıştırmak, belgelerinizdeki kod örneklerini test olarak çalıştıracaktır! Hiçbir şey örnekli dokümantasyondan daha iyi olamaz. Ancak hiçbir şey, dokümantasyonun yazılmasından bu yana kod değiştiği için çalışmayan örneklerden daha kötü olamaz. Liste 14-1'deki add_one fonksiyonunun dokümantasyonu ile cargo test'i çalıştırırsak, test sonuçlarında aşağıdaki gibi bir bölüm görürüz:

   Doc-tests my_crate

running 1 test
test src/lib.rs - add_one (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s

Şimdi fonksiyonu ya da örneği değiştirirsek, örnekteki assert_eq! panik yapar ve cargo test'i tekrar çalıştırırsak, dokümantasyon testlerinin örnek ve kodun birbiriyle senkronize olmadığını yakaladığını göreceğiz!

İçerdiği Öğeleri Yorumlama

Dokümantasyon yorum satırı (//!) stili, belgeleri yorumları takip eden öğeler yerine yorumları içeren öğeye ekler. Bu dokümantasyon yorumları genellikle kasa kök dosyasının içinde (geleneksel olarak src/lib.rs) veya bir modülün içinde kasayı veya modülü bir bütün olarak belgelemek için kullanırız.

Örneğin, add_one fonksiyonunu içeren my_crate kasasının amacını açıklayan belgeler eklemek için, Liste 14-2'de gösterildiği gibi src/lib.rs dosyasının başına //! ile başlayan belge yorumları ekleriz:

Dosya adı: src/lib.rs

//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.

/// Adds one to the number given.
// --snip--
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}

Liste 14-2: Bir bütün olarak my_crate kasası için dokümantasyon

Son satırdan sonra //! ile başlayan herhangi bir kod olmadığına dikkat edin. Yorumlara /// yerine //! ile başladığımız için, bu yorumu takip eden bir öğe yerine bu yorumu içeren öğeyi belgeliyoruz. Bu durumda, bu öğe kasa kökü olan src/lib.rs dosyasıdır. Bu yorumlar tüm kasayı tanımlar.

cargo doc --open komutunu çalıştırdığımızda, bu yorumlar Şekil 14-2'de gösterildiği gibi my_crate dokümantasyonunun ön sayfasında kasadaki genel öğelerin listesinin üzerinde görüntülenecektir:

Bir bütün olarak kasa için bir yorum içeren işlenmiş HTML dokümantasyonu

Öğeler içindeki dokümantasyon yorumları özellikle kasaları ve modülleri tanımlamak için kullanışlıdır. Kullanıcılarınızın kasanın organizasyonunu anlamalarına yardımcı olmak için konteynerin genel amacını açıklamak için bunları kullanın.

pub use ile Kullanışlı Genel API'yi Dışa Aktarma

Genel API'nizin yapısı, bir kasa yayınlarken göz önünde bulundurulması gereken önemli bir husustur. Kasanızı kullanan kişiler yapıya sizden daha az aşinadır ve kasanızın büyük bir modül hiyerarşisine sahipse kullanmak istedikleri parçaları bulmakta zorluk çekebilirler.

Bölüm 7'de, pub anahtar sözcüğünü kullanarak öğeleri nasıl herkese açık hale getireceğimizi ve use anahtar sözcüğünü kullanarak öğeleri bir kapsama nasıl dahil edeceğimizi ele almıştık. Ancak, bir kasa geliştirirken size mantıklı gelen yapı, kullanıcılarınız için çok uygun olmayabilir. Yapılarınızı birden fazla seviye içeren bir hiyerarşide düzenlemek isteyebilirsiniz, ancak bu durumda hiyerarşinin derinliklerinde tanımladığınız bir türü kullanmak isteyen kişiler bu türün var olduğunu bulmakta zorlanabilir. Ayrıca, use my_crate::UsefulType; yerine use my_crate::some_module::another_module::UsefulType; girmek zorunda kalmaktan da rahatsız olabilirler.

İyi haber şu ki, yapı başkalarının başka bir kütüphaneden kullanması için uygun değilse, iç organizasyonunuzu yeniden düzenlemeniz gerekmez: bunun yerine, pub use kullanarak özel yapınızdan farklı bir genel yapı oluşturmak için öğeleri yeniden dışa aktarabilirsiniz. Yeniden dışa aktarma, bir konumdaki herkese açık bir öğeyi alır ve sanki diğer konumda tanımlanmış gibi başka bir konumda herkese açık hale getirir.

Örneğin, sanatsal kavramları modellemek için art adında bir kütüphane oluşturduğumuzu varsayalım. Bu kütüphanede iki modül vardır: PrimaryColor ve SecondaryColor adında iki enum içeren bir kinds modülü ve Liste 14-3'te gösterildiği gibi mix adında bir fonksiyon içeren bir utils modülü:

Dosya adı: src/lib.rs

//! # Art
//!
//! A library for modeling artistic concepts.

pub mod kinds {
    /// The primary colors according to the RYB color model.
    pub enum PrimaryColor {
        Red,
        Yellow,
        Blue,
    }

    /// The secondary colors according to the RYB color model.
    pub enum SecondaryColor {
        Orange,
        Green,
        Purple,
    }
}

pub mod utils {
    use crate::kinds::*;

    /// Combines two primary colors in equal amounts to create
    /// a secondary color.
    pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
        // --snip--
        unimplemented!();
    }
}

Liste 14-3: kinds ve utils halinde düzenlenmiş öğeler içeren bir art kütüphanesi

Şekil 14-3, bu kasa için cargo doc tarafından oluşturulan belgelerin ön sayfasının nasıl görüneceğini göstermektedir:

`art` kasası için `kinds` ve `utils` modüllerini listeleyen işlenmiş dokümantasyonlar

Şekil 14-3: kinds ve utils modüllerini listeleyen art dokümantasyonunun ön sayfası

PrimaryColor ve SecondaryColor türlerinin ön sayfada listelenmediğine ve mix fonksiyonunun da bulunmadığına dikkat edin. Bunları görmek için türlere ve yardımcı programlara tıklamamız gerekiyor.

Bu kütüphaneye bağlı olan başka bir kasa, şu anda tanımlanmış olan modül yapısını belirterek, art'taki öğeleri kapsama getiren use ifade yapılarına ihtiyaç duyacaktır. Liste 14-4, art kasasındaki PrimaryColor ve mix öğelerini kullanan bir kasa örneğini göstermektedir:

Dosya adı: src/main.rs

use art::kinds::PrimaryColor;
use art::utils::mix;

fn main() {
    let red = PrimaryColor::Red;
    let yellow = PrimaryColor::Yellow;
    mix(red, yellow);
}

Liste 14-4: İç yapısı dışa aktarılmış art kasasının öğelerini kullanan bir kasa

Liste 14-4'teki art kasasını kullanan kodun yazarı, PrimaryColor'ın kinds modülünde ve mix'in utils modülünde olduğunu anlamak zorunda kalmıştır. art kasasının modül yapısı, onu kullananlardan ziyade art kasası üzerinde çalışan geliştiriciler için daha önemlidir. İç yapı, art kasasının nasıl kullanılacağını anlamaya çalışan biri için herhangi bir yararlı bilgi içermez, aksine kafa karışıklığına neden olur, çünkü onu kullanan geliştiriciler nereye bakacaklarını bulmak ve use deyimlerinde modül adlarını belirtmek zorundadır.

Dahili organizasyonu genel API'den kaldırmak için, Liste 14-3'teki art kasa kodunu değiştirerek, Liste 14-5'te gösterildiği gibi üst düzeydeki öğeleri yeniden dışa aktarmak için pub use ifade yapısını ekleyebiliriz:

Dosya adı: src/lib.rs

//! # Art
//!
//! A library for modeling artistic concepts.

pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;

pub mod kinds {
    // --snip--
    /// The primary colors according to the RYB color model.
    pub enum PrimaryColor {
        Red,
        Yellow,
        Blue,
    }

    /// The secondary colors according to the RYB color model.
    pub enum SecondaryColor {
        Orange,
        Green,
        Purple,
    }
}

pub mod utils {
    // --snip--
    use crate::kinds::*;

    /// Combines two primary colors in equal amounts to create
    /// a secondary color.
    pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
        SecondaryColor::Orange
    }
}

Liste 14-5: Öğeleri yeniden dışa aktarmak için pub use ifade yapıları ekleme

cargo doc'un bu kasa için oluşturduğu API dokümantasyonu artık Şekil 14-4'te gösterildiği gibi ön sayfada yeniden dışa aktarmaları listeleyecek ve bağlayacak, böylece PrimaryColor ve SecondaryColor türleri ile mix fonksiyonunun bulunması kolaylaşacaktır.

Ön sayfadaki yeniden dışa aktarımlarla birlikte `art` kasası için oluşturulmuş dokümantasyonlar

Şekil 14-4: Yeniden dahil etmeyi listeleyen art dokümantasyonunun ön sayfası

art kasası kullanıcıları, Liste 14-4'te gösterildiği gibi Liste 14-3'teki dahili yapıyı görmeye ve kullanmaya devam edebilir ya da Liste 14-6'da gösterildiği gibi Liste 14-5'teki daha kullanışlı yapıyı kullanabilirler: Dosya adı: src/main.rs

use art::mix;
use art::PrimaryColor;

fn main() {
    // --snip--
    let red = PrimaryColor::Red;
    let yellow = PrimaryColor::Yellow;
    mix(red, yellow);
}

Liste 14-6: art kasasından dahil edilen öğeleri kullanan bir program

İç içe geçmiş çok sayıda modülün bulunduğu durumlarda, en üst seviyedeki türlerin pub kullanımıyla yeniden dışa aktarılması, kasayı kullanan kişilerin deneyiminde önemli bir fark yaratabilir.

Kullanışlı bir genel API yapısı oluşturmak bilimden çok bir sanattır ve kullanıcılarınız için en iyi çalışan API'yi bulmak için yineleme yapabilirsiniz. pub kullanımını seçmek, kasanızı dahili olarak nasıl yapılandırdığınız konusunda size esneklik sağlar ve bu dahili yapıyı kullanıcılarınıza sunduğunuzdan ayırır. İç yapılarının genel API'lerinden farklı olup olmadığını görmek için yüklediğiniz bazı kasaların kodlarına bakın.

Crates.io Hesabı Oluşturma

Herhangi bir kasayı yayınlayabilmeniz için önce crates.io'da bir hesap oluşturmanız ve bir API anahtarı almanız gerekir. Bunu yapmak için crates.io adresindeki ana sayfayı ziyaret edin ve bir GitHub hesabı aracılığıyla oturum açın. (GitHub hesabı şu anda bir gerekliliktir, ancak site gelecekte hesap oluşturmanın diğer yollarını da gelecekte destekleyebilir). Giriş yaptıktan sonra https://crates.io/me/ adresinden hesap ayarlarınızı ziyaret edin ve API anahtarınızı alın. Ardından cargo login komutunu API anahtarınızla aşağıdaki gibi çalıştırın:

$ cargo login abcdefghijklmnopqrstuvwxyz012345

Bu komut Cargo'ya API token'ınızı bildirecek ve yerel olarak ~/.cargo/credentials içinde saklayacaktır. Bu belirtecin bir sır olduğunu unutmayın: başkasıyla paylaşmayın. Herhangi bir nedenle herhangi biriyle paylaşırsanız, iptal etmeli ve crates.io'dan yeni bir token oluşturmalısınız.

Yeni Bir Kasaya Meta Veri Ekleme

Diyelim ki yayınlamak istediğiniz bir kasanız var. Yayınlamadan önce, kasanın Cargo.toml dosyasının [package] bölümüne bazı meta veriler eklemeniz gerekir.

Kasanızın benzersiz bir isme ihtiyacı olacaktır. Yerel olarak bir kasa üzerinde çalışırken, sandığa istediğiniz adı verebilirsiniz. Ancak crates.io'daki kasa adları ilk gelene ilk hizmet esasına göre tahsis edilir. Bir kasa adı alındıktan sonra, başka hiç kimse bu adla bir kasa yayınlayamaz. Bir kasa yayınlamaya çalışmadan önce, kullanmak istediğiniz adı arayın. İsim kullanılmışsa, başka bir isim bulmanız ve Cargo.toml dosyasında [package] bölümü altındaki name alanını, yayınlama için yeni ismi kullanacak şekilde düzenlemeniz gerekecektir:

Dosya adı: Cargo.toml

[package]
name = "guessing_game"

Benzersiz bir ad seçmiş olsanız bile, bu noktada kasayı yayınlamak için cargo publish'i çalıştırdığınızda, bir uyarı ve ardından bir hata alırsınız:

$ cargo publish
    Updating crates.io index
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
--snip--
error: failed to publish to registry at https://crates.io

Caused by:
  the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata

Bu hata, bazı önemli bilgilerin eksik olmasından kaynaklanmaktadır: insanların kasanızın ne işe yaradığını ve hangi koşullar altında kullanabileceklerini bilmeleri için bir açıklama ve lisans gereklidir. Cargo.toml dosyasına sadece bir veya iki cümlelik bir açıklama ekleyin, çünkü bu açıklama arama sonuçlarında kasanızla birlikte görünecektir. license alanı için bir lisans tanımlayıcı değeri vermeniz gerekir. Linux Foundation’un Software Package Data Exchange (SPDX) listesi bu değer için kullanabileceğiniz tanımlayıcıları listeler. Örneğin, kasanızı MIT Lisansı kullanarak lisansladığınızı belirtmek için MIT tanımlayıcısını ekleyin:

Dosya adı: Cargo.toml

[package]
name = "guessing_game"
license = "MIT"

SPDX'te görünmeyen bir lisans kullanmak istiyorsanız, bu lisansın metnini bir dosyaya yerleştirmeniz, dosyayı projenize dahil etmeniz ve ardından lisans anahtarını kullanmak yerine bu dosyanın adını belirtmek için license-file kullanmanız gerekir.

Projeniz için hangi lisansın uygun olduğuna ilişkin rehberlik bu kitabın kapsamı dışındadır. Rust topluluğundaki birçok kişi, MIT VEYA Apache-2.0 ikili lisansını kullanarak projelerini Rust ile aynı şekilde lisanslar. Bu uygulama, projeniz için birden fazla lisansa sahip olmak için OR ile ayrılmış birden fazla lisans tanımlayıcısı da belirtebileceğinizi göstermektedir.

Benzersiz bir ad, sürüm, açıklamanız ve bir lisans eklendiğinde, yayınlamaya hazır bir proje için Cargo.toml dosyası aşağıdaki gibi görünebilir:

Dosya adı: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"

[dependencies]

Cargo’nun dokümantasyonunda, başkalarının kasanızı daha kolay keşfedip kullanabilmesini sağlamak için belirtebileceğiniz diğer meta veriler açıklanmaktadır.

Crates.io'da Yayınlama Süreci

Artık bir hesap oluşturduğunuza, API token'ınızı kaydettiğinize, kasanız için bir ad seçtiğinize ve gerekli meta verileri belirlediğinize göre yayınlamaya hazırsınız! Bir kasa yayınlamak, başkalarının kullanması için crates.io'ya belirli bir sürümü yükler.

Dikkatli olun, çünkü yayınlama kalıcıdır. Sürümün üzerine asla yazılamaz ve kod silinemez. crates.io'nun en önemli amaçlarından biri kalıcı bir kod arşivi olarak hareket etmektir, böylece crates.io'daki kasalara bağlı olan tüm projelerin yapıları çalışmaya devam edecektir. Sürüm silme işlemlerine izin vermek bu hedefin gerçekleştirilmesini imkansız hale getirecektir. Bununla birlikte, yayınlayabileceğiniz kasa sürümlerinin sayısında bir sınır yoktur.

cargo publish komutunu tekrar çalıştırın. Şimdi çalışması gerekir:

$ cargo publish
    Updating crates.io index
   Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
   Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
   Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
    Finished dev [unoptimized + debuginfo] target(s) in 0.19s
   Uploading guessing_game v0.1.0 (file:///projects/guessing_game)

Tebrikler! Artık kodunuzu Rust topluluğu ile paylaştınız ve herkes kasanızı kolayca projelerinin bir bağımlılığı olarak ekleyebilir.

Mevcut Bir Kasanın Yeni Sürümünü Yayınlama

Sandığınızda değişiklikler yaptığınızda ve yeni bir sürüm yayınlamaya hazır olduğunuzda, Cargo.toml dosyanızda belirtilen sürüm değerini değiştirir ve yeniden yayınlarsınız. Yaptığınız değişiklik türlerine göre uygun bir sonraki sürüm numarasının ne olduğuna karar vermek için Anlamsal Sürüm Oluşturma kurallarını kullanın. Ardından yeni sürümü göndermek için cargo publish'ı çalıştırın.

cargo yank ile Crates.io'dan Sürümleri Kaldırma

Bir sandığın önceki sürümlerini kaldıramasanız da, gelecekteki projelerin bunları yeni bir bağımlılık olarak eklemesini önleyebilirsiniz. Bu, bir kasa sürümü bir nedenle bozulduğunda kullanışlıdır. Bu gibi durumlarda, Cargo kasa sürümünün kaldırılmasını destekler.

Bir sürümü çekmek, yeni projelerin o sürüme bağlı olmasını engellerken, ona bağlı olan tüm mevcut projelerin devam etmesine izin verir. Esasen, çekme, Cargo.lock'a sahip tüm projelerin bozulmayacağı ve gelecekte üretilen herhangi bir Cargo.lock dosyasının çekilmiş sürümü kullanmayacağı anlamına gelir.

Bir kasanın sürümünü çekmek için, daha önce yayınladığınız kasanın dizininde cargo yank komutunu çalıştırın ve hangi sürümü çekmek istediğinizi belirtin:

$ cargo yank --vers 1.0.1

Ayrıca komuta --undo ekleyerek bir çekme işlemini geri alabilir ve projelerin yeniden bir sürüme bağlı olarak başlamasına izin verebilirsiniz:

$ cargo yank --vers 1.0.1 --undo

Bir çekme herhangi bir kodu silmez. Örneğin, yanlışlıkla yüklenen sırları silemez. Böyle bir durumda, bu sırları derhal sıfırlamanız gerekir.

Cargo Workspaces

In Chapter 12, we built a package that included a binary crate and a library crate. As your project develops, you might find that the library crate continues to get bigger and you want to split your package further into multiple library crates. Cargo offers a feature called workspaces that can help manage multiple related packages that are developed in tandem.

Creating a Workspace

A workspace is a set of packages that share the same Cargo.lock and output directory. Let’s make a project using a workspace—we’ll use trivial code so we can concentrate on the structure of the workspace. There are multiple ways to structure a workspace, so we'll just show one common way. We’ll have a workspace containing a binary and two libraries. The binary, which will provide the main functionality, will depend on the two libraries. One library will provide an add_one function, and a second library an add_two function. These three crates will be part of the same workspace. We’ll start by creating a new directory for the workspace:

$ mkdir add
$ cd add

Next, in the add directory, we create the Cargo.toml file that will configure the entire workspace. This file won’t have a [package] section or the metadata we’ve seen in other Cargo.toml files. Instead, it will start with a [workspace] section that will allow us to add members to the workspace by specifying the path to the package with our binary crate; in this case, that path is adder:

Filename: Cargo.toml

[workspace]

members = [
    "adder",
]

Next, we’ll create the adder binary crate by running cargo new within the add directory:

$ cargo new adder
     Created binary (application) `adder` package

At this point, we can build the workspace by running cargo build. The files in your add directory should look like this:

├── Cargo.lock
├── Cargo.toml
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

The workspace has one target directory at the top level that the compiled artifacts will be placed into; the adder package doesn’t have its own target directory. Even if we were to run cargo build from inside the adder directory, the compiled artifacts would still end up in add/target rather than add/adder/target. Cargo structures the target directory in a workspace like this because the crates in a workspace are meant to depend on each other. If each crate had its own target directory, each crate would have to recompile each of the other crates in the workspace to place the artifacts in its own target directory. By sharing one target directory, the crates can avoid unnecessary rebuilding.

Creating the Second Package in the Workspace

Next, let’s create another member package in the workspace and call it add_one. Change the top-level Cargo.toml to specify the add_one path in the members list:

Filename: Cargo.toml

[workspace]

members = [
    "adder",
    "add_one",
]

Then generate a new library crate named add_one:

$ cargo new add_one --lib
     Created library `add_one` package

Your add directory should now have these directories and files:

├── Cargo.lock
├── Cargo.toml
├── add_one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

In the add_one/src/lib.rs file, let’s add an add_one function:

Filename: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

Now we can have the adder package with our binary depend on the add_one package that has our library. First, we’ll need to add a path dependency on add_one to adder/Cargo.toml.

Filename: adder/Cargo.toml

[dependencies]
add_one = { path = "../add_one" }

Cargo doesn’t assume that crates in a workspace will depend on each other, so we need to be explicit about the dependency relationships.

Next, let’s use the add_one function (from the add_one crate) in the adder crate. Open the adder/src/main.rs file and add a use line at the top to bring the new add_one library crate into scope. Then change the main function to call the add_one function, as in Listing 14-7.

Filename: adder/src/main.rs

use add_one;

fn main() {
    let num = 10;
    println!(
        "Hello, world! {} plus one is {}!",
        num,
        add_one::add_one(num)
    );
}

Listing 14-7: Using the add_one library crate from the adder crate

Let’s build the workspace by running cargo build in the top-level add directory!

$ cargo build
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 0.68s

To run the binary crate from the add directory, we can specify which package in the workspace we want to run by using the -p argument and the package name with cargo run:

$ cargo run -p adder
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/adder`
Hello, world! 10 plus one is 11!

This runs the code in adder/src/main.rs, which depends on the add_one crate.

Depending on an External Package in a Workspace

Notice that the workspace has only one Cargo.lock file at the top level, rather than having a Cargo.lock in each crate’s directory. This ensures that all crates are using the same version of all dependencies. If we add the rand package to the adder/Cargo.toml and add_one/Cargo.toml files, Cargo will resolve both of those to one version of rand and record that in the one Cargo.lock. Making all crates in the workspace use the same dependencies means the crates will always be compatible with each other. Let’s add the rand crate to the [dependencies] section in the add_one/Cargo.toml file so we can use the rand crate in the add_one crate:

Filename: add_one/Cargo.toml

[dependencies]
rand = "0.8.3"

We can now add use rand; to the add_one/src/lib.rs file, and building the whole workspace by running cargo build in the add directory will bring in and compile the rand crate. We will get one warning because we aren’t referring to the rand we brought into scope:

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.3
   --snip--
   Compiling rand v0.8.3
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
 --> add_one/src/lib.rs:1:5
  |
1 | use rand;
  |     ^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: 1 warning emitted

   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 10.18s

The top-level Cargo.lock now contains information about the dependency of add_one on rand. However, even though rand is used somewhere in the workspace, we can’t use it in other crates in the workspace unless we add rand to their Cargo.toml files as well. For example, if we add use rand; to the adder/src/main.rs file for the adder package, we’ll get an error:

$ cargo build
  --snip--
   Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
 --> adder/src/main.rs:2:5
  |
2 | use rand;
  |     ^^^^ no external crate `rand`

To fix this, edit the Cargo.toml file for the adder package and indicate that rand is a dependency for it as well. Building the adder package will add rand to the list of dependencies for adder in Cargo.lock, but no additional copies of rand will be downloaded. Cargo has ensured that every crate in every package in the workspace using the rand package will be using the same version, saving us space and ensuring that the crates in the workspace will be compatible with each other.

Adding a Test to a Workspace

For another enhancement, let’s add a test of the add_one::add_one function within the add_one crate:

Filename: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(3, add_one(2));
    }
}

Now run cargo test in the top-level add directory. Running cargo test in a workspace structured like this one will run the tests for all the crates in the workspace:

$ cargo test
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.27s
     Running target/debug/deps/add_one-f0253159197f7841

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running target/debug/deps/adder-49979ff40686fa8e

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

The first section of the output shows that the it_works test in the add_one crate passed. The next section shows that zero tests were found in the adder crate, and then the last section shows zero documentation tests were found in the add_one crate.

We can also run tests for one particular crate in a workspace from the top-level directory by using the -p flag and specifying the name of the crate we want to test:

$ cargo test -p add_one
    Finished test [unoptimized + debuginfo] target(s) in 0.00s
     Running target/debug/deps/add_one-b3235fea9a156f74

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

This output shows cargo test only ran the tests for the add_one crate and didn’t run the adder crate tests.

If you publish the crates in the workspace to crates.io, each crate in the workspace will need to be published separately. Like cargo test, we can publish a particular crate in our workspace by using the -p flag and specifying the name of the crate we want to publish.

For additional practice, add an add_two crate to this workspace in a similar way as the add_one crate!

As your project grows, consider using a workspace: it’s easier to understand smaller, individual components than one big blob of code. Furthermore, keeping the crates in a workspace can make coordination between crates easier if they are often changed at the same time.

cargo install ile İkili Dosyaları Yükleme

cargo install komutu, ikili kasaları yerelde kurmanıza ve kullanmanıza olanak tanır. Bunun sistem paketlerinin yerini alması amaçlanmamıştır; Rust geliştiricilerinin, diğer geliştiricilerin crates.io'da paylaştığı araçları yüklemeleri için uygun bir yol olması amaçlanmıştı. Yalnızca ikili hedefleri olan paketleri kurabileceğinizi unutmayın. İkili hedef, sandıkta bir src/main.rs dosyası veya ikili olarak belirtilen başka bir dosya varsa o kasa çalıştırılabilir kod içeriyor olabilir. Genellikle kasalar, README dosyasında bir kasanın kütüphane mi, çalıştırılabilir mi yoksa her ikisi mi olduğu hakkında bilgi içermektedir.

cargo install ile kurulan tüm ikili dosyalar, kurulum kökünün bin dizininde saklanır. Rust'ı rustup.rs kullanarak yüklediyseniz ve herhangi bir özel yapılandırmanız yoksa bu dizin $HOME/.cargo/bin olacaktır. cargo install'i kullanarak kurduğunuz programları çalıştırabilmek için dizinin $PATH içinde olduğundan emin olun.

Örneğin, Bölüm 12'de, dosyaları aramak için ripgrep adlı grep aracının bir Rust süreklemesi olduğundan bahsetmiştik. Ripgrep'i kurmak için aşağıdakileri çalıştırabiliriz:

$ cargo install ripgrep
    Updating crates.io index
  Downloaded ripgrep v11.0.2
  Downloaded 1 crate (243.3 KB) in 0.88s
  Installing ripgrep v11.0.2
--snip--
   Compiling ripgrep v11.0.2
    Finished release [optimized + debuginfo] target(s) in 3m 10s
  Installing ~/.cargo/bin/rg
   Installed package `ripgrep v11.0.2` (executable `rg`)

Aynı şekilde, bir inşa sistemi olan Elite'i de aynı yolla (cargo install elite) kurabiliriz.

Özel Komutlarla Cargo'yu Genişletme

Cargo, Cargo'yu değiştirmek zorunda kalmadan yeni alt komutlarla genişletebilmeniz için tasarlanmıştır. $PATH'inizdeki bir ikili dosya cargo-bir şey olarak adlandırılmışsa, cargo-bir şey'i çalıştırarak onu bir Cargo alt komutuymuş gibi çalıştırabilirsiniz. Bunun gibi özel komutlar, cargo --list çalıştırdığınızda da listelenir. Uzantıları yüklemek için cargo install'ı kullanabilmek ve ardından bunları yerleşik Cargo araçları gibi çalıştırabilmek,

Özet

Cargo ve crates.io ile kod paylaşımı, Rust ekosistemini birçok farklı görev için faydalı kılan şeyin bir parçasıdır. Rust'ın standart kütüphanesi küçük ve kararlıdır, ancak kasaların dilden farklı bir zaman çizelgesinde paylaşılması, kullanılması ve geliştirilmesi kolaydır. crates.io'da işinize yarayacak kodu paylaşmaktan çekinmeyin; muhtemelen bir başkası için de faydalı olacaktır!

Akıllı İşaretçiler

İşaretçi, bellekte bir adres içeren bir değişken için genel bir kavramdır. Bu adres, diğer bazı verilere atıfta bulunur veya “işaret eder”. Rust'taki en yaygın işaretçi türü, Bölüm 4'te öğrendiğiniz bir referanstır. Referanslar & sembolü ile gösterilir ve işaret ettikleri değeri ödünç alırlar. Verilere atıfta bulunmak dışında herhangi bir özel yetenekleri yoktur ve ek yükleri yoktur.

Akıllı işaretçiler ise işaretçi gibi davranan ancak ek meta veri ve yeteneklere sahip veri yapılarıdır. Akıllı işaretçiler kavramı Rust'a özgü değildir: akıllı işaretçiler ilk C++'ta ortaya çıkmıştır ve diğer birçok dilde de mevcuttur. Rust, standart kütüphanede tanımlanan ve referanslar tarafından sağlananın ötesinde işlevsellik sağlayan çeşitli akıllı işaretçilere sahiptir. Genel konsepti keşfetmek için, bir referans sayma akıllı işaretçi türü de dahil olmak üzere birkaç farklı akıllı işaretçi örneğine bakacağız. Bu işaretçi, sahiplerin sayısını takip ederek ve hiçbir sahip kalmadığında verileri temizleyerek verilerin birden fazla sahibinin olmasına izin vermenizi sağlar.

Sahiplik ve ödünç alma kavramıyla Rust, referanslar ve akıllı işaretçiler arasında ek bir farka sahiptir: referanslar yalnızca veri ödünç alırken, çoğu durumda akıllı işaretçiler işaret ettikleri verilere sahiptir.

O zamanlar onları bu kadar çok farklı şekillerde adlandırmasak da, Bölüm 8'de konuştuğumuz String ve Vec<T> dahil olmak üzere bu kitapta birkaç akıllı işaretçiyle zaten karşılaştık. Ek olarak bu işaretçiler meta verilere ve ekstra yeteneklere veya garantilere sahiptirler. Örneğin String, kapasitesini meta veri olarak depolar ve verilerinin her zaman geçerli bir UTF-8 olmasını sağlamak için ekstra yeteneğe sahiptir.

Akıllı işaretçiler genellikle yapılar kullanılarak süreklenirler. Sıradan bir yapının aksine, akıllı işaretçiler Deref ve Drop tanımlarını uygular. Deref özelliği, akıllı işaretçi yapısının bir örneğinin referansı gibi davranmasına izin verir, böylece kodunuzu başvurularla veya akıllı işaretçilerle çalışacak şekilde yazabilirsiniz. Drop tanımı, akıllı işaretçinin bir örneği kapsam dışına çıktığında çalıştırılan kodu özelleştirmenize olanak tanır. Bu bölümde, her iki özelliği de tartışacağız ve akıllı işaretçiler için neden önemli olduklarını göstereceğiz.

Akıllı işaretçi kalıbının Rust'ta sıkça kullanılan genel bir tasarım kalıbı olduğu düşünüldüğünde, bu bölüm mevcut her akıllı işaretçiyi kapsamayacaktır. Birçok kütüphanenin kendi akıllı işaretçileri vardır ve hatta kendinizinkini bile yazabilirsiniz. Bu bölümde standart kütüphanedeki en yaygın akıllı işaretçileri ele alacağız:

  • Box<T>, yığın üzerinde yer tahsis eden bir tür
  • Rc<T>, çoklu sahiplik sağlayan bir referans sayma türü
  • Ref<T> ve RefMut<T>, RefCell<T> aracılığıyla erişilen, derleme zamanı yerine çalışma zamanında ödünç alma kurallarını uygulamaya zorlayan bir tür

Ek olarak, değişmez bir türün bir iç değeri mutasyona uğratmak için çıkardığı iç değişebilirlik modelini ele alacağız. Ayrıca referans döngülerinin nasıl bellek sızdırabilecekleri ve nasıl önlenebilecekleri hakkında tartışacağız.

Öyleyse, hadi dalalım!

Yığın Üzerindeki Verilere İşaret Etmek için Box<T> Kullanma

En basit akıllı işaretçi, türü Box<T> olarak yazılan bir çeşit kutudur. Kutular, verileri yığıt yerine yığın (heap) üzerinde saklamanıza olanak tanır. Yığıtta kalan şey, yığın verisinin işaretçisidir. Yığıt ve yığın arasındaki farkı incelemek için Bölüm 4'e bakın.

Kutular, verilerini yığıt yerine yığın üzerinde saklamak dışında performans ek yüküne sahip değildir. Ancak çok fazla ekstra yetenekleri de yoktur. Onları en çok bu durumlarda kullanacaksınız:

  • Boyutu derleme zamanında bilinemeyen bir türünüz olduğunda ve bu türden bir değeri tam boyut gerektiren bir bağlamda kullanmak istediğinizde
  • Büyük miktarda veriye sahip olduğunuzda ve sahipliği devretmek istediğinizde ancak bunu yaparken verilerin kopyalanmayacağından emin olmak istediğinizde
  • Bir değere sahip olmak istediğinizde ve belirli bir türde olmasından ziyade yalnızca belirli bir tanımı sürekleyen bir tür olmasını önemsediğinizde

İlk durumu “Kutularla Özyinelemeli Türleri Etkinleştirme” bölümünde göstereceğiz. İkinci durumda, büyük miktarda verinin sahipliğinin aktarılması uzun sürebilir çünkü veriler yığıt üzerinde kopyalanır. Bu durumda performansı artırmak için, büyük miktardaki veriyi yığında bir kutu içinde saklayabiliriz. Ardından, yalnızca küçük miktarda işaretçi verisi yığıt üzerinde kopyalanırken, referans verdiği veriler yığın üzerinde tek bir yerde kalır. Üçüncü durum özellik nesnesi olarak bilinir ve Bölüm 17, “Farklı Türlerde Değerlere İzin Veren Tanım Nesnelerini Kullanma” başlıklı bir bölümün tamamını bu konuya ayırmıştır. Yani burada öğrendiklerinizi Bölüm 17'de tekrar uygulayacaksınız!

Verileri Yığın Üzerinde Saklamak için Box<T> Kullanma

Box<T> için yığın depolama kullanım durumunu tartışmadan önce, söz dizimini ve bir Box<T> içinde depolanan değerlerle nasıl etkileşimde bulunacağımızı ele alacağız.

Liste 15-1, bir i32 değerini yığın üzerinde saklamak için bir kutunun nasıl kullanılacağını gösterir:

Dosya adı: src/main.rs

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

Liste 15-1: Bir kutu kullanarak bir i32 değerini yığın üzerinde saklama

b değişkenini, yığın üzerinde ayrılmış olan 5 değerine işaret eden bir Box değerine sahip olacak şekilde tanımlarız. Bu program b = 5 yazdıracaktır; bu durumda, kutudaki verilere, bu veriler yığıtta olsaydı yapacağımız gibi erişebiliriz. Tıpkı sahip olunan herhangi bir değer gibi, main'in sonunda b'nin yaptığı gibi bir kutu kapsam dışına çıktığında, bellekten silinecektir. Bellekten silme işlemi hem kutu (yığıt üzerinde saklanır) hem de işaret ettiği veri (yığın üzerinde saklanır) için gerçekleşir.

Yığına tek bir değer koymak çok kullanışlı değildir, bu nedenle kutuları bu şekilde tek başlarına çok sık kullanmazsınız. Varsayılan olarak saklandıkları yığıtta tek bir i32 gibi değerlere sahip olmak, çoğu durumda daha uygundur. Kutuların, kutular olmasaydı tanımlamamıza izin verilmeyecek türleri tanımlamamıza izin verdiği bir duruma bakalım.

Kutularla Özyinelemeli Türleri Etkinleştirme

Özyinelemeli türdeki bir değer, kendisinin bir parçası olarak aynı türde başka bir değere sahip olabilir. Özyinelemeli türler bir sorun teşkil eder çünkü derleme zamanında Rust'ın bir türün ne kadar yer kapladığını bilmesi gerekir. Ancak, özyinelemeli türlerin değerlerinin iç içe geçmesi teorik olarak sonsuza kadar devam edebilir, bu nedenle Rust değerin ne kadar alana ihtiyaç duyduğunu bilemez. Kutular bilinen bir boyuta sahip olduğundan, özyinelemeli tür tanımına bir kutu ekleyerek özyinelemeli türleri etkinleştirebiliriz.

Bir özyinelemeli tür örneği olarak, cons listesini inceleyelim. Bu, fonksiyonel programlama dillerinde yaygın olarak bulunan bir veri türüdür. Tanımlayacağımız cons listesi türü, özyineleme dışında basittir; bu nedenle, üzerinde çalışacağımız örnekteki kavramlar, özyinelemeli türleri içeren daha karmaşık durumlarla karşılaştığınızda yararlı olacaktır.

Cons Listesi Hakkında Daha Fazla Bilgi

Cons listesi, Lisp programlama dili ve lehçelerinden gelen ve iç içe geçmiş çiftlerden oluşan bir veri yapısıdır. Adı, Lisp'te iki argümanından yeni bir çift oluşturan cons fonksiyonundan (“construct function”'ın kısaltması) gelir. Bir değer ve başka bir çiftten oluşan bir çift üzerinde cons çağırarak, özyinelemeli çiftlerden oluşan cons listeleri oluşturabiliriz.

Örneğin, burada 1, 2, 3 listesini içeren ve her bir çifti parantez içinde olan bir cons listesinin sözde kod gösterimi yer almaktadır:

(1, (2, (3, Nil)))

Bir cons listesindeki her öğe iki öğe içerir: geçerli öğenin değeri ve bir sonraki öğe. Listedeki son öğe, bir sonraki öğe olmaksızın yalnızca Nil adlı bir değer içerir. Bir cons listesi, cons fonksiyonunun özyinelemeli olarak çağrılmasıyla oluşturulur. Özyinelemenin temel durumunu gösteren yaygın kullanılan ad Nil'dir. Bunun Bölüm 6'daki “null” or “nil” kavramıyla aynı olmadığına dikkat edin; bu kavram geçersiz veya olmayan bir değerdir.

Cons listesi Rust'ta yaygın olarak kullanılan bir veri yapısı değildir. Rust'ta bir öğe listesine sahip olduğunuzda çoğu zaman Vec<T> kullanmak daha iyi bir seçimdir. Diğer, daha karmaşık özyinelemeli veri tipleri çeşitli durumlarda kullanışlıdır, ancak bu bölümde cons listesi ile başlayarak, kutuların fazla dikkat dağıtmadan özyinelemeli bir veri tipi tanımlamamıza nasıl izin verdiğini keşfedebiliriz.

Liste 15-2, cons listesi için bir enum tanımını içerir. Bu kodun henüz derlenmeyeceğini unutmayın çünkü List türünün bilinen bir boyutu yoktur, bunu daha sonra göstereceğiz.

Dosya adı: src/main.rs

enum List {
    Cons(i32, List),
    Nil,
}

fn main() {}

Liste 15-2: i32 değerlerinden oluşan bir cons listesi veri yapısını temsil etmek için bir enum tanımlamaya yönelik ilk girişim

Not: Bu örneğin amaçları doğrultusunda yalnızca i32 değerlerini tutan bir cons listesi yapıyoruz. Bölüm 10'da tartıştığımız gibi, herhangi bir türden değerleri saklayabilen bir cons liste türü tanımlamak için yaygınları kullanabilirdik.

List türünü 1, 2, 3 listesini saklamak için kullanırsak, Liste 15-3'teki gibi bir kod yazmamız gerekir:

Dosya adı: src/main.rs

enum List {
    Cons(i32, List),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

Listing 15-3: 1, 2, 3 listesini saklamak için List enum'unu kullanma

İlk Cons değeri 1'i ve başka bir `` değerini tutar. Bu List değeri, 2 ve başka bir List değerini tutan başka bir Cons değeridir. Bu List değeri, 3'ü ve bir diğer List değerini tutan bir Cons değeridir ve son olarak listenin sonunu işaret eden özyinelemesiz tür olan Nil'dir.

Liste 15-3'teki kodu derlemeye çalışırsak, Liste 15-4'te gösterilen hatayı alırız:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

error[E0391]: cycle detected when computing drop-check constraints for `List`
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
  |
  = note: ...which immediately requires computing drop-check constraints for `List` again
  = note: cycle used when computing dropck types for `Canonical { max_universe: U0, variables: [], value: ParamEnvAnd { param_env: ParamEnv { caller_bounds: [], reveal: UserFacing, constness: NotConst }, value: List } }`

Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` due to 2 previous errors

Liste 15-4: Özyinelemeli bir enum tanımlamaya çalıştığımızda aldığımız hata

Hata, bu türün “sonsuz boyutu” olduğunu gösterir. Bunun nedeni, List'i özyinelemeli bir değişkenle tanımlamış olmamızdır: doğrudan kendisinin başka bir değerini tutar. Sonuç olarak, Rust List değerini saklamak için ne kadar alana ihtiyacı olduğunu bulamaz. Bu hatayı neden aldığımızı inceleyelim. İlk olarak, Rust'ın özyinelemeli olmayan bir türdeki bir değeri saklamak için ne kadar alana ihtiyaç duyduğuna nasıl karar verdiğine bakacağız.

Özyinelemeli Olmayan Bir Türün Boyutunu Hesaplama

Bölüm 6'da enum tanımlarını tartışırken Liste 6-2'de tanımladığımız Message enum'unu hatırlayın:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}

Bir Message değeri için ne kadar alan ayrılacağını belirlemek için Rust, hangi varyantın en fazla alana ihtiyaç duyduğunu görmek için varyantların her birini gözden geçirir. Rust, Message::Quit'in herhangi bir alana ihtiyaç duymadığını, Message::Move'un iki i32 değerini depolamak için yeterli alana ihtiyaç duyduğunu ve benzerlerini görür. Yalnızca bir değişken kullanılacağından, bir Message değerinin ihtiyaç duyacağı en fazla alan, değişkenlerinin en büyüğünü depolamak için gereken alandır.

Rust, Liste 15-2'deki List enum'u gibi özyinelemeli bir türün ne kadar alana ihtiyaç duyduğunu belirlemeye çalıştığında ortaya çıkan durumla bunu karşılaştırın. Derleyici, i32 türünde bir değer ve List türünde bir değer tutan Cons değişkenine bakarak başlar. Bu nedenle Cons, i32'nin boyutu artı bir List'in boyutuna eşit miktarda alana ihtiyaç duyar. Derleyici, List türünün ne kadar belleğe ihtiyacı olduğunu bulmak için Cons değişkeninden başlayarak tüm sıralı değişkenlere bakar. Cons değişkeni i32 türünde bir değer ve List türünde bir değer tutar ve bu işlem Şekil 15-1'de gösterildiği gibi sonsuza kadar devam eder.

Sonsuz bir Cons listesi

Şekil 15-1: Sonsuz Cons varyantlarından oluşan sonsuz bir List

Boyutu Bilinen Özyinelemeli Bir Tür Elde Etmek için Box<T> Kullanmak

Rust, özyinelemeli olarak tanımlanan türler için ne kadar alan ayırması gerektiğini bulamadığından, derleyici bu yararlı öneriyle birlikte bir hata verir:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
  |
2 |     Cons(i32, Box<List>),
  |               ^^^^    ^

Bu öneride “dolayı yönlendirme” (“indirection”), bir değeri doğrudan depolamak yerine, veri yapısını değiştirerek değeri dolaylı olarak depolamamız gerektiği ve bunun yerine değere bir işaretçi depolamamız gerektiği anlamına gelir.

Box<T> bir işaretçi olduğundan, Rust her zaman bir Box<T>'nin ne kadar alana ihtiyacı olduğunu bilir: bir işaretçinin boyutu, işaret ettiği veri miktarına bağlı olarak değişmez. Bu, doğrudan başka bir List değeri yerine Cons değişkeninin içine bir Box<T> koyabileceğimiz anlamına gelir. Box<T>, Cons değişkeninin içinde olmak yerine yığın üzerinde olacak bir sonraki List değerine işaret edecektir. Kavramsal olarak, hala diğer listeleri tutan listelerle oluşturulmuş bir listemiz var, ancak bu uygulama artık öğeleri birbirinin içine yerleştirmek yerine yan yana yerleştirmeye benziyor.

Liste 15-2'deki List enum'unun tanımını ve Liste 15-3'teki List kullanımını, derlenecek olan Liste 15-5'teki kodla değiştirebiliriz:

Dosya adı: src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

Liste 15-5: Bilinen bir boyuta sahip olmak için Box<T> kullanan List tanımı

Cons varyantı, bir i32 boyutuna ve kutunun işaretçi verilerini depolamak için alana ihtiyaç duyar. Nil değişkeni hiçbir değer saklamaz, bu nedenle Cons değişkeninden daha az alana ihtiyaç duyar. Artık herhangi bir List değerinin bir i32 boyutu artı bir kutunun işaretçi verisinin boyutunu kaplayacağını biliyoruz. Bir kutu kullanarak sonsuz, özyinelemeli zinciri kırdık, böylece derleyici bir List değerini saklamak için gereken boyutu bulabilir. Şekil 15-2, Cons varyantının şimdi nasıl göründüğünü göstermektedir.

Sonlu bir Cons listesi

Şekil 15-2: Cons Box'ı tuttuğu için sonsuz boyutlu olmayan bir List

Kutular yalnızca dolaylama ve heap tahsisi sağlar; diğer akıllı işaretçi türlerinde göreceğimiz gibi başka özel yetenekleri yoktur. Ayrıca, bu özel yeteneklerin neden olduğu performans ek yüküne de sahip değildirler, bu nedenle dolaylamanın ihtiyaç duyduğumuz tek özellik olduğu cons listesi gibi durumlarda yararlı olabilirler. Bölüm 17'de kutular için daha fazla kullanım alanına da bakacağız.

Box<T> türü akıllı bir işaretçidir çünkü Box<T> değerlerinin referanslar gibi ele alınmasını sağlayan Deref tanımını sürekler. Box<T> değeri kapsam dışına çıktığında, Drop tanımının süreklenmesi nedeniyle kutunun işaret ettiği yığın verileri de temizlenir. Bu iki tanım, bu bölümün geri kalanında tartışacağımız diğer akıllı işaretçi türleri tarafından sağlanan işlevsellik için daha da önemli olacaktır. Şimdi bu iki tanımı daha ayrıntılı olarak inceleyelim.

Treating Smart Pointers Like Regular References with the Deref Trait

Implementing the Deref trait allows you to customize the behavior of the dereference operator * (not to be confused with the multiplication or glob operator). By implementing Deref in such a way that a smart pointer can be treated like a regular reference, you can write code that operates on references and use that code with smart pointers too.

Let’s first look at how the dereference operator works with regular references. Then we’ll try to define a custom type that behaves like Box<T>, and see why the dereference operator doesn’t work like a reference on our newly defined type. We’ll explore how implementing the Deref trait makes it possible for smart pointers to work in ways similar to references. Then we’ll look at Rust’s deref coercion feature and how it lets us work with either references or smart pointers.

Note: there’s one big difference between the MyBox<T> type we’re about to build and the real Box<T>: our version will not store its data on the heap. We are focusing this example on Deref, so where the data is actually stored is less important than the pointer-like behavior.

Following the Pointer to the Value

A regular reference is a type of pointer, and one way to think of a pointer is as an arrow to a value stored somewhere else. In Listing 15-6, we create a reference to an i32 value and then use the dereference operator to follow the reference to the value:

Filename: src/main.rs

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Listing 15-6: Using the dereference operator to follow a reference to an i32 value

The variable x holds an i32 value 5. We set y equal to a reference to x. We can assert that x is equal to 5. However, if we want to make an assertion about the value in y, we have to use *y to follow the reference to the value it’s pointing to (hence dereference) so the compiler can compare the actual value. Once we dereference y, we have access to the integer value y is pointing to that we can compare with 5.

If we tried to write assert_eq!(5, y); instead, we would get this compilation error:

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
  |
  = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
  = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` due to previous error

Comparing a number and a reference to a number isn’t allowed because they’re different types. We must use the dereference operator to follow the reference to the value it’s pointing to.

Using Box<T> Like a Reference

We can rewrite the code in Listing 15-6 to use a Box<T> instead of a reference; the dereference operator used on the Box<T> in Listing 15-7 functions in the same way as the dereference operator used on the reference in Listing 15-6:

Filename: src/main.rs

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Listing 15-7: Using the dereference operator on a Box<i32>

The main difference between Listing 15-7 and Listing 15-6 is that here we set y to be an instance of a box pointing to a copied value of x rather than a reference pointing to the value of x. In the last assertion, we can use the dereference operator to follow the box’s pointer in the same way that we did when y was a reference. Next, we’ll explore what is special about Box<T> that enables us to use the dereference operator by defining our own box type.

Defining Our Own Smart Pointer

Let’s build a smart pointer similar to the Box<T> type provided by the standard library to experience how smart pointers behave differently from references by default. Then we’ll look at how to add the ability to use the dereference operator.

The Box<T> type is ultimately defined as a tuple struct with one element, so Listing 15-8 defines a MyBox<T> type in the same way. We’ll also define a new function to match the new function defined on Box<T>.

Filename: src/main.rs

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {}

Listing 15-8: Defining a MyBox<T> type

We define a struct named MyBox and declare a generic parameter T, because we want our type to hold values of any type. The MyBox type is a tuple struct with one element of type T. The MyBox::new function takes one parameter of type T and returns a MyBox instance that holds the value passed in.

Let’s try adding the main function in Listing 15-7 to Listing 15-8 and changing it to use the MyBox<T> type we’ve defined instead of Box<T>. The code in Listing 15-9 won’t compile because Rust doesn’t know how to dereference MyBox.

Filename: src/main.rs

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Listing 15-9: Attempting to use MyBox<T> in the same way we used references and Box<T>

Here’s the resulting compilation error:

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` due to previous error

Our MyBox<T> type can’t be dereferenced because we haven’t implemented that ability on our type. To enable dereferencing with the * operator, we implement the Deref trait.

Treating a Type Like a Reference by Implementing the Deref Trait

As discussed in the “Implementing a Trait on a Type” section of Chapter 10, to implement a trait, we need to provide implementations for the trait’s required methods. The Deref trait, provided by the standard library, requires us to implement one method named deref that borrows self and returns a reference to the inner data. Listing 15-10 contains an implementation of Deref to add to the definition of MyBox:

Filename: src/main.rs

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Listing 15-10: Implementing Deref on MyBox<T>

The type Target = T; syntax defines an associated type for the Deref trait to use. Associated types are a slightly different way of declaring a generic parameter, but you don’t need to worry about them for now; we’ll cover them in more detail in Chapter 19.

We fill in the body of the deref method with &self.0 so deref returns a reference to the value we want to access with the * operator; recall from the “Using Tuple Structs without Named Fields to Create Different Types” section of Chapter 5 that .0 accesses the first value in a tuple struct. The main function in Listing 15-9 that calls * on the MyBox<T> value now compiles, and the assertions pass!

Without the Deref trait, the compiler can only dereference & references. The deref method gives the compiler the ability to take a value of any type that implements Deref and call the deref method to get a & reference that it knows how to dereference.

When we entered *y in Listing 15-9, behind the scenes Rust actually ran this code:

*(y.deref())

Rust substitutes the * operator with a call to the deref method and then a plain dereference so we don’t have to think about whether or not we need to call the deref method. This Rust feature lets us write code that functions identically whether we have a regular reference or a type that implements Deref.

The reason the deref method returns a reference to a value, and that the plain dereference outside the parentheses in *(y.deref()) is still necessary, is to do with the ownership system. If the deref method returned the value directly instead of a reference to the value, the value would be moved out of self. We don’t want to take ownership of the inner value inside MyBox<T> in this case or in most cases where we use the dereference operator.

Note that the * operator is replaced with a call to the deref method and then a call to the * operator just once, each time we use a * in our code. Because the substitution of the * operator does not recurse infinitely, we end up with data of type i32, which matches the 5 in assert_eq! in Listing 15-9.

Implicit Deref Coercions with Functions and Methods

Deref coercion converts a reference to a type that implements the Deref trait into a reference to another type. For example, deref coercion can convert &String to &str because String implements the Deref trait such that it returns &str. Deref conversion is a convenience Rust performs on arguments to functions and methods, and works only on types that implement the Deref trait. It happens automatically when we pass a reference to a particular type’s value as an argument to a function or method that doesn’t match the parameter type in the function or method definition. A sequence of calls to the deref method converts the type we provided into the type the parameter needs.

Deref coercion was added to Rust so that programmers writing function and method calls don’t need to add as many explicit references and dereferences with & and *. The deref coercion feature also lets us write more code that can work for either references or smart pointers.

To see deref coercion in action, let’s use the MyBox<T> type we defined in Listing 15-8 as well as the implementation of Deref that we added in Listing 15-10. Listing 15-11 shows the definition of a function that has a string slice parameter:

Filename: src/main.rs

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {}

Listing 15-11: A hello function that has the parameter name of type &str

We can call the hello function with a string slice as an argument, such as hello("Rust"); for example. Deref coercion makes it possible to call hello with a reference to a value of type MyBox<String>, as shown in Listing 15-12:

Filename: src/main.rs

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

Listing 15-12: Calling hello with a reference to a MyBox<String> value, which works because of deref coercion

Here we’re calling the hello function with the argument &m, which is a reference to a MyBox<String> value. Because we implemented the Deref trait on MyBox<T> in Listing 15-10, Rust can turn &MyBox<String> into &String by calling deref. The standard library provides an implementation of Deref on String that returns a string slice, and this is in the API documentation for Deref. Rust calls deref again to turn the &String into &str, which matches the hello function’s definition.

If Rust didn’t implement deref coercion, we would have to write the code in Listing 15-13 instead of the code in Listing 15-12 to call hello with a value of type &MyBox<String>.

Filename: src/main.rs

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

Listing 15-13: The code we would have to write if Rust didn’t have deref coercion

The (*m) dereferences the MyBox<String> into a String. Then the & and [..] take a string slice of the String that is equal to the whole string to match the signature of hello. This code without deref coercions is harder to read, write, and understand with all of these symbols involved. Deref coercion allows Rust to handle these conversions for us automatically.

When the Deref trait is defined for the types involved, Rust will analyze the types and use Deref::deref as many times as necessary to get a reference to match the parameter’s type. The number of times that Deref::deref needs to be inserted is resolved at compile time, so there is no runtime penalty for taking advantage of deref coercion!

How Deref Coercion Interacts with Mutability

Similar to how you use the Deref trait to override the * operator on immutable references, you can use the DerefMut trait to override the * operator on mutable references.

Rust does deref coercion when it finds types and trait implementations in three cases:

  • From &T to &U when T: Deref<Target=U>
  • From &mut T to &mut U when T: DerefMut<Target=U>
  • From &mut T to &U when T: Deref<Target=U>

The first two cases are the same as each other except that the second implements mutability. The first case states that if you have a &T, and T implements Deref to some type U, you can get a &U transparently. The second case states that the same deref coercion happens for mutable references.

The third case is trickier: Rust will also coerce a mutable reference to an immutable one. But the reverse is not possible: immutable references will never coerce to mutable references. Because of the borrowing rules, if you have a mutable reference, that mutable reference must be the only reference to that data (otherwise, the program wouldn’t compile). Converting one mutable reference to one immutable reference will never break the borrowing rules. Converting an immutable reference to a mutable reference would require that the initial immutable reference is the only immutable reference to that data, but the borrowing rules don’t guarantee that. Therefore, Rust can’t make the assumption that converting an immutable reference to a mutable reference is possible.

Drop Tanımı ile Temizleme Üzerinde Kod Çalıştırma

Akıllı işaretçi modeli için önemli olan ikinci tanım, bir değer kapsam dışına çıkmak üzereyken ne olacağını özelleştirmenize olanak tanıyan Drop'tur. Herhangi bir tür üzerinde Drop için bir sürekleme sağlayabilirsiniz ve bu kod, dosyalar veya ağ bağlantıları gibi kaynakları serbest bırakmak için kullanılabilir. Drop'u akıllı işaretçiler bağlamında tanıtıyoruz çünkü Drop'un işlevselliği neredeyse her zaman bir akıllı işaretçi uygulanırken kullanılır. Örneğin, bir Box<T> bırakıldığında, yığın üzerinde kutunun işaret ettiği alan belleğe iade edilir.

Bazı dillerde, bazı türler için, programcı bu türlerin bir örneğini kullanmayı her bitirdiğinde belleği veya kaynakları boşaltmak için kod çağırmalıdır. Örnekler arasında dosya tutamaçları, soketler veya kilitler yer alır. Eğer unuturlarsa, sistem aşırı yüklenebilir ve çökebilir. Rust'ta, bir değer kapsam dışına çıktığında belirli bir kod parçasının çalıştırılmasını belirtebilirsiniz ve derleyici bu kodu otomatik olarak ekleyecektir. Sonuç olarak, belirli bir türün bir örneğinin bittiği bir programın her yerine temizleme kodu yerleştirme konusunda dikkatli olmanız gerekmez—yine de kaynak sızdırmazsınız!

Drop tanımını sürekleyerek bir değer kapsam dışına çıktığında çalıştırılacak kodu belirlersiniz. Drop, self öğesine değiştirilebilir bir referans alan drop adlı bir metodu tanımlamanızı gerektirir. Rust'ın drop'u ne zaman çağırdığını görmek için şimdilik drop'u println! ifade yapılarıyla kullanalım.

Liste 15-14, Rust'ın drop fonksiyonunu ne zaman çalıştırdığını göstermek için, örnek kapsam dışına çıktığında Dropping CustomSmartPointer! yazdıracak olan tek gizli fonksiyonu olan bir CustomSmartPointer yapısını gösterir.

Dosya adı: src/main.rs

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created.");
}

Liste 15-14: Temizleme kodumuzu koyacağımız Drop'u sürekleyen bir CustomSmartPointer yapısı

Drop tanımı genel Rust yapısına dahil edilmiştir, bu nedenle onu kapsam içine almamıza gerek yoktur. CustomSmartPointer üzerinde Drop'u sürekliyoruz ve println! çağrısı yapan drop metodu için bir tanımlama sağlıyoruz. Drop fonksiyonunun gövdesi, türünüzün bir örneği kapsam dışına çıktığında çalıştırmak istediğiniz herhangi bir mantığı yerleştireceğiniz yerdir. Rust'ın drop'u ne zaman çağıracağını görsel olarak göstermek için burada bazı metinler yazdırıyoruz.

main'de, iki CustomSmartPointer örneği oluşturuyoruz ve ardından oluşturulan CustomSmartPointer'ları yazdırıyoruz. main'in sonunda, CustomSmartPointer örneklerimiz kapsam dışına çıkacak ve Rust, drop'a koyduğumuz kodu çağırarak son mesajımızı yazdıracak. drop metodunu açıkça çağırmamıza gerek olmadığına dikkat edin.

Bu programı çalıştırdığımızda aşağıdaki çıktıyı göreceğiz:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/drop-example`
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

Örneklerimiz kapsam dışına çıktığında Rust bizim için otomatik olarak drop'u çağırdı ve belirttiğimiz kodu çalıştırdı. Değişkenler oluşturulma sıralarının tersine bırakılır, bu nedenle d, c'den önce bırakılır. Bu örneğin amacı, drop metodunun nasıl çalıştığına dair görsel bir kılavuz sunmaktır; genellikle bir yazdırma mesajı yerine türünüzün çalışması gereken temizleme kodunu belirtirsiniz.

std::mem::drop ile Bir Değeri Erken Bırakma

Ne yazık ki, otomatik drop fonksiyonunu devre dışı bırakmak kolay değildir. drop'u devre dışı bırakmak genellikle gerekli değildir; Drop tanımının tüm amacı bunun otomatik olarak halledilmesidir. Ancak bazen, bir değeri erkenden temizlemek isteyebilirsiniz. Buna bir örnek, kilitleri yöneten akıllı işaretçiler kullanırken verilebilir: kilidi serbest bırakan drop metodunu zorlamak isteyebilirsiniz, böylece aynı kapsamdaki diğer kodlar kilidi alabilir. Rust, Drop'un drop metodunu manuel olarak çağırmanıza izin vermez; bunun yerine, bir değeri kapsamının sonundan önce bırakılmaya zorlamak istiyorsanız standart kütüphane tarafından sağlanan std::mem::drop işlevini çağırmanız gerekir.

Liste 15-15'te gösterildiği gibi, Liste 15-14'teki main fonksiyonunu değiştirerek Drop'un drop metodunu manuel olarak çağırmaya çalışırsak, bir derleyici hatası alırız:

Dosya adı: src/main.rs

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    c.drop();
    println!("CustomSmartPointer dropped before the end of main.");
}

Liste 15-15: Erken temizlemek için Drop tanımından drop metodunu manuel olarak çağırma girişimi

Bu kodu derlemeye çalıştığımızda alacağımız hata budur:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
  --> src/main.rs:16:7
   |
16 |     c.drop();
   |     --^^^^--
   |     | |
   |     | explicit destructor calls not allowed
   |     help: consider using `drop` function: `drop(c)`

For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` due to previous error

Bu hata mesajı, drop'u açıkça çağırmamıza izin verilmediğini belirtir. Hata mesajı, bir örneği temizleyen bir fonksiyon için genel programlama terimi olan yıkıcı terimini kullanır. Bir yıkıcı, bir örnek oluşturan bir yapıcıya benzer. Rust'taki drop fonksiyonu belirli bir yıkıcıdır.

Rust, drop'u açıkça çağırmamıza izin vermez, çünkü Rust yine de main'in sonundaki değer üzerinde otomatik olarak drop'u çağıracaktır. Bu, Rust aynı değeri iki kez temizlemeye çalışacağı için çift serbest bırakma hatasına neden olur.

Bir değer kapsam dışına çıktığında drop'un otomatik olarak eklenmesini devre dışı bırakamayız ve drop metodunu açıkça çağıramayız. Bu nedenle, bir değeri erken temizlenmeye zorlamamız gerekiyorsa, std::mem::drop fonksiyonunu kullanırız.

std::mem::drop fonksiyonu Drop tanımındaki drop metodundan farklıdır. Düşürmeye zorlamak istediğimiz değeri argüman olarak ileterek çağırırız. Fonksiyon genel Rust yapısındadır, bu nedenle Liste 15-15'teki main'i, Liste 15-16'da gösterildiği gibi drop fonksiyonunu çağıracak şekilde değiştirebiliriz:

Dosya adı: src/main.rs

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

Liste 15-16: Bir değeri kapsam dışına çıkmadan önce açıkça bırakmak için std::mem::drop çağrısı

Bu kodu çalıştırmak aşağıdakileri yazdıracaktır:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

CustomSmartPointer oluşturulduğu esnada Dropping CustomSmartPointer with data (some data)! metni yazdırılır ve CustomSmartPointer dropped before the end of main. metni, drop metod kodunun bu noktada c'yi bırakmak için çağrıldığını gösterir.

Drop tanım süreklemesinde belirtilen kodu, temizlemeyi kolay ve güvenli hale getirmek için birçok şekilde kullanabilirsiniz: örneğin, kendi bellek ayırıcınızı oluşturmak için kullanabilirsiniz! Drop tanımı ve Rust'ın sahiplik sistemi sayesinde, temizlemeyi hatırlamak zorunda kalmazsınız çünkü Rust bunu otomatik olarak yapar.

Ayrıca, hala kullanımda olan değerlerin yanlışlıkla temizlenmesinden kaynaklanan sorunlar hakkında endişelenmenize de gerek yoktur: referansların her zaman geçerli olmasını sağlayan sahiplik sistemi, drop'un değer artık kullanılmadığında yalnızca bir kez çağrılmasını da sağlar.

Box<T>'yi ve akıllı işaretçilerin bazı özelliklerini incelediğimize göre, şimdi standart kütüphanede tanımlanan diğer birkaç akıllı işaretçiye bakalım.

Rc<T>, Referans Sayaçlı Akıllı İşaretçi

Çoğu durumda, sahiplik açıktır: Belirli bir değere hangi değişkenin sahip olduğunu tam olarak bilirsiniz. Ancak, tek bir değerin birden çok sahibi olabileceği durumlar vardır. Örneğin, grafik veri yapılarında, birden çok kenar aynı düğüme işaret edebilir ve bu düğüm kavramsal olarak ona işaret eden tüm kenarlara aittir. Bir düğüm, kendisine işaret eden herhangi bir kenarı olmadığı ve dolayısıyla sahibi olmadığı sürece temizlenmemelidir.

Referans sayımının kısaltması olan Rust türü Rc<T>'yi kullanarak birden çok sahipliği açıkça etkinleştirmeniz gerekir. Rc<T> türü, değerin hala kullanımda olup olmadığını belirlemek için bir değere yapılan başvuruların sayısını takip eder. Bir değere sıfır referans varsa, hiçbir referans geçersiz hale gelmeden değer temizlenebilir.

Rc<T>'yi bir aile odasındaki bir TV olarak hayal edin. Bir kişi TV izlemek için girdiğinde onu açar. Diğerleri odaya gelip televizyon izleyebilir. Son kişi odadan ayrıldığında, artık kullanılmadığı için televizyonu kapatırlar. Biri televizyonu kapatırsa, diğerleri hala onu seyrediyorsa, kalan televizyon izleyicilerinden büyük bir kargaşa çıkar!

Rc<T> türünü, programımızın birden fazla bölümünün okuması için yığın üzerinde bazı verileri tahsis etmek istediğimizde kullanırız ve derleme zamanında verileri en son hangi bölümün bitireceğini belirleyemeyiz. Hangi bölümün en son biteceğini bilseydik, o bölümü verinin sahibi yapabilirdik ve derleme zamanında uygulanan normal sahiplik kuralları yürürlüğe girecekti.

Rc<T> öğesinin yalnızca tek iş parçacıklı senaryolarda kullanım için olduğunu unutmayın. Bölüm 16'da eşzamanlılığı tartıştığımızda, çok iş parçacıklı programlarda referans sayımının nasıl yapıldığını ele alacağız.

Veri Paylaşmak için Rc<T> Kullanmak

Liste 15-5'teki liste örneğimize dönelim. Box<T> kullanarak tanımladığımızı hatırlayın. Bu sefer, her ikisi de üçüncü bir listenin sahipliğini paylaşan iki liste oluşturacağız. Kavramsal olarak, bu Şekil 15-3'e benziyor:

Üçüncü bir listenin sahipliğini paylaşan iki liste

Şekil 15-3: İki liste, b ve c, üçüncü bir listenin sahipliğini paylaşan a

5 ve ardından 10'u içeren bir a listesi oluşturacağız. Ardından, 3 ile başlayan b ve 4 ile başlayan c olmak üzere iki liste daha yapacağız. Daha sonra hem b hem de c listeleri, 5'i ve 10'u içeren ilk listeye devam edecek. Başka bir deyişle, her iki liste de 5 ve 10'u içeren ilk listeyi paylaşacaktır.

Liste 15-17'de gösterildiği gibi, Box<T>'li List tanımımızı kullanarak bu senaryoyu uygulamaya çalışmak işe yaramaz:

Dosya adı: src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

Liste 15-17: Üçüncü bir listenin sahipliğini paylaşmaya çalışan Box<T> kullanan iki listeye sahip olmamıza izin verilmediğini gösteriyor

Kodu derlediğimizde, şu hatayı alırız:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
9  |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` due to previous error

Cons varyantları, tuttukları verilere sahiptir, bu nedenle b listesini oluşturduğumuzda a, b'ye taşınır ve b, a'ya sahiptir. Ardından, c'yi oluştururken a'yı tekrar kullanmaya çalıştığımızda, a taşınmış olduğu için buna izin verilmiyor.

Bunun yerine referansları tutmak için Cons'un tanımını değiştirebilirdik, ancak daha sonra ömür boyu parametreleri belirtmemiz gerekecekti. Yaşam süresi parametreleri belirterek, listedeki her öğenin en az tüm liste kadar yaşayacağını belirtmiş oluruz. Bu, Liste 15-17'deki öğeler ve listeler için geçerlidir, ancak her senaryoda geçerli değildir.

Bunun yerine List tanımımızı, Liste 15-18'de gösterildiği gibi Box<T> yerine Rc<T> kullanacak şekilde değiştireceğiz. Her Cons varyantı artık bir değere ve bir Listeye işaret eden bir Rc<T>'ye sahip olacaktır. b'yi yarattığımızda, a'nın sahipliğini almak yerine, a'nın sahip olduğu Rc<List>'i klonlayacağız, böylece referans sayısını birden ikiye çıkaracağız ve a ve b'nin o Rc<List>'deki verilerin sahipliğini paylaşmasına izin vereceğiz. Ayrıca c'yi oluştururken a'yı klonlayarak referans sayısını ikiden üçe çıkaracağız. Rc::clone'u her çağırdığımızda, Rc<List> içindeki verilere referans sayısı artacak ve sıfır referans olmadıkça veriler temizlenmeyecektir.

Dosya adı: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Liste 15-18: Rc<T> kullanan bir List tanımı

Rc<T>'yi kapsam içine almak için bir use ifadesi eklememiz gerekiyor çünkü bu başlangıç kısmında değil. main'de, 5 ve 10'u tutan listeyi oluşturuyoruz ve onu a'da yeni bir Rc<List> içinde saklıyoruz. Daha sonra b ve c'yi oluşturduğumuzda, Rc::clone fonksiyonunu çağırırız ve argüman olarak a'daki Rc<List>'e bir referansını iletiriz.

Rc::clone(&a) yerine a.clone()'u çağırabilirdik, ancak Rust'ın kuralı bu durumda Rc::clone kullanmaktır. Rc::clone'un süreklenmesi, çoğu türde clone süreklemesinin yaptığı gibi tüm verilerin derin bir kopyasını oluşturmaz. Rc::clone çağrısı yalnızca referans sayısını artırır, bu da fazla zaman almaz. Verilerin derin kopyaları çok zaman alabilir. Referans sayımı için Rc::clone kullanarak, derin kopyalı klon türleri ile referans sayısını artıran klon türleri arasında görsel olarak ayrım yapabiliriz. Kodda performans sorunları ararken, yalnızca derin kopya klonlarını dikkate almamız gerekir ve Rc::clone'a yapılan çağrıları göz ardı edebiliriz.

Rc<T>'yi Klonlamak Referans Sayısını Artırır

Liste 15-18'deki çalışma örneğimizi değiştirelim, böylece a içindeki Rc<List>e referansları oluşturup bıraktıkça referans sayılarının değiştiğini görebilelim.

Liste 15-19'da, main'i, c listesi etrafında bir iç kapsama sahip olacak şekilde değiştireceğiz; o zaman c kapsam dışına çıktığında referans sayısının nasıl değiştiğini görebiliriz.

Dosya adı: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

Liste 15-19: Referans sayısının yazdırılması

Referans sayısının değiştiği programdaki her noktada, Rc::strong_count fonksiyonunu çağırarak elde ettiğimiz referans sayısını yazdırırız. Bu fonksiyon, count yerine strong_count olarak adlandırılır, çünkü Rc<T> türünde bir weak_count da vardır; “Referans Döngülerini Önleme: Bir Rc<T>'yi Weak<T>'ye Çevirme” bölümünde weak_count'ın ne için kullanıldığını göreceğiz.

Bu kod şunları yazdırır:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

a içindeki Rc<List> öğesinin ilk referans sayısının 1 olduğunu görebiliriz; sonra clone'u her çağırdığımızda, sayı 1 artar. c kapsam dışına çıktığında, sayı 1 azalır. Rc'yi çağırmamız gerektiği gibi referans sayısını azaltmak için bir fonksiyon çağırmamız gerekmez: referans sayısını artırmak için klonlayın: Drop tanımının süreklenmesi, bir Rc<T> değeri kapsam dışına çıktığında referans sayısını otomatik olarak azaltır.

Bu örnekte göremediğimiz şey, main sonunda b ve ardından a kapsam dışına çıktığında, sayının 0 olduğu ve Rc<List>'in tamamen temizlendiğidir. Rc<T> kullanmak, tek bir değerin birden çok sahibine sahip olmasına izin verir ve sayı, sahiplerden herhangi biri var olduğu sürece değerin geçerli kalmasını sağlar.

Değişmez referanslar aracılığıyla, Rc<T>, programınızın birden çok bölümü arasında salt okuma için veri paylaşmanıza olanak tanır. Eğer Rc<T> birden çok değişken referansa sahip olmanıza da izin veriyorsa, Bölüm 4'te tartışılan ödünç alma kurallarından birini ihlal etmiş olabilirsiniz: aynı yere birden fazla değişken ödünç alma veri yarışlarına ve tutarsızlıklara neden olabilir. Ancak verileri değiştirebilmek çok faydalıdır! Bir sonraki bölümde, bu değişmezlik kısıtlamasıyla çalışmak için bir Rc<T> ile birlikte kullanabileceğiniz iç değişkenlik modelini ve RefCell<T> türünü tartışacağız.

RefCell<T> and the Interior Mutability Pattern

Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to that data; normally, this action is disallowed by the borrowing rules. To mutate data, the pattern uses unsafe code inside a data structure to bend Rust’s usual rules that govern mutation and borrowing. We haven’t yet covered unsafe code that indicates we're checking the rules manually instead of the compiler checking them for us; we will discuss unsafe code more in Chapter 19. We can use types that use the interior mutability pattern only when we can ensure that the borrowing rules will be followed at runtime, even though the compiler can’t guarantee that. The unsafe code involved is then wrapped in a safe API, and the outer type is still immutable.

Let’s explore this concept by looking at the RefCell<T> type that follows the interior mutability pattern.

Enforcing Borrowing Rules at Runtime with RefCell<T>

Unlike Rc<T>, the RefCell<T> type represents single ownership over the data it holds. So, what makes RefCell<T> different from a type like Box<T>? Recall the borrowing rules you learned in Chapter 4:

  • At any given time, you can have either (but not both) one mutable reference or any number of immutable references.
  • References must always be valid.

With references and Box<T>, the borrowing rules’ invariants are enforced at compile time. With RefCell<T>, these invariants are enforced at runtime. With references, if you break these rules, you’ll get a compiler error. With RefCell<T>, if you break these rules, your program will panic and exit.

The advantages of checking the borrowing rules at compile time are that errors will be caught sooner in the development process, and there is no impact on runtime performance because all the analysis is completed beforehand. For those reasons, checking the borrowing rules at compile time is the best choice in the majority of cases, which is why this is Rust’s default.

The advantage of checking the borrowing rules at runtime instead is that certain memory-safe scenarios are then allowed, where they would’ve been disallowed by the compile-time checks. Static analysis, like the Rust compiler, is inherently conservative. Some properties of code are impossible to detect by analyzing the code: the most famous example is the Halting Problem, which is beyond the scope of this book but is an interesting topic to research.

Because some analysis is impossible, if the Rust compiler can’t be sure the code complies with the ownership rules, it might reject a correct program; in this way, it’s conservative. If Rust accepted an incorrect program, users wouldn’t be able to trust in the guarantees Rust makes. However, if Rust rejects a correct program, the programmer will be inconvenienced, but nothing catastrophic can occur. The RefCell<T> type is useful when you’re sure your code follows the borrowing rules but the compiler is unable to understand and guarantee that.

Similar to Rc<T>, RefCell<T> is only for use in single-threaded scenarios and will give you a compile-time error if you try using it in a multithreaded context. We’ll talk about how to get the functionality of RefCell<T> in a multithreaded program in Chapter 16.

Here is a recap of the reasons to choose Box<T>, Rc<T>, or RefCell<T>:

  • Rc<T> enables multiple owners of the same data; Box<T> and RefCell<T> have single owners.
  • Box<T> allows immutable or mutable borrows checked at compile time; Rc<T> allows only immutable borrows checked at compile time; RefCell<T> allows immutable or mutable borrows checked at runtime.
  • Because RefCell<T> allows mutable borrows checked at runtime, you can mutate the value inside the RefCell<T> even when the RefCell<T> is immutable.

Mutating the value inside an immutable value is the interior mutability pattern. Let’s look at a situation in which interior mutability is useful and examine how it’s possible.

Interior Mutability: A Mutable Borrow to an Immutable Value

A consequence of the borrowing rules is that when you have an immutable value, you can’t borrow it mutably. For example, this code won’t compile:

fn main() {
    let x = 5;
    let y = &mut x;
}

If you tried to compile this code, you’d get the following error:

$ cargo run
   Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
2 |     let x = 5;
  |         - help: consider changing this to be mutable: `mut x`
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` due to previous error

However, there are situations in which it would be useful for a value to mutate itself in its methods but appear immutable to other code. Code outside the value’s methods would not be able to mutate the value. Using RefCell<T> is one way to get the ability to have interior mutability, but RefCell<T> doesn’t get around the borrowing rules completely: the borrow checker in the compiler allows this interior mutability, and the borrowing rules are checked at runtime instead. If you violate the rules, you’ll get a panic! instead of a compiler error.

Let’s work through a practical example where we can use RefCell<T> to mutate an immutable value and see why that is useful.

A Use Case for Interior Mutability: Mock Objects

Sometimes during testing a programmer will use a type in place of another type, in order to observe particular behavior and assert it's implemented correctly. This placeholder type is called a test double. Think of it in the sense of a "stunt double" in filmmaking, where a person steps in and substitutes for an actor to do a particular tricky scene. Test doubles stand in for other types when we're running tests. Mock objects are specific types of test doubles that record what happens during a test so you can assert that the correct actions took place.

Rust doesn’t have objects in the same sense as other languages have objects, and Rust doesn’t have mock object functionality built into the standard library as some other languages do. However, you can definitely create a struct that will serve the same purposes as a mock object.

Here’s the scenario we’ll test: we’ll create a library that tracks a value against a maximum value and sends messages based on how close to the maximum value the current value is. This library could be used to keep track of a user’s quota for the number of API calls they’re allowed to make, for example.

Our library will only provide the functionality of tracking how close to the maximum a value is and what the messages should be at what times. Applications that use our library will be expected to provide the mechanism for sending the messages: the application could put a message in the application, send an email, send a text message, or something else. The library doesn’t need to know that detail. All it needs is something that implements a trait we’ll provide called Messenger. Listing 15-20 shows the library code:

Filename: src/lib.rs

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

Listing 15-20: A library to keep track of how close a value is to a maximum value and warn when the value is at certain levels

One important part of this code is that the Messenger trait has one method called send that takes an immutable reference to self and the text of the message. This trait is the interface our mock object needs to implement so that the mock can be used in the same way a real object is. The other important part is that we want to test the behavior of the set_value method on the LimitTracker. We can change what we pass in for the value parameter, but set_value doesn’t return anything for us to make assertions on. We want to be able to say that if we create a LimitTracker with something that implements the Messenger trait and a particular value for max, when we pass different numbers for value, the messenger is told to send the appropriate messages.

We need a mock object that, instead of sending an email or text message when we call send, will only keep track of the messages it’s told to send. We can create a new instance of the mock object, create a LimitTracker that uses the mock object, call the set_value method on LimitTracker, and then check that the mock object has the messages we expect. Listing 15-21 shows an attempt to implement a mock object to do just that, but the borrow checker won’t allow it:

Filename: src/lib.rs

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: vec![],
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}

Listing 15-21: An attempt to implement a MockMessenger that isn’t allowed by the borrow checker

This test code defines a MockMessenger struct that has a sent_messages field with a Vec of String values to keep track of the messages it’s told to send. We also define an associated function new to make it convenient to create new MockMessenger values that start with an empty list of messages. We then implement the Messenger trait for MockMessenger so we can give a MockMessenger to a LimitTracker. In the definition of the send method, we take the message passed in as a parameter and store it in the MockMessenger list of sent_messages.

In the test, we’re testing what happens when the LimitTracker is told to set value to something that is more than 75 percent of the max value. First, we create a new MockMessenger, which will start with an empty list of messages. Then we create a new LimitTracker and give it a reference to the new MockMessenger and a max value of 100. We call the set_value method on the LimitTracker with a value of 80, which is more than 75 percent of 100. Then we assert that the list of messages that the MockMessenger is keeping track of should now have one message in it.

However, there’s one problem with this test, as shown here:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
2  |     fn send(&self, msg: &str);
   |             ----- help: consider changing that to be a mutable reference: `&mut self`
...
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` due to previous error
warning: build failed, waiting for other jobs to finish...
error: build failed

We can’t modify the MockMessenger to keep track of the messages, because the send method takes an immutable reference to self. We also can’t take the suggestion from the error text to use &mut self instead, because then the signature of send wouldn’t match the signature in the Messenger trait definition (feel free to try and see what error message you get).

This is a situation in which interior mutability can help! We’ll store the sent_messages within a RefCell<T>, and then the send method will be able to modify sent_messages to store the messages we’ve seen. Listing 15-22 shows what that looks like:

Filename: src/lib.rs

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

Listing 15-22: Using RefCell<T> to mutate an inner value while the outer value is considered immutable

The sent_messages field is now of type RefCell<Vec<String>> instead of Vec<String>. In the new function, we create a new RefCell<Vec<String>> instance around the empty vector.

For the implementation of the send method, the first parameter is still an immutable borrow of self, which matches the trait definition. We call borrow_mut on the RefCell<Vec<String>> in self.sent_messages to get a mutable reference to the value inside the RefCell<Vec<String>>, which is the vector. Then we can call push on the mutable reference to the vector to keep track of the messages sent during the test.

The last change we have to make is in the assertion: to see how many items are in the inner vector, we call borrow on the RefCell<Vec<String>> to get an immutable reference to the vector.

Now that you’ve seen how to use RefCell<T>, let’s dig into how it works!

Keeping Track of Borrows at Runtime with RefCell<T>

When creating immutable and mutable references, we use the & and &mut syntax, respectively. With RefCell<T>, we use the borrow and borrow_mut methods, which are part of the safe API that belongs to RefCell<T>. The borrow method returns the smart pointer type Ref<T>, and borrow_mut returns the smart pointer type RefMut<T>. Both types implement Deref, so we can treat them like regular references.

The RefCell<T> keeps track of how many Ref<T> and RefMut<T> smart pointers are currently active. Every time we call borrow, the RefCell<T> increases its count of how many immutable borrows are active. When a Ref<T> value goes out of scope, the count of immutable borrows goes down by one. Just like the compile-time borrowing rules, RefCell<T> lets us have many immutable borrows or one mutable borrow at any point in time.

If we try to violate these rules, rather than getting a compiler error as we would with references, the implementation of RefCell<T> will panic at runtime. Listing 15-23 shows a modification of the implementation of send in Listing 15-22. We’re deliberately trying to create two mutable borrows active for the same scope to illustrate that RefCell<T> prevents us from doing this at runtime.

Filename: src/lib.rs

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            let mut one_borrow = self.sent_messages.borrow_mut();
            let mut two_borrow = self.sent_messages.borrow_mut();

            one_borrow.push(String::from(message));
            two_borrow.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

Listing 15-23: Creating two mutable references in the same scope to see that RefCell<T> will panic

We create a variable one_borrow for the RefMut<T> smart pointer returned from borrow_mut. Then we create another mutable borrow in the same way in the variable two_borrow. This makes two mutable references in the same scope, which isn’t allowed. When we run the tests for our library, the code in Listing 15-23 will compile without any errors, but the test will fail:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished test [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests (target/debug/deps/limit_tracker-e599811fa246dbde)

running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED

failures:

---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_sends_an_over_75_percent_warning_message

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

Notice that the code panicked with the message already borrowed: BorrowMutError. This is how RefCell<T> handles violations of the borrowing rules at runtime.

Choosing to catch borrowing errors at runtime rather than compile time, as we've done here, means you'd potentially be finding mistakes in your code later in the development process: possibly not until your code was deployed to production. Also, your code would incur a small runtime performance penalty as a result of keeping track of the borrows at runtime rather than compile time. However, using RefCell<T> makes it possible to write a mock object that can modify itself to keep track of the messages it has seen while you’re using it in a context where only immutable values are allowed. You can use RefCell<T> despite its trade-offs to get more functionality than regular references provide.

Having Multiple Owners of Mutable Data by Combining Rc<T> and RefCell<T>

A common way to use RefCell<T> is in combination with Rc<T>. Recall that Rc<T> lets you have multiple owners of some data, but it only gives immutable access to that data. If you have an Rc<T> that holds a RefCell<T>, you can get a value that can have multiple owners and that you can mutate!

For example, recall the cons list example in Listing 15-18 where we used Rc<T> to allow multiple lists to share ownership of another list. Because Rc<T> holds only immutable values, we can’t change any of the values in the list once we’ve created them. Let’s add in RefCell<T> to gain the ability to change the values in the lists. Listing 15-24 shows that by using a RefCell<T> in the Cons definition, we can modify the value stored in all the lists:

Filename: src/main.rs

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

Listing 15-24: Using Rc<RefCell<i32>> to create a List that we can mutate

We create a value that is an instance of Rc<RefCell<i32>> and store it in a variable named value so we can access it directly later. Then we create a List in a with a Cons variant that holds value. We need to clone value so both a and value have ownership of the inner 5 value rather than transferring ownership from value to a or having a borrow from value.

We wrap the list a in an Rc<T> so when we create lists b and c, they can both refer to a, which is what we did in Listing 15-18.

After we’ve created the lists in a, b, and c, we want to add 10 to the value in value. We do this by calling borrow_mut on value, which uses the automatic dereferencing feature we discussed in Chapter 5 (see the section “Where’s the -> Operator?”) to dereference the Rc<T> to the inner RefCell<T> value. The borrow_mut method returns a RefMut<T> smart pointer, and we use the dereference operator on it and change the inner value.

When we print a, b, and c, we can see that they all have the modified value of 15 rather than 5:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

This technique is pretty neat! By using RefCell<T>, we have an outwardly immutable List value. But we can use the methods on RefCell<T> that provide access to its interior mutability so we can modify our data when we need to. The runtime checks of the borrowing rules protect us from data races, and it’s sometimes worth trading a bit of speed for this flexibility in our data structures.

The standard library has other types that provide interior mutability, such as Cell<T>, which is similar except that instead of giving references to the inner value, the value is copied in and out of the Cell<T>. There’s also Mutex<T>, which offers interior mutability that’s safe to use across threads; we’ll discuss its use in Chapter 16. Check out the standard library docs for more details on the differences between these types.

Reference Cycles Can Leak Memory

Rust’s memory safety guarantees make it difficult, but not impossible, to accidentally create memory that is never cleaned up (known as a memory leak). Preventing memory leaks entirely is not one of Rust’s guarantees, meaning memory leaks are memory safe in Rust. We can see that Rust allows memory leaks by using Rc<T> and RefCell<T>: it’s possible to create references where items refer to each other in a cycle. This creates memory leaks because the reference count of each item in the cycle will never reach 0, and the values will never be dropped.

Creating a Reference Cycle

Let’s look at how a reference cycle might happen and how to prevent it, starting with the definition of the List enum and a tail method in Listing 15-25:

Filename: src/main.rs

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {}

Listing 15-25: A cons list definition that holds a RefCell<T> so we can modify what a Cons variant is referring to

We’re using another variation of the List definition from Listing 15-5. The second element in the Cons variant is now RefCell<Rc<List>>, meaning that instead of having the ability to modify the i32 value as we did in Listing 15-24, we want to modify the List value a Cons variant is pointing to. We’re also adding a tail method to make it convenient for us to access the second item if we have a Cons variant.

In Listing 15-26, we’re adding a main function that uses the definitions in Listing 15-25. This code creates a list in a and a list in b that points to the list in a. Then it modifies the list in a to point to b, creating a reference cycle. There are println! statements along the way to show what the reference counts are at various points in this process.

Filename: src/main.rs

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack
    // println!("a next item = {:?}", a.tail());
}

Listing 15-26: Creating a reference cycle of two List values pointing to each other

We create an Rc<List> instance holding a List value in the variable a with an initial list of 5, Nil. We then create an Rc<List> instance holding another List value in the variable b that contains the value 10 and points to the list in a.

We modify a so it points to b instead of Nil, creating a cycle. We do that by using the tail method to get a reference to the RefCell<Rc<List>> in a, which we put in the variable link. Then we use the borrow_mut method on the RefCell<Rc<List>> to change the value inside from an Rc<List> that holds a Nil value to the Rc<List> in b.

When we run this code, keeping the last println! commented out for the moment, we’ll get this output:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.53s
     Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

The reference count of the Rc<List> instances in both a and b are 2 after we change the list in a to point to b. At the end of main, Rust drops the variable b, which decreases the reference count of the Rc<List> instance from 2 to 1. The memory that Rc<List> has on the heap won’t be dropped at this point, because its reference count is 1, not 0. Then Rust drops a, which decreases the reference count of the a Rc<List> instance from 2 to 1 as well. This instance’s memory can’t be dropped either, because the other Rc<List> instance still refers to it. The memory allocated to the list will remain uncollected forever. To visualize this reference cycle, we’ve created a diagram in Figure 15-4.

Reference cycle of lists

Figure 15-4: A reference cycle of lists a and b pointing to each other

If you uncomment the last println! and run the program, Rust will try to print this cycle with a pointing to b pointing to a and so forth until it overflows the stack.

Compared to a real-world program, the consequences creating a reference cycle in this example aren’t very dire: right after we create the reference cycle, the program ends. However, if a more complex program allocated lots of memory in a cycle and held onto it for a long time, the program would use more memory than it needed and might overwhelm the system, causing it to run out of available memory.

Creating reference cycles is not easily done, but it’s not impossible either. If you have RefCell<T> values that contain Rc<T> values or similar nested combinations of types with interior mutability and reference counting, you must ensure that you don’t create cycles; you can’t rely on Rust to catch them. Creating a reference cycle would be a logic bug in your program that you should use automated tests, code reviews, and other software development practices to minimize.

Another solution for avoiding reference cycles is reorganizing your data structures so that some references express ownership and some references don’t. As a result, you can have cycles made up of some ownership relationships and some non-ownership relationships, and only the ownership relationships affect whether or not a value can be dropped. In Listing 15-25, we always want Cons variants to own their list, so reorganizing the data structure isn’t possible. Let’s look at an example using graphs made up of parent nodes and child nodes to see when non-ownership relationships are an appropriate way to prevent reference cycles.

Preventing Reference Cycles: Turning an Rc<T> into a Weak<T>

So far, we’ve demonstrated that calling Rc::clone increases the strong_count of an Rc<T> instance, and an Rc<T> instance is only cleaned up if its strong_count is 0. You can also create a weak reference to the value within an Rc<T> instance by calling Rc::downgrade and passing a reference to the Rc<T>. Strong references are how you can share ownership of an Rc<T> instance. Weak references don’t express an ownership relationship, and their count doesn't affect when an Rc<T> instance is cleaned up. They won’t cause a reference cycle because any cycle involving some weak references will be broken once the strong reference count of values involved is 0.

When you call Rc::downgrade, you get a smart pointer of type Weak<T>. Instead of increasing the strong_count in the Rc<T> instance by 1, calling Rc::downgrade increases the weak_count by 1. The Rc<T> type uses weak_count to keep track of how many Weak<T> references exist, similar to strong_count. The difference is the weak_count doesn’t need to be 0 for the Rc<T> instance to be cleaned up.

Because the value that Weak<T> references might have been dropped, to do anything with the value that a Weak<T> is pointing to, you must make sure the value still exists. Do this by calling the upgrade method on a Weak<T> instance, which will return an Option<Rc<T>>. You’ll get a result of Some if the Rc<T> value has not been dropped yet and a result of None if the Rc<T> value has been dropped. Because upgrade returns an Option<Rc<T>>, Rust will ensure that the Some case and the None case are handled, and there won’t be an invalid pointer.

As an example, rather than using a list whose items know only about the next item, we’ll create a tree whose items know about their children items and their parent items.

Creating a Tree Data Structure: a Node with Child Nodes

To start, we’ll build a tree with nodes that know about their child nodes. We’ll create a struct named Node that holds its own i32 value as well as references to its children Node values:

Filename: src/main.rs

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

We want a Node to own its children, and we want to share that ownership with variables so we can access each Node in the tree directly. To do this, we define the Vec<T> items to be values of type Rc<Node>. We also want to modify which nodes are children of another node, so we have a RefCell<T> in children around the Vec<Rc<Node>>.

Next, we’ll use our struct definition and create one Node instance named leaf with the value 3 and no children, and another instance named branch with the value 5 and leaf as one of its children, as shown in Listing 15-27:

Filename: src/main.rs

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

Listing 15-27: Creating a leaf node with no children and a branch node with leaf as one of its children

We clone the Rc<Node> in leaf and store that in branch, meaning the Node in leaf now has two owners: leaf and branch. We can get from branch to leaf through branch.children, but there’s no way to get from leaf to branch. The reason is that leaf has no reference to branch and doesn’t know they’re related. We want leaf to know that branch is its parent. We’ll do that next.

Adding a Reference from a Child to Its Parent

To make the child node aware of its parent, we need to add a parent field to our Node struct definition. The trouble is in deciding what the type of parent should be. We know it can’t contain an Rc<T>, because that would create a reference cycle with leaf.parent pointing to branch and branch.children pointing to leaf, which would cause their strong_count values to never be 0.

Thinking about the relationships another way, a parent node should own its children: if a parent node is dropped, its child nodes should be dropped as well. However, a child should not own its parent: if we drop a child node, the parent should still exist. This is a case for weak references!

So instead of Rc<T>, we’ll make the type of parent use Weak<T>, specifically a RefCell<Weak<Node>>. Now our Node struct definition looks like this:

Filename: src/main.rs

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

A node will be able to refer to its parent node but doesn’t own its parent. In Listing 15-28, we update main to use this new definition so the leaf node will have a way to refer to its parent, branch:

Filename: src/main.rs

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

Listing 15-28: A leaf node with a weak reference to its parent node branch

Creating the leaf node looks similar to Listing 15-27 with the exception of the parent field: leaf starts out without a parent, so we create a new, empty Weak<Node> reference instance.

At this point, when we try to get a reference to the parent of leaf by using the upgrade method, we get a None value. We see this in the output from the first println! statement:

leaf parent = None

When we create the branch node, it will also have a new Weak<Node> reference in the parent field, because branch doesn’t have a parent node. We still have leaf as one of the children of branch. Once we have the Node instance in branch, we can modify leaf to give it a Weak<Node> reference to its parent. We use the borrow_mut method on the RefCell<Weak<Node>> in the parent field of leaf, and then we use the Rc::downgrade function to create a Weak<Node> reference to branch from the Rc<Node> in branch.

When we print the parent of leaf again, this time we’ll get a Some variant holding branch: now leaf can access its parent! When we print leaf, we also avoid the cycle that eventually ended in a stack overflow like we had in Listing 15-26; the Weak<Node> references are printed as (Weak):

leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

The lack of infinite output indicates that this code didn’t create a reference cycle. We can also tell this by looking at the values we get from calling Rc::strong_count and Rc::weak_count.

Visualizing Changes to strong_count and weak_count

Let’s look at how the strong_count and weak_count values of the Rc<Node> instances change by creating a new inner scope and moving the creation of branch into that scope. By doing so, we can see what happens when branch is created and then dropped when it goes out of scope. The modifications are shown in Listing 15-29:

Filename: src/main.rs

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
    }

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}

Listing 15-29: Creating branch in an inner scope and examining strong and weak reference counts

After leaf is created, its Rc<Node> has a strong count of 1 and a weak count of 0. In the inner scope, we create branch and associate it with leaf, at which point when we print the counts, the Rc<Node> in branch will have a strong count of 1 and a weak count of 1 (for leaf.parent pointing to branch with a Weak<Node>). When we print the counts in leaf, we’ll see it will have a strong count of 2, because branch now has a clone of the Rc<Node> of leaf stored in branch.children, but will still have a weak count of 0.

When the inner scope ends, branch goes out of scope and the strong count of the Rc<Node> decreases to 0, so its Node is dropped. The weak count of 1 from leaf.parent has no bearing on whether or not Node is dropped, so we don’t get any memory leaks!

If we try to access the parent of leaf after the end of the scope, we’ll get None again. At the end of the program, the Rc<Node> in leaf has a strong count of 1 and a weak count of 0, because the variable leaf is now the only reference to the Rc<Node> again.

All of the logic that manages the counts and value dropping is built into Rc<T> and Weak<T> and their implementations of the Drop trait. By specifying that the relationship from a child to its parent should be a Weak<T> reference in the definition of Node, you’re able to have parent nodes point to child nodes and vice versa without creating a reference cycle and memory leaks.

Summary

This chapter covered how to use smart pointers to make different guarantees and trade-offs from those Rust makes by default with regular references. The Box<T> type has a known size and points to data allocated on the heap. The Rc<T> type keeps track of the number of references to data on the heap so that data can have multiple owners. The RefCell<T> type with its interior mutability gives us a type that we can use when we need an immutable type but need to change an inner value of that type; it also enforces the borrowing rules at runtime instead of at compile time.

Also discussed were the Deref and Drop traits, which enable a lot of the functionality of smart pointers. We explored reference cycles that can cause memory leaks and how to prevent them using Weak<T>.

If this chapter has piqued your interest and you want to implement your own smart pointers, check out “The Rustonomicon” for more useful information.

Next, we’ll talk about concurrency in Rust. You’ll even learn about a few new smart pointers.

Korkusuz Eşzamanlılık

Eşzamanlı programlamayı güvenli ve verimli bir şekilde kullanmak, Rust'ın ana hedeflerinden birisidir. Bir programın farklı bölümlerinin bağımsız olarak yürütüldüğü eşzamanlı programlama ve bir programın farklı bölümlerinin aynı anda yürütüldüğü paralel programlama giderek daha önemli hale geliyor.

Tarihsel olarak, bu tarz bir programlama zor ve hataya açıktı: Rust, bunu değiştirmeyi umuyor. Başlangıçta Rust ekibi, bellek güvenliğini sağlamanın ve eşzamanlılık sorunlarını önlemenin farklı yöntemlerle çözülmesi gereken iki ayrı zorluk olduğunu düşündü. Zamanla ekip, sahiplik ve tür sistemlerinin bellek güvenliği ve eşzamanlılık sorunlarını yönetmeye yardımcı olacak güçlü bir araç seti olduğunu keşfetti! Rust, bir çalışma zamanı eşzamanlılık hatasının oluştuğu kesin koşulları yeniden oluşturmaya çalışmak için fazladan zaman harcamak yerine, yanlış kod derlemeyi reddedecek ve sorunu açıklayan bir hata sunacaktır. Sonuç olarak, kodunuz üretime gönderildikten sonra değil, siz üzerinde çalışırken kodunuzu düzeltebilirsiniz. Rust'ın korkusuz eşzamanlılığının bu yönüne bir takma ad verdik. Korkusuz eşzamanlılık, ince hatalardan arınmış ve yeni hatalar eklemeden yeniden düzenlenebilmesi yönüyle kolay kod yazmanıza olanak tanır.

Not: Basitlik olması babından, eşzamanlı ve/veya paralel diyerek daha kesin olmak yerine birçok soruna eşzamanlı olarak değineceğiz. Bu kitap eşzamanlılık ve/veya paralellik hakkında olsaydı, daha spesifik açıklardık. Bu bölüm için, eşzamanlı kullandığımızda lütfen zihinsel olarak eşzamanlı ve/veya paralel olarak değiştirin.

Birçok dil, eşzamanlı sorunları ele almak için sundukları çözümler konusunda dogmatiktir. Örneğin, Erlang, mesaj iletme eşzamanlılığı için zarif bir işlevselliğe sahiptir, ancak iş parçacıkları arasında durumu paylaşmak için yalnızca belirsiz yollara sahiptir. Olası çözümlerin yalnızca bir alt kümesini desteklemek, üst düzey diller için makul bir stratejidir, çünkü daha yüksek düzeyli bir dil, soyutlamalar elde etmek için bazı kontrollerden vazgeçmenin yararlarını vaat eder. Bununla birlikte, daha düşük seviyeli dillerin, herhangi bir durumda en iyi performansla çözümü sağlaması ve donanım üzerinde daha az soyutlamaya sahip olması beklenir. Bu nedenle Rust, durumunuza ve gereksinimlerinize uygun olan herhangi bir şekilde problemleri modellemek için çeşitli araçlar sunar.

İşte bu bölümde ele alacağımız konulardan bazıları:

  • Aynı anda birden çok kod parçasını çalıştırmak için iş parçacıklarının nasıl oluşturulacağı
  • Mesaj geçişi türünde eşzamanlılık, iş parçacıklarının ileti dizileri arasında mesaj gönderdiği yer
  • Paylaşılan durum türünde eşzamanlılık, birden çok iş parçacığının bir veri parçasına erişimi olduğu yer
  • Rust'ın eşzamanlılık garantilerini kullanıcı tanımlı türlere ve standart kütüphane tarafından sağlanan türlere genişleten Sync ve Send tanımları

Using Threads to Run Code Simultaneously

In most current operating systems, an executed program’s code is run in a process, and the operating system will manage multiple processes at once. Within a program, you can also have independent parts that run simultaneously. The features that run these independent parts are called threads. For example, a web server could have multiple threads so that it could respond to more than one request at the same time.

Splitting the computation in your program into multiple threads to run multiple tasks at the same time can improve performance, but it also adds complexity. Because threads can run simultaneously, there’s no inherent guarantee about the order in which parts of your code on different threads will run. This can lead to problems, such as:

  • Race conditions, where threads are accessing data or resources in an inconsistent order
  • Deadlocks, where two threads are waiting for each other, preventing both threads from continuing
  • Bugs that happen only in certain situations and are hard to reproduce and fix reliably

Rust attempts to mitigate the negative effects of using threads, but programming in a multithreaded context still takes careful thought and requires a code structure that is different from that in programs running in a single thread.

Programming languages implement threads in a few different ways, and many operating systems provide an API the language can call for creating new threads. The Rust standard library uses a 1:1 model of thread implementation, whereby a program uses one operating system thread per one language thread. There are crates that implement other models of threading that make different tradeoffs to the 1:1 model.

Creating a New Thread with spawn

To create a new thread, we call the thread::spawn function and pass it a closure (we talked about closures in Chapter 13) containing the code we want to run in the new thread. The example in Listing 16-1 prints some text from a main thread and other text from a new thread:

Filename: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

Listing 16-1: Creating a new thread to print one thing while the main thread prints something else

Note that when the main thread of a Rust program completes, all spawned threads are shut down, whether or not they have finished running. The output from this program might be a little different every time, but it will look similar to the following:

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

The calls to thread::sleep force a thread to stop its execution for a short duration, allowing a different thread to run. The threads will probably take turns, but that isn’t guaranteed: it depends on how your operating system schedules the threads. In this run, the main thread printed first, even though the print statement from the spawned thread appears first in the code. And even though we told the spawned thread to print until i is 9, it only got to 5 before the main thread shut down.

If you run this code and only see output from the main thread, or don’t see any overlap, try increasing the numbers in the ranges to create more opportunities for the operating system to switch between the threads.

Waiting for All Threads to Finish Using join Handles

The code in Listing 16-1 not only stops the spawned thread prematurely most of the time due to the main thread ending, but because there is no guarantee on the order in which threads run, we also can’t guarantee that the spawned thread will get to run at all!

We can fix the problem of the spawned thread not running or ending prematurely by saving the return value of thread::spawn in a variable. The return type of thread::spawn is JoinHandle. A JoinHandle is an owned value that, when we call the join method on it, will wait for its thread to finish. Listing 16-2 shows how to use the JoinHandle of the thread we created in Listing 16-1 and call join to make sure the spawned thread finishes before main exits:

Filename: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}

Listing 16-2: Saving a JoinHandle from thread::spawn to guarantee the thread is run to completion

Calling join on the handle blocks the thread currently running until the thread represented by the handle terminates. Blocking a thread means that thread is prevented from performing work or exiting. Because we’ve put the call to join after the main thread’s for loop, running Listing 16-2 should produce output similar to this:

hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

The two threads continue alternating, but the main thread waits because of the call to handle.join() and does not end until the spawned thread is finished.

But let’s see what happens when we instead move handle.join() before the for loop in main, like this:

Filename: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

The main thread will wait for the spawned thread to finish and then run its for loop, so the output won’t be interleaved anymore, as shown here:

hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!

Small details, such as where join is called, can affect whether or not your threads run at the same time.

Using move Closures with Threads

We'll often use the move keyword with closures passed to thread::spawn because the closure will then take ownership of the values it uses from the environment, thus transferring ownership of those values from one thread to another. In the “Capturing the Environment with Closures” section of Chapter 13, we discussed move in the context of closures. Now, we’ll concentrate more on the interaction between move and thread::spawn.

Notice in Listing 16-1 that the closure we pass to thread::spawn takes no arguments: we’re not using any data from the main thread in the spawned thread’s code. To use data from the main thread in the spawned thread, the spawned thread’s closure must capture the values it needs. Listing 16-3 shows an attempt to create a vector in the main thread and use it in the spawned thread. However, this won’t yet work, as you’ll see in a moment.

Filename: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

Listing 16-3: Attempting to use a vector created by the main thread in another thread

The closure uses v, so it will capture v and make it part of the closure’s environment. Because thread::spawn runs this closure in a new thread, we should be able to access v inside that new thread. But when we compile this example, we get the following error:

$ cargo run
   Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `v`
7 |         println!("Here's a vector: {:?}", v);
  |                                           - `v` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:6:18
  |
6 |       let handle = thread::spawn(|| {
  |  __________________^
7 | |         println!("Here's a vector: {:?}", v);
8 | |     });
  | |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` due to previous error

Rust infers how to capture v, and because println! only needs a reference to v, the closure tries to borrow v. However, there’s a problem: Rust can’t tell how long the spawned thread will run, so it doesn’t know if the reference to v will always be valid.

Listing 16-4 provides a scenario that’s more likely to have a reference to v that won’t be valid:

Filename: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    drop(v); // oh no!

    handle.join().unwrap();
}

Listing 16-4: A thread with a closure that attempts to capture a reference to v from a main thread that drops v

If Rust allowed us to run this code, there’s a possibility the spawned thread would be immediately put in the background without running at all. The spawned thread has a reference to v inside, but the main thread immediately drops v, using the drop function we discussed in Chapter 15. Then, when the spawned thread starts to execute, v is no longer valid, so a reference to it is also invalid. Oh no!

To fix the compiler error in Listing 16-3, we can use the error message’s advice:

help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

By adding the move keyword before the closure, we force the closure to take ownership of the values it’s using rather than allowing Rust to infer that it should borrow the values. The modification to Listing 16-3 shown in Listing 16-5 will compile and run as we intend:

Filename: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

Listing 16-5: Using the move keyword to force a closure to take ownership of the values it uses

We might be tempted to try the same thing to fix the code in Listing 16-4 where the main thread called drop by using a move closure. However, this fix will not work because what Listing 16-4 is trying to do is disallowed for a different reason. If we added move to the closure, we would move v into the closure’s environment, and we could no longer call drop on it in the main thread. We would get this compiler error instead:

$ cargo run
   Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
  --> src/main.rs:10:10
   |
4  |     let v = vec![1, 2, 3];
   |         - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
5  | 
6  |     let handle = thread::spawn(move || {
   |                                ------- value moved into closure here
7  |         println!("Here's a vector: {:?}", v);
   |                                           - variable moved due to use in closure
...
10 |     drop(v); // oh no!
   |          ^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` due to previous error

Rust’s ownership rules have saved us again! We got an error from the code in Listing 16-3 because Rust was being conservative and only borrowing v for the thread, which meant the main thread could theoretically invalidate the spawned thread’s reference. By telling Rust to move ownership of v to the spawned thread, we’re guaranteeing Rust that the main thread won’t use v anymore. If we change Listing 16-4 in the same way, we’re then violating the ownership rules when we try to use v in the main thread. The move keyword overrides Rust’s conservative default of borrowing; it doesn’t let us violate the ownership rules.

With a basic understanding of threads and the thread API, let’s look at what we can do with threads.

İş Parçacıkları Arasında Veri Aktarmak için Mesaj Geçişini Kullanma

Güvenli eşzamanlılık sağlamak için giderek daha popüler hale gelen bir yaklaşım, iş parçacıklarının veya aktörlerin birbirlerine veri içeren mesajlar göndererek iletişim kurduğu mesaj geçişidir. İşte Go dil dokümantasyonundan bir slogan: “Belleği paylaşarak iletişim kurmayın; bunun yerine iletişim kurarak belleği paylaşın.”

Mesaj gönderme eşzamanlılığını gerçekleştirmek için Rust'ın standart kütüphanesi bir kanal süreklemesi sağlar. Kanal, verilerin bir iş parçacığından diğerine gönderildiği genel bir programlama kavramıdır.

Programlamada bir kanalı, bir dere veya nehir gibi yönlü bir su kanalı gibi düşünebilirsiniz. Bir nehre lastik ördek gibi bir şey koyarsanız, su yolunun sonuna kadar aşağı doğru hareket edecektir.

Bir kanalın iki yarısı vardır: bir verici ve bir alıcı. Verici yarı, nehre lastik ördek koyduğunuz yukarı akış konumudur ve alıcı yarı, lastik ördeğin aşağı akışta sona erdiği yerdir. Kodunuzun bir kısmı göndermek istediğiniz verilerle vericideki yöntemleri çağırır ve başka bir kısmı da gelen mesajlar için alıcı ucunu kontrol eder. Verici veya alıcı yarısından herhangi biri düşerse bir kanalın kapalı olduğu söylenir.

Burada, değerler üreten ve bunları bir kanaldan gönderen bir iş parçacığına ve değerleri alıp yazdıracak başka bir iş parçacığına sahip bir program üzerinde çalışacağız. Özelliği göstermek için bir kanal kullanarak iş parçacıkları arasında basit değerler göndereceğiz. Tekniğe aşina olduktan sonra, sohbet sistemi veya birçok iş parçacığının bir hesaplamanın parçalarını gerçekleştirdiği ve parçaları sonuçları toplayan bir iş parçacığına gönderdiği bir sistem gibi birbirleri arasında iletişim kurması gereken herhangi bir iş parçacığı için kanalları kullanabilirsiniz.

İlk olarak, Liste 16-6'da bir kanal oluşturacağız ancak onunla hiçbir şey yapmayacağız. Bunun henüz derlenmeyeceğini unutmayın çünkü Rust kanal üzerinden ne tür değerler göndermek istediğimizi söyleyemez.

Dosya adı: src/main.rs

use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();
}

Liste 16-6: Bir kanal oluşturma ve iki yarıyı tx ve rx olarak atama

mpsc::channel fonksiyonunu kullanarak yeni bir kanal oluşturuyoruz; mpsc çoklu üretici, tekli tüketici anlamına geliyor. Kısacası, Rust'ın standart kütüphanesinin kanalları sürekleme şekli, bir kanalın değer üreten birden fazla gönderen uca sahip olabileceği, ancak bu değerleri tüketen yalnızca bir alıcı uca sahip olabileceği anlamına gelir. Birden fazla akarsuyun birlikte büyük bir nehre aktığını düşünün: akarsulardan herhangi birine gönderilen her şey sonunda tek bir nehirde son bulacaktır. Şimdilik tek bir üretici ile başlayacağız, ancak bu örneği çalıştırdığımızda birden fazla üretici ekleyeceğiz.

mpsc::channel fonksiyonu bir tuple döndürür, ilk elemanı gönderen uç-verici- ve ikinci elemanı alan uç-alıcıdır. tx ve rx kısaltmaları geleneksel olarak birçok alanda sırasıyla verici ve alıcı için kullanılır, bu nedenle değişkenlerimizi her bir ucu belirtmek için bu şekilde adlandırıyoruz. tuple'ları yıkıma uğratan bir kalıp ile let ifade yapısını kullanıyoruz; let ifade yapılarında kalıp kullanımı ve yıkım konusunu Bölüm 18'de tartışacağız. Şimdilik, let deyimini bu şekilde kullanmanın mpsc::channel tarafından döndürülen tuple parçalarını ayıklamak için uygun bir yaklaşım olduğunu bilin.

İletim ucunu oluşturulmuş bir iş parçacığına taşıyalım ve bir dize göndermesini sağlayalım, böylece oluşturulmuş iş parçacığı Liste 16-7'de gösterildiği gibi ana iş parçacığı ile iletişim kurar. Bu, nehrin yukarısına bir lastik ördek koymak veya bir iş parçacığından diğerine bir sohbet mesajı göndermek gibidir.

Dosya adı: src/main.rs

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });
}

Liste 16-7: txi doğmuş bir iş parçacığına taşıma ve “hello” gönderme

Yine, yeni bir iş parçacığı oluşturmak için thread::spawn kullanıyoruz ve ardından tx'i kapanışa taşımak için move kullanıyoruz, böylece oluşturulan iş parçacığı tx'e sahip oluyor. Ortaya çıkan iş parçacığının kanal üzerinden mesaj gönderebilmesi için vericiye sahip olması gerekir. Verici, göndermek istediğimiz değeri alan bir send metoda sahiptir. send metodu Result<T, E> türü döndürür, bu nedenle alıcı zaten bırakılmışsa ve değer gönderilecek bir yer yoksa, send metodu bir hata döndürür. Bu örnekte, hata durumunda paniklemek için unwrap'i çağırıyoruz. Ancak gerçek bir süreklemede bunu düzgün bir şekilde ele alırız: düzgün hata işleme stratejilerini gözden geçirmek için Bölüm 9'a dönün.

Liste 16-8'de, ana iş parçacığındaki alıcıdan değeri alacağız. Bu, nehrin sonundaki sudan lastik ördeği almak veya bir sohbet mesajı almak gibidir.

Dosya adı: src/main.rs

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}

Liste 16-8: Ana iş parçacığında “hi” değerinin alınması ve yazdırılması

Alıcının iki faydalı yöntemi vardır: recv ve try_recv. Ana iş parçacığının çalışmasını engelleyecek ve kanaldan bir değer gönderilinceye kadar bekleyecek olan recv'yi kullanıyoruz. Bir değer gönderildiğinde, recv bu değeri Result<T, E> olarak döndürür. Verici kapandığında, recv daha fazla değer gelmeyeceğini belirtmek için bir hata döndürecektir.

try_recv yöntemi bloke olmaz, ancak bunun yerine hemen bir Result<T, E> döndürür: mevcutsa bir mesaj içeren bir Ok değeri ve bu sefer herhangi bir mesaj yoksa bir Err değeri. Bu iş parçacığının mesajları beklerken yapacak başka işleri varsa try_recv'i kullanmak yararlıdır: try_recv'i sık sık çağıran, varsa bir mesajı işleyen ve aksi takdirde tekrar kontrol edene kadar kısa bir süre başka işler yapan bir döngü yazabiliriz.

Bu örnekte basitlik için recv kullandık; ana iş parçacığının mesajları beklemek dışında yapacağı başka bir işimiz yok, bu nedenle ana iş parçacığını engellemek uygundur.

Liste 16-8'deki kodu çalıştırdığımızda, ana iş parçacığından yazdırılan değeri göreceğiz:

Got: hi

Mükemmel!

Kanallar ve Sahiplik Transferi

Sahiplik kuralları mesaj göndermede hayati bir rol oynar çünkü güvenli, eşzamanlı kod yazmanıza yardımcı olurlar. Eşzamanlı programlamada hataları önlemek, Rust programlarınız boyunca sahiplik hakkında düşünmenin avantajıdır. Kanalların ve sahipliğin sorunları önlemek için nasıl birlikte çalıştığını göstermek için bir deney yapalım: kanaldan aşağı gönderdikten sonra ortaya çıkan iş parçacığında bir val değeri kullanmaya çalışacağız. Bu koda neden izin verilmediğini görmek için Liste 16-9'daki kodu derlemeyi deneyin:

Dosya adı: src/main.rs

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
        println!("val is {}", val);
    });

    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}

Liste 16-9: Kanaldan gönderdikten sonra val kullanmayı denemek

Burada, tx.send aracılığıyla kanala gönderdikten sonra val'ı yazdırmayı deniyoruz. Buna izin vermek kötü bir fikir olacaktır: değer başka bir iş parçacığına gönderildikten sonra, biz değeri tekrar kullanmaya çalışmadan önce o iş parçacığı değeri değiştirebilir veya düşürebilir. Potansiyel olarak, diğer iş parçacığının değişiklikleri tutarsız veya var olmayan veriler nedeniyle hatalara veya beklenmedik sonuçlara neden olabilir. Ancak, Liste 16-9'daki kodu derlemeye çalışırsak Rust bize bir hata verir:

$ cargo run
   Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
  --> src/main.rs:10:31
   |
8  |         let val = String::from("hi");
   |             --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
9  |         tx.send(val).unwrap();
   |                 --- value moved here
10 |         println!("val is {}", val);
   |                               ^^^ value borrowed here after move
   |
   = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.
error: could not compile `message-passing` due to previous error

Eşzamanlılık hatamız bir derleme zamanı hatasına neden oldu. send fonksiyonu parametresinin sahipliğini alır ve değer taşındığında alıcı onun sahipliğini alır. Bu, değeri gönderdikten sonra yanlışlıkla tekrar kullanmamızı engeller; sahiplik sistemi her şeyin yolunda olup olmadığını kontrol eder.

Birden Fazla Değer Gönderme ve Alıcının Beklediğini Görme

Liste 16-8'deki kod derlendi ve çalıştırıldı, ancak bize iki ayrı iş parçacığının kanal üzerinden birbiriyle konuştuğunu açıkça göstermedi. Liste 16-10'da, Liste 16-8'deki kodun eşzamanlı olarak çalıştığını kanıtlayacak bazı değişiklikler yaptık: ortaya çıkan iş parçacığı artık birden fazla mesaj gönderecek ve her mesaj arasında bir saniye duracak.

Dosya adı: src/main.rs

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}

Liste 16-10: Birden fazla mesaj gönderme ve her biri arasında duraklama

Bu kez, ortaya çıkan iş parçacığı ana iş parçacığına göndermek istediğimiz dizelerden oluşan bir vektöre sahiptir. Bunların üzerinde yineleme yaparak her birini ayrı ayrı gönderiyoruz ve thread::sleep fonksiyonunu 1 saniyelik bir Duration değeriyle çağırarak her biri arasında duraklatıyoruz.

Ana iş parçacığında, artık recv fonksiyonunu açıkça çağırmıyoruz: bunun yerine, rx'i yineleyici olarak ele alıyoruz. Alınan her değer için onu yazdırıyoruz. Kanal kapatıldığında, yineleme sona erecektir.

Liste 16-10'daki kodu çalıştırdığınızda, her satır arasında 1 saniyelik bir duraklama ile aşağıdaki çıktıyı görmelisiniz:

Got: hi
Got: from
Got: the
Got: thread

Ana iş parçacığındaki for döngüsünde duraklatan veya geciktiren herhangi bir kodumuz olmadığından, ana iş parçacığının ortaya çıkan iş parçacığından değer almayı beklediğini söyleyebiliriz.

Vericiyi Klonlayarak Birden Fazla Üretici Oluşturma

Daha önce mpsc'nin çoklu üretici, tekli tüketici için kullanılan bir kısaltma olduğundan bahsetmiştik. Şimdi mpsc'yi kullanalım ve hepsi aynı alıcıya değer gönderen birden fazla iş parçacığı oluşturmak için Liste 16-10'daki kodu genişletelim. Bunu, Liste 16-11'de gösterildiği gibi vericiyi klonlayarak yapabiliriz:

Dosya adı: src/main.rs

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    // --snip--

    let (tx, rx) = mpsc::channel();

    let tx1 = tx.clone();
    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx1.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    thread::spawn(move || {
        let vals = vec![
            String::from("more"),
            String::from("messages"),
            String::from("for"),
            String::from("you"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }

    // --snip--
}

Liste 16-11: Birden fazla üreticiden birden fazla mesaj gönderme

Bu kez, ilk iş parçacığını oluşturmadan önce, vericide clone çağrısı yapıyoruz. Bu bize ilk iş parçacığına aktarabileceğimiz yeni bir verici verecektir. Orijinal vericiyi ikinci bir iş parçacığına aktarırız. Bu bize her biri bir alıcıya farklı mesajlar gönderen iki iş parçacığı verir.

Kodu çalıştırdığınızda, çıktınız aşağıdaki gibi görünmelidir:

Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you

Sisteminize bağlı olarak değerleri farklı bir sırada görebilirsiniz. Eşzamanlılığı zor olduğu kadar ilginç kılan da budur. Thread::sleep ile denemeler yaparsanız, farklı iş parçacıklarında farklı değerler verirseniz, her çalıştırma daha belirsiz olacak ve her seferinde farklı çıktılar oluşturacaktır.

Kanalların nasıl çalıştığına baktığımıza göre, şimdi farklı bir eşzamanlılık yöntemine bakalım.

Paylaşılan Durum Eşzamanlılığı

Mesaj geçişi, eşzamanlılığı ele almanın iyi bir yoludur, ancak tek yol değildir. Başka bir yöntem de birden fazla iş parçacığının aynı paylaşılan veriye erişmesidir. Go dil dokümantasyonundaki sloganın bu kısmını tekrar düşünün: “belleği paylaşarak iletişim kurmayın.”

Bellek paylaşarak iletişim kurmak neye benzer? Buna ek olarak, mesaj geçişi meraklıları neden bellek paylaşımını kullanmamaya dikkat ederler?

Bir bakıma, herhangi bir programlama dilindeki kanallar tekil sahipliğe benzer, çünkü bir değeri bir kanaldan aşağı aktardığınızda, artık o değeri kullanmamalısınız. Paylaşılan bellek eşzamanlılığı çoklu sahiplik gibidir: birden fazla iş parçacığı aynı bellek konumuna aynı anda erişebilir. Akıllı işaretçilerin çoklu sahipliği mümkün kıldığı Bölüm 15'te gördüğünüz gibi, çoklu sahiplik karmaşıklık yaratabilir çünkü bu farklı sahiplerin yönetilmesi gerekir. Rust'ın tür sistemi ve sahiplik kuralları bu yönetimin doğru yapılmasına büyük ölçüde yardımcı olur. Bir örnek olarak, paylaşılan bellek için en yaygın eşzamanlılık ilkellerinden biri olan mutekslere bakalım.

Aynı Anda Bir İş Parçacığından Veriye Erişime İzin Vermek için Muteksleri Kullanma

Muteks, karşılıklı dışlamanın kısaltmasıdır, yani bir muteks herhangi bir zamanda yalnızca bir iş parçacığının bazı verilere erişmesine izin verir. Bir muteks içindeki verilere erişmek için, bir iş parçacığı önce muteksin kilidini almak isteyerek erişim istediğini belirtmelidir. Kilit, muteksin bir parçası olan ve o anda verilere kimin özel erişime sahip olduğunu takip eden bir veri yapısıdır. Bu nedenle muteks, kilitleme sistemi aracılığıyla tuttuğu verileri koruyor olarak tanımlanır.

Mutekslerin kullanımı zor olmakla ünlüdür çünkü iki kuralı hatırlamanız gerekir:

  • Veriyi kullanmadan önce kilidi elde etmeye çalışmalısınız.
  • Muteksin koruduğu verilerle işiniz bittiğinde, diğer iş parçacıklarının kilidi alabilmesi için verilerin kilidini açmanız gerekir.

Muteks için gerçek dünyadan bir benzetme yapmak gerekirse, bir konferansta yalnızca bir mikrofonun olduğu bir panel tartışması hayal edin. Bir panelist konuşmadan önce mikrofonu kullanmak istediğini söylemeli ya da işaret etmelidir. Mikrofonu aldıklarında, istedikleri kadar konuşabilirler ve daha sonra mikrofonu konuşmak isteyen bir sonraki paneliste verirler. Eğer bir panelist işi bittiğinde mikrofonu vermeyi unutursa, başka kimse konuşamaz. Paylaşılan mikrofonun yönetimi yanlış giderse, panel planlandığı gibi çalışmaz!

Mutekslerin yönetimini doğru yapmak inanılmaz derecede zor olabilir, bu yüzden pek çok insan kanallar konusunda heveslidir. Ancak Rust'ın tür sistemi ve sahiplik kuralları sayesinde kilitleme ve kilit açma işlemlerini yanlış yapamazsınız.

Mutex<T> API'si

Bir muteksin nasıl kullanılacağına örnek olarak, Liste 16-12'de gösterildiği gibi tek iş parçacıklı bir bağlamda bir muteks kullanarak başlayalım:

Dosya adı: src/main.rs

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {:?}", m);
}

Liste 16-12: Basitlik için tek iş parçacıklı bir bağlamda Mutex<T> API'sini keşfetmek

Birçok türde olduğu gibi, ilişkili new fonksiyonunu kullanarak bir Mutex<T> oluşturuyoruz. Mutex içindeki verilere erişmek için lock metodunu kullanarak kilidi alırız. Bu çağrı mevcut iş parçacığını bloke eder, böylece kilide sahip olma sırası bize gelene kadar herhangi bir iş yapamaz.

Kilidi elinde tutan başka bir iş parçacığı paniğe kapılırsa lock çağrısı başarısız olur. Bu durumda, hiç kimse kilidi alamaz, bu nedenle böyle bir durumla karşılaşırsak kilidi açmayı ve bu iş parçacığının paniklemesini sağlamayı seçtik.

Kilidi elde ettikten sonra, bu durumda num olarak adlandırılan geri dönüş değerini, içindeki verilere değiştirilebilir bir referans olarak ele alabiliriz. Tür sistemi, m içindeki değeri kullanmadan önce bir kilit elde etmemizi sağlar. m'nin tipi i32 değil Mutex<i32>'dir, bu nedenle i32 değerini kullanabilmek için lock'u çağırmalıyız. Unutmamalıyız; aksi takdirde tür sistemi içteki i32'ye erişmemize izin vermez.

Tahmin edebileceğiniz gibi, Mutex<T> akıllı bir işaretçidir. Daha doğrusu, lock çağrısı, unwrap çağrısıyla işlediğimiz bir LockResult'a sarılmış MutexGuard adlı bir akıllı işaretçi döndürür. MutexGuard akıllı işaretçisi, iç verilerimize işaret etmek için Deref'i uygular; akıllı işaretçi ayrıca, bir MutexGuard kapsam dışına çıktığında kilidi otomatik olarak serbest bırakan bir Drop'a sahiptir, bu da iç kapsamın sonunda gerçekleşir.

Sonuç olarak, kilidi serbest bırakmayı unutma. Muteksin diğer iş parçacıkları tarafından kullanılmasını engelleme riskimiz yoktur, çünkü kilit serbest bırakma işlemi otomatik olarak gerçekleşir.

Kilidi bıraktıktan sonra muteks değerini yazdırabilir ve i32'yi 6 olarak değiştirebildiğimizi görebiliriz.

Birden Fazla İş Parçacığı Arasında Mutex<T> Paylaşımı

Şimdi, Mutex<T> kullanarak bir değeri birden fazla iş parçacığı arasında paylaştırmayı deneyelim. 10 iş parçacığı oluşturacağız ve her birinin bir sayaç değerini 1 artırmasını sağlayacağız, böylece sayaç 0'dan 10'a gidecek. Liste 16-13'teki bir sonraki örnekte bir derleyici hatası olacak ve bu hatayı Mutex<T> kullanımı ve Rust'ın bunu doğru kullanmamıza nasıl yardımcı olduğu hakkında daha fazla bilgi edinmek için kullanacağız.

Dosya adı: src/main.rs

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];

    for _ in 0..10 {
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Liste 16-13: On iş parçacığının her biri bir Mutex<T> tarafından korunan bir sayacı artırır

Liste 16-12'de yaptığımız gibi, Mutex<T> içinde i32 tutmak için bir counter değişkeni oluşturuyoruz. , Ardından, bir dizi sayı üzerinde yineleme yaparak 10 iş parçacığı oluşturuyoruz. Thread::spawn kullanıyoruz ve tüm iş parçacıklarına aynı kapanışı veriyoruz: sayacı iş parçacığına taşıyan, lock metodunu çağırarak Mutex<T> üzerinde bir kilit elde ediyor ve ardından muteksteki değere 1 eklemiş oluyoruz. Bir iş parçacığı kapanışını çalıştırmayı bitirdiğinde, num kapsam dışına çıkar ve kilidi serbest bırakır, böylece başka bir iş parçacığı onu alabilir.

Ana iş parçacığında, tüm birleştirme tutamaçlarını toplarız. Ardından, Liste 16-2'de yaptığımız gibi, tüm iş parçacıklarının bittiğinden emin olmak için her bir tanıtıcıda join çağrısı yaparız. Bu noktada, ana iş parçacığı kilidi alacak ve bu programın sonucunu yazdıracaktır.

Bu örneğin derlenmeyeceğini demiştik. Şimdi nedenini bulalım!

$ cargo run
   Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: use of moved value: `counter`
  --> src/main.rs:9:36
   |
5  |     let counter = Mutex::new(0);
   |         ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
9  |         let handle = thread::spawn(move || {
   |                                    ^^^^^^^ value moved into closure here, in previous iteration of loop
10 |             let mut num = counter.lock().unwrap();
   |                           ------- use occurs due to use in closure

For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` due to previous error

Hata mesajı, counter değerinin döngünün önceki yinelemesinde taşındığını belirtir. Rust bize kilit sayacının sahipliğini birden fazla iş parçacığına taşıyamayacağımızı söylüyor. Derleyici hatasını Bölüm 15'te tartıştığımız çoklu sahiplik yöntemi ile düzeltelim.

Çoklu İş Parçacığı ile Çoklu Sahiplik

Bölüm 15'te, referans sayılan bir değer oluşturmak için Rc<T> akıllı işaretçisini kullanarak bir değere birden fazla sahip vermiştik. Burada da aynısını yapalım ve ne olacağını görelim. Liste 16-14'te Mutex<T>'yi Rc<T>'ye saracağız ve sahipliği iş parçacığına taşımadan önce Rc<T>'yi klonlayacağız.

Dosya adı: src/main.rs

use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Rc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Rc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Liste 16-14: Birden fazla iş parçacığının Mutex<T>ye sahip olmasına izin vermek için Rc<T> kullanılmaya çalışılıyor

Bir kez daha derliyoruz ve... farklı farklı hatalar alıyoruz! Derleyici bize çok şey öğretiyor.

$ cargo run
   Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
   --> src/main.rs:11:22
    |
11  |           let handle = thread::spawn(move || {
    |  ______________________^^^^^^^^^^^^^_-
    | |                      |
    | |                      `Rc<Mutex<i32>>` cannot be sent between threads safely
12  | |             let mut num = counter.lock().unwrap();
13  | |
14  | |             *num += 1;
15  | |         });
    | |_________- within this `[closure@src/main.rs:11:36: 15:10]`
    |
    = help: within `[closure@src/main.rs:11:36: 15:10]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
    = note: required because it appears within the type `[closure@src/main.rs:11:36: 15:10]`
note: required by a bound in `spawn`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` due to previous error

İlgiçtir ki, bu hata mesajı çok karışık duruyor! İşte odaklanmanız gereken önemli kısım: Rc<Mutex<i32>> iş parçacıkları arasında güvenli bir şekilde gönderilemez (`Rc<Mutex<i32>>` cannot be sent between threads safely) . Derleyici bize bunun nedenini de söylüyor: Send tanımı Rc<Mutex<i32>> için uygulanmıyor. Send hakkında bir sonraki bölümde konuşacağız: thread'lerle kullandığımız türlerin eşzamanlı durumlarda kullanılmasını sağlayan özelliklerden biridir.

Ne yazık ki, Rc<T>'nin iş parçacıkları arasında paylaşılması güvenli değildir. Rc<T> referans sayımını yönetirken, her clone çağrısı için sayıma ekleme yapar ve her klon bırakıldığında sayıdan çıkarma yapar. Ancak, sayıdaki değişikliklerin başka bir iş parçacığı tarafından kesintiye uğratılamayacağından emin olmak için herhangi bir eşzamanlılık ilkeli kullanmaz. Bu, yanlış sayımlara yol açabilir - bu da bellek sızıntılarına veya bir değerin işimiz bitmeden önce bırakılmasına neden olabilecek ince hatalara yol açabilir. İhtiyacımız olan şey tam olarak Rc<T> gibi bir türdür, ancak referans sayımındaki değişiklikleri iş parçacığı güvenli bir şekilde yapan bir türdür.

Arc<T> ile Atomik Referans Sayma

Neyse ki Arc<T>, Rc<T> gibi eşzamanlı durumlarda kullanımı güvenli olan bir türdür. A atomik anlamına gelir, yani atomik olarak referans sayılan bir türdür. Atomikler, burada ayrıntılı olarak ele almayacağımız ek bir eşzamanlılık ilkelidir: daha fazla ayrıntı için std::sync::atomic için standart kütüphane dokümantasyonlarına bakın. Bu noktada, atomiklerin ilkel tipler gibi çalıştığını ancak iş parçacıkları arasında paylaşılmasının güvenli olduğunu bilmeniz yeterlidir.

O zaman neden tüm ilkel tiplerin atomik olmadığını ve neden standart kütüphane tiplerinin varsayılan olarak Arc<T> kullanacak şekilde uygulanmadığını merak edebilirsiniz. Bunun nedeni, iş parçacığı güvenliğinin yalnızca gerçekten ihtiyaç duyduğunuzda ödemek isteyeceğiniz bir performans cezası ile birlikte gelmesidir. Sadece tek bir iş parçacığı içinde değerler üzerinde işlem yapıyorsanız, atomiklerin sağladığı garantileri uygulamak zorunda kalmazsanız kodunuz daha hızlı çalışabilir.

Örneğimize geri dönelim: Arc<T> ve Rc<T> aynı API'ye sahiptir, bu nedenle use satırını, new çağrısını ve clone çağrısını değiştirerek programımızı düzeltiriz. Liste 16-15'teki kod nihayet derlenecek ve çalışacaktır:

Dosya adı: src/main.rs

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Liste 16-15: Sahipliği birden fazla iş parçacığı arasında paylaştırabilmek için Mutex<T>yi sarmak üzere bir Arc<T> kullanmak

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

Result: 10

Başardık! 0'dan 10'a kadar saydık, bu çok etkileyici görünmeyebilir, ancak bize Mutex<T> ve iş parçacığı güvenliği hakkında çok şey öğretti. Bu programın yapısını bir sayacı artırmaktan daha karmaşık işlemler yapmak için de kullanabilirsiniz. Bu stratejiyi kullanarak, bir hesaplamayı bağımsız parçalara bölebilir, bu parçaları iş parçacıkları arasında paylaştırabilir ve ardından her iş parçacığının nihai sonucu kendi parçasıyla güncellemesini sağlamak için Mutex<T>'i kullanabilirsiniz.

RefCell<T>/Rc<T> ve Mutex<T>/Arc<T> Arasındaki Benzerlikler

Sayacın değişmez olduğunu ancak içindeki değere değişebilir bir referans alabileceğimizi fark etmiş olabilirsiniz; bu, Mutex<T>'nin Cell ailesinin yaptığı gibi iç değişebilirlik sağladığı anlamına gelir. Bölüm 15'te RefCell<T>'yi bir Rc<T> içindeki içeriği değiştirmemize izin vermek için kullandığımız gibi, Mutex<T>'yi bir Arc<T> içindeki içeriği değiştirmek için kullanırız.

Unutulmaması gereken bir diğer ayrıntı da Mutex<T> kullandığınızda Rust'ın sizi her türlü mantık hatasından koruyamayacağıdır. Bölüm 15'te Rc<T> kullanmanın, iki Rc<T> değerinin birbirine atıfta bulunduğu ve bellek sızıntılarına neden olan referans döngüleri oluşturma riskiyle birlikte geldiğini hatırlayın. Benzer şekilde, Mutex<T> de kilitlenme yaratma riskini beraberinde getirir. Bunlar, bir işlemin iki kaynağı kilitlemesi gerektiğinde ve iki iş parçacığının her biri kilitlerden birini aldığında ortaya çıkar ve birbirlerini sonsuza kadar beklemelerine neden olur. Kilitlenmelerle ilgileniyorsanız, kilitlenmeye sahip bir Rust programı oluşturmayı deneyin; daha sonra herhangi bir dilde muteksler için kilitlenme azaltma stratejilerini araştırın ve bunları Rust'ta uygulamayı deneyin. Mutex<T> ve MutexGuard için standart kütüphane API belgeleri faydalı bilgiler sunar.

Bu bölümü Send ve Sync tanımlarından ve bunları özel türlerle nasıl kullanabileceğimizden bahsederek tamamlayacağız.

Sync ve Send Tanımlarıyla Genişletilebilir Eşzamanlılık

İlginç bir şekilde, Rust dili çok az eşzamanlılık özelliğine sahiptir. Bu bölümde şimdiye kadar bahsettiğimiz neredeyse tüm eşzamanlılık özellikleri dilin değil, standart kütüphanenin bir parçasıydı. Eşzamanlılığı ele almak için seçenekleriniz dil veya standart kütüphane ile sınırlı değildir; kendi eşzamanlılık özelliklerinizi yazabilir veya başkaları tarafından yazılanları kullanabilirsiniz.

Ancak, iki eşzamanlılık kavramı dilin içine yerleştirilmiştir: std::marker tanımları Sync ve Send.

Send ile İş Parçacıkları Arasında Sahiplik Aktarımına İzin Verme

Send işaretleyici tanımı, Send'i sürekleyen türdeki değerlerin sahipliğinin iş parçacıkları arasında aktarılabileceğini gösterir. Hemen hemen her Rust türü Send'dir, ancak Rc<T> gibi bazı istisnalar vardır: bu tür Send olamaz, çünkü bir Rc<T> değerini klonlarsanız ve klonun sahipliğini başka bir iş parçacığına aktarmaya çalışırsanız, her iki iş parçacığı da referans sayısını aynı anda güncelleyebilir. Bu nedenle Rc<T>, iş parçacığı güvenli performans cezasını ödemek istemediğiniz tek iş parçacıklı durumlarda kullanılmak üzere uygulanmıştır.

Bu nedenle, Rust'ın tür sistemi ve tanım bağlılığı, bir Rc<T> değerini asla yanlışlıkla iş parçacıkları arasında güvenli olmayan bir şekilde gönderemeyeceğinizi garanti eder. Bunu Liste 16-14'te yapmaya çalıştığımızda, Send tanımının Rc<Mutex<i32>> için süreklenmediği hatasını almıştık. Send'i süreklemiş Arc<T>'ye geçtiğimizde kod derlendi.

Tamamen Send türlerinden oluşan herhangi bir tür de otomatik olarak Send olarak işaretlenir. Bölüm 19'da tartışacağımız ham işaretçiler dışında neredeyse tüm ilkel tipler Send'dir.

Sync ile Birden Fazla İş Parçacığından Erişime İzin Verme

Sync işaretleyici tanımı, Sync'i sürekleyen türe birden fazla iş parçacığından başvurulmasının güvenli olduğunu belirtir. Başka bir deyişle, &T (T'ye değişmez bir referans) Send ise herhangi bir T türü Sync'tir, yani referans başka bir iş parçacığına güvenle gönderilebilir. Send'e benzer şekilde, ilkel tipler Sync'tir ve tamamen Sync olan tiplerden oluşan tipler de Sync'tir.

Akıllı işaretçi Rc<T> de Send olmamasıyla aynı nedenlerden dolayı Sync değildir. RefCell<T> türü (Bölüm 15'te bahsetmiştik) ve ilgili Cell<T> türleri ailesi Sync değildir. RefCell<T>'nin çalışma zamanında yaptığı ödünç alma denetimi uygulaması iş parçacığı güvenli değildir. Akıllı işaretçi Mutex<T> Sync'tir ve “Bir Mutex<T>'i Birden Fazla İş Parçacığı Arasında Paylaşma” bölümünde gördüğünüz gibi erişimi birden fazla iş parçacığı ile paylaşmak için kullanılabilir.

Send ve Sync'i Manuel Olarak Süreklemek Güvenli Değildir

Send ve Sync tanımlarından oluşan türler otomatik olarak Send ve Sync özelliklerine de sahip olduğundan, bu özellikleri manuel olarak süreklememiz gerekmez. İşaretleyici tanımlar olarak, süreklenecek herhangi bir metodları bile yoktur. Sadece eşzamanlılıkla ilgili değişmezleri uygulamak için kullanışlıdırlar.

Bu tanımların manuel olarak uygulanması, güvenli olmayan Rust kodunun uygulanmasını gerektirir. Güvensiz Rust kodunun kullanımı hakkında Bölüm 19'da konuşacağız; şimdilik önemli bilgi, Send ve Sync parçalarından oluşmayan yeni eşzamanlı türler oluşturmanın güvenlik garantilerini korumak için dikkatli düşünmeyi gerektirdiğidir. “The Rustonomicon” bu garantiler ve bunların nasıl korunacağı hakkında daha fazla bilgi içerir.

Özet

Bu kitapta eşzamanlılıkla ilgili göreceğiniz son şey bu değil: Bölüm 20'deki proje, bu bölümdeki kavramları burada tartışılan küçük örneklerden daha gerçekçi bir durumda kullanacaktır.

Daha önce de belirtildiği gibi, Rust'ın eşzamanlılığı nasıl ele aldığının çok azı dilin bir parçası olduğu için, birçok eşzamanlılık çözümü kasa olarak süreklenmektedir. Bunlar standart kütüphaneden daha hızlı gelişir, bu nedenle çok iş parçacıklı durumlarda kullanılacak güncel, son teknoloji ürünü kasalar için doğru çevrimiçi arama yaptığınızdan emin olun.

Rust standart kütüphanesi, mesaj geçişi için kanallar ve eş zamanlı bağlamlarda kullanımı güvenli olan Mutex<T> ve Arc<T> gibi akıllı işaretçi türleri sağlar. Tür sistemi ve ödünç denetleyicisi, bu çözümleri kullanan kodun veri yarışları veya geçersiz referanslarla sonuçlanmamasını sağlar. Kodunuzun derlenmesini sağladıktan sonra, diğer dillerde yaygın olan izlenmesi zor hata türleri olmadan birden fazla iş parçacığında mutlu bir şekilde çalışacağından emin olabilirsiniz. Eşzamanlı programlama artık korkulacak bir kavram değil: gidin ve programlarınızı korkusuzca eşzamanlı hale getirin!

Daha sonra, Rust programlarınız büyüdükçe sorunları modellemenin ve çözümleri yapılandırmanın deyimsel yollarından bahsedeceğiz. Ayrıca, Rust'ın deyimlerinin nesne yönelimli programlamadan aşina olabileceğiniz deyimlerle nasıl ilişkili olduğunu tartışacağız.

Rust'ta Nesne Yönelimli Programlama Özellikleri

Nesne Yönelimli Programlama (NYP, OOP) programları modellemenin bir yoludur.

Programlanabilir bir kavram olarak nesneler, 1960'larda Simula dilinde tanıtıldı. Bu yaklaşım, Alan Kay'in nesnelerin birbirine mesaj ilettiği programlama mimarisini etkiledi. Bu mimariyi tanımlamak için 1967'de nesne yönelimli programlama terimini türetti. Birçok rakip dil NYP'nin ne olduğunu tanımlar ve bu tanımların bazılarını baz aldığımız taktirde Rust nesne yönelimlidir, ancak diğerleri için değildir. Bu bölümde, yaygın olarak nesne yönelimli dillerin desteklediği kabul edilen belirli özellikleri ve bu özelliklerin Rust'a nasıl çevrildiğini keşfedeceğiz. Daha sonra size Rust'ta nesne yönelimli bir tasarım deseninin nasıl uygulanacağını göstereceğiz ve bunun yerine Rust'ın bazı güçlü yanlarını kullanarak bir çözüm uygulamakla bunun arasındaki farkları tartışacağız.

Nesne Yönelimli Dillerin Karakteristik Özellikleri

Programlama topluluğunda, bir dilin nesne yönelimli olarak kabul edilmesi için hangi özelliklerin olması gerektiği konusunda bir fikir birliği yoktur. Rust, NYP (OOP) dahil olmak üzere birçok programlama paradigmasından etkilenir; örneğin, Bölüm 13'te işlevsel programlamadan gelen özellikleri araştırdık. Muhtemelen, NYP dilleri nesneler, kapsülleme ve kalıtım gibi belirli ortak özellikleri paylaşır. Bu özelliklerin her birinin ne anlama geldiğine ve Rust'ın bunu destekleyip desteklemediğine bakalım.

Nesneler Veri ve Davranış İçeriyor

Erich Gamma, Richard Helm, Ralph Johnson ve John Vlissides (Addison-Wesley Professional, 1994) tarafından yazılan Design Patterns: Elements of Reusable Object-Oriented Software kitabı (Addison-Wesley Professional, 1994), halk arasında The Gang of Four kitabı olarak anılır, bir nesne kataloğudur. Bu kitap, NYP'ı şu şekilde tanımlar:

Nesne yönelimli programlar nesnelerden oluşur. Bir nesne hem verileri hem de bu veriler üzerinde çalışan prosedürleri paketler. Prosedürlere tipik olarak yöntemler veya işlemler denir.

Bu tanımı kullanırsak, Rust nesne yönelimlidir: yapılar ve numaralandırılmışlar verilere sahiptir ve impl blokları, yapılar ve numaralandırılmışlar üzerinde metodlar sağlar. Metodları olan yapılar ve numaralandırılmışlar nesne olarak adlandırılmasa da, The Gang of Four'un nesne tanımına göre aynı işlevselliği sağlarlar.

Sürekleme Ayrıntılarını Gizleyen Kapsülleme

NYP ile yaygın olarak ilişkilendirilen başka bir yön, kapsülleme fikridir; bu, bir nesnenin uygulama ayrıntılarına o nesneyi kullanarak erişemeyeceği anlamına gelir. Bu nedenle, bir nesneyle etkileşim kurmanın tek yolu, onun genel API'sidir; nesneyi kullanan kod, nesnenin iç kısımlarına erişememeli ve verileri veya davranışı doğrudan değiştirememelidir. Bu, programcının, nesneyi kullanan kodu değiştirmeye gerek kalmadan bir nesnenin içindekileri değiştirmesini ve yeniden düzenlemesini sağlar.

Kapsüllemenin nasıl kontrol edileceğini Bölüm 7'de tartıştık: kodumuzdaki hangi modüllerin, türlerin, fonksiyonların ve yöntemlerin genel olacağına karar vermek için pub anahtar sözcüğünü kullanabiliriz ve varsayılan olarak diğer her şey private olur. Örneğin, bir i32 vektörü bir AveragedCollection yapısı tanımlayabiliriz. Yapı, vektördeki değerlerin ortalamasını içeren bir alana da sahip olabilir; bu da, herhangi birinin ihtiyaç duyduğunda ortalamanın talep üzerine hesaplanması gerekmediği anlamına gelir. Başka bir deyişle, AveragedCollection hesaplanan ortalamayı bizim için önbelleğe alacaktır. Liste 17-1, AveragedCollection yapısını tanımlar:

Dosya adı: src/lib.rs

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

Liste 17-1: AveragedCollection yapısı koleksiyondaki öğelerin tamsayılarının ve ortalamalarının bir listesini tutar

Yapı, diğer kodların kullanabilmesi için pub olarak işaretlenir, ancak yapı içindeki alanlar özel kalır. Bu, bu durumda önemlidir, çünkü listeye bir değer eklendiğinde veya listeden çıkarıldığında, ortalamanın da güncellenmesini sağlamak istiyoruz. Bunu, Liste 17-2'de gösterildiği gibi, yapıya ekleme, kaldırma ve ortalama fonksiyonlarını uygulayarak yapıyoruz:

Dosya adı: src/lib.rs

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

impl AveragedCollection {
    pub fn add(&mut self, value: i32) {
        self.list.push(value);
        self.update_average();
    }

    pub fn remove(&mut self) -> Option<i32> {
        let result = self.list.pop();
        match result {
            Some(value) => {
                self.update_average();
                Some(value)
            }
            None => None,
        }
    }

    pub fn average(&self) -> f64 {
        self.average
    }

    fn update_average(&mut self) {
        let total: i32 = self.list.iter().sum();
        self.average = total as f64 / self.list.len() as f64;
    }
}

Liste 17-2: Ortalama toplama, kaldırma ve ortalamaya ilişkin genel fonksiyonların süreklenmesi

Genel fonksiyonlar olan ekleme (add), kaldırma (remove) ve ortalama (average), bir AveragedCollection örneğindeki verilere erişmenin veya verileri değiştirmenin tek yoludur. Bir öğe, ekleme yöntemi kullanılarak listeye eklendiğinde veya kaldır yöntemi kullanılarak kaldırıldığında, her birinin süreklemeleri, ortalama alanı güncellemeyi de işleyen özel update_average metodunu çağırır.

list'i ve average alanlarını özel olarak tutuyoruz, bu nedenle harici kodun liste alanına doğrudan öğe eklemesi veya liste alanından öğe kaldırması mümkün değildir; aksi takdirde, liste değiştiğinde average alanı senkronize olmayabilir. average metodu, avetage alanındaki değeri döndürür ve harici kodun ortalamayı okumasına ancak değiştirmemesine izin verir. AveragedCollection yapısının sürekleme detaylarını kapsüllediğimiz için, gelecekte veri yapısı gibi yönlerini kolayca değiştirebiliriz.

Örneğin, list alanı için Vec<i32> yerine HashSet<i32> kullanabiliriz. Ekleme, kaldırma ve ortalama genel metodlarının imzaları aynı kaldığı sürece, AveragedCollection kullanan kodun değişmesi gerekmez. Bunun yerine list'i public olarak işleseydik, durum böyle olmazdı: HashSet<i32> ve Vec<i32> öğeleri eklemek ve kaldırmak için farklı yöntemlere sahipti, bu nedenle, listeyi doğrudan değiştiriyorsa harici kodun büyük olasılıkla değişmesi gerekirdi. Bir dilin nesne yönelimli olarak kabul edilmesi için kapsülleme gerekli bir özellikse, Rust bu gereksinimi karşılar. Kodun farklı bölümleri için pub kullanma veya kullanmama seçeneği, uygulama ayrıntılarının kapsüllenmesini sağlar.

Tür Sistemi ve Kod Paylaşımı Olarak Kalıtım

Kalıtım, bir nesnenin başka bir nesnenin tanımından öğeleri devralabileceği, böylece onları yeniden tanımlamanıza gerek kalmadan üst nesnenin verilerini ve davranışını kazanabileceği bir mekanizmadır.

Bir dilin nesne yönelimli bir dil olması için kalıtıma sahip olması gerekiyorsa, Rust bunlardan birisi değildir. Üst yapının alanlarını ve fonksiyon süreklemelerini devralan bir yapı tanımlamanın bir yolu yoktur. Bununla birlikte, programlama araç kutunuzdan kalıtım almaya alıştıysanız, ilk etapta kalıtım için ulaşma nedeninize bağlı olarak Rust'taki diğer çözümleri kullanabilirsiniz.

İki ana nedenden dolayı kalıtımı seçersiniz. Biri kodun yeniden kullanımı içindir: bir tür için belirli davranışı uygulayabilirsiniz ve kalıtım, bu uygulamayı farklı bir tür için yeniden kullanmanızı sağlar. Rust kodunu, Summary tanımındaki summarize metodunun varsayılan bir uygulamasını eklediğimizde Liste 10-14'te gördüğünüz gibi, varsayılan tanım fonksiyon süreklemelerini kullanarak paylaşabilirsiniz.

Summary tanımını sürekleyen herhangi bir tür, üzerinde başka bir kod olmadan summarize metoduna da sahip olacaktır. Bu, bir metodun süreklemesine sahip bir üst sınıfa ve aynı zamanda yöntemin uygulanmasına sahip olan miras alan bir alt sınıfa benzer. Ayrıca, bir üst sınıftan miras alınan bir metodun uygulanmasını geçersiz kılan bir alt sınıfa benzer olan Summary tanımını süreklediğimizde, summarize metodunun varsayılan uygulamasını da geçersiz kılabiliriz.

Kalıtımı kullanmanın diğer nedeni, tür sistemiyle ilgilidir: bir alt türün üst türle aynı yerlerde kullanılmasını sağlamaktır. Buna polimorfizm, çok biçimlilik de denir; bu, belirli karakteristik özellikleri paylaşıyorlarsa çalışma zamanında birden çok nesneyi birbirinin yerine koyabileceğiniz anlamına gelir.

Çok Biçimlilik

Birçok insan için çok biçimlilik kalıtımla eş anlamlıdır. Aslında, birden çok türdeki verilerle çalışabilen kodu ifade eden daha genel bir kavramdır. Kalıtım için bu türler genellikle alt sınıflardır. Bunun yerine Rust, farklı olası türler üzerinde soyutlamak için yaygın türleri ve bu türlerin sağlaması gerekenlere kısıtlamalar getirmek için özellik sınırlarını kullanır. Buna bazen sınırlı parametrik çok biçimlilik de denir.

Kalıtım, son zamanlarda birçok programlama dilinde bir programlama tasarım çözümü olarak gözden düştü çünkü genellikle gereğinden fazla kod paylaşma riskini taşıyor. Alt sınıflar her zaman üst sınıflarının tüm özelliklerini paylaşmamalıdır, ancak bunu kalıtımı kullanırsalar yapacaklardır. Bu, bir programın tasarımını daha az esnek hale getirebilir. Ayrıca, mantıklı olmayan veya metodların alt sınıfa süreklenmediği durumlar için hatalara neden olan alt sınıflardaki metodları çağırma olasılığını da sunar. Ek olarak, bazı diller bir alt sınıfın yalnızca bir sınıftan miras almasına izin vererek program tasarımının esnekliğini daha da kısıtlar.

Bu nedenlerle Rust, kalıtım yerine tanım nesnelerini kullanma konusunda farklı bir yaklaşım benimser. Tanım nesnelerinin Rust'ta çok biçimliliği nasıl sağladığına bakalım.

Farklı Türlerdeki Değerlere İzin Veren Tanım Nesnelerini Kullanma

Bölüm 8'de, vektörlerin bir sınırlamasının yalnızca tek bir türden elemanları saklayabilmeleri olduğundan bahsetmiştik. Liste 8-9'da tam sayıları, kayan değerleri ve metni tutmak için varyantları olan bir SpreadsheetCell enum'u tanımladığımız geçici bir çözüm oluşturduk. Bu, her hücrede farklı veri türlerini saklayabileceğimiz ve yine de bir hücre satırını temsil eden bir vektöre sahip olabileceğimiz anlamına geliyordu. Bu, değiştirilebilir öğelerimiz kodumuz derlendiğinde bildiğimiz sabit bir tür kümesi olduğunda mükemmel bir çözümdür.

Ancak, bazen kütüphane kullanıcımızın belirli bir durumda geçerli olan türler kümesini genişletebilmesini isteriz. Bunu nasıl başarabileceğimizi göstermek için, GUI araçları için yaygın bir teknik olan, bir öğe listesini yineleyerek her birini ekrana çizmek için bir draw yöntemini çağıran örnek bir grafik kullanıcı arayüzü (GUI) aracı oluşturacağız. GUI kütüphanesinin yapısını içeren gui adında bir kütüphane kasası oluşturacağız. Bu kasa insanların kullanması için Button veya TextField gibi bazı türler içerebilir. Buna ek olarak, gui kullanıcıları çizilebilecek kendi türlerini oluşturmak isteyeceklerdir: örneğin, bir programcı bir Image ekleyebilir ve bir diğeri bir SelectBox ekleyebilir.

Bu örnek için tam teşekküllü bir GUI kütüphanesi uygulamayacağız ancak parçaların birbirine nasıl uyacağını göstereceğiz. Kütüphaneyi yazarken, diğer programcıların oluşturmak isteyebileceği tüm türleri bilemeyiz ve tanımlayamayız. Ancak gui'nin farklı tiplerdeki birçok değeri takip etmesi gerektiğini ve bu farklı tipteki değerlerin her biri için bir draw metodu çağırması gerektiğini biliyoruz. draw metodunu çağırdığımızda tam olarak ne olacağını bilmesine gerek yoktur, sadece değerin çağırmamız için bu metoda sahip olması yeterlidir.

Bunu kalıtımın olduğu bir dilde yapmak için, üzerinde draw adında bir yöntem bulunan Component adında bir sınıf tanımlayabiliriz. Button, Image ve SelectBox gibi diğer sınıflar Component'ten miras alır ve böylece draw yöntemini miras alır. Her biri kendi özel davranışlarını tanımlamak için draw yöntemini geçersiz kılabilir, ancak çerçeve tüm türlere Component örneğiymiş gibi davranabilir ve draw yöntemini çağırabilir. Ancak Rust'ta kalıtım olmadığı için, kullanıcıların yeni türlerle genişletmesine izin vermek üzere gui kütüphanesini yapılandırmak için başka bir yola ihtiyacımız var.

Ortak Davranış için Bir Özellik Tanımlama

gui'nin sahip olmasını istediğimiz davranışı uygulamak için, draw adında bir metoda sahip olacak Draw adında bir trait tanımlayacağız. Daha sonra bir trait nesnesi alan bir vektör tanımlayabiliriz. Bir trait nesnesi, hem belirttiğimiz trait'i uygulayan bir türün örneğine hem de çalışma zamanında bu türdeki trait yöntemlerini aramak için kullanılan bir tabloya işaret eder. Bir & referansı veya Box<T> akıllı işaretçisi gibi bir tür işaretçi, ardından dyn anahtar sözcüğü ve ardından ilgili özelliği belirterek bir trait nesnesi oluştururuz. (Özellik nesnelerinin neden bir işaretçi kullanması gerektiğinden Bölüm 19'da “Dinamik Olarak Boyutlandırılmış Türler ve Sized Tanımı” bölümünde bahsedeceğiz). Tanım nesnelerini yaygın veya somut bir tip yerine kullanabiliriz. Bir trait nesnesi kullandığımız her yerde, Rust'ın tür sistemi derleme zamanında bu bağlamda kullanılan herhangi bir değerin trait nesnesinin özelliğini uygulamasını sağlayacaktır. Sonuç olarak, derleme zamanında tüm olası türleri bilmemize gerek yoktur.

Rust'ta struct ve enum'ları diğer dillerin nesnelerinden ayırmak için “nesne” olarak adlandırmaktan kaçındığımızdan bahsetmiştik. Bir struct veya enum'da, struct alanlarındaki veri ve impl bloklarındaki davranış birbirinden ayrılırken, diğer dillerde veri ve davranış tek bir kavramda birleştirilir ve genellikle bir nesne olarak etiketlenir. Bununla birlikte, tanım nesneleri, veri ve davranışı birleştirmeleri açısından diğer dillerdeki nesnelere daha çok benzemektedir. Ancak trait nesneleri, bir trait nesnesine veri ekleyemediğimiz için geleneksel nesnelerden farklıdır. Özellik nesneleri diğer dillerdeki nesneler kadar genel olarak kullanışlı değildir: özel amaçları ortak davranışlar arasında soyutlamaya izin vermektir.

Liste 17-3, draw adında bir yöntemle Draw adında bir özelliğin nasıl tanımlanacağını gösterir:

Dosya adı: src/lib.rs

pub trait Draw {
    fn draw(&self);
}

Liste 17-3: Draw tanımının tanımı

This syntax should look familiar from our discussions on how to define traits in Chapter 10. Next comes some new syntax: Listing 17-4 defines a struct named Screen that holds a vector named components. This vector is of type Box<dyn Draw>, which is a trait object; it’s a stand-in for any type inside a Box that implements the Draw trait.

Dosya adı: src/lib.rs

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

Liste 17-4: Draw tanımını uygulayan tanım nesnelerinden oluşan bir vektörü tutan components alanına sahip Screen yapısının tanımı

Screen yapısında, Liste 17-5'te gösterildiği gibi, components'in her birinde draw yöntemini çağıracak run adında bir yöntem tanımlayacağız:

Dosya adı: src/lib.rs

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

Liste 17-5: Her bileşende draw yöntemini çağıran Screen üzerinde bir run yöntemi

Bu, özellik sınırları olan genel bir tür parametresi kullanan bir struct tanımlamaktan farklı çalışır. Bir yaygın tür parametresi bir seferde yalnızca bir somut tiple değiştirilebilirken, tanım nesneleri çalışma zamanında birden fazla somut tipin özellik nesnesinin yerini doldurmasına izin verir. Örneğin, Screen yapısını Liste 17-6'daki gibi bir yaygın tip ve bir tanım bağı kullanarak tanımlayabilirdik:

Dosya adı: src/lib.rs

pub trait Draw {
    fn draw(&self);
}

pub struct Screen<T: Draw> {
    pub components: Vec<T>,
}

impl<T> Screen<T>
where
    T: Draw,
{
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

Liste 17-6: Yaygınlar ve tanım sınırlarını kullanarak Screen yapısının ve run yönteminin alternatif bir uygulaması

Bu, bizi, tümü Button türünden veya tümü TextField türünden bileşenlerin bir listesini içeren bir Screen örneğiyle sınırlar. Yalnızca homojen koleksiyonlarınız olacaksa, yaygınlar ve özellik sınırlarının kullanılması tercih edilir, çünkü somut türleri kullanmak için tanımlar derleme zamanında monomorfize edilecektir.

Öte yandan, özellik nesnelerini kullanan yöntemle, bir Screen örneği, Box<Button> ve Box<TextField> içeren bir Vec<T> içerebilir. Bunun nasıl çalıştığına bakalım ve ardından çalışma zamanı performans sonuçları hakkında konuşacağız.

Tanımı Uygulamak

Şimdi Draw özelliğini uygulayan bazı türleri ekleyeceğiz. Button türünü sağlayacağız. Yine, aslında bir GUI kütüphanesini uygulamak bu kitabın kapsamı dışındadır, bu nedenle draw yönteminin gövdesinde herhangi bir çalışan süreklemesi olmayacaktır. Uygulamanın nasıl görünebileceğini hayal etmek için, bir Button yapısında Liste 17-7'de gösterildiği gibi width, height ve label alanları olabilir:

Dosya adı: src/lib.rs

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw(&self) {
        // code to actually draw a button
    }
}

Liste 17-7: Draw tanımını uygulayan bir Button yapısı

Button'daki width, height ve label alanları diğer bileşenlerdeki alanlardan farklı olacaktır; örneğin, bir TextField türü aynı alanlara ve bir yer tutucu alana sahip olabilir. Ekranda çizmek istediğimiz türlerin her biri Draw tanımını uygular, ancak burada Button'da olduğu gibi (belirtildiği gibi gerçek GUI kodu olmadan) söz konusu türün nasıl çizileceğini tanımlamak için draw yönteminde farklı kod kullanır. Örneğin Button tipi, kullanıcı düğmeye tıkladığında ne olacağıyla ilgili metotları içeren ek bir impl bloğuna sahip olabilir. Bu tür yöntemler TextField gibi türler için geçerli olmayacaktır.

Kütüphanemizi kullanan biri width, height ve options alanları olan bir SelectBox yapısını uygulamaya karar verirse, Liste 17-8'de gösterildiği gibi SelectBox türüne Draw tanımını da uygular:

Dosya adı: src/main.rs

use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

fn main() {}

Liste 17-8: Bir SelectBox yapısı üzerinde gui kullanan ve Draw tanımını uygulayan başka bir kasa

Kütüphanemizin kullanıcısı artık bir Screen örneği oluşturmak için main fonksiyonunu yazabilir. Screen örneğine bir SelectBox ve bir Button ekleyebilir ve her birini bir Box<T> içine koyarak bir trait nesnesi haline getirebilirler. Daha sonra Screen örneğinde run metodunu çağırabilirler, bu da her bir bileşen üzerinde draw metodunu çağıracaktır. Liste 17-9 bu uygulamayı göstermektedir:

Dosya adı: src/main.rs

use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

use gui::{Button, Screen};

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No"),
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("OK"),
            }),
        ],
    };

    screen.run();
}

Liste 17-9: Aynı özelliği uygulayan farklı türlerin değerlerini depolamak için tanım nesnelerini kullanma

Kütüphaneyi yazarken, birinin SelectBox türünü ekleyebileceğini bilmiyorduk, ancak Screen uygulamamız yeni tür üzerinde çalışabiliyor ve onu çizebiliyordu çünkü SelectBox Draw özelliğini uyguluyor, yani draw yöntemini uyguluyor.

Bu kavram - bir değerin somut türünden ziyade yalnızca değerin yanıt verdiği mesajlarla ilgilenmek - dinamik olarak yazılan dillerdeki ördek gibi yazma kavramına benzer: ördek gibi yürüyorsa ve ördek gibi vaklıyorsa, o zaman bir ördek olmalıdır! Liste 17-5'teki run on Screen uygulamasında, run'ın her bir bileşenin somut türünün ne olduğunu bilmesine gerek yoktur. Bir bileşenin Button ya da SelectBox örneği olup olmadığını kontrol etmez, sadece bileşen üzerindeki draw yöntemini çağırır. Bileşenler vektöründeki değerlerin türü olarak Box<dyn Draw> belirterek, Screen'i draw yöntemini çağırabileceğimiz değerlere ihtiyaç duyacak şekilde tanımladık.

Ördek tiplemesi kullanan kodlara benzer kod yazmak için trait nesnelerini ve Rust'ın tür sistemini kullanmanın avantajı, çalışma zamanında bir değerin belirli bir yöntemi uygulayıp uygulamadığını kontrol etmek zorunda kalmamamız veya bir değer bir yöntemi uygulamıyorsa ancak yine de çağırırsak hata alma konusunda endişelenmememizdir. Değerler, özellik nesnelerinin ihtiyaç duyduğu özellikleri uygulamıyorsa Rust kodumuzu derlemeyecektir.

Örneğin, Liste 17-10, bileşen olarak String içeren bir Screen oluşturmaya çalıştığımızda ne olacağını gösterir:

Dosya adı: src/main.rs

use gui::Screen;

fn main() {
    let screen = Screen {
        components: vec![Box::new(String::from("Hi"))],
    };

    screen.run();
}

Liste 17-10: Tanım nesnesinin özelliğini uygulamayan bir tür kullanmaya çalışmak

String, Draw özelliğini uygulamadığı için bu hatayı alıyoruz:

$ cargo run
   Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
 --> src/main.rs:5:26
  |
5 |         components: vec![Box::new(String::from("Hi"))],
  |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
  |
  = note: required for the cast to the object type `dyn Draw`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` due to previous error

Bu hata bize ya Screen'e geçmek istemediğimiz bir şey geçirdiğimizi ve bu nedenle farklı bir tür geçirmemiz gerektiğini ya da Screen'in üzerinde draw çağrısı yapabilmesi için Draw on String'i uygulamamız gerektiğini bildirir.

Tanım Nesneleri Dinamik Gönderim Gerçekleştirir

Bölüm 10'daki “Yaygınları Kullanan Kodun Performansı” bölümünde, yaygınlarda özellik sınırları kullandığımızda derleyici tarafından gerçekleştirilen monomorfizasyon işlemi hakkındaki tartışmamızı hatırlayın: derleyici, yaygın tür parametresi yerine kullandığımız her somut tip için fonksiyonların ve metodların yaygın olmayan uygulamalarını üretir. Monomorfizasyondan kaynaklanan kod, derleyicinin derleme zamanında hangi yöntemi çağırdığınızı bildiği statik gönderim yapıyor. Bu, derleyicinin derleme sırasında hangi yöntemi çağırdığınızı bilemediği dinamik gönderime zıttır. Dinamik gönderim durumlarında, derleyici çalışma zamanında hangi yöntemin çağrılacağını belirleyecek kodu yayınlar.

Tanım nesnelerini kullandığımızda, Rust dinamik gönderim kullanacaktır. Derleyici, trait nesnelerini kullanan kodla kullanılabilecek tüm türleri bilmez, bu nedenle hangi türde hangi yöntemin uygulanacağını bilemez. Bunun yerine, çalışma zamanında Rust, hangi yöntemin çağrılacağını bilmek için trait nesnesinin içindeki işaretçileri kullanır. Bu arama, statik gönderim ile oluşmayan bir çalışma zamanı maliyetine neden olur. Dinamik gönderim ayrıca derleyicinin bir yöntemin kodunu satır içi yapmayı seçmesini engeller ve bu da bazı optimizasyonları önler. Ancak, Liste 17-5'te yazdığımız ve Liste 17-9'da destekleyebildiğimiz kodda ekstra esneklik elde ettik, bu nedenle dikkate alınması gereken bir değiş tokuş.

Nesneye Yönelik Tasarım Modeli Uygulamak

Durum kalıbı, nesne yönelimli bir tasarım kalıbıdır. Desenin özü, bir değerin dahili olarak sahip olabileceği bir dizi durum tanımlamamızdır. Durumlar bir dizi durum nesnesi ile temsil edilir ve değerin davranışı durumuna bağlı olarak değişir. "draft", "review" veya "published" kümesinden bir durum nesnesi olacak şekilde durumunu tutmak için bir alana sahip olan bir blog yazısı yapısı örneği üzerinde çalışacağız.

Durum nesneleri işlevselliği paylaşır: Rust'ta elbette nesneler ve kalıtım yerine yapıları ve özellikleri kullanırız. Her durum nesnesi kendi davranışından ve ne zaman başka bir duruma geçmesi gerektiğini yönetmekten sorumludur. Bir durum nesnesini tutan değer, durumların farklı davranışları veya durumlar arasında ne zaman geçiş yapılacağı hakkında hiçbir şey bilmez.

Durum kalıbını kullanmanın avantajı, programın iş gereksinimleri değiştiğinde, durumu tutan değerin kodunu veya değeri kullanan kodu değiştirmemize gerek kalmamasıdır. Kurallarını değiştirmek ya da belki daha fazla durum nesnesi eklemek için yalnızca durum nesnelerinden birinin içindeki kodu güncellememiz gerekecektir. Durum modelini kullanarak bir blog yazısı iş akışını aşamalı olarak uygulamaya başlayalım.

Nihai işlevsellik şu şekilde görünecektir:

The final functionality will look like this:

  1. Bir blog gönderisi boş bir taslak olarak başlar.
  2. Taslak yapıldığında, gönderinin gözden geçirilmesi istenir.
  3. Gönderi onaylandığında yayınlanır.
  4. Yalnızca yayınlanan blog gönderileri, içeriği yazdırılacak şekilde döndürür, bu nedenle onaylanmamış gönderiler yanlışlıkla yayınlanamaz.

Bir gönderide yapılmaya çalışılan diğer değişikliklerin hiçbir etkisi olmamalıdır. Örneğin, inceleme talep etmeden önce taslak bir blog gönderisini onaylamaya çalışırsak, gönderi yayınlanmamış bir taslak olarak kalmalıdır.

Liste 17-11 bu iş akışını kod biçiminde göstermektedir: bu, blog adlı bir kütüphane kasasına uygulayacağımız API'nin örnek bir kullanımıdır. Bu henüz derlenmeyecektir çünkü blog crate'ini henüz yazmadık.

Dosya adı: src/main.rs

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

Liste 17-11: blog kasamızda olmasını istediğimiz davranışı gösteren kod

Kullanıcının Post::new ile yeni bir taslak blog yazısı oluşturmasına izin vermek istiyoruz. Blog yazısına metin eklenmesine izin vermek istiyoruz. Onaydan önce yazının içeriğini hemen almaya çalışırsak, yazı hala taslak olduğu için herhangi bir metin almamalıyız. Gösterim amacıyla koda assert_eq! ekledik. Bunun için mükemmel bir birim testi, taslak bir blog gönderisinin içerik yönteminden boş bir dize döndürdüğünü iddia etmek olurdu, ancak bu örnek için test yazmayacağız.

Daha sonra, yazının incelenmesi için bir isteği etkinleştirmek istiyoruz ve inceleme beklenirken içeriğin boş bir dize döndürmesini istiyoruz. Gönderi onay aldığında yayınlanmalıdır, yani content çağrıldığında gönderi metni döndürülecektir.

Kasadan etkileşimde bulunduğumuz tek türün Post türü olduğuna dikkat edin. Bu tür durum kalıbını kullanacak ve bir gönderinin taslak halinde, inceleme için bekliyor veya yayınlanmış olabileceği çeşitli durumları temsil eden üç durum nesnesinden biri olacak bir değer tutacaktır. Bir durumdan diğerine geçiş Post türü içinde dahili olarak yönetilecektir. Durumlar, kütüphanemizin kullanıcıları tarafından Post tanımı üzerinde çağrılan metodlara yanıt olarak değişir, ancak durum değişikliklerini doğrudan yönetmeleri gerekmez. Ayrıca kullanıcılar, bir gönderiyi incelenmeden önce yayınlamak gibi durumlarla ilgili bir hata yapamazlar.

Post'u Tanımlama ve Taslak Durumunda Yeni Bir Örnek Oluşturma

Kütüphanenin yazılmasına başlayalım! Bazı içerikleri tutan genel bir Post yapısına ihtiyacımız olduğunu biliyoruz, bu nedenle yapının tanımı ve bir Post tanımı oluşturmak için ilişkili bir genel new fonksiyonu ile başlayacağız, Liste 17-12'de gösterildiği gibi. Ayrıca bir Post için tüm durum nesnelerinin sahip olması gereken davranışı tanımlayacak özel bir State tanımı oluşturacağız.

Ardından Post, durum nesnesini tutmak için state adlı özel bir alanda bir Option<T> içinde Box<dyn State>'in trait nesnesini tutacaktır. Option<T>'nin neden gerekli olduğunu birazdan göreceksiniz.

Dosya adı: src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

Liste 17-12: Yeni bir Post örneği, bir State tanımı ve bir Draft yapısı oluşturan Post yapısının ve new fonksiyonunun tanımı

State özelliği, farklı posta durumları tarafından paylaşılan davranışı tanımlar. State nesneleri Draft, PendingReview ve Published'dir ve hepsi State tanımını uygular. Şimdilik, tanımın herhangi bir metodu yoktur ve sadece Draft durumunu tanımlayarak başlayacağız çünkü bir gönderinin başlamasını istediğimiz durum budur.

Yeni bir Post oluşturduğumuzda, state alanını bir Box tutan Some değerine ayarlarız. Bu Box, Draft yapısının yeni bir örneğine işaret eder. Bu, yeni bir Post örneği oluşturduğumuzda, taslak olarak başlamasını sağlar. Post'un durum alanı özel olduğu için, başka bir durumda bir Post oluşturmanın hiçbir yolu yoktur! Post::new fonksiyonunda, content alanını yeni, boş bir String olarak ayarlarız.

Gönderi İçeriği Metnini Saklama

Liste 17-11'de add_text adlı bir metodu çağırabilmek ve ona blog yazısının metin içeriği olarak eklenecek bir &str iletebilmek istediğimizi gördük. Bunu, içerik alanını pub olarak göstermek yerine bir metod olarak uyguluyoruz, böylece daha sonra içerik alanının verilerinin nasıl okunacağını kontrol edecek bir metod uygulayabiliriz. add_text metodu oldukça basittir, bu nedenle Liste 17-13'teki uygulamayı impl Post bloğuna ekleyelim:

Dosya adı: src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

Liste 17-13: Bir gönderinin content'ine metin eklemek için add_text metodunu süreklemek

add_text metodu self öğesine değişebilir bir referans alır, çünkü add_text öğesini çağırdığımız Post tanımını değiştiriyoruz. Daha sonra content'teki String üzerinde push_str'yi çağırıyoruz ve kaydedilen içeriğe eklemek için metin argümanını iletiyoruz. Bu davranış, gönderinin içinde bulunduğu duruma bağlı değildir, bu nedenle durum modelinin bir parçası değildir. add_text yöntemi state alanıyla hiç etkileşime girmez, ancak desteklemek istediğimiz davranışın bir parçasıdır.

Taslak Gönderinin İçeriğinin Boş Olmasını Sağlama

add_text öğesini çağırdıktan ve gönderimize bir miktar içerik ekledikten sonra bile, Liste 17-11'in 7. satırında gösterildiği gibi, gönderi hala taslak durumunda olduğu için content metodunun boş bir dizgi dilimi döndürmesini istiyoruz. Şimdilik, content metodunu bu gereksinimi karşılayacak en basit şeyle uygulayalım: her zaman boş bir dizgi dilimi döndürmek. Bunu daha sonra bir gönderinin durumunu değiştirip yayınlanabilmesini sağladığımızda değiştireceğiz. Şimdiye kadar, yazılar yalnızca taslak durumunda olabilir, bu nedenle yazı içeriği her zaman boş olmalıdır. Liste 17-14 bu yer tutucu uygulamasını göstermektedir:

Dosya adı: src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

Liste 17-14: Post'a content metodu için her zaman boş bir dizgi dilimi döndüren bir yer tutucu süreklemesi ekleme

Bu eklenen content metoduyla, Liste 17-11'den 7. satıra kadar her şey amaçlandığı gibi çalışır.

Gönderinin Durumu Değişikliklerinin Gözden Geçirilmesini Talep Etme

Ardından, durumunu Draft'tan PendingReview olarak değiştirmesi gereken bir gönderinin gözden geçirilmesini istemek için işlevsellik eklememiz gerekiyor. Liste 17-15 bu kodu gösterir:

Dosya adı: src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

Liste 17-15: Post ve State tanımı üzerinde request_review metodlarının yazılması

Post'a, self öğesine değişken bir referans alacak request_review adında bir genel metod veriyoruz. Ardından Post'un mevcut durumu üzerinde dahili bir request_review metodunu çağırıyoruz ve bu ikinci request_review metodu mevcut durumu tüketip yeni bir durum döndürüyor.

request_review metodunu State tanımını ekliyoruz; tanımı uygulayan tüm türlerin artık request_review metodunu uygulaması gerekecektir. Metodun ilk parametresinin self, &self veya &mut self yerine self olduğuna dikkat edin: Box<Self>. Bu söz dizimi, yöntemin yalnızca türü taşıyan bir Box üzerinde çağrıldığında geçerli olduğu anlamına gelir. Bu söz dizimi Box<Self>'in sahipliğini alarak eski durumu geçersiz kılar, böylece Post'un durum değeri yeni bir duruma dönüşebilir.

Eski durumu kullanmak için request_review yönteminin durum değerinin sahipliğini alması gerekir. Post'un state alanındaki Option burada devreye girer: take metodunu çağırarak Some değerini state alanından çıkarırız ve yerine None değerini bırakırız, çünkü Rust yapılarda doldurulmamış alanlar olmasına izin vermez. Bu, state değerini ödünç almak yerine Post'un dışına taşımamızı sağlar. Daha sonra post'un state değerini bu işlemin sonucuna ayarlayacağız.

State değerinin sahipliğini almak için self.state = self.state.request_review(); gibi bir kodla doğrudan ayarlamak yerine state değerini geçici olarak None olarak ayarlamamız gerekir. Bu, Post'un biz onu yeni bir duruma dönüştürdükten sonra eski durum değerini kullanamamasını sağlar.

Draft üzerindeki request_review yöntemi, bir gönderinin inceleme için beklediği durumu temsil eden yeni bir PendingReview yapısının yeni, Box'un bir örneğini döndürür. PendingReview struct'ı da request_review yöntemini uygular ancak herhangi bir dönüştürme yapmaz. Bunun yerine, kendisini döndürür, çünkü zaten PendingReview durumunda olan bir gönderi için inceleme istediğimizde, gönderi PendingReview durumunda kalmalıdır.

Şimdi state modelinin avantajlarını görmeye başlayabiliriz: Post üzerindeki request_review yöntemi, state değeri ne olursa olsun aynıdır. Her state kendi kurallarından sorumludur.

Post üzerindeki content metodunu olduğu gibi bırakacağız ve boş bir dizgi dilimi döndüreceğiz. Artık hem PendingReview durumunda hem de Draft durumunda bir Post'a sahip olabiliriz, ancak PendingReview durumunda aynı davranışı istiyoruz. Liste 17-11 artık 10. satıra kadar çalışıyor!

content'in Davranışını Değiştirmek için approve Ekleme

approve metodu request_review metoduna benzer olacaktır: state'i, mevcut state'in onaylandığında sahip olması gerektiğini söylediği değere ayarlayacaktır, Liste 17-16'da gösterildiği gibi:

Dosya adı: src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

Liste 17-16: Post ve State tanımı üzerinde approve metodunu uygulama

State tanımına approve metodunu ekliyoruz ve Published durum olan State'i uygulayan yeni bir struct ekliyoruz.

PendingReview üzerinde request_review metodunun çalışmasına benzer şekilde, bir Draft üzerinde approve yöntemini çağırırsak, approve self değerini döndüreceği için hiçbir etkisi olmayacaktır. PendingReview üzerinde approve yöntemini çağırdığımızda, Published yapısının yeni, Box'un bir tanımını döndürür. Published struct'ı, State tanımını uygular ve hem request_review yöntemi hem de approve yöntemi için kendini döndürür, çünkü bu durumlarda gönderi Published durumunda kalmalıdır.

Şimdi Post üzerindeki content metodunu güncellememiz gerekiyor. Content'ten döndürülen değerin Post'un mevcut durumuna bağlı olmasını istiyoruz, bu nedenle Post'un Liste 17-17'de gösterildiği gibi durumuna göre tanımlanmış bir content metoduna temsilci göndermesini sağlayacağız:

Dosya adı: src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
    // --snip--

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

Liste 17-17: State'de bir content metoduna yetki vermek için Post üzerindeki content metodunu güncelleme

Amaç tüm bu kuralları State'i uygulayan yapıların içinde tutmak olduğundan, state'teki değer üzerinde bir content yöntemi çağırıyoruz ve post örneğini (yani self'i) bir argüman olarak geçiriyoruz. Daha sonra state değeri üzerinde content metodunu kullanarak döndürülen değeri döndürüyoruz.

Option üzerinde as_ref yöntemini çağırıyoruz çünkü değerin sahibi olmak yerine Option içindeki değere bir referans istiyoruz. state bir Option<Box<dyn State>> olduğu için, as_ref yöntemini çağırdığımızda bir Option<&Box<dyn State>> döndürülür. Eğer as_ref'i çağırmasaydık, state'i fonksiyon parametresinin ödünç alınan &self'inin dışına taşıyamayacağımız için bir hata alırdık.

Daha sonra unwrap metodunu çağırıyoruz, ki bu metodun asla panik yaratmayacağını biliyoruz, çünkü Post üzerindeki metodların, bu metodlar tamamlandığında state'in her zaman Some değeri içereceğini garanti ettiğini biliyoruz. Bu, Bölüm 9'un “Derleyiciden Daha Fazla Bilgiye Sahip Olduğunuz Durumlar” kısmında bahsettiğimiz, derleyici bunu anlayamasa da None değerinin asla mümkün olmadığını bildiğimiz durumlardan biridir.

Bu noktada, &Box<dyn State> üzerinde content'i çağırdığımızda, deref zorlaması & ve Box üzerinde etkili olacak, böylece content yöntemi sonuçta State tanımını uygulayan tür üzerinde çağrılacaktır. Bu, State özellik tanımına içerik eklememiz gerektiği anlamına gelir ve Liste 17-18'de gösterildiği gibi, hangi duruma sahip olduğumuza bağlı olarak hangi içeriğin döndürüleceğine ilişkin mantığı buraya koyacağız:

Dosya adı: src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        ""
    }
}

// --snip--

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

Liste 17-18: State tanımına content yöntemini ekleme

content yöntemi için boş bir dizgi dilimi döndüren varsayılan bir uygulama ekliyoruz. Bu, Draft ve PendingReview yapılarında içerik uygulamamıza gerek olmadığı anlamına gelir. Published struct'ı, content yöntemini geçersiz kılacak ve post.content içindeki değeri döndürecektir.

Bölüm 10'da tartıştığımız gibi, bu yöntem üzerinde yaşam süresi ek açıklamalarına ihtiyacımız olduğunu unutmayın. Argüman olarak bir gönderiye referans alıyoruz ve bu gönderinin bir kısmına referans döndürüyoruz, bu nedenle döndürülen referansın yaşam süresi post argümanının yaşam süresiyle ilişkilidir.

Ve işimiz bitti - Liste 17-11'in tamamı artık çalışıyor! State modelini blog yazısı iş akışı kurallarıyla uyguladık. Kurallarla ilgili mantık, Post'un içine dağılmak yerine state nesnelerinde yaşıyor.

Durum Kalıbının Ödünleşimleri

Rust'ın, bir gönderinin her bir durumda sahip olması gereken farklı davranış türlerini kapsüllemek için nesne yönelimli durum modelini uygulayabildiğini gösterdik. Post üzerindeki yöntemler çeşitli davranışlar hakkında hiçbir şey bilmiyor. Kodu düzenlediğimiz şekilde, yayınlanan bir gönderinin farklı davranış biçimlerini öğrenmek için tek bir yere bakmamız gerekiyor: Published yapısındaki State özelliğinin uygulanması.

State kalıbını kullanmayan alternatif bir uygulama oluşturacak olsaydık, bunun yerine Post üzerindeki yöntemlerde veya hatta gönderinin durumunu kontrol eden ve bu yerlerde davranışı değiştiren ana kodda eşleşme ifadeleri kullanabilirdik. Bu, bir gönderinin yayınlanmış durumda olmasının tüm sonuçlarını anlamak için birkaç yere bakmamız gerektiği anlamına gelirdi! Bu, ne kadar çok durum eklersek o kadar artacaktır: bu match ifadelerinin her biri başka bir kola ihtiyaç duyacaktır.

State kalıbı ile Post metodları ve Post'u kullandığımız yerler eşleşme ifadelerine ihtiyaç duymaz ve yeni bir state eklemek için sadece yeni bir struct eklememiz ve trait metodlarını bu struct üzerinde uygulamamız gerekir.

State kalıbını kullanan uygulamanın daha fazla işlevsellik eklemek için genişletilmesi kolaydır. State kalıbını kullanan kodu korumanın basitliğini görmek için bu önerilerden birkaçını deneyin:

  • Gönderinin durumunu PendingReview'den Draft'a değiştiren bir reddetme yöntemi ekleyin.
  • State Published olarak değiştirilmeden önce onaylamak için iki çağrı yapılmasını zorunlu kılın.
  • Kullanıcıların yalnızca bir gönderi Draft durumundayken metin içeriği eklemesine izin verin. İpucu: state nesnesinin içerikle ilgili değişebilecek şeylerden sorumlu olmasını ancak Post'u değiştirmekten sorumlu olmamasını sağlayın.
  • State modelinin bir dezavantajı, durumlar arasındaki geçişleri durumlar gerçekleştirdiği için bazı durumların birbirine bağlı olmasıdır. PendingReview ile Published arasına Scheduled gibi başka bir state eklersek, PendingReview'daki kodu Scheduled'a geçiş yapacak şekilde değiştirmemiz gerekir. PendingReview'in yeni bir durum eklendiğinde değişmesi gerekmeseydi daha az iş olurdu, ancak bu başka bir tasarım modeline geçmek anlamına gelir.

Diğer bir dezavantajı ise bazı mantıkları tekrarlamış olmamızdır. Yinelemenin bir kısmını ortadan kaldırmak için, State özelliğindeki request_review ve approve yöntemleri için self döndüren varsayılan uygulamalar yapmayı deneyebiliriz; ancak bu, nesne güvenliğini ihlal eder, çünkü özellik somut self'in tam olarak ne olacağını bilmez. State'i bir özellik nesnesi olarak kullanabilmek istiyoruz, bu nedenle yöntemlerinin nesne güvenli olmasına ihtiyacımız var.

Diğer tekrarlar, Post üzerindeki request_review ve approve yöntemlerinin benzer uygulamalarını içerir. Her iki yöntem de Option'ın state alanındaki değer üzerinde aynı yöntemin uygulanmasına delege eder ve state alanının yeni değerini sonuca ayarlar. Post üzerinde bu kalıbı izleyen çok sayıda yöntemimiz olsaydı, tekrarı ortadan kaldırmak için bir makro tanımlamayı düşünebilirdik (Bölüm 19'daki "Makrolar" bölümüne bakın).

State kalıbını tam olarak nesne yönelimli diller için tanımlandığı gibi uygulayarak, Rust'ın güçlü yanlarından olabildiğince yararlanamıyoruz. Geçersiz durumları ve geçişleri derleme zamanı hatalarına dönüştürebilecek blog kasasında yapabileceğimiz bazı değişikliklere bakalım.

Durumları ve Davranışları Tür Olarak Kodlama

Farklı bir dizi ödünleşim elde etmek için durum modelini nasıl yeniden düşüneceğinizi göstereceğiz. Durumları ve geçişleri tamamen kapsüllemek yerine, dış kodun bunlar hakkında hiçbir bilgiye sahip olmaması için durumları farklı türlere kodlayacağız. Sonuç olarak, Rust'ın tür kontrol sistemi, yalnızca yayınlanmış gönderilere izin verilen taslak gönderileri kullanma girişimlerini bir derleyici hatası vererek önleyecektir.

Liste 17-11'deki main'in ilk bölümünü ele alalım:

Dosya adı: src/main.rs

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

Post::new kullanarak taslak durumunda yeni gönderilerin oluşturulmasını ve gönderinin içeriğine metin ekleme özelliğini hala etkinleştiriyoruz. Ancak taslak bir gönderide boş bir dize döndüren bir content yöntemine sahip olmak yerine, taslak gönderilerin içerik yöntemine hiç sahip olmamasını sağlayacağız. Bu şekilde, bir taslak gönderinin içeriğini almaya çalışırsak, bize yöntemin mevcut olmadığını söyleyen bir derleyici hatası alırız. Sonuç olarak, taslak gönderi içeriğini üretimde yanlışlıkla görüntülememiz imkansız olacaktır, çünkü bu kod derlenmeyecektir bile. Liste 17-19, bir Post yapısının ve bir DraftPost yapısının tanımının yanı sıra her birindeki yöntemleri gösterir:

Dosya adı: src/lib.rs

pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
    pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

    pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

Liste 17-19: content yöntemine sahip bir Post ve content metodu olmayan bir DraftPost

Hem Post hem de DraftPost yapıları, blog yazısı metnini saklayan özel bir içerik alanına sahiptir. Yapılar artık state alanına sahip değildir çünkü state kodlamasını yapıların türlerine taşıyoruz. Post yapısı yayınlanmış bir gönderiyi temsil edecektir ve içeriği döndüren bir içerik yöntemine sahiptir.

Hala bir Post::new fonksiyonumuz var, ancak Post'un bir örneğini döndürmek yerine DraftPost'un bir örneğini döndürüyor. content gizli olduğundan ve Post döndüren herhangi bir fonksiyon bulunmadığından, şu anda bir Post tanımı oluşturmak mümkün değildir.

DraftPost yapısının bir add_text yöntemi vardır, bu nedenle daha önce olduğu gibi içeriğe metin ekleyebiliriz, ancak DraftPost'un tanımlanmış bir content yöntemi olmadığını unutmayın! Böylece program tüm gönderilerin taslak gönderiler olarak başlamasını sağlar ve taslak gönderilerin içerikleri görüntülenemez. Bu kısıtlamaları aşmaya yönelik herhangi bir girişim derleyici hatasıyla sonuçlanacaktır.

Geçişleri Farklı Türlere Dönüşümler Olarak Uygulama

Peki yayınlanmış bir gönderiyi nasıl alacağız? Bir taslak gönderinin yayınlanmadan önce gözden geçirilmesi ve onaylanması gerektiği kuralını uygulamak istiyoruz. Bekleyen inceleme durumundaki bir gönderi hala herhangi bir içerik göstermemelidir. Bu kısıtlamaları, PendingReviewPost adında başka bir yapı ekleyerek, bir PendingReviewPost döndürmek için DraftPost üzerinde request_review yöntemini tanımlayarak ve bir Post döndürmek için PendingReviewPost üzerinde bir approve yöntemi tanımlayarak, Liste 17-20'de gösterildiği gibi yazalım:

Geçişleri Farklı Türlere Dönüştürme Olarak Uygulama

Peki yayınlanmış bir gönderiyi nasıl alacağız? Bir taslak gönderinin yayınlanmadan önce gözden geçirilmesi ve onaylanması gerektiği kuralını uygulamak istiyoruz. Bekleyen inceleme durumundaki bir gönderi hala herhangi bir içerik göstermemelidir. Bu kısıtlamaları, PendingReviewPost adında başka bir yapı ekleyerek, bir PendingReviewPost döndürmek için DraftPost üzerinde request_review yöntemini tanımlayarak ve bir Post döndürmek için PendingReviewPost üzerinde bir approve yöntemi tanımlayarak, Liste 17-20'de gösterildiği gibi uygulayalım:

Dosya adı: src/lib.rs

pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
    pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

    pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
    // --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn request_review(self) -> PendingReviewPost {
        PendingReviewPost {
            content: self.content,
        }
    }
}

pub struct PendingReviewPost {
    content: String,
}

impl PendingReviewPost {
    pub fn approve(self) -> Post {
        Post {
            content: self.content,
        }
    }
}

Liste 17-20: DraftPost'ta request_review çağrılarak oluşturulan bir PendingReviewPost ve PendingReviewPost'u yayınlanmış bir Post'a dönüştüren bir approve yöntemi

request_review ve approve yöntemleri self'in sahipliğini alır, böylece DraftPost ve PendingReviewPost örneklerini tüketir ve bunları sırasıyla bir PendingReviewPost'a ve yayınlanmış bir Post'a dönüştürür. Bu şekilde, üzerlerinde request_review çağrısı yaptıktan sonra kalan DraftPost örneklerimiz olmayacaktır. PendingReviewPost yapısının üzerinde tanımlanmış bir content yöntemi yoktur, bu nedenle içeriğini okumaya çalışmak DraftPost'ta olduğu gibi bir derleyici hatasıyla sonuçlanır. Tanımlanmış bir content yöntemi olan yayınlanmış bir Post örneği almanın tek yolu bir PendingReviewPost üzerinde approve yöntemini çağırmak olduğundan ve bir PendingReviewPost almanın tek yolu bir DraftPost üzerinde request_review yöntemini çağırmak olduğundan, artık blog yazısı iş akışını tür sistemine kodladık.

Ancak main'de de bazı küçük değişiklikler yapmamız gerekiyor. request_review ve approve yöntemleri, çağrıldıkları yapıyı değiştirmek yerine yeni örnekler döndürür, bu nedenle döndürülen örnekleri kaydetmek için daha fazla let post = gölgeleme ataması eklememiz gerekir. Ayrıca, taslak ve bekleyen inceleme gönderilerinin içerikleriyle ilgili iddiaların boş dizgiler olmasını sağlayamayız ve bunlara ihtiyacımız da yok: artık bu durumlardaki gönderilerin içeriğini kullanmaya çalışan kodu derleyemeyiz. main'deki güncellenmiş kod Liste 17-21'de gösterilmektedir:

Dosya adı: src/main.rs

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");

    let post = post.request_review();

    let post = post.approve();

    assert_eq!("I ate a salad for lunch today", post.content());
}

Liste 17-21: Blog gönderisi iş akışının yeni süreklemesinin kullanmak için main'deki değişiklikler

Post'u yeniden atamak için main'de yapmamız gereken değişiklikler, bu uygulamanın artık nesne yönelimli durum modeline tam olarak uymadığı anlamına geliyor: durumlar arasındaki dönüşümler artık tamamen Post uygulaması içinde kapsüllenmiyor. Bununla birlikte, kazancımız, tür sistemi ve derleme zamanında gerçekleşen tür denetimi nedeniyle geçersiz durumların artık imkansız olmasıdır! Bu, yayınlanmamış bir gönderinin içeriğinin görüntülenmesi gibi belirli hataların üretime geçmeden önce keşfedilmesini sağlar.

Kodun bu versiyonunun tasarımı hakkında ne düşündüğünüzü görmek için bu bölümün başında önerilen görevleri Liste 17-21'den sonra olduğu gibi blog sandığı üzerinde deneyin. Bazı görevlerin bu tasarımda zaten tamamlanmış olabileceğini unutmayın.

Rust'ın nesne yönelimli tasarım kalıplarını uygulama yeteneğine sahip olmasına rağmen, durumu tür sistemine kodlamak gibi diğer kalıpların da Rust'ta mevcut olduğunu gördük. Bu kalıpların farklı ödünleşimleri vardır. Nesne yönelimli kalıplara çok aşina olsanız da, Rust'ın özelliklerinden yararlanmak için sorunu yeniden düşünmek, derleme zamanında bazı hataları önlemek gibi faydalar sağlayabilir. Nesne yönelimli kalıplar, nesne yönelimli dillerin sahip olmadığı sahiplik gibi bazı özellikler nedeniyle Rust'ta her zaman en iyi çözüm olmayacaktır.

Özet

Bu bölümü okuduktan sonra Rust'ın nesne yönelimli bir dil olduğunu düşünseniz de düşünmeseniz de, artık Rust'ta bazı nesne yönelimli özellikler elde etmek için trait nesnelerini kullanabileceğinizi biliyorsunuz. Dinamik gönderim, kodunuza biraz çalışma zamanı performansı karşılığında biraz esneklik sağlayabilir. Bu esnekliği, kodunuzun sürdürülebilirliğine yardımcı olabilecek nesne yönelimli kalıpları uygulamak için kullanabilirsiniz. Rust ayrıca sahiplik gibi nesne yönelimli dillerin sahip olmadığı başka özelliklere de sahiptir. Nesne yönelimli bir kalıp, Rust'ın güçlü yönlerinden yararlanmanın her zaman en iyi yolu olmayacaktır, ancak kullanılabilir bir seçenektir.

Daha sonra, Rust'ın çok fazla esneklik sağlayan bir başka özelliği olan kalıplara bakacağız. Kitap boyunca bunlara kısaca baktık ancak henüz tam kapasitelerini görmedik. Hadi başlayalım!

Modeller ve Eşleştirme

Modeller, hem karmaşık hem de basit türleri yapısıyla eşleştirmek için bir özellik sağlar. Modelleri eşleştirmede match'i ifadelerle ve diğer yapılarla birlikte kullanmak, bir programın kontrol akışı üzerinde size daha fazla kontrol sağlar. Bir model, aşağıdakilerin bazı kombinasyonlarından oluşur:

  • Değişmezler
  • Bozulmuş diziler, numaralandırılmış yapılar, yapılar veya demetler
  • Değişkenler
  • Joker dizgiler
  • Yer tutucular

Bu bileşenler, birlikte çalıştığımız verilerin şeklini tanımlar ve daha sonra programımızın belirli bir kod parçasını çalıştırmaya devam etmek için doğru verilere sahip olup olmadığını belirlemek için değerlerle eşleştiririz.

Bir model kullanmak için, onu bir değerle karşılaştırırız. Model, değerle eşleşirse kodumuzda değer kısımlarını kullanırız. Madeni para sıralama makinesi örneği gibi; Bölüm 6'daki, modelleri kullanan eşleşme ifadelerini hatırlayın.

Bu bölüm, modellerle ilgili her şey için bir referanstır. Modelleri kullanmak için geçerli yerleri, reddedilebilir ve reddedilemez modeller arasındaki farkı ve görebileceğiniz farklı model söz dizimi türlerini ele alacağız. Bölümün sonunda, birçok kavramı açık bir şekilde ifade etmek için modelleri nasıl kullanacağınızı öğreneceksiniz.

All the Places Patterns Can Be Used

Patterns pop up in a number of places in Rust, and you’ve been using them a lot without realizing it! This section discusses all the places where patterns are valid.

match Arms

As discussed in Chapter 6, we use patterns in the arms of match expressions. Formally, match expressions are defined as the keyword match, a value to match on, and one or more match arms that consist of a pattern and an expression to run if the value matches that arm’s pattern, like this:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

For example, here's the match expression from Listing 6-5 that matches on an Option<i32> value in the variable x:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

The patterns in this match expression are the None and Some(i) on the left of each arrow.

One requirement for match expressions is that they need to be exhaustive in the sense that all possibilities for the value in the match expression must be accounted for. One way to ensure you’ve covered every possibility is to have a catchall pattern for the last arm: for example, a variable name matching any value can never fail and thus covers every remaining case.

The particular pattern _ will match anything, but it never binds to a variable, so it’s often used in the last match arm. The _ pattern can be useful when you want to ignore any value not specified, for example. We’ll cover the _ pattern in more detail in the “Ignoring Values in a Pattern” section later in this chapter.

Conditional if let Expressions

In Chapter 6 we discussed how to use if let expressions mainly as a shorter way to write the equivalent of a match that only matches one case. Optionally, if let can have a corresponding else containing code to run if the pattern in the if let doesn’t match.

Listing 18-1 shows that it’s also possible to mix and match if let, else if, and else if let expressions. Doing so gives us more flexibility than a match expression in which we can express only one value to compare with the patterns. Also, Rust doesn't require that the conditions in a series of if let, else if, else if let arms relate to each other.

The code in Listing 18-1 determines what color to make your background based on a series of checks for several conditions. For this example, we’ve created variables with hardcoded values that a real program might receive from user input.

Filename: src/main.rs

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {}, as the background", color);
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}

Listing 18-1: Mixing if let, else if, else if let, and else

If the user specifies a favorite color, that color is used as the background. If no favorite color is specified and today is Tuesday, the background color is green. Otherwise, if the user specifies their age as a string and we can parse it as a number successfully, the color is either purple or orange depending on the value of the number. If none of these conditions apply, the background color is blue.

This conditional structure lets us support complex requirements. With the hardcoded values we have here, this example will print Using purple as the background color.

You can see that if let can also introduce shadowed variables in the same way that match arms can: the line if let Ok(age) = age introduces a new shadowed age variable that contains the value inside the Ok variant. This means we need to place the if age > 30 condition within that block: we can’t combine these two conditions into if let Ok(age) = age && age > 30. The shadowed age we want to compare to 30 isn’t valid until the new scope starts with the curly bracket.

The downside of using if let expressions is that the compiler doesn’t check for exhaustiveness, whereas with match expressions it does. If we omitted the last else block and therefore missed handling some cases, the compiler would not alert us to the possible logic bug.

while let Conditional Loops

Similar in construction to if let, the while let conditional loop allows a while loop to run for as long as a pattern continues to match. In Listing 18-2 we code a while let loop that uses a vector as a stack and prints the values in the vector in the opposite order in which they were pushed.

fn main() {
    let mut stack = Vec::new();

    stack.push(1);
    stack.push(2);
    stack.push(3);

    while let Some(top) = stack.pop() {
        println!("{}", top);
    }
}

Listing 18-2: Using a while let loop to print values for as long as stack.pop() returns Some

This example prints 3, 2, and then 1. The pop method takes the last element out of the vector and returns Some(value). If the vector is empty, pop returns None. The while loop continues running the code in its block as long as pop returns Some. When pop returns None, the loop stops. We can use while let to pop every element off our stack.

for Loops

In a for loop, the value that directly follows the keyword for is a pattern. For example, in for x in y the x is the pattern. Listing 18-3 demonstrates how to use a pattern in a for loop to destructure, or break apart, a tuple as part of the for loop.

fn main() {
    let v = vec!['a', 'b', 'c'];

    for (index, value) in v.iter().enumerate() {
        println!("{} is at index {}", value, index);
    }
}

Listing 18-3: Using a pattern in a for loop to destructure a tuple

The code in Listing 18-3 will print the following:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

We adapt an iterator using the enumerate method so it produces a value and the index for that value, placed into a tuple. The first value produced is the tuple (0, 'a'). When this value is matched to the pattern (index, value), index will be 0 and value will be 'a', printing the first line of the output.

let Statements

Prior to this chapter, we had only explicitly discussed using patterns with match and if let, but in fact, we’ve used patterns in other places as well, including in let statements. For example, consider this straightforward variable assignment with let:


#![allow(unused)]
fn main() {
let x = 5;
}

Every time you've used a let statement like this you've been using patterns, although you might not have realized it! More formally, a let statement looks like this:

let PATTERN = EXPRESSION;

In statements like let x = 5; with a variable name in the PATTERN slot, the variable name is just a particularly simple form of a pattern. Rust compares the expression against the pattern and assigns any names it finds. So in the let x = 5; example, x is a pattern that means “bind what matches here to the variable x.” Because the name x is the whole pattern, this pattern effectively means “bind everything to the variable x, whatever the value is.”

To see the pattern matching aspect of let more clearly, consider Listing 18-4, which uses a pattern with let to destructure a tuple.

fn main() {
    let (x, y, z) = (1, 2, 3);
}

Listing 18-4: Using a pattern to destructure a tuple and create three variables at once

Here, we match a tuple against a pattern. Rust compares the value (1, 2, 3) to the pattern (x, y, z) and sees that the value matches the pattern, so Rust binds 1 to x, 2 to y, and 3 to z. You can think of this tuple pattern as nesting three individual variable patterns inside it.

If the number of elements in the pattern doesn’t match the number of elements in the tuple, the overall type won’t match and we’ll get a compiler error. For example, Listing 18-5 shows an attempt to destructure a tuple with three elements into two variables, which won’t work.

fn main() {
    let (x, y) = (1, 2, 3);
}

Listing 18-5: Incorrectly constructing a pattern whose variables don’t match the number of elements in the tuple

Attempting to compile this code results in this type error:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^ expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` due to previous error

To fix the error, we could ignore one or more of the values in the tuple using _ or .., as you’ll see in the “Ignoring Values in a Pattern” section. If the problem is that we have too many variables in the pattern, the solution is to make the types match by removing variables so the number of variables equals the number of elements in the tuple.

Function Parameters

Function parameters can also be patterns. The code in Listing 18-6, which declares a function named foo that takes one parameter named x of type i32, should by now look familiar.

fn foo(x: i32) {
    // code goes here
}

fn main() {}

Listing 18-6: A function signature uses patterns in the parameters

The x part is a pattern! As we did with let, we could match a tuple in a function’s arguments to the pattern. Listing 18-7 splits the values in a tuple as we pass it to a function.

Filename: src/main.rs

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

Listing 18-7: A function with parameters that destructure a tuple

This code prints Current location: (3, 5). The values &(3, 5) match the pattern &(x, y), so x is the value 3 and y is the value 5.

We can also use patterns in closure parameter lists in the same way as in function parameter lists, because closures are similar to functions, as discussed in Chapter 13.

At this point, you’ve seen several ways of using patterns, but patterns don’t work the same in every place we can use them. In some places, the patterns must be irrefutable; in other circumstances, they can be refutable. We’ll discuss these two concepts next.

Reddedilebilirlik: Bir Model; Eşleşmede Başarısız Olabilir mi?

Modeller iki biçimde gelir: reddedilebilir ve reddedilemez. Geçilen herhangi bir olası değer için eşleşecek kalıplar reddedilemez. Bir örnek, let x = 5 ifade yapısındaki x olabilir; çünkü x herhangi bir şeyle eşleşir ve bu nedenle eşleşme başarısız olamaz. Bazı olası değerler için eşleşmeyen kalıplar reddedilebilir.

Fonksiyon parametreleri, let deyimleri ve for döngüleri yalnızca reddedilemez kalıpları kabul edebilir, çünkü değerler eşleşmediğinde program anlamlı bir şey yapamaz. if let ve while let ifadeleri reddedilemez ve reddedilemez modelleri kabul eder, ancak derleyici reddedilemez kalıplara karşı uyarır çünkü tanımları gereği olası başarısızlığı ele almayı amaçlarlar: bir koşulun işlevselliği, başarıya veya başarısızlığa bağlı olarak farklı performans gösterme yeteneğindedir.

Genel olarak, reddedilebilir ve reddedilemez modeller arasındaki ayrım hakkında endişelenmenize gerek yoktur; ancak, bir hata mesajında gördüğünüzde yanıt verebilmeniz için reddedilebilirlik kavramına aşina olmanız gerekir. Bu durumlarda, kodun amaçlanan davranışına bağlı olarak, modeli veya modeli kullandığınız yapıyı değiştirmeniz gerekir.

Rust'ın reddedilemez bir model gerektirdiği ve bunun tersi olduğu halde, çürütülebilir bir model kullanmaya çalıştığımızda ne olduğuna dair bir örneğe bakalım. Liste 18-8, bir let ifade yapısını gösterir, ancak Some(x) belirttiğimiz model için reddedilebilir bir modeldir. Tahmin edebileceğiniz gibi, bu kod derlenmeyecektir:

fn main() {
    let some_option_value: Option<i32> = None;
    let Some(x) = some_option_value;
}

Liste 18-8: let ile çürütülebilir bir model kullanmaya çalışmak

some_option_value, None değerinde olsaydı, Some(x) modeliyle eşleşmezdi, bu da kalıbın reddedilebilir olduğu anlamına gelir. Ancak, let ifade yapısı yalnızca reddedilemez bir kalıbı kabul edebilir, çünkü kodun None değeriyle yapabileceği geçerli hiçbir şey yoktur. Derleme zamanında; Rust, reddedilemez bir modelin gerekli olduğu durumlarda reddedilebilir bir model kullanmaya çalıştığımızdan şikayet edecektir:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding: `None` not covered
   --> src/main.rs:3:9
    |
3   |     let Some(x) = some_option_value;
    |         ^^^^^^^ pattern `None` not covered
    |
    = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
    = note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
    = note: the matched value is of type `Option<i32>`
help: you might want to use `if let` to ignore the variant that isn't matched
    |
3   |     if let Some(x) = some_option_value { /* */ }
    |

For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` due to previous error

Some(x) modeliyle geçerli her değeri kapsamadığımız (ve kapsayamadığımız) için, Rust haklı olarak bir derleyici hatası üretir.

Çürütülemez bir modele ihtiyaç duyulan bir çürütülebilir modelimiz varsa, modeli kullanan kodu değiştirerek düzeltebiliriz: let yerine if let kullanabiliriz. Ardından, model eşleşmezse, kod, süslü parantezler arasındaki kodu atlayarak geçerli bir şekilde devam etmesinin bir yolunu sunar. Liste 18-9, Liste 18-8'deki kodun nasıl düzeltileceğini gösterir.

fn main() {
    let some_option_value: Option<i32> = None;
    if let Some(x) = some_option_value {
        println!("{}", x);
    }
}

Liste 18-9: let yerine if let kullanan ve reddedilebilir modellere sahip bir blok kullanma

Kodu bir çıkış verdik! Bu kod tamamen geçerlidir, ancak bir hata almadan reddedilemez bir model kullanamayacağımız anlamına gelir. Liste 18-10'da gösterildiği gibi x gibi her zaman eşleşecek bir model verirsek, derleyici bir uyarı verecektir.

fn main() {
    if let x = 5 {
        println!("{}", x);
    };
}

Liste 18-10: if let ile reddedilemez bir model kullanmaya çalışmak

Rust, reddedilemez bir modelle if let kullanmanın mantıklı olmadığından şikayet ediyor:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `if let` pattern
 --> src/main.rs:2:8
  |
2 |     if let x = 5 {
  |        ^^^^^^^^^
  |
  = note: `#[warn(irrefutable_let_patterns)]` on by default
  = note: this pattern will always match, so the `if let` is useless
  = help: consider replacing the `if let` with a `let`

warning: `patterns` (bin "patterns") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/patterns`
5

Bu nedenle, eşleşme kolları, kalan değerleri reddedilemez bir modelle eşleştirmesi gereken son kol hariç, reddedilebilir modeller kullanmalıdır. Rust, tek kollu bir eşleşmede reddedilemez bir model kullanmamıza izin verir, ancak bu söz dizimi çok kullanışlı değildir ve daha basit bir let ifade yapısı ile değiştirilebilir.

Artık modeleri nerede kullanacağınızı ve reddedilebilir ve reddedilemez modeller arasındaki farkı bildiğinize göre, model oluşturmak için kullanabileceğimiz söz dizimini ele alalım.

Pattern Syntax

In this section, we gather all the syntax valid in patterns and discuss why and when you might want to use each one.

Matching Literals

As you saw in Chapter 6, you can match patterns against literals directly. The following code gives some examples:

fn main() {
    let x = 1;

    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

This code prints one because the value in x is 1. This syntax is useful when you want your code to take an action if it gets a particular concrete value.

Matching Named Variables

Named variables are irrefutable patterns that match any value, and we’ve used them many times in the book. However, there is a complication when you use named variables in match expressions. Because match starts a new scope, variables declared as part of a pattern inside the match expression will shadow those with the same name outside the match construct, as is the case with all variables. In Listing 18-11, we declare a variable named x with the value Some(5) and a variable y with the value 10. We then create a match expression on the value x. Look at the patterns in the match arms and println! at the end, and try to figure out what the code will print before running this code or reading further.

Filename: src/main.rs

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {:?}", y),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {:?}", x, y);
}

Listing 18-11: A match expression with an arm that introduces a shadowed variable y

Let’s walk through what happens when the match expression runs. The pattern in the first match arm doesn’t match the defined value of x, so the code continues.

The pattern in the second match arm introduces a new variable named y that will match any value inside a Some value. Because we’re in a new scope inside the match expression, this is a new y variable, not the y we declared at the beginning with the value 10. This new y binding will match any value inside a Some, which is what we have in x. Therefore, this new y binds to the inner value of the Some in x. That value is 5, so the expression for that arm executes and prints Matched, y = 5.

If x had been a None value instead of Some(5), the patterns in the first two arms wouldn’t have matched, so the value would have matched to the underscore. We didn’t introduce the x variable in the pattern of the underscore arm, so the x in the expression is still the outer x that hasn’t been shadowed. In this hypothetical case, the match would print Default case, x = None.

When the match expression is done, its scope ends, and so does the scope of the inner y. The last println! produces at the end: x = Some(5), y = 10.

To create a match expression that compares the values of the outer x and y, rather than introducing a shadowed variable, we would need to use a match guard conditional instead. We’ll talk about match guards later in the “Extra Conditionals with Match Guards” section.

Multiple Patterns

In match expressions, you can match multiple patterns using the | syntax, which is the pattern or operator. For example, in the following code we match the value of x against the match arms, the first of which has an or option, meaning if the value of x matches either of the values in that arm, that arm’s code will run:

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("one or two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

This code prints one or two.

Matching Ranges of Values with ..=

The ..= syntax allows us to match to an inclusive range of values. In the following code, when a pattern matches any of the values within the given range, that arm will execute:

fn main() {
    let x = 5;

    match x {
        1..=5 => println!("one through five"),
        _ => println!("something else"),
    }
}

If x is 1, 2, 3, 4, or 5, the first arm will match. This syntax is more convenient for multiple match values than using the | operator to express the same idea; if we were to use | we would have to specify 1 | 2 | 3 | 4 | 5. Specifying a range is much shorter, especially if we want to match, say, any number between 1 and 1,000!

The compiler checks that the range isn’t empty at compile time, and because the only types for which Rust can tell if a range is empty or not are char and numeric values, ranges are only allowed with numeric or char values.

Here is an example using ranges of char values:

fn main() {
    let x = 'c';

    match x {
        'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }
}

Rust can tell that 'c' is within the first pattern’s range and prints early ASCII letter.

Destructuring to Break Apart Values

We can also use patterns to destructure structs, enums, and tuples to use different parts of these values. Let’s walk through each value.

Destructuring Structs

Listing 18-12 shows a Point struct with two fields, x and y, that we can break apart using a pattern with a let statement.

Filename: src/main.rs

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}

Listing 18-12: Destructuring a struct’s fields into separate variables

This code creates the variables a and b that match the values of the x and y fields of the p struct. This example shows that the names of the variables in the pattern don’t have to match the field names of the struct. However, it’s common to match the variable names to the field names to make it easier to remember which variables came from which fields. Because of this common usage, and because writing let Point { x: x, y: y } = p; contains a lot of duplication, Rust has a shorthand for patterns that match struct fields: you only need to list the name of the struct field, and the variables created from the pattern will have the same names. Listing 18-13 behaves in the same way as the code in Listing 18-12, but the variables created in the let pattern are x and y instead of a and b.

Filename: src/main.rs

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}

Listing 18-13: Destructuring struct fields using struct field shorthand

This code creates the variables x and y that match the x and y fields of the p variable. The outcome is that the variables x and y contain the values from the p struct.

We can also destructure with literal values as part of the struct pattern rather than creating variables for all the fields. Doing so allows us to test some of the fields for particular values while creating variables to destructure the other fields.

In Listing 18-14, we have a match expression that separates Point values into three cases: points that lie directly on the x axis (which is true when y = 0), on the y axis (x = 0), or neither.

Filename: src/main.rs

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {}", x),
        Point { x: 0, y } => println!("On the y axis at {}", y),
        Point { x, y } => println!("On neither axis: ({}, {})", x, y),
    }
}

Listing 18-14: Destructuring and matching literal values in one pattern

The first arm will match any point that lies on the x axis by specifying that the y field matches if its value matches the literal 0. The pattern still creates an x variable that we can use in the code for this arm.

Similarly, the second arm matches any point on the y axis by specifying that the x field matches if its value is 0 and creates a variable y for the value of the y field. The third arm doesn’t specify any literals, so it matches any other Point and creates variables for both the x and y fields.

In this example, the value p matches the second arm by virtue of x containing a 0, so this code will print On the y axis at 7.

Destructuring Enums

We've destructured enums in this book (for example, Listing 6-5 in Chapter 6), but haven’t yet explicitly discussed that the pattern to destructure an enum corresponds to the way the data stored within the enum is defined. As an example, in Listing 18-15 we use the Message enum from Listing 6-2 and write a match with patterns that will destructure each inner value.

Filename: src/main.rs

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.")
        }
        Message::Move { x, y } => {
            println!(
                "Move in the x direction {} and in the y direction {}",
                x, y
            );
        }
        Message::Write(text) => println!("Text message: {}", text),
        Message::ChangeColor(r, g, b) => println!(
            "Change the color to red {}, green {}, and blue {}",
            r, g, b
        ),
    }
}

Listing 18-15: Destructuring enum variants that hold different kinds of values

This code will print Change the color to red 0, green 160, and blue 255. Try changing the value of msg to see the code from the other arms run.

For enum variants without any data, like Message::Quit, we can’t destructure the value any further. We can only match on the literal Message::Quit value, and no variables are in that pattern.

For struct-like enum variants, such as Message::Move, we can use a pattern similar to the pattern we specify to match structs. After the variant name, we place curly brackets and then list the fields with variables so we break apart the pieces to use in the code for this arm. Here we use the shorthand form as we did in Listing 18-13.

For tuple-like enum variants, like Message::Write that holds a tuple with one element and Message::ChangeColor that holds a tuple with three elements, the pattern is similar to the pattern we specify to match tuples. The number of variables in the pattern must match the number of elements in the variant we’re matching.

Destructuring Nested Structs and Enums

So far, our examples have all been matching structs or enums one level deep, but matching can work on nested items too! For example, we can refactor the code in Listing 18-15 to support RGB and HSV colors in the ChangeColor message, as shown in Listing 18-16.

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => println!(
            "Change the color to red {}, green {}, and blue {}",
            r, g, b
        ),
        Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
            "Change the color to hue {}, saturation {}, and value {}",
            h, s, v
        ),
        _ => (),
    }
}

Listing 18-16: Matching on nested enums

The pattern of the first arm in the match expression matches a Message::ChangeColor enum variant that contains a Color::Rgb variant; then the pattern binds to the three inner i32 values. The pattern of the second arm also matches a Message::ChangeColor enum variant, but the inner enum matches Color::Hsv instead. We can specify these complex conditions in one match expression, even though two enums are involved.

Destructuring Structs and Tuples

We can mix, match, and nest destructuring patterns in even more complex ways. The following example shows a complicated destructure where we nest structs and tuples inside a tuple and destructure all the primitive values out:

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }

    let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}

This code lets us break complex types into their component parts so we can use the values we’re interested in separately.

Destructuring with patterns is a convenient way to use pieces of values, such as the value from each field in a struct, separately from each other.

Ignoring Values in a Pattern

You’ve seen that it’s sometimes useful to ignore values in a pattern, such as in the last arm of a match, to get a catchall that doesn’t actually do anything but does account for all remaining possible values. There are a few ways to ignore entire values or parts of values in a pattern: using the _ pattern (which you’ve seen), using the _ pattern within another pattern, using a name that starts with an underscore, or using .. to ignore remaining parts of a value. Let’s explore how and why to use each of these patterns.

Ignoring an Entire Value with _

We’ve used the underscore as a wildcard pattern that will match any value but not bind to the value. This is especially useful as the last arm in a match expression, but we can also use it in any pattern, including function parameters, as shown in Listing 18-17.

Filename: src/main.rs

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {}", y);
}

fn main() {
    foo(3, 4);
}

Listing 18-17: Using _ in a function signature

This code will completely ignore the value 3 passed as the first argument, and will print This code only uses the y parameter: 4.

In most cases when you no longer need a particular function parameter, you would change the signature so it doesn’t include the unused parameter. Ignoring a function parameter can be especially useful in cases when, for example, you're implementing a trait when you need a certain type signature but the function body in your implementation doesn’t need one of the parameters. You then avoid getting a compiler warning about unused function parameters, as you would if you used a name instead.

Ignoring Parts of a Value with a Nested _

We can also use _ inside another pattern to ignore just part of a value, for example, when we want to test for only part of a value but have no use for the other parts in the corresponding code we want to run. Listing 18-18 shows code responsible for managing a setting’s value. The business requirements are that the user should not be allowed to overwrite an existing customization of a setting but can unset the setting and give it a value if it is currently unset.

fn main() {
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);

    match (setting_value, new_setting_value) {
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    println!("setting is {:?}", setting_value);
}

Listing 18-18: Using an underscore within patterns that match Some variants when we don’t need to use the value inside the Some

This code will print Can't overwrite an existing customized value and then setting is Some(5). In the first match arm, we don’t need to match on or use the values inside either Some variant, but we do need to test for the case when setting_value and new_setting_value are the Some variant. In that case, we print the reason for not changing setting_value, and it doesn’t get changed.

In all other cases (if either setting_value or new_setting_value are None) expressed by the _ pattern in the second arm, we want to allow new_setting_value to become setting_value.

We can also use underscores in multiple places within one pattern to ignore particular values. Listing 18-19 shows an example of ignoring the second and fourth values in a tuple of five items.

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {}, {}, {}", first, third, fifth)
        }
    }
}

Listing 18-19: Ignoring multiple parts of a tuple

This code will print Some numbers: 2, 8, 32, and the values 4 and 16 will be ignored.

Ignoring an Unused Variable by Starting Its Name with _

If you create a variable but don’t use it anywhere, Rust will usually issue a warning because an unused variable could be a bug. However, sometimes it’s useful to be able to create a variable you won’t use yet, such as when you’re prototyping or just starting a project. In this situation, you can tell Rust not to warn you about the unused variable by starting the name of the variable with an underscore. In Listing 18-20, we create two unused variables, but when we compile this code, we should only get a warning about one of them.

Filename: src/main.rs

fn main() {
    let _x = 5;
    let y = 10;
}

Listing 18-20: Starting a variable name with an underscore to avoid getting unused variable warnings

Here we get a warning about not using the variable y, but we don’t get a warning about not using _x.

Note that there is a subtle difference between using only _ and using a name that starts with an underscore. The syntax _x still binds the value to the variable, whereas _ doesn’t bind at all. To show a case where this distinction matters, Listing 18-21 will provide us with an error.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
        println!("found a string");
    }

    println!("{:?}", s);
}

Listing 18-21: An unused variable starting with an underscore still binds the value, which might take ownership of the value

We’ll receive an error because the s value will still be moved into _s, which prevents us from using s again. However, using the underscore by itself doesn’t ever bind to the value. Listing 18-22 will compile without any errors because s doesn’t get moved into _.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_) = s {
        println!("found a string");
    }

    println!("{:?}", s);
}

Listing 18-22: Using an underscore does not bind the value

This code works just fine because we never bind s to anything; it isn’t moved.

Ignoring Remaining Parts of a Value with ..

With values that have many parts, we can use the .. syntax to use specific parts and ignore the rest, avoiding the need to list underscores for each ignored value. The .. pattern ignores any parts of a value that we haven’t explicitly matched in the rest of the pattern. In Listing 18-23, we have a Point struct that holds a coordinate in three-dimensional space. In the match expression, we want to operate only on the x coordinate and ignore the values in the y and z fields.

fn main() {
    struct Point {
        x: i32,
        y: i32,
        z: i32,
    }

    let origin = Point { x: 0, y: 0, z: 0 };

    match origin {
        Point { x, .. } => println!("x is {}", x),
    }
}

Listing 18-23: Ignoring all fields of a Point except for x by using ..

We list the x value and then just include the .. pattern. This is quicker than having to list y: _ and z: _, particularly when we’re working with structs that have lots of fields in situations where only one or two fields are relevant.

The syntax .. will expand to as many values as it needs to be. Listing 18-24 shows how to use .. with a tuple.

Filename: src/main.rs

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {}, {}", first, last);
        }
    }
}

Listing 18-24: Matching only the first and last values in a tuple and ignoring all other values

In this code, the first and last value are matched with first and last. The .. will match and ignore everything in the middle.

However, using .. must be unambiguous. If it is unclear which values are intended for matching and which should be ignored, Rust will give us an error. Listing 18-25 shows an example of using .. ambiguously, so it will not compile.

Filename: src/main.rs

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {}", second)
        },
    }
}

Listing 18-25: An attempt to use .. in an ambiguous way

When we compile this example, we get this error:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:22
  |
5 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

error: could not compile `patterns` due to previous error

It’s impossible for Rust to determine how many values in the tuple to ignore before matching a value with second and then how many further values to ignore thereafter. This code could mean that we want to ignore 2, bind second to 4, and then ignore 8, 16, and 32; or that we want to ignore 2 and 4, bind second to 8, and then ignore 16 and 32; and so forth. The variable name second doesn’t mean anything special to Rust, so we get a compiler error because using .. in two places like this is ambiguous.

Extra Conditionals with Match Guards

A match guard is an additional if condition, specified after the pattern in a match arm, that must also match for that arm to be chosen. Match guards are useful for expressing more complex ideas than a pattern alone allows.

The condition can use variables created in the pattern. Listing 18-26 shows a match where the first arm has the pattern Some(x) and also has a match guard of if x % 2 == 0 (which will be true if the number is even).

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("The number {} is even", x),
        Some(x) => println!("The number {} is odd", x),
        None => (),
    }
}

Listing 18-26: Adding a match guard to a pattern

This example will print The number 4 is even. When num is compared to the pattern in the first arm, it matches, because Some(4) matches Some(x). Then the match guard checks whether the remainder of dividing x by 2 is equal to 0, and because it is, the first arm is selected.

If num had been Some(5) instead, the match guard in the first arm would have been false because the remainder of 5 divided by 2 is 1, which is not equal to 0. Rust would then go to the second arm, which would match because the second arm doesn’t have a match guard and therefore matches any Some variant.

There is no way to express the if x % 2 == 0 condition within a pattern, so the match guard gives us the ability to express this logic. The downside of this additional expressiveness is that the compiler doesn't try to check for exhaustiveness when match guard expressions are involved.

In Listing 18-11, we mentioned that we could use match guards to solve our pattern-shadowing problem. Recall that we created a new variable inside the pattern in the match expression instead of using the variable outside the match. That new variable meant we couldn’t test against the value of the outer variable. Listing 18-27 shows how we can use a match guard to fix this problem.

Filename: src/main.rs

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {}", n),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {}", x, y);
}

Listing 18-27: Using a match guard to test for equality with an outer variable

This code will now print Default case, x = Some(5). The pattern in the second match arm doesn’t introduce a new variable y that would shadow the outer y, meaning we can use the outer y in the match guard. Instead of specifying the pattern as Some(y), which would have shadowed the outer y, we specify Some(n). This creates a new variable n that doesn’t shadow anything because there is no n variable outside the match.

The match guard if n == y is not a pattern and therefore doesn’t introduce new variables. This y is the outer y rather than a new shadowed y, and we can look for a value that has the same value as the outer y by comparing n to y.

You can also use the or operator | in a match guard to specify multiple patterns; the match guard condition will apply to all the patterns. Listing 18-28 shows the precedence when combining a pattern that uses | with a match guard. The important part of this example is that the if y match guard applies to 4, 5, and 6, even though it might look like if y only applies to 6.

fn main() {
    let x = 4;
    let y = false;

    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"),
    }
}

Listing 18-28: Combining multiple patterns with a match guard

The match condition states that the arm only matches if the value of x is equal to 4, 5, or 6 and if y is true. When this code runs, the pattern of the first arm matches because x is 4, but the match guard if y is false, so the first arm is not chosen. The code moves on to the second arm, which does match, and this program prints no. The reason is that the if condition applies to the whole pattern 4 | 5 | 6, not only to the last value 6. In other words, the precedence of a match guard in relation to a pattern behaves like this:

(4 | 5 | 6) if y => ...

rather than this:

4 | 5 | (6 if y) => ...

After running the code, the precedence behavior is evident: if the match guard were applied only to the final value in the list of values specified using the | operator, the arm would have matched and the program would have printed yes.

@ Bindings

The at operator @ lets us create a variable that holds a value at the same time as we’re testing that value for a pattern match. In Listing 18-29, we want to test that a Message::Hello id field is within the range 3..=7. We also want to bind the value to the variable id_variable so we can use it in the code associated with the arm. We could name this variable id, the same as the field, but for this example we’ll use a different name.

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello {
            id: id_variable @ 3..=7,
        } => println!("Found an id in range: {}", id_variable),
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range")
        }
        Message::Hello { id } => println!("Found some other id: {}", id),
    }
}

Listing 18-29: Using @ to bind to a value in a pattern while also testing it

This example will print Found an id in range: 5. By specifying id_variable @ before the range 3..=7, we’re capturing whatever value matched the range while also testing that the value matched the range pattern.

In the second arm, where we only have a range specified in the pattern, the code associated with the arm doesn’t have a variable that contains the actual value of the id field. The id field’s value could have been 10, 11, or 12, but the code that goes with that pattern doesn’t know which it is. The pattern code isn’t able to use the value from the id field, because we haven’t saved the id value in a variable.

In the last arm, where we’ve specified a variable without a range, we do have the value available to use in the arm’s code in a variable named id. The reason is that we’ve used the struct field shorthand syntax. But we haven’t applied any test to the value in the id field in this arm, as we did with the first two arms: any value would match this pattern.

Using @ lets us test a value and save it in a variable within one pattern.

Summary

Rust’s patterns are very useful in distinguishing between different kinds of data. When used in match expressions, Rust ensures your patterns cover every possible value, or your program won’t compile. Patterns in let statements and function parameters make those constructs more useful, enabling the destructuring of values into smaller parts at the same time as assigning to variables. We can create simple or complex patterns to suit our needs.

Next, for the penultimate chapter of the book, we’ll look at some advanced aspects of a variety of Rust’s features.

Gelişmiş Özellikler

Şimdiye kadar Rust programlama dilinin en sık kullanılan kısımlarını öğrendiniz. Bölüm 20'de bir proje daha yapmadan önce, arada bir karşılaşabileceğiniz, ancak her gün kullanmayabileceğiniz dilin birkaç farklı yönüne bakacağız. Herhangi bir bilmediğiniz özelliklerle karşılaştığınızda bu bölümü referans olarak kullanabilirsiniz. Burada kapsanan özellikler, adı gibi özel durumlarda faydalıdır. Bunlara sık sık ulaşamasanız da, Rust'ın sunduğu tüm özellikleri kavradığınızdan emin olmak istiyoruz.

Bu bölümde şunları ele alacağız:

  • Güvensiz Rust: Rust'ın bellek garantilerinin bazılarından nasıl vazgeçilir ve bu garantilerin manuel olarak desteklenmesi için gerekli sorumluluk nasıl alınır
  • Gelişmiş özellikler: ilişkili türler, varsayılan tür parametreleri, tam nitelikli söz dizimi, süper özellikler ve özelliklerle ilgili yeni tür modeli
  • Gelişmiş türler: hakkında daha fazla bilgi yeni tip desen, tür takma adları ve dinamik olarak boyutlandırılmış türler
  • Gelişmiş işlevler ve kapatmalar: fonksiyon işaretçileri ve dönüşlü kapanış ifadeleri
  • Makrolar: derleme zamanında çalıştırılan daha fazla kod tanımlamanın yolları

Bu, herkes için bir şeyler içeren bir dizi Rust özelliğidir! Hadi dalalım!

Unsafe Rust

All the code we’ve discussed so far has had Rust’s memory safety guarantees enforced at compile time. However, Rust has a second language hidden inside it that doesn’t enforce these memory safety guarantees: it’s called unsafe Rust and works just like regular Rust, but gives us extra superpowers.

Unsafe Rust exists because, by nature, static analysis is conservative. When the compiler tries to determine whether or not code upholds the guarantees, it’s better for it to reject some valid programs than to accept some invalid programs. Although the code might be okay, if the Rust compiler doesn’t have enough information to be confident, it will reject the code. In these cases, you can use unsafe code to tell the compiler, “Trust me, I know what I’m doing.” Be warned, however, that you use unsafe Rust at your own risk: if you use unsafe code incorrectly, problems can occur due to memory unsafety, such as null pointer dereferencing.

Another reason Rust has an unsafe alter ego is that the underlying computer hardware is inherently unsafe. If Rust didn’t let you do unsafe operations, you couldn’t do certain tasks. Rust needs to allow you to do low-level systems programming, such as directly interacting with the operating system or even writing your own operating system. Working with low-level systems programming is one of the goals of the language. Let’s explore what we can do with unsafe Rust and how to do it.

Unsafe Superpowers

To switch to unsafe Rust, use the unsafe keyword and then start a new block that holds the unsafe code. You can take five actions in unsafe Rust that you can’t in safe Rust, which we call unsafe superpowers. Those superpowers include the ability to:

  • Dereference a raw pointer
  • Call an unsafe function or method
  • Access or modify a mutable static variable
  • Implement an unsafe trait
  • Access fields of unions

It’s important to understand that unsafe doesn’t turn off the borrow checker or disable any other of Rust’s safety checks: if you use a reference in unsafe code, it will still be checked. The unsafe keyword only gives you access to these five features that are then not checked by the compiler for memory safety. You’ll still get some degree of safety inside of an unsafe block.

In addition, unsafe does not mean the code inside the block is necessarily dangerous or that it will definitely have memory safety problems: the intent is that as the programmer, you’ll ensure the code inside an unsafe block will access memory in a valid way.

People are fallible, and mistakes will happen, but by requiring these five unsafe operations to be inside blocks annotated with unsafe you’ll know that any errors related to memory safety must be within an unsafe block. Keep unsafe blocks small; you’ll be thankful later when you investigate memory bugs.

To isolate unsafe code as much as possible, it’s best to enclose unsafe code within a safe abstraction and provide a safe API, which we’ll discuss later in the chapter when we examine unsafe functions and methods. Parts of the standard library are implemented as safe abstractions over unsafe code that has been audited. Wrapping unsafe code in a safe abstraction prevents uses of unsafe from leaking out into all the places that you or your users might want to use the functionality implemented with unsafe code, because using a safe abstraction is safe.

Let’s look at each of the five unsafe superpowers in turn. We’ll also look at some abstractions that provide a safe interface to unsafe code.

Dereferencing a Raw Pointer

In Chapter 4, in the “Dangling References” section, we mentioned that the compiler ensures references are always valid. Unsafe Rust has two new types called raw pointers that are similar to references. As with references, raw pointers can be immutable or mutable and are written as *const T and *mut T, respectively. The asterisk isn’t the dereference operator; it’s part of the type name. In the context of raw pointers, immutable means that the pointer can’t be directly assigned to after being dereferenced.

Different from references and smart pointers, raw pointers:

  • Are allowed to ignore the borrowing rules by having both immutable and mutable pointers or multiple mutable pointers to the same location
  • Aren’t guaranteed to point to valid memory
  • Are allowed to be null
  • Don’t implement any automatic cleanup

By opting out of having Rust enforce these guarantees, you can give up guaranteed safety in exchange for greater performance or the ability to interface with another language or hardware where Rust’s guarantees don’t apply.

Listing 19-1 shows how to create an immutable and a mutable raw pointer from references.

fn main() {
    let mut num = 5;

    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;
}

Listing 19-1: Creating raw pointers from references

Notice that we don’t include the unsafe keyword in this code. We can create raw pointers in safe code; we just can’t dereference raw pointers outside an unsafe block, as you’ll see in a bit.

We’ve created raw pointers by using as to cast an immutable and a mutable reference into their corresponding raw pointer types. Because we created them directly from references guaranteed to be valid, we know these particular raw pointers are valid, but we can’t make that assumption about just any raw pointer.

To demonstrate this, next we’ll create a raw pointer whose validity we can’t be so certain of. Listing 19-2 shows how to create a raw pointer to an arbitrary location in memory. Trying to use arbitrary memory is undefined: there might be data at that address or there might not, the compiler might optimize the code so there is no memory access, or the program might error with a segmentation fault. Usually, there is no good reason to write code like this, but it is possible.

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}

Listing 19-2: Creating a raw pointer to an arbitrary memory address

Recall that we can create raw pointers in safe code, but we can’t dereference raw pointers and read the data being pointed to. In Listing 19-3, we use the dereference operator * on a raw pointer that requires an unsafe block.

fn main() {
    let mut num = 5;

    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2);
    }
}

Listing 19-3: Dereferencing raw pointers within an unsafe block

Creating a pointer does no harm; it’s only when we try to access the value that it points at that we might end up dealing with an invalid value.

Note also that in Listing 19-1 and 19-3, we created *const i32 and *mut i32 raw pointers that both pointed to the same memory location, where num is stored. If we instead tried to create an immutable and a mutable reference to num, the code would not have compiled because Rust’s ownership rules don’t allow a mutable reference at the same time as any immutable references. With raw pointers, we can create a mutable pointer and an immutable pointer to the same location and change data through the mutable pointer, potentially creating a data race. Be careful!

With all of these dangers, why would you ever use raw pointers? One major use case is when interfacing with C code, as you’ll see in the next section, “Calling an Unsafe Function or Method.” Another case is when building up safe abstractions that the borrow checker doesn’t understand. We’ll introduce unsafe functions and then look at an example of a safe abstraction that uses unsafe code.

Calling an Unsafe Function or Method

The second type of operation you can perform in an unsafe block is calling unsafe functions. Unsafe functions and methods look exactly like regular functions and methods, but they have an extra unsafe before the rest of the definition. The unsafe keyword in this context indicates the function has requirements we need to uphold when we call this function, because Rust can’t guarantee we’ve met these requirements. By calling an unsafe function within an unsafe block, we’re saying that we’ve read this function’s documentation and take responsibility for upholding the function’s contracts.

Here is an unsafe function named dangerous that doesn’t do anything in its body:

fn main() {
    unsafe fn dangerous() {}

    unsafe {
        dangerous();
    }
}

We must call the dangerous function within a separate unsafe block. If we try to call dangerous without the unsafe block, we’ll get an error:

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function is unsafe and requires unsafe function or block
 --> src/main.rs:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on how to avoid undefined behavior

For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` due to previous error

With the unsafe block, we’re asserting to Rust that we’ve read the function’s documentation, we understand how to use it properly, and we’ve verified that we’re fulfilling the contract of the function.

Bodies of unsafe functions are effectively unsafe blocks, so to perform other unsafe operations within an unsafe function, we don’t need to add another unsafe block.

Creating a Safe Abstraction over Unsafe Code

Just because a function contains unsafe code doesn’t mean we need to mark the entire function as unsafe. In fact, wrapping unsafe code in a safe function is a common abstraction. As an example, let’s study the split_at_mut function from the standard library, which requires some unsafe code. We’ll explore how we might implement it. This safe method is defined on mutable slices: it takes one slice and makes it two by splitting the slice at the index given as an argument. Listing 19-4 shows how to use split_at_mut.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];

    let r = &mut v[..];

    let (a, b) = r.split_at_mut(3);

    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5, 6]);
}

Listing 19-4: Using the safe split_at_mut function

We can’t implement this function using only safe Rust. An attempt might look something like Listing 19-5, which won’t compile. For simplicity, we’ll implement split_at_mut as a function rather than a method and only for slices of i32 values rather than for a generic type T.

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert!(mid <= len);

    (&mut values[..mid], &mut values[mid..])
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}

Listing 19-5: An attempted implementation of split_at_mut using only safe Rust

This function first gets the total length of the slice. Then it asserts that the index given as a parameter is within the slice by checking whether it’s less than or equal to the length. The assertion means that if we pass an index that is greater than the length to split the slice at, the function will panic before it attempts to use that index.

Then we return two mutable slices in a tuple: one from the start of the original slice to the mid index and another from mid to the end of the slice.

When we try to compile the code in Listing 19-5, we’ll get an error.

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:6:31
  |
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  |                         - let's call the lifetime of this reference `'1`
...
6 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`

For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` due to previous error

Rust’s borrow checker can’t understand that we’re borrowing different parts of the slice; it only knows that we’re borrowing from the same slice twice. Borrowing different parts of a slice is fundamentally okay because the two slices aren’t overlapping, but Rust isn’t smart enough to know this. When we know code is okay, but Rust doesn’t, it’s time to reach for unsafe code.

Listing 19-6 shows how to use an unsafe block, a raw pointer, and some calls to unsafe functions to make the implementation of split_at_mut work.

use std::slice;

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}

Listing 19-6: Using unsafe code in the implementation of the split_at_mut function

Recall from “The Slice Type” section in Chapter 4 that slices are a pointer to some data and the length of the slice. We use the len method to get the length of a slice and the as_mut_ptr method to access the raw pointer of a slice. In this case, because we have a mutable slice to i32 values, as_mut_ptr returns a raw pointer with the type *mut i32, which we’ve stored in the variable ptr.

We keep the assertion that the mid index is within the slice. Then we get to the unsafe code: the slice::from_raw_parts_mut function takes a raw pointer and a length, and it creates a slice. We use this function to create a slice that starts from ptr and is mid items long. Then we call the add method on ptr with mid as an argument to get a raw pointer that starts at mid, and we create a slice using that pointer and the remaining number of items after mid as the length.

The function slice::from_raw_parts_mut is unsafe because it takes a raw pointer and must trust that this pointer is valid. The add method on raw pointers is also unsafe, because it must trust that the offset location is also a valid pointer. Therefore, we had to put an unsafe block around our calls to slice::from_raw_parts_mut and add so we could call them. By looking at the code and by adding the assertion that mid must be less than or equal to len, we can tell that all the raw pointers used within the unsafe block will be valid pointers to data within the slice. This is an acceptable and appropriate use of unsafe.

Note that we don’t need to mark the resulting split_at_mut function as unsafe, and we can call this function from safe Rust. We’ve created a safe abstraction to the unsafe code with an implementation of the function that uses unsafe code in a safe way, because it creates only valid pointers from the data this function has access to.

In contrast, the use of slice::from_raw_parts_mut in Listing 19-7 would likely crash when the slice is used. This code takes an arbitrary memory location and creates a slice 10,000 items long.

fn main() {
    use std::slice;

    let address = 0x01234usize;
    let r = address as *mut i32;

    let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}

Listing 19-7: Creating a slice from an arbitrary memory location

We don’t own the memory at this arbitrary location, and there is no guarantee that the slice this code creates contains valid i32 values. Attempting to use values as though it’s a valid slice results in undefined behavior.

Using extern Functions to Call External Code

Sometimes, your Rust code might need to interact with code written in another language. For this, Rust has the keyword extern that facilitates the creation and use of a Foreign Function Interface (FFI). An FFI is a way for a programming language to define functions and enable a different (foreign) programming language to call those functions.

Listing 19-8 demonstrates how to set up an integration with the abs function from the C standard library. Functions declared within extern blocks are always unsafe to call from Rust code. The reason is that other languages don’t enforce Rust’s rules and guarantees, and Rust can’t check them, so responsibility falls on the programmer to ensure safety.

Filename: src/main.rs

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

Listing 19-8: Declaring and calling an extern function defined in another language

Within the extern "C" block, we list the names and signatures of external functions from another language we want to call. The "C" part defines which application binary interface (ABI) the external function uses: the ABI defines how to call the function at the assembly level. The "C" ABI is the most common and follows the C programming language’s ABI.

Calling Rust Functions from Other Languages

We can also use extern to create an interface that allows other languages to call Rust functions. Instead of an creating a whole extern block, we add the extern keyword and specify the ABI to use just before the fn keyword for the relevant function. We also need to add a #[no_mangle] annotation to tell the Rust compiler not to mangle the name of this function. Mangling is when a compiler changes the name we’ve given a function to a different name that contains more information for other parts of the compilation process to consume but is less human readable. Every programming language compiler mangles names slightly differently, so for a Rust function to be nameable by other languages, we must disable the Rust compiler’s name mangling.

In the following example, we make the call_from_c function accessible from C code, after it’s compiled to a shared library and linked from C:


#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}
}

This usage of extern does not require unsafe.

Accessing or Modifying a Mutable Static Variable

In this book, we’ve not yet talked about global variables, which Rust does support but can be problematic with Rust’s ownership rules. If two threads are accessing the same mutable global variable, it can cause a data race.

In Rust, global variables are called static variables. Listing 19-9 shows an example declaration and use of a static variable with a string slice as a value.

Filename: src/main.rs

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("name is: {}", HELLO_WORLD);
}

Listing 19-9: Defining and using an immutable static variable

Static variables are similar to constants, which we discussed in the “Differences Between Variables and Constants” section in Chapter 3. The names of static variables are in SCREAMING_SNAKE_CASE by convention. Static variables can only store references with the 'static lifetime, which means the Rust compiler can figure out the lifetime and we aren’t required to annotate it explicitly. Accessing an immutable static variable is safe.

A subtle difference between constants and immutable static variables is that values in a static variable have a fixed address in memory. Using the value will always access the same data. Constants, on the other hand, are allowed to duplicate their data whenever they’re used. Another difference is that static variables can be mutable. Accessing and modifying mutable static variables is unsafe. Listing 19-10 shows how to declare, access, and modify a mutable static variable named COUNTER.

Filename: src/main.rs

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

Listing 19-10: Reading from or writing to a mutable static variable is unsafe

As with regular variables, we specify mutability using the mut keyword. Any code that reads or writes from COUNTER must be within an unsafe block. This code compiles and prints COUNTER: 3 as we would expect because it’s single threaded. Having multiple threads access COUNTER would likely result in data races.

With mutable data that is globally accessible, it’s difficult to ensure there are no data races, which is why Rust considers mutable static variables to be unsafe. Where possible, it’s preferable to use the concurrency techniques and thread-safe smart pointers we discussed in Chapter 16 so the compiler checks that data accessed from different threads is done safely.

Implementing an Unsafe Trait

We can use unsafe to implement an unsafe trait. A trait is unsafe when at least one of its methods has some invariant that the compiler can’t verify. We declare that a trait is unsafe by adding the unsafe keyword before trait and marking the implementation of the trait as unsafe too, as shown in Listing 19-11.

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

fn main() {}

Listing 19-11: Defining and implementing an unsafe trait

By using unsafe impl, we’re promising that we’ll uphold the invariants that the compiler can’t verify.

As an example, recall the Sync and Send marker traits we discussed in the “Extensible Concurrency with the Sync and Send Traits” section in Chapter 16: the compiler implements these traits automatically if our types are composed entirely of Send and Sync types. If we implement a type that contains a type that is not Send or Sync, such as raw pointers, and we want to mark that type as Send or Sync, we must use unsafe. Rust can’t verify that our type upholds the guarantees that it can be safely sent across threads or accessed from multiple threads; therefore, we need to do those checks manually and indicate as such with unsafe.

Accessing Fields of a Union

The final action that works only with unsafe is accessing fields of a union. A union is similar to a struct, but only one declared field is used in a particular instance at one time. Unions are primarily used to interface with unions in C code. Accessing union fields is unsafe because Rust can’t guarantee the type of the data currently being stored in the union instance. You can learn more about unions in the Rust Reference.

When to Use Unsafe Code

Using unsafe to take one of the five actions (superpowers) just discussed isn’t wrong or even frowned upon. But it is trickier to get unsafe code correct because the compiler can’t help uphold memory safety. When you have a reason to use unsafe code, you can do so, and having the explicit unsafe annotation makes it easier to track down the source of problems when they occur.

Advanced Traits

We first covered traits in the “Traits: Defining Shared Behavior” section of Chapter 10, but we didn’t discuss the more advanced details. Now that you know more about Rust, we can get into the nitty-gritty.

Specifying Placeholder Types in Trait Definitions with Associated Types

Associated types connect a type placeholder with a trait such that the trait method definitions can use these placeholder types in their signatures. The implementor of a trait will specify the concrete type to be used instead of the placeholder type for the particular implementation. That way, we can define a trait that uses some types without needing to know exactly what those types are until the trait is implemented.

We’ve described most of the advanced features in this chapter as being rarely needed. Associated types are somewhere in the middle: they’re used more rarely than features explained in the rest of the book but more commonly than many of the other features discussed in this chapter.

One example of a trait with an associated type is the Iterator trait that the standard library provides. The associated type is named Item and stands in for the type of the values the type implementing the Iterator trait is iterating over. The definition of the Iterator trait is as shown in Listing 19-12.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

Listing 19-12: The definition of the Iterator trait that has an associated type Item

The type Item is a placeholder, and the next method’s definition shows that it will return values of type Option<Self::Item>. Implementors of the Iterator trait will specify the concrete type for Item, and the next method will return an Option containing a value of that concrete type.

Associated types might seem like a similar concept to generics, in that the latter allow us to define a function without specifying what types it can handle. To examine the difference between the two concepts, we’ll look at an implementation of the Iterator trait on a type named Counter that specifies the Item type is u32:

Filename: src/lib.rs

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

This syntax seems comparable to that of generics. So why not just define the Iterator trait with generics, as shown in Listing 19-13?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

Listing 19-13: A hypothetical definition of the Iterator trait using generics

The difference is that when using generics, as in Listing 19-13, we must annotate the types in each implementation; because we can also implement Iterator<String> for Counter or any other type, we could have multiple implementations of Iterator for Counter. In other words, when a trait has a generic parameter, it can be implemented for a type multiple times, changing the concrete types of the generic type parameters each time. When we use the next method on Counter, we would have to provide type annotations to indicate which implementation of Iterator we want to use.

With associated types, we don’t need to annotate types because we can’t implement a trait on a type multiple times. In Listing 19-12 with the definition that uses associated types, we can only choose what the type of Item will be once, because there can only be one impl Iterator for Counter. We don’t have to specify that we want an iterator of u32 values everywhere that we call next on Counter.

Default Generic Type Parameters and Operator Overloading

When we use generic type parameters, we can specify a default concrete type for the generic type. This eliminates the need for implementors of the trait to specify a concrete type if the default type works. You specify a default type when declaring a generic type with the <PlaceholderType=ConcreteType> syntax.

A great example of a situation where this technique is useful is with operator overloading, in which you customize the behavior of an operator (such as +) in particular situations.

Rust doesn’t allow you to create your own operators or overload arbitrary operators. But you can overload the operations and corresponding traits listed in std::ops by implementing the traits associated with the operator. For example, in Listing 19-14 we overload the + operator to add two Point instances together. We do this by implementing the Add trait on a Point struct:

Filename: src/main.rs

use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}

Listing 19-14: Implementing the Add trait to overload the + operator for Point instances

The add method adds the x values of two Point instances and the y values of two Point instances to create a new Point. The Add trait has an associated type named Output that determines the type returned from the add method.

The default generic type in this code is within the Add trait. Here is its definition:


#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
}

This code should look generally familiar: a trait with one method and an associated type. The new part is Rhs=Self: this syntax is called default type parameters. The Rhs generic type parameter (short for “right hand side”) defines the type of the rhs parameter in the add method. If we don’t specify a concrete type for Rhs when we implement the Add trait, the type of Rhs will default to Self, which will be the type we’re implementing Add on.

When we implemented Add for Point, we used the default for Rhs because we wanted to add two Point instances. Let’s look at an example of implementing the Add trait where we want to customize the Rhs type rather than using the default.

We have two structs, Millimeters and Meters, holding values in different units. This thin wrapping of an existing type in another struct is known as the newtype pattern, which we describe in more detail in the “Using the Newtype Pattern to Implement External Traits on External Types” section. We want to add values in millimeters to values in meters and have the implementation of Add do the conversion correctly. We can implement Add for Millimeters with Meters as the Rhs, as shown in Listing 19-15.

Filename: src/lib.rs

use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

Listing 19-15: Implementing the Add trait on Millimeters to add Millimeters to Meters

To add Millimeters and Meters, we specify impl Add<Meters> to set the value of the Rhs type parameter instead of using the default of Self.

You’ll use default type parameters in two main ways:

  • To extend a type without breaking existing code
  • To allow customization in specific cases most users won’t need

The standard library’s Add trait is an example of the second purpose: usually, you’ll add two like types, but the Add trait provides the ability to customize beyond that. Using a default type parameter in the Add trait definition means you don’t have to specify the extra parameter most of the time. In other words, a bit of implementation boilerplate isn’t needed, making it easier to use the trait.

The first purpose is similar to the second but in reverse: if you want to add a type parameter to an existing trait, you can give it a default to allow extension of the functionality of the trait without breaking the existing implementation code.

Fully Qualified Syntax for Disambiguation: Calling Methods with the Same Name

Nothing in Rust prevents a trait from having a method with the same name as another trait’s method, nor does Rust prevent you from implementing both traits on one type. It’s also possible to implement a method directly on the type with the same name as methods from traits.

When calling methods with the same name, you’ll need to tell Rust which one you want to use. Consider the code in Listing 19-16 where we’ve defined two traits, Pilot and Wizard, that both have a method called fly. We then implement both traits on a type Human that already has a method named fly implemented on it. Each fly method does something different.

Filename: src/main.rs

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {}

Listing 19-16: Two traits are defined to have a fly method and are implemented on the Human type, and a fly method is implemented on Human directly

When we call fly on an instance of Human, the compiler defaults to calling the method that is directly implemented on the type, as shown in Listing 19-17.

Filename: src/main.rs

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();
}

Listing 19-17: Calling fly on an instance of Human

Running this code will print *waving arms furiously*, showing that Rust called the fly method implemented on Human directly.

To call the fly methods from either the Pilot trait or the Wizard trait, we need to use more explicit syntax to specify which fly method we mean. Listing 19-18 demonstrates this syntax.

Filename: src/main.rs

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

Listing 19-18: Specifying which trait’s fly method we want to call

Specifying the trait name before the method name clarifies to Rust which implementation of fly we want to call. We could also write Human::fly(&person), which is equivalent to the person.fly() that we used in Listing 19-18, but this is a bit longer to write if we don’t need to disambiguate.

Running this code prints the following:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*

Because the fly method takes a self parameter, if we had two types that both implement one trait, Rust could figure out which implementation of a trait to use based on the type of self.

However, associated functions that are not methods don’t have a self parameter. When there are multiple types or traits that define non-method functions with the same function name, Rust doesn't always know which type you mean unless you use fully qualified syntax. For example, in Listing 19-19 we create a trait for an animal shelter that wants to name all baby dogs Spot. We make an Animal trait with an associated non-method function baby_name. The Animal trait is implemented for the struct Dog, on which we also provide an associated non-method function baby_name directly.

Filename: src/main.rs

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}

Listing 19-19: A trait with an associated function and a type with an associated function of the same name that also implements the trait

We implement the code for naming all puppies Spot in the baby_name associated function that is defined on Dog. The Dog type also implements the trait Animal, which describes characteristics that all animals have. Baby dogs are called puppies, and that is expressed in the implementation of the Animal trait on Dog in the baby_name function associated with the Animal trait.

In main, we call the Dog::baby_name function, which calls the associated function defined on Dog directly. This code prints the following:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/traits-example`
A baby dog is called a Spot

This output isn’t what we wanted. We want to call the baby_name function that is part of the Animal trait that we implemented on Dog so the code prints A baby dog is called a puppy. The technique of specifying the trait name that we used in Listing 19-18 doesn’t help here; if we change main to the code in Listing 19-20, we’ll get a compilation error.

Filename: src/main.rs

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}

Listing 19-20: Attempting to call the baby_name function from the Animal trait, but Rust doesn’t know which implementation to use

Because Animal::baby_name doesn’t have a self parameter, and there could be other types that implement the Animal trait, Rust can’t figure out which implementation of Animal::baby_name we want. We’ll get this compiler error:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0283]: type annotations needed
  --> src/main.rs:20:43
   |
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^ cannot infer type
   |
   = note: cannot satisfy `_: Animal`

For more information about this error, try `rustc --explain E0283`.
error: could not compile `traits-example` due to previous error

To disambiguate and tell Rust that we want to use the implementation of Animal for Dog as opposed to the implementation of Animal for some other type, we need to use fully qualified syntax. Listing 19-21 demonstrates how to use fully qualified syntax.

Filename: src/main.rs

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

Listing 19-21: Using fully qualified syntax to specify that we want to call the baby_name function from the Animal trait as implemented on Dog

We’re providing Rust with a type annotation within the angle brackets, which indicates we want to call the baby_name method from the Animal trait as implemented on Dog by saying that we want to treat the Dog type as an Animal for this function call. This code will now print what we want:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/traits-example`
A baby dog is called a puppy

In general, fully qualified syntax is defined as follows:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

For associated functions that aren’t methods, there would not be a receiver: there would only be the list of other arguments. You could use fully qualified syntax everywhere that you call functions or methods. However, you’re allowed to omit any part of this syntax that Rust can figure out from other information in the program. You only need to use this more verbose syntax in cases where there are multiple implementations that use the same name and Rust needs help to identify which implementation you want to call.

Using Supertraits to Require One Trait’s Functionality Within Another Trait

Sometimes, you might write a trait definition that depends on another trait: for a type to implement the first trait, you want to require that type to also implement the second trait. You would do this so that your trait definition can make use of the associated items of the second trait. The trait your trait definition is relying on is called a supertrait of your trait.

For example, let’s say we want to make an OutlinePrint trait with an outline_print method that will print a given value formatted so that it's framed in asterisks. That is, given a Point struct that implements Display to result in (x, y), when we call outline_print on a Point instance that has 1 for x and 3 for y, it should print the following:

**********
*        *
* (1, 3) *
*        *
**********

In the implementation of the outline_print method, we want to use the Display trait’s functionality. Therefore, we need to specify that the OutlinePrint trait will work only for types that also implement Display and provide the functionality that OutlinePrint needs. We can do that in the trait definition by specifying OutlinePrint: Display. This technique is similar to adding a trait bound to the trait. Listing 19-22 shows an implementation of the OutlinePrint trait.

Filename: src/main.rs

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

fn main() {}

Listing 19-22: Implementing the OutlinePrint trait that requires the functionality from Display

Because we’ve specified that OutlinePrint requires the Display trait, we can use the to_string function that is automatically implemented for any type that implements Display. If we tried to use to_string without adding a colon and specifying the Display trait after the trait name, we’d get an error saying that no method named to_string was found for the type &Self in the current scope.

Let’s see what happens when we try to implement OutlinePrint on a type that doesn’t implement Display, such as the Point struct:

Filename: src/main.rs

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

We get an error saying that Display is required but not implemented:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:20:6
   |
20 | impl OutlinePrint for Point {}
   |      ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Point`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
  --> src/main.rs:3:21
   |
3  | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` due to previous error

To fix this, we implement Display on Point and satisfy the constraint that OutlinePrint requires, like so:

Filename: src/main.rs

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

Then implementing the OutlinePrint trait on Point will compile successfully, and we can call outline_print on a Point instance to display it within an outline of asterisks.

Using the Newtype Pattern to Implement External Traits on External Types

In Chapter 10 in the “Implementing a Trait on a Type” section, we mentioned the orphan rule that states we’re only allowed to implement a trait on a type if either the trait or the type are local to our crate. It’s possible to get around this restriction using the newtype pattern, which involves creating a new type in a tuple struct. (We covered tuple structs in the “Using Tuple Structs without Named Fields to Create Different Types” section of Chapter 5.) The tuple struct will have one field and be a thin wrapper around the type we want to implement a trait for. Then the wrapper type is local to our crate, and we can implement the trait on the wrapper. Newtype is a term that originates from the Haskell programming language. There is no runtime performance penalty for using this pattern, and the wrapper type is elided at compile time.

As an example, let’s say we want to implement Display on Vec<T>, which the orphan rule prevents us from doing directly because the Display trait and the Vec<T> type are defined outside our crate. We can make a Wrapper struct that holds an instance of Vec<T>; then we can implement Display on Wrapper and use the Vec<T> value, as shown in Listing 19-23.

Filename: src/main.rs

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}

Listing 19-23: Creating a Wrapper type around Vec<String> to implement Display

The implementation of Display uses self.0 to access the inner Vec<T>, because Wrapper is a tuple struct and Vec<T> is the item at index 0 in the tuple. Then we can use the functionality of the Display type on Wrapper.

The downside of using this technique is that Wrapper is a new type, so it doesn’t have the methods of the value it’s holding. We would have to implement all the methods of Vec<T> directly on Wrapper such that the methods delegate to self.0, which would allow us to treat Wrapper exactly like a Vec<T>. If we wanted the new type to have every method the inner type has, implementing the Deref trait (discussed in Chapter 15 in the “Treating Smart Pointers Like Regular References with the Deref Trait” section) on the Wrapper to return the inner type would be a solution. If we don’t want the Wrapper type to have all the methods of the inner type—for example, to restrict the Wrapper type’s behavior—we would have to implement just the methods we do want manually.

This newtype pattern is also useful even when traits are not involved. Let’s switch focus and look at some advanced ways to interact with Rust’s type system.

Advanced Types

The Rust type system has some features that we’ve so far mentioned but haven’t yet discussed. We’ll start by discussing newtypes in general as we examine why newtypes are useful as types. Then we’ll move on to type aliases, a feature similar to newtypes but with slightly different semantics. We’ll also discuss the ! type and dynamically sized types.

Using the Newtype Pattern for Type Safety and Abstraction

Note: This section assumes you’ve read the earlier section “Using the Newtype Pattern to Implement External Traits on External Types.”

The newtype pattern is also useful for tasks beyond those we’ve discussed so far, including statically enforcing that values are never confused and indicating the units of a value. You saw an example of using newtypes to indicate units in Listing 19-15: recall that the Millimeters and Meters structs wrapped u32 values in a newtype. If we wrote a function with a parameter of type Millimeters, we couldn’t compile a program that accidentally tried to call that function with a value of type Meters or a plain u32.

We can also use the newtype pattern to abstract away some implementation details of a type: the new type can expose a public API that is different from the API of the private inner type.

Newtypes can also hide internal implementation. For example, we could provide a People type to wrap a HashMap<i32, String> that stores a person’s ID associated with their name. Code using People would only interact with the public API we provide, such as a method to add a name string to the People collection; that code wouldn’t need to know that we assign an i32 ID to names internally. The newtype pattern is a lightweight way to achieve encapsulation to hide implementation details, which we discussed in the “Encapsulation that Hides Implementation Details” section of Chapter 17.

Creating Type Synonyms with Type Aliases

Rust provides the ability to declare a type alias to give an existing type another name. For this we use the type keyword. For example, we can create the alias Kilometers to i32 like so:

fn main() {
    type Kilometers = i32;

    let x: i32 = 5;
    let y: Kilometers = 5;

    println!("x + y = {}", x + y);
}

Now, the alias Kilometers is a synonym for i32; unlike the Millimeters and Meters types we created in Listing 19-15, Kilometers is not a separate, new type. Values that have the type Kilometers will be treated the same as values of type i32:

fn main() {
    type Kilometers = i32;

    let x: i32 = 5;
    let y: Kilometers = 5;

    println!("x + y = {}", x + y);
}

Because Kilometers and i32 are the same type, we can add values of both types and we can pass Kilometers values to functions that take i32 parameters. However, using this method, we don’t get the type checking benefits that we get from the newtype pattern discussed earlier.

The main use case for type synonyms is to reduce repetition. For example, we might have a lengthy type like this:

Box<dyn Fn() + Send + 'static>

Writing this lengthy type in function signatures and as type annotations all over the code can be tiresome and error prone. Imagine having a project full of code like that in Listing 19-24.

fn main() {
    let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

    fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
        // --snip--
    }

    fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
        // --snip--
        Box::new(|| ())
    }
}

Listing 19-24: Using a long type in many places

A type alias makes this code more manageable by reducing the repetition. In Listing 19-25, we’ve introduced an alias named Thunk for the verbose type and can replace all uses of the type with the shorter alias Thunk.

fn main() {
    type Thunk = Box<dyn Fn() + Send + 'static>;

    let f: Thunk = Box::new(|| println!("hi"));

    fn takes_long_type(f: Thunk) {
        // --snip--
    }

    fn returns_long_type() -> Thunk {
        // --snip--
        Box::new(|| ())
    }
}

Listing 19-25: Introducing a type alias Thunk to reduce repetition

This code is much easier to read and write! Choosing a meaningful name for a type alias can help communicate your intent as well (thunk is a word for code to be evaluated at a later time, so it’s an appropriate name for a closure that gets stored).

Type aliases are also commonly used with the Result<T, E> type for reducing repetition. Consider the std::io module in the standard library. I/O operations often return a Result<T, E> to handle situations when operations fail to work. This library has a std::io::Error struct that represents all possible I/O errors. Many of the functions in std::io will be returning Result<T, E> where the E is std::io::Error, such as these functions in the Write trait:

use std::fmt;
use std::io::Error;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
    fn flush(&mut self) -> Result<(), Error>;

    fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}

The Result<..., Error> is repeated a lot. As such, std::io has this type alias declaration:

use std::fmt;

type Result<T> = std::result::Result<T, std::io::Error>;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}

Because this declaration is in the std::io module, we can use the fully qualified alias std::io::Result<T>; that is, a Result<T, E> with the E filled in as std::io::Error. The Write trait function signatures end up looking like this:

use std::fmt;

type Result<T> = std::result::Result<T, std::io::Error>;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}

The type alias helps in two ways: it makes code easier to write and it gives us a consistent interface across all of std::io. Because it’s an alias, it’s just another Result<T, E>, which means we can use any methods that work on Result<T, E> with it, as well as special syntax like the ? operator.

The Never Type that Never Returns

Rust has a special type named ! that’s known in type theory lingo as the empty type because it has no values. We prefer to call it the never type because it stands in the place of the return type when a function will never return. Here is an example:

fn bar() -> ! {
    // --snip--
    panic!();
}

This code is read as “the function bar returns never.” Functions that return never are called diverging functions. We can’t create values of the type ! so bar can never possibly return.

But what use is a type you can never create values for? Recall the code from Listing 2-5, part of the number guessing game; we’ve reproduced a bit of it here in Listing 19-26.

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        // --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Listing 19-26: A match with an arm that ends in continue

At the time, we skipped over some details in this code. In Chapter 6 in “The match Control Flow Operator” section, we discussed that match arms must all return the same type. So, for example, the following code doesn’t work:

fn main() {
    let guess = "3";
    let guess = match guess.trim().parse() {
        Ok(_) => 5,
        Err(_) => "hello",
    };
}

The type of guess in this code would have to be an integer and a string, and Rust requires that guess have only one type. So what does continue return? How were we allowed to return a u32 from one arm and have another arm that ends with continue in Listing 19-26?

As you might have guessed, continue has a ! value. That is, when Rust computes the type of guess, it looks at both match arms, the former with a value of u32 and the latter with a ! value. Because ! can never have a value, Rust decides that the type of guess is u32.

The formal way of describing this behavior is that expressions of type ! can be coerced into any other type. We’re allowed to end this match arm with continue because continue doesn’t return a value; instead, it moves control back to the top of the loop, so in the Err case, we never assign a value to guess.

The never type is useful with the panic! macro as well. Recall the unwrap function that we call on Option<T> values to produce a value or panic with this definition:

enum Option<T> {
    Some(T),
    None,
}

use crate::Option::*;

impl<T> Option<T> {
    pub fn unwrap(self) -> T {
        match self {
            Some(val) => val,
            None => panic!("called `Option::unwrap()` on a `None` value"),
        }
    }
}

In this code, the same thing happens as in the match in Listing 19-26: Rust sees that val has the type T and panic! has the type !, so the result of the overall match expression is T. This code works because panic! doesn’t produce a value; it ends the program. In the None case, we won’t be returning a value from unwrap, so this code is valid.

One final expression that has the type ! is a loop:

fn main() {
    print!("forever ");

    loop {
        print!("and ever ");
    }
}

Here, the loop never ends, so ! is the value of the expression. However, this wouldn’t be true if we included a break, because the loop would terminate when it got to the break.

Dynamically Sized Types and the Sized Trait

Rust needs to know certain details about its types, such as how much space to allocate for a value of a particular type. This leaves one corner of its type system a little confusing at first: the concept of dynamically sized types. Sometimes referred to as DSTs or unsized types, these types let us write code using values whose size we can know only at runtime.

Let’s dig into the details of a dynamically sized type called str, which we’ve been using throughout the book. That’s right, not &str, but str on its own, is a DST. We can’t know how long the string is until runtime, meaning we can’t create a variable of type str, nor can we take an argument of type str. Consider the following code, which does not work:

fn main() {
    let s1: str = "Hello there!";
    let s2: str = "How's it going?";
}

Rust needs to know how much memory to allocate for any value of a particular type, and all values of a type must use the same amount of memory. If Rust allowed us to write this code, these two str values would need to take up the same amount of space. But they have different lengths: s1 needs 12 bytes of storage and s2 needs 15. This is why it’s not possible to create a variable holding a dynamically sized type.

So what do we do? In this case, you already know the answer: we make the types of s1 and s2 a &str rather than a str. Recall from the “String Slices” section of Chapter 4 that the slice data structure just stores the starting position and the length of the slice. So although a &T is a single value that stores the memory address of where the T is located, a &str is two values: the address of the str and its length. As such, we can know the size of a &str value at compile time: it’s twice the length of a usize. That is, we always know the size of a &str, no matter how long the string it refers to is. In general, this is the way in which dynamically sized types are used in Rust: they have an extra bit of metadata that stores the size of the dynamic information. The golden rule of dynamically sized types is that we must always put values of dynamically sized types behind a pointer of some kind.

We can combine str with all kinds of pointers: for example, Box<str> or Rc<str>. In fact, you’ve seen this before but with a different dynamically sized type: traits. Every trait is a dynamically sized type we can refer to by using the name of the trait. In Chapter 17 in the “Using Trait Objects That Allow for Values of Different Types” section, we mentioned that to use traits as trait objects, we must put them behind a pointer, such as &dyn Trait or Box<dyn Trait> (Rc<dyn Trait> would work too).

To work with DSTs, Rust provides the Sized trait to determine whether or not a type’s size is known at compile time. This trait is automatically implemented for everything whose size is known at compile time. In addition, Rust implicitly adds a bound on Sized to every generic function. That is, a generic function definition like this:

fn generic<T>(t: T) {
    // --snip--
}

is actually treated as though we had written this:

fn generic<T: Sized>(t: T) {
    // --snip--
}

By default, generic functions will work only on types that have a known size at compile time. However, you can use the following special syntax to relax this restriction:

fn generic<T: ?Sized>(t: &T) {
    // --snip--
}

A trait bound on ?Sized means “T may or may not be Sized” and this notation overrides the default that generic types must have a known size at compile time. The ?Trait syntax with this meaning is only available for Sized, not any other traits.

Also note that we switched the type of the t parameter from T to &T. Because the type might not be Sized, we need to use it behind some kind of pointer. In this case, we’ve chosen a reference.

Next, we’ll talk about functions and closures!

Gelişmiş Fonksiyonlar ve Kapanış İfadeleri

Bu bölümde, fonksiyon işaretçileri ve dönen kapanışlar da dahil olmak üzere fonksiyonlar ve kapanışlarla ilgili bazı gelişmiş özellikler incelenmektedir.

Fonksiyon İşaretçileri

Fonksiyonlara kapanışların nasıl aktarılacağından bahsetmiştik; normal fonksiyonları da fonksiyonlara aktarabilirsiniz! Bu teknik, yeni bir kapanış tanımlamak yerine daha önce tanımladığınız bir fonksiyonu geçirmek istediğinizde kullanışlıdır. Fonksiyonlar, Fn kapanış tanımı ile karıştırılmaması gereken fn (küçük f harfi ile) türüne zorlanır. fn türüne fonksiyon işaretçisi denir. Fonksiyonları fonksiyon işaretçileriyle geçirmek, fonksiyonları diğer fonksiyonlara argüman olarak kullanmanızı sağlar.

Bir parametrenin bir fonksiyon işaretçisi olduğunu belirtmek için kullanılan söz dizimi, parametresine bir ekleyen add_one fonksiyonunu tanımladığımız Liste 19-27'de gösterildiği gibi, kapanışlarınkine benzer. do_twice fonksiyonu iki parametre alır: bir i32 parametresi alan ve bir i32 döndüren herhangi bir fonksiyonun fonksiyon işaretçisi ve bir i32 değeri. do_twice fonksiyonu f işlevini iki kez çağırır, arg değerini geçirir ve ardından iki fonksiyon çağrısı sonucunu birbirine ekler. main fonksiyonu do_twice fonksiyonunu add_one ve 5 argümanlarıyla çağırır.

Dosya adı: src/main.rs

fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {}", answer);
}

Liste 19-27: Bir fonksiyon işaretçisini argüman olarak kabul etmek için fn türünü kullanma

Bu kod çıktısı The answer is: 12 olur. do_twice içindeki f parametresinin, i32 türünde bir parametre alan ve bir i32 döndüren bir fn olduğunu belirtiriz. Daha sonra f'i do_twice'ın gövdesinden çağırabiliriz. main içinde, add_one fonksiyon adını do_twice'a ilk argüman olarak aktarabiliriz.

Kapanışların aksine, fn bir özellikten ziyade bir türdür, bu nedenle Fn tanımlarından birine sahip yaygın tür parametresini bir tanım bağı olarak bildirmek yerine fn'yi doğrudan parametre türü olarak belirtiriz.

Fonksiyon işaretçileri, kapanış tanımlarının (Fn, FnMut ve FnOnce) üçünü de uygular, yani bir kapanış bekleyen bir fonksiyona argüman olarak her zaman bir fonksiyon işaretçisi aktarabilirsiniz. Fonksiyonlarınızı yaygın tür ve kapanış tanımlarından birini kullanarak yazmak en iyisidir, böylece fonksiyonlarınız hem fonksiyonları hem de kapanışları kabul edebilir.

Bununla birlikte, kapanışları değil de yalnızca fn'leri kabul etmek isteyeceğiniz bir örnek, kapanışları olmayan harici kodlarla arayüz oluştururken ortaya çıkar: C fonksiyonları argüman olarak fonksiyon kabul edebilir, ancak C'de kapanış yoktur.

Satır içi tanımlanmış bir kapanış ya da adlandırılmış bir fonksiyon kullanabileceğiniz bir örnek olarak, standart kütüphanedeki Iterator tanımı tarafından sağlanan map metodunun kullanımına bakalım. Sayılardan oluşan bir vektörü dizelerden oluşan bir vektöre dönüştürmek üzere map fonksiyonunu kullanmak için aşağıdaki gibi bir kapanış kullanabiliriz:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(|i| i.to_string()).collect();
}

Veya kapanış yerine map argümanı olarak bir fonksiyonu şöyle adlandırabiliriz:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(ToString::to_string).collect();
}

Daha önce “Gelişmiş Özellikler” bölümünde bahsettiğimiz tam nitelikli söz dizimini kullanmamız gerektiğini unutmayın çünkü to_string adında birden fazla fonksiyon mevcuttur. Burada, standart kütüphanenin Display'i sürekleyen tüm türler için süreklediği ToString tanımında tanımlanan to_string fonksiyonunu kullanıyoruz.

Bölüm 6'daki enum değerleri” bölümünde tanımladığımız her enum varyantının adının aynı zamanda bir başlatıcı fonksiyon olduğunu hatırlayın. Bu başlatıcı fonksiyonları, kapanış tanımlarını sürekleyen fonksiyon işaretçileri olarak kullanabiliriz; yani başlatıcı fonksiyonları, kapanışları alan metodlar için argüman olarak belirtebiliriz:

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}

Burada, Status::Value'nun başlatıcı fonksiyonunu kullanarak map'in çağrıldığı aralıktaki her u32 değerini kullanarak Status::Value örnekleri oluşturuyoruz. Bazı insanlar bu stili tercih ederken bazıları da kapanışları kullanmayı tercih eder. Her ikisi de aynı koda derlenir, bu nedenle hangi stil sizin için daha iyiyse onu kullanın.

Dönen Kapanışlar

Kapanışlar tanımlar tarafından temsil edilir, bu da kapanışları doğrudan iade edemeyeceğiniz anlamına gelir. Bir tanım döndürmek isteyebileceğiniz çoğu durumda, bunun yerine fonksiyonun dönüş değeri olarak trait'i sürekleyen somut tipi kullanabilirsiniz. Ancak, kapanışlarda bunu yapamazsınız çünkü döndürülebilir somut bir tipleri yoktur; örneğin, fn fonksiyon işaretçisini bir dönüş tipi olarak kullanmanıza izin verilmez.

Aşağıdaki kod doğrudan bir kapanış döndürmeye çalışır, ancak derlenmez:

Here we create Status::Value instances using each u32 value in the range that map is called on by using the initializer function of Status::Value. Some people prefer this style, and some people prefer to use closures. They compile to the same code, so use whichever style is clearer to you.

fn returns_closure() -> dyn Fn(i32) -> i32 {
    |x| x + 1
}

Derleyici hatası aşağıdaki gibidir:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
 --> src/lib.rs:1:25
  |
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
  |                         ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
  = note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
  |
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
  |                         ~~~~~~~~~~~~~~~~~~~

For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` due to previous error

Hata yine Sized tanımına atıfta bulunuyor! Rust, kapanışı depolamak için ne kadar alana ihtiyaç duyacağını bilmiyor. Bu sorunun çözümünü daha önce görmüştük. Bir trait nesnesi kullanabiliriz:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

Bu kod sorunsuz derlenecektir. trait nesneleri hakkında daha fazla bilgi edinmek için Bölüm 17'deki “Farklı Türlerdeki Değerlere İzin Veren trait Nesnelerini Kullanma” başlığına bakabilirsiniz.

Şimdi devam edelim ve makrolara bakalım!

Macros

We’ve used macros like println! throughout this book, but we haven’t fully explored what a macro is and how it works. The term macro refers to a family of features in Rust: declarative macros with macro_rules! and three kinds of procedural macros:

  • Custom #[derive] macros that specify code added with the derive attribute used on structs and enums
  • Attribute-like macros that define custom attributes usable on any item
  • Function-like macros that look like function calls but operate on the tokens specified as their argument

We’ll talk about each of these in turn, but first, let’s look at why we even need macros when we already have functions.

The Difference Between Macros and Functions

Fundamentally, macros are a way of writing code that writes other code, which is known as metaprogramming. In Appendix C, we discuss the derive attribute, which generates an implementation of various traits for you. We’ve also used the println! and vec! macros throughout the book. All of these macros expand to produce more code than the code you’ve written manually.

Metaprogramming is useful for reducing the amount of code you have to write and maintain, which is also one of the roles of functions. However, macros have some additional powers that functions don’t.

A function signature must declare the number and type of parameters the function has. Macros, on the other hand, can take a variable number of parameters: we can call println!("hello") with one argument or println!("hello {}", name) with two arguments. Also, macros are expanded before the compiler interprets the meaning of the code, so a macro can, for example, implement a trait on a given type. A function can’t, because it gets called at runtime and a trait needs to be implemented at compile time.

The downside to implementing a macro instead of a function is that macro definitions are more complex than function definitions because you’re writing Rust code that writes Rust code. Due to this indirection, macro definitions are generally more difficult to read, understand, and maintain than function definitions.

Another important difference between macros and functions is that you must define macros or bring them into scope before you call them in a file, as opposed to functions you can define anywhere and call anywhere.

Declarative Macros with macro_rules! for General Metaprogramming

The most widely used form of macros in Rust is the declarative macro. These are also sometimes referred to as “macros by example,” “macro_rules! macros,” or just plain “macros.” At their core, declarative macros allow you to write something similar to a Rust match expression. As discussed in Chapter 6, match expressions are control structures that take an expression, compare the resulting value of the expression to patterns, and then run the code associated with the matching pattern. Macros also compare a value to patterns that are associated with particular code: in this situation, the value is the literal Rust source code passed to the macro; the patterns are compared with the structure of that source code; and the code associated with each pattern, when matched, replaces the code passed to the macro. This all happens during compilation.

To define a macro, you use the macro_rules! construct. Let’s explore how to use macro_rules! by looking at how the vec! macro is defined. Chapter 8 covered how we can use the vec! macro to create a new vector with particular values. For example, the following macro creates a new vector containing three integers:


#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}

We could also use the vec! macro to make a vector of two integers or a vector of five string slices. We wouldn’t be able to use a function to do the same because we wouldn’t know the number or type of values up front.

Listing 19-28 shows a slightly simplified definition of the vec! macro.

Filename: src/lib.rs

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

Listing 19-28: A simplified version of the vec! macro definition

Note: The actual definition of the vec! macro in the standard library includes code to preallocate the correct amount of memory up front. That code is an optimization that we don’t include here to make the example simpler.

The #[macro_export] annotation indicates that this macro should be made available whenever the crate in which the macro is defined is brought into scope. Without this annotation, the macro can’t be brought into scope.

We then start the macro definition with macro_rules! and the name of the macro we’re defining without the exclamation mark. The name, in this case vec, is followed by curly brackets denoting the body of the macro definition.

The structure in the vec! body is similar to the structure of a match expression. Here we have one arm with the pattern ( $( $x:expr ),* ), followed by => and the block of code associated with this pattern. If the pattern matches, the associated block of code will be emitted. Given that this is the only pattern in this macro, there is only one valid way to match; any other pattern will result in an error. More complex macros will have more than one arm.

Valid pattern syntax in macro definitions is different than the pattern syntax covered in Chapter 18 because macro patterns are matched against Rust code structure rather than values. Let’s walk through what the pattern pieces in Listing 19-28 mean; for the full macro pattern syntax, see the Rust Reference.

First, we use a set of parentheses to encompass the whole pattern. We use a dollar sign ($) to declare a variable in the macro system that will contain the Rust code matching the pattern. The dollar sign makes it clear this is a macro variable as opposed to a regular Rust variable. Next comes a set of parentheses that captures values that match the pattern within the parentheses for use in the replacement code. Within $() is $x:expr, which matches any Rust expression and gives the expression the name $x.

The comma following $() indicates that a literal comma separator character could optionally appear after the code that matches the code in $(). The * specifies that the pattern matches zero or more of whatever precedes the *.

When we call this macro with vec![1, 2, 3];, the $x pattern matches three times with the three expressions 1, 2, and 3.

Now let’s look at the pattern in the body of the code associated with this arm: temp_vec.push() within $()* is generated for each part that matches $() in the pattern zero or more times depending on how many times the pattern matches. The $x is replaced with each expression matched. When we call this macro with vec![1, 2, 3];, the code generated that replaces this macro call will be the following:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

We’ve defined a macro that can take any number of arguments of any type and can generate code to create a vector containing the specified elements.

There are some strange edge cases with macro_rules!. In the future, Rust will have a second kind of declarative macro that will work in a similar fashion but fix some of these edge cases. After that update, macro_rules! will be effectively deprecated. With this in mind, as well as the fact that most Rust programmers will use macros more than write macros, we won’t discuss macro_rules! any further. To learn more about how to write macros, consult the online documentation or other resources, such as “The Little Book of Rust Macros” started by Daniel Keep and continued by Lukas Wirth.

Procedural Macros for Generating Code from Attributes

The second form of macros is the procedural macro, which acts more like a function (and is a type of procedure). Procedural macros accept some code as an input, operate on that code, and produce some code as an output rather than matching against patterns and replacing the code with other code as declarative macros do. The three kinds of procedural macros are custom derive, attribute-like, and function-like, and all work in a similar fashion.

When creating procedural macros, the definitions must reside in their own crate with a special crate type. This is for complex technical reasons that we hope to eliminate in the future. In Listing 19-29, we show how to define a procedural macro, where some_attribute is a placeholder for using a specific macro variety.

Filename: src/lib.rs

use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}

Listing 19-29: An example of defining a procedural macro

The function that defines a procedural macro takes a TokenStream as an input and produces a TokenStream as an output. The TokenStream type is defined by the proc_macro crate that is included with Rust and represents a sequence of tokens. This is the core of the macro: the source code that the macro is operating on makes up the input TokenStream, and the code the macro produces is the output TokenStream. The function also has an attribute attached to it that specifies which kind of procedural macro we’re creating. We can have multiple kinds of procedural macros in the same crate.

Let’s look at the different kinds of procedural macros. We’ll start with a custom derive macro and then explain the small dissimilarities that make the other forms different.

How to Write a Custom derive Macro

Let’s create a crate named hello_macro that defines a trait named HelloMacro with one associated function named hello_macro. Rather than making our users implement the HelloMacro trait for each of their types, we’ll provide a procedural macro so users can annotate their type with #[derive(HelloMacro)] to get a default implementation of the hello_macro function. The default implementation will print Hello, Macro! My name is TypeName! where TypeName is the name of the type on which this trait has been defined. In other words, we’ll write a crate that enables another programmer to write code like Listing 19-30 using our crate.

Filename: src/main.rs

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}

Listing 19-30: The code a user of our crate will be able to write when using our procedural macro

This code will print Hello, Macro! My name is Pancakes! when we’re done. The first step is to make a new library crate, like this:

$ cargo new hello_macro --lib

Next, we’ll define the HelloMacro trait and its associated function:

Filename: src/lib.rs

pub trait HelloMacro {
    fn hello_macro();
}

We have a trait and its function. At this point, our crate user could implement the trait to achieve the desired functionality, like so:

use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro! My name is Pancakes!");
    }
}

fn main() {
    Pancakes::hello_macro();
}

However, they would need to write the implementation block for each type they wanted to use with hello_macro; we want to spare them from having to do this work.

Additionally, we can’t yet provide the hello_macro function with default implementation that will print the name of the type the trait is implemented on: Rust doesn’t have reflection capabilities, so it can’t look up the type’s name at runtime. We need a macro to generate code at compile time.

The next step is to define the procedural macro. At the time of this writing, procedural macros need to be in their own crate. Eventually, this restriction might be lifted. The convention for structuring crates and macro crates is as follows: for a crate named foo, a custom derive procedural macro crate is called foo_derive. Let’s start a new crate called hello_macro_derive inside our hello_macro project:

$ cargo new hello_macro_derive --lib

Our two crates are tightly related, so we create the procedural macro crate within the directory of our hello_macro crate. If we change the trait definition in hello_macro, we’ll have to change the implementation of the procedural macro in hello_macro_derive as well. The two crates will need to be published separately, and programmers using these crates will need to add both as dependencies and bring them both into scope. We could instead have the hello_macro crate use hello_macro_derive as a dependency and re-export the procedural macro code. However, the way we’ve structured the project makes it possible for programmers to use hello_macro even if they don’t want the derive functionality.

We need to declare the hello_macro_derive crate as a procedural macro crate. We’ll also need functionality from the syn and quote crates, as you’ll see in a moment, so we need to add them as dependencies. Add the following to the Cargo.toml file for hello_macro_derive:

Filename: hello_macro_derive/Cargo.toml

[lib]
proc-macro = true

[dependencies]
syn = "1.0"
quote = "1.0"

To start defining the procedural macro, place the code in Listing 19-31 into your src/lib.rs file for the hello_macro_derive crate. Note that this code won’t compile until we add a definition for the impl_hello_macro function.

Filename: hello_macro_derive/src/lib.rs

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_hello_macro(&ast)
}

Listing 19-31: Code that most procedural macro crates will require in order to process Rust code

Notice that we’ve split the code into the hello_macro_derive function, which is responsible for parsing the TokenStream, and the impl_hello_macro function, which is responsible for transforming the syntax tree: this makes writing a procedural macro more convenient. The code in the outer function (hello_macro_derive in this case) will be the same for almost every procedural macro crate you see or create. The code you specify in the body of the inner function (impl_hello_macro in this case) will be different depending on your procedural macro’s purpose.

We’ve introduced three new crates: proc_macro, syn, and quote. The proc_macro crate comes with Rust, so we didn’t need to add that to the dependencies in Cargo.toml. The proc_macro crate is the compiler’s API that allows us to read and manipulate Rust code from our code.

The syn crate parses Rust code from a string into a data structure that we can perform operations on. The quote crate turns syn data structures back into Rust code. These crates make it much simpler to parse any sort of Rust code we might want to handle: writing a full parser for Rust code is no simple task.

The hello_macro_derive function will be called when a user of our library specifies #[derive(HelloMacro)] on a type. This is possible because we’ve annotated the hello_macro_derive function here with proc_macro_derive and specified the name HelloMacro, which matches our trait name; this is the convention most procedural macros follow.

The hello_macro_derive function first converts the input from a TokenStream to a data structure that we can then interpret and perform operations on. This is where syn comes into play. The parse function in syn takes a TokenStream and returns a DeriveInput struct representing the parsed Rust code. Listing 19-32 shows the relevant parts of the DeriveInput struct we get from parsing the struct Pancakes; string:

DeriveInput {
    // --snip--

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}

Listing 19-32: The DeriveInput instance we get when parsing the code that has the macro’s attribute in Listing 19-30

The fields of this struct show that the Rust code we’ve parsed is a unit struct with the ident (identifier, meaning the name) of Pancakes. There are more fields on this struct for describing all sorts of Rust code; check the syn documentation for DeriveInput for more information.

Soon we’ll define the impl_hello_macro function, which is where we’ll build the new Rust code we want to include. But before we do, note that the output for our derive macro is also a TokenStream. The returned TokenStream is added to the code that our crate users write, so when they compile their crate, they’ll get the extra functionality that we provide in the modified TokenStream.

You might have noticed that we’re calling unwrap to cause the hello_macro_derive function to panic if the call to the syn::parse function fails here. It’s necessary for our procedural macro to panic on errors because proc_macro_derive functions must return TokenStream rather than Result to conform to the procedural macro API. We’ve simplified this example by using unwrap; in production code, you should provide more specific error messages about what went wrong by using panic! or expect.

Now that we have the code to turn the annotated Rust code from a TokenStream into a DeriveInput instance, let’s generate the code that implements the HelloMacro trait on the annotated type, as shown in Listing 19-33.

Filename: hello_macro_derive/src/lib.rs

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    gen.into()
}

Listing 19-33: Implementing the HelloMacro trait using the parsed Rust code

We get an Ident struct instance containing the name (identifier) of the annotated type using ast.ident. The struct in Listing 19-32 shows that when we run the impl_hello_macro function on the code in Listing 19-30, the ident we get will have the ident field with a value of "Pancakes". Thus, the name variable in Listing 19-33 will contain an Ident struct instance that, when printed, will be the string "Pancakes", the name of the struct in Listing 19-30.

The quote! macro lets us define the Rust code that we want to return. The compiler expects something different to the direct result of the quote! macro’s execution, so we need to convert it to a TokenStream. We do this by calling the into method, which consumes this intermediate representation and returns a value of the required TokenStream type.

The quote! macro also provides some very cool templating mechanics: we can enter #name, and quote! will replace it with the value in the variable name. You can even do some repetition similar to the way regular macros work. Check out the quote crate’s docs for a thorough introduction.

We want our procedural macro to generate an implementation of our HelloMacro trait for the type the user annotated, which we can get by using #name. The trait implementation has the one function hello_macro, whose body contains the functionality we want to provide: printing Hello, Macro! My name is and then the name of the annotated type.

The stringify! macro used here is built into Rust. It takes a Rust expression, such as 1 + 2, and at compile time turns the expression into a string literal, such as "1 + 2". This is different than format! or println!, macros which evaluate the expression and then turn the result into a String. There is a possibility that the #name input might be an expression to print literally, so we use stringify!. Using stringify! also saves an allocation by converting #name to a string literal at compile time.

At this point, cargo build should complete successfully in both hello_macro and hello_macro_derive. Let’s hook up these crates to the code in Listing 19-30 to see the procedural macro in action! Create a new binary project in your projects directory using cargo new pancakes. We need to add hello_macro and hello_macro_derive as dependencies in the pancakes crate’s Cargo.toml. If you’re publishing your versions of hello_macro and hello_macro_derive to crates.io, they would be regular dependencies; if not, you can specify them as path dependencies as follows:

hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

Put the code in Listing 19-30 into src/main.rs, and run cargo run: it should print Hello, Macro! My name is Pancakes! The implementation of the HelloMacro trait from the procedural macro was included without the pancakes crate needing to implement it; the #[derive(HelloMacro)] added the trait implementation.

Next, let’s explore how the other kinds of procedural macros differ from custom derive macros.

Attribute-like macros

Attribute-like macros are similar to custom derive macros, but instead of generating code for the derive attribute, they allow you to create new attributes. They’re also more flexible: derive only works for structs and enums; attributes can be applied to other items as well, such as functions. Here’s an example of using an attribute-like macro: say you have an attribute named route that annotates functions when using a web application framework:

#[route(GET, "/")]
fn index() {

This #[route] attribute would be defined by the framework as a procedural macro. The signature of the macro definition function would look like this:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

Here, we have two parameters of type TokenStream. The first is for the contents of the attribute: the GET, "/" part. The second is the body of the item the attribute is attached to: in this case, fn index() {} and the rest of the function’s body.

Other than that, attribute-like macros work the same way as custom derive macros: you create a crate with the proc-macro crate type and implement a function that generates the code you want!

Function-like macros

Function-like macros define macros that look like function calls. Similarly to macro_rules! macros, they’re more flexible than functions; for example, they can take an unknown number of arguments. However, macro_rules! macros can be defined only using the match-like syntax we discussed in the section “Declarative Macros with macro_rules! for General Metaprogramming” earlier. Function-like macros take a TokenStream parameter and their definition manipulates that TokenStream using Rust code as the other two types of procedural macros do. An example of a function-like macro is an sql! macro that might be called like so:

let sql = sql!(SELECT * FROM posts WHERE id=1);

This macro would parse the SQL statement inside it and check that it’s syntactically correct, which is much more complex processing than a macro_rules! macro can do. The sql! macro would be defined like this:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

This definition is similar to the custom derive macro’s signature: we receive the tokens that are inside the parentheses and return the code we wanted to generate.

Summary

Whew! Now you have some Rust features in your toolbox that you likely won’t use often, but you’ll know they’re available in very particular circumstances. We’ve introduced several complex topics so that when you encounter them in error message suggestions or in other peoples’ code, you’ll be able to recognize these concepts and syntax. Use this chapter as a reference to guide you to solutions.

Next, we’ll put everything we’ve discussed throughout the book into practice and do one more project!

Bitirme Projesi: Çok İş Parçacıklı Bir Web Sunucusu Oluşturma

Uzun bir yolculuk oldu ama kitabın sonuna geldik. Bu bölümde, son bölümlerde ele aldığımız bazı kavramları göstermek ve daha önceki bazı bölümleri özetlemek için birlikte bir proje daha oluşturacağız.

Bitirme projemiz için, bir web tarayıcısına “hello” yazan ve Şekil 20-1'dekine benzeyen bir web sunucusu yapacağız.

rust'tan bir merhaba

Şekil 20-1: Nihai bitirme projemiz

İşte web sunucusunu oluşturma planı:

  1. TCP ve HTTP hakkında biraz bilgi edinmek.
  2. Bir soketteki TCP bağlantılarını dinlemek.
  3. Az sayıdaki HTTP isteklerini ayrıştırmak.
  4. Uygun bir HTTP yanıtı oluşturmak.
  5. Bir iş parçacığı havuzuyla sunucunun verimini iyileştirmek.

Ancak başlamadan önce bir ayrıntıdan bahsetmeliyiz: Kullanacağımız yöntem Rust ile bir web sunucusu oluşturmanın en iyi yolu olmayacak.

crates.io'da, oluşturacağımızdan daha eksiksiz web sunucusu ve iş parçacığı havuzu süreklemelerini sağlayan üretime hazır bir dizi kasa mevcuttur.

Ancak bu bölümdeki amacımız, kolay yolu seçmek değil, öğrenmenize yardımcı olmaktır. Rust bir sistem programlama dili olduğu için, çalışmak istediğimiz soyutlama seviyesini seçebilir ve diğer dillerde mümkün olan veya pratik olandan daha düşük bir seviyeye gidebiliriz. Gelecekte kullanabileceğiniz kasaların arkasındaki genel fikirleri ve teknikleri öğrenebilmeniz için temel HTTP sunucusunu ve iş parçacığı havuzunu kendimiz yazacağız.

Building a Single-Threaded Web Server

We’ll start by getting a single-threaded web server working. Before we begin, let’s look at a quick overview of the protocols involved in building web servers. The details of these protocols are beyond the scope of this book, but a brief overview will give you the information you need.

The two main protocols involved in web servers are the Hypertext Transfer Protocol (HTTP) and the Transmission Control Protocol (TCP). Both protocols are request-response protocols, meaning a client initiates requests and a server listens to the requests and provides a response to the client. The contents of those requests and responses are defined by the protocols.

TCP is the lower-level protocol that describes the details of how information gets from one server to another but doesn’t specify what that information is. HTTP builds on top of TCP by defining the contents of the requests and responses. It’s technically possible to use HTTP with other protocols, but in the vast majority of cases, HTTP sends its data over TCP. We’ll work with the raw bytes of TCP and HTTP requests and responses.

Listening to the TCP Connection

Our web server needs to listen to a TCP connection, so that’s the first part we’ll work on. The standard library offers a std::net module that lets us do this. Let’s make a new project in the usual fashion:

$ cargo new hello
     Created binary (application) `hello` project
$ cd hello

Now enter the code in Listing 20-1 in src/main.rs to start. This code will listen at the address 127.0.0.1:7878 for incoming TCP streams. When it gets an incoming stream, it will print Connection established!.

Filename: src/main.rs

use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        println!("Connection established!");
    }
}

Listing 20-1: Listening for incoming streams and printing a message when we receive a stream

Using TcpListener, we can listen for TCP connections at the address 127.0.0.1:7878. In the address, the section before the colon is an IP address representing your computer (this is the same on every computer and doesn’t represent the authors’ computer specifically), and 7878 is the port. We’ve chosen this port for two reasons: HTTP isn’t normally accepted on this port, and 7878 is rust typed on a telephone.

The bind function in this scenario works like the new function in that it will return a new TcpListener instance. The reason the function is called bind is that in networking, connecting to a port to listen to is known as “binding to a port.”

The bind function returns a Result<T, E>, which indicates that binding might fail. For example, connecting to port 80 requires administrator privileges (nonadministrators can listen only on ports higher than 1023), so if we tried to connect to port 80 without being an administrator, binding wouldn’t work. As another example, binding wouldn’t work if we ran two instances of our program and so had two programs listening to the same port. Because we’re writing a basic server just for learning purposes, we won’t worry about handling these kinds of errors; instead, we use unwrap to stop the program if errors happen.

The incoming method on TcpListener returns an iterator that gives us a sequence of streams (more specifically, streams of type TcpStream). A single stream represents an open connection between the client and the server. A connection is the name for the full request and response process in which a client connects to the server, the server generates a response, and the server closes the connection. As such, we will read from the TcpStream to see what the client sent and then write our response to the stream to send data back to the client. Overall, this for loop will process each connection in turn and produce a series of streams for us to handle.

For now, our handling of the stream consists of calling unwrap to terminate our program if the stream has any errors; if there aren’t any errors, the program prints a message. We’ll add more functionality for the success case in the next listing. The reason we might receive errors from the incoming method when a client connects to the server is that we’re not actually iterating over connections. Instead, we’re iterating over connection attempts. The connection might not be successful for a number of reasons, many of them operating system specific. For example, many operating systems have a limit to the number of simultaneous open connections they can support; new connection attempts beyond that number will produce an error until some of the open connections are closed.

Let’s try running this code! Invoke cargo run in the terminal and then load 127.0.0.1:7878 in a web browser. The browser should show an error message like “Connection reset,” because the server isn’t currently sending back any data. But when you look at your terminal, you should see several messages that were printed when the browser connected to the server!

     Running `target/debug/hello`
Connection established!
Connection established!
Connection established!

Sometimes, you’ll see multiple messages printed for one browser request; the reason might be that the browser is making a request for the page as well as a request for other resources, like the favicon.ico icon that appears in the browser tab.

It could also be that the browser is trying to connect to the server multiple times because the server isn’t responding with any data. When stream goes out of scope and is dropped at the end of the loop, the connection is closed as part of the drop implementation. Browsers sometimes deal with closed connections by retrying, because the problem might be temporary. The important factor is that we’ve successfully gotten a handle to a TCP connection!

Remember to stop the program by pressing ctrl-c when you’re done running a particular version of the code. Then restart the program by invoking the cargo run command after you’ve made each set of code changes to make sure you’re running the newest code.

Reading the Request

Let’s implement the functionality to read the request from the browser! To separate the concerns of first getting a connection and then taking some action with the connection, we’ll start a new function for processing connections. In this new handle_connection function, we’ll read data from the TCP stream and print it so we can see the data being sent from the browser. Change the code to look like Listing 20-2.

Filename: src/main.rs

use std::{
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    println!("Request: {:#?}", http_request);
}

Listing 20-2: Reading from the TcpStream and printing the data

We bring std::io::prelude and std::io::BufReader into scope to get access to traits and types that let us read from and write to the stream. In the for loop in the main function, instead of printing a message that says we made a connection, we now call the new handle_connection function and pass the stream to it.

In the handle_connection function, we create a new BufReader instance that wraps a mutable reference to the stream. BufReader adds buffering by managing calls to the std::io::Read trait methods for us.

We create a variable named http_request to collect the lines of the request the browser sends to our server. We indicate that we want to collect these lines in a vector by adding the Vec<_> type annotation.

BufReader implements the std::io::BufRead trait, which provides the lines method. The lines method returns an iterator of Result<String, std::io::Error> by splitting the stream of data whenever it sees a newline byte. To get each String, we map and unwrap each Result. The Result might be an error if the data isn’t valid UTF-8 or if there was a problem reading from the stream. Again, a production program should handle these errors more gracefully, but we’re choosing to stop the program in the error case for simplicity.

The browser signals the end of an HTTP request by sending two newline characters in a row, so to get one request from the stream, we take lines while they’re not the empty string. Once we’ve collected the lines into the vector, we’re printing them out using pretty debug formatting so we can take a look at the instructions the web browser is sending to our server.

Let’s try this code! Start the program and make a request in a web browser again. Note that we’ll still get an error page in the browser, but our program’s output in the terminal will now look similar to this:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/hello`
Request: [
    "GET / HTTP/1.1",
    "Host: 127.0.0.1:7878",
    "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
    "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
    "Accept-Language: en-US,en;q=0.5",
    "Accept-Encoding: gzip, deflate, br",
    "DNT: 1",
    "Connection: keep-alive",
    "Upgrade-Insecure-Requests: 1",
    "Sec-Fetch-Dest: document",
    "Sec-Fetch-Mode: navigate",
    "Sec-Fetch-Site: none",
    "Sec-Fetch-User: ?1",
    "Cache-Control: max-age=0",
]

Depending on your browser, you might get slightly different output. Now that we’re printing the request data, we can see why we get multiple connections from one browser request by looking at the path after GET in the first line of the request. If the repeated connections are all requesting /, we know the browser is trying to fetch / repeatedly because it’s not getting a response from our program.

Let’s break down this request data to understand what the browser is asking of our program.

A Closer Look at an HTTP Request

HTTP is a text-based protocol, and a request takes this format:

Method Request-URI HTTP-Version CRLF
headers CRLF
message-body

The first line is the request line that holds information about what the client is requesting. The first part of the request line indicates the method being used, such as GET or POST, which describes how the client is making this request. Our client used a GET request.

The next part of the request line is /, which indicates the Uniform Resource Identifier (URI) the client is requesting: a URI is almost, but not quite, the same as a Uniform Resource Locator (URL). The difference between URIs and URLs isn’t important for our purposes in this chapter, but the HTTP spec uses the term URI, so we can just mentally substitute URL for URI here.

The last part is the HTTP version the client uses, and then the request line ends in a CRLF sequence. (CRLF stands for carriage return and line feed, which are terms from the typewriter days!) The CRLF sequence can also be written as \r\n, where \r is a carriage return and \n is a line feed. The CRLF sequence separates the request line from the rest of the request data. Note that when the CRLF is printed, we see a new line start rather than \r\n.

Looking at the request line data we received from running our program so far, we see that GET is the method, / is the request URI, and HTTP/1.1 is the version.

After the request line, the remaining lines starting from Host: onward are headers. GET requests have no body.

Try making a request from a different browser or asking for a different address, such as 127.0.0.1:7878/test, to see how the request data changes.

Now that we know what the browser is asking for, let’s send back some data!

Writing a Response

We’re going to implement sending data in response to a client request. Responses have the following format:

HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body

The first line is a status line that contains the HTTP version used in the response, a numeric status code that summarizes the result of the request, and a reason phrase that provides a text description of the status code. After the CRLF sequence are any headers, another CRLF sequence, and the body of the response.

Here is an example response that uses HTTP version 1.1, has a status code of 200, an OK reason phrase, no headers, and no body:

HTTP/1.1 200 OK\r\n\r\n

The status code 200 is the standard success response. The text is a tiny successful HTTP response. Let’s write this to the stream as our response to a successful request! From the handle_connection function, remove the println! that was printing the request data and replace it with the code in Listing 20-3.

Filename: src/main.rs

use std::{
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let response = "HTTP/1.1 200 OK\r\n\r\n";

    stream.write_all(response.as_bytes()).unwrap();
}

Listing 20-3: Writing a tiny successful HTTP response to the stream

The first new line defines the response variable that holds the success message’s data. Then we call as_bytes on our response to convert the string data to bytes. The write_all method on stream takes a &[u8] and sends those bytes directly down the connection. Because the write_all operation could fail, we use unwrap on any error result as before. Again, in a real application you would add error handling here.

With these changes, let’s run our code and make a request. We’re no longer printing any data to the terminal, so we won’t see any output other than the output from Cargo. When you load 127.0.0.1:7878 in a web browser, you should get a blank page instead of an error. You’ve just hand-coded receiving an HTTP request and sending a response!

Returning Real HTML

Let’s implement the functionality for returning more than a blank page. Create a new file, hello.html, in the root of your project directory, not in the src directory. You can input any HTML you want; Listing 20-4 shows one possibility.

Filename: hello.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Hello!</h1>
    <p>Hi from Rust</p>
  </body>
</html>

Listing 20-4: A sample HTML file to return in a response

This is a minimal HTML5 document with a heading and some text. To return this from the server when a request is received, we’ll modify handle_connection as shown in Listing 20-5 to read the HTML file, add it to the response as a body, and send it.

Filename: src/main.rs

use std::{
    fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};
// --snip--

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let status_line = "HTTP/1.1 200 OK";
    let contents = fs::read_to_string("hello.html").unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}

Listing 20-5: Sending the contents of hello.html as the body of the response

We’ve added fs to the use statement to bring the standard library’s filesystem module into scope. The code for reading the contents of a file to a string should look familiar; we used it in Chapter 12 when we read the contents of a file for our I/O project in Listing 12-4.

Next, we use format! to add the file’s contents as the body of the success response. To ensure a valid HTTP response, we add the Content-Length header which is set to the size of our response body, in this case the size of hello.html.

Run this code with cargo run and load 127.0.0.1:7878 in your browser; you should see your HTML rendered!

Currently, we’re ignoring the request data in http_request and just sending back the contents of the HTML file unconditionally. That means if you try requesting 127.0.0.1:7878/something-else in your browser, you’ll still get back this same HTML response. Our server is very limited and is not what most web servers do. We want to customize our responses depending on the request and only send back the HTML file for a well-formed request to /.

Validating the Request and Selectively Responding

Right now, our web server will return the HTML in the file no matter what the client requested. Let’s add functionality to check that the browser is requesting / before returning the HTML file and return an error if the browser requests anything else. For this we need to modify handle_connection, as shown in Listing 20-6. This new code checks the content of the request received against what we know a request for / looks like and adds if and else blocks to treat requests differently.

Filename: src/main.rs

use std::{
    fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}
// --snip--

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    } else {
        // some other request
    }
}

Listing 20-6: Looking at the request line and handling requests to / differently from other requests

We’re only going to be looking at the first line of the HTTP request, so rather than reading the entire request into a vector, we’re calling next to get the first item from the iterator. The first unwrap takes care of the Option and stops the program if the iterator has no items. The second unwrap handles the Result and has the same effect as the unwrap that was in the map added in Listing 20-2.

Next, we check the request_line to see if it equals the request line of a GET request to the / path. If it does, the if block returns the contents of our HTML file.

If the request_line does not equal the GET request to the / path, it means we’ve received some other request. We’ll add code to the else block in a moment to respond to all other requests.

Run this code now and request 127.0.0.1:7878; you should get the HTML in hello.html. If you make any other request, such as 127.0.0.1:7878/something-else, you’ll get a connection error like those you saw when running the code in Listing 20-1 and Listing 20-2.

Now let’s add the code in Listing 20-7 to the else block to return a response with the status code 404, which signals that the content for the request was not found. We’ll also return some HTML for a page to render in the browser indicating the response to the end user.

Filename: src/main.rs

use std::{
    fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    // --snip--
    } else {
        let status_line = "HTTP/1.1 404 NOT FOUND";
        let contents = fs::read_to_string("404.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    }
}

Listing 20-7: Responding with status code 404 and an error page if anything other than / was requested

Here, our response has a status line with status code 404 and the reason phrase NOT FOUND. The body of the response will be the HTML in the file 404.html. You’ll need to create a 404.html file next to hello.html for the error page; again feel free to use any HTML you want or use the example HTML in Listing 20-8.

Filename: 404.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Oops!</h1>
    <p>Sorry, I don't know what you're asking for.</p>
  </body>
</html>

Listing 20-8: Sample content for the page to send back with any 404 response

With these changes, run your server again. Requesting 127.0.0.1:7878 should return the contents of hello.html, and any other request, like 127.0.0.1:7878/foo, should return the error HTML from 404.html.

A Touch of Refactoring

At the moment the if and else blocks have a lot of repetition: they’re both reading files and writing the contents of the files to the stream. The only differences are the status line and the filename. Let’s make the code more concise by pulling out those differences into separate if and else lines that will assign the values of the status line and the filename to variables; we can then use those variables unconditionally in the code to read the file and write the response. Listing 20-9 shows the resulting code after replacing the large if and else blocks.

Filename: src/main.rs

use std::{
    fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}
// --snip--

fn handle_connection(mut stream: TcpStream) {
    // --snip--
    let buf_reader = BufReader::new(&mut stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}

Listing 20-9: Refactoring the if and else blocks to contain only the code that differs between the two cases

Now the if and else blocks only return the appropriate values for the status line and filename in a tuple; we then use destructuring to assign these two values to status_line and filename using a pattern in the let statement, as discussed in Chapter 18.

The previously duplicated code is now outside the if and else blocks and uses the status_line and filename variables. This makes it easier to see the difference between the two cases, and it means we have only one place to update the code if we want to change how the file reading and response writing work. The behavior of the code in Listing 20-9 will be the same as that in Listing 20-8.

Awesome! We now have a simple web server in approximately 40 lines of Rust code that responds to one request with a page of content and responds to all other requests with a 404 response.

Currently, our server runs in a single thread, meaning it can only serve one request at a time. Let’s examine how that can be a problem by simulating some slow requests. Then we’ll fix it so our server can handle multiple requests at once.

Turning Our Single-Threaded Server into a Multithreaded Server

Right now, the server will process each request in turn, meaning it won’t process a second connection until the first is finished processing. If the server received more and more requests, this serial execution would be less and less optimal. If the server receives a request that takes a long time to process, subsequent requests will have to wait until the long request is finished, even if the new requests can be processed quickly. We’ll need to fix this, but first, we’ll look at the problem in action.

Simulating a Slow Request in the Current Server Implementation

We’ll look at how a slow-processing request can affect other requests made to our current server implementation. Listing 20-10 implements handling a request to /sleep with a simulated slow response that will cause the server to sleep for 5 seconds before responding.

Filename: src/main.rs

use std::{
    fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};
// --snip--

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    // --snip--

    let buf_reader = BufReader::new(&mut stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    // --snip--

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}

Listing 20-10: Simulating a slow request by recognizing /sleep and sleeping for 5 seconds

We switched from if to match now that we have three cases. We need to explicitly match on a slice of request_line to pattern match against the string literal values; match doesn’t do automatic referencing and dereferencing like the equality method does.

The first arm is the same as the if block from Listing 20-9. The second arm matches a request to /sleep. When that request is received, the server will sleep for 5 seconds before rendering the successful HTML page. The third arm is the same as the else block from Listing 20-9.

You can see how primitive our server is: real libraries would handle the recognition of multiple requests in a much less verbose way!

Start the server using cargo run. Then open two browser windows: one for http://127.0.0.1:7878/ and the other for http://127.0.0.1:7878/sleep. If you enter the / URI a few times, as before, you’ll see it respond quickly. But if you enter /sleep and then load /, you’ll see that / waits until sleep has slept for its full 5 seconds before loading.

There are multiple ways we could change how our web server works to avoid having more requests back up behind a slow request; the one we’ll implement is a thread pool.

Improving Throughput with a Thread Pool

A thread pool is a group of spawned threads that are waiting and ready to handle a task. When the program receives a new task, it assigns one of the threads in the pool to the task, and that thread will process the task. The remaining threads in the pool are available to handle any other tasks that come in while the first thread is processing. When the first thread is done processing its task, it’s returned to the pool of idle threads, ready to handle a new task. A thread pool allows you to process connections concurrently, increasing the throughput of your server.

We’ll limit the number of threads in the pool to a small number to protect us from Denial of Service (DoS) attacks; if we had our program create a new thread for each request as it came in, someone making 10 million requests to our server could create havoc by using up all our server’s resources and grinding the processing of requests to a halt.

Rather than spawning unlimited threads, we’ll have a fixed number of threads waiting in the pool. As requests come in, they’ll be sent to the pool for processing. The pool will maintain a queue of incoming requests. Each of the threads in the pool will pop off a request from this queue, handle the request, and then ask the queue for another request. With this design, we can process N requests concurrently, where N is the number of threads. If each thread is responding to a long-running request, subsequent requests can still back up in the queue, but we’ve increased the number of long-running requests we can handle before reaching that point.

This technique is just one of many ways to improve the throughput of a web server. Other options you might explore are the fork/join model and the single-threaded async I/O model. If you’re interested in this topic, you can read more about other solutions and try to implement them; with a low-level language like Rust, all of these options are possible.

Before we begin implementing a thread pool, let’s talk about what using the pool should look like. When you’re trying to design code, writing the client interface first can help guide your design. Write the API of the code so it’s structured in the way you want to call it; then implement the functionality within that structure rather than implementing the functionality and then designing the public API.

Similar to how we used test-driven development in the project in Chapter 12, we’ll use compiler-driven development here. We’ll write the code that calls the functions we want, and then we’ll look at errors from the compiler to determine what we should change next to get the code to work.

Code Structure If We Could Spawn a Thread for Each Request

First, let’s explore how our code might look if it did create a new thread for every connection. As mentioned earlier, this isn’t our final plan due to the problems with potentially spawning an unlimited number of threads, but it is a starting point. Listing 20-11 shows the changes to make to main to spawn a new thread to handle each stream within the for loop.

Filename: src/main.rs

use std::{
    fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        thread::spawn(|| {
            handle_connection(stream);
        });
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}

Listing 20-11: Spawning a new thread for each stream

As you learned in Chapter 16, thread::spawn will create a new thread and then run the code in the closure in the new thread. If you run this code and load /sleep in your browser, then / in two more browser tabs, you’ll indeed see that the requests to / don’t have to wait for /sleep to finish. But as we mentioned, this will eventually overwhelm the system because you’d be making new threads without any limit.

Creating a Similar Interface for a Finite Number of Threads

We want our thread pool to work in a similar, familiar way so switching from threads to a thread pool doesn’t require large changes to the code that uses our API. Listing 20-12 shows the hypothetical interface for a ThreadPool struct we want to use instead of thread::spawn.

Filename: src/main.rs

use std::{
    fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}

Listing 20-12: Our ideal ThreadPool interface

We use ThreadPool::new to create a new thread pool with a configurable number of threads, in this case four. Then, in the for loop, pool.execute has a similar interface as thread::spawn in that it takes a closure the pool should run for each stream. We need to implement pool.execute so it takes the closure and gives it to a thread in the pool to run. This code won’t yet compile, but we’ll try so the compiler can guide us in how to fix it.

Building the ThreadPool Struct Using Compiler Driven Development

Make the changes in Listing 20-12 to src/main.rs, and then let’s use the compiler errors from cargo check to drive our development. Here is the first error we get:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
  --> src/main.rs:10:16
   |
10 |     let pool = ThreadPool::new(4);
   |                ^^^^^^^^^^ use of undeclared type `ThreadPool`

For more information about this error, try `rustc --explain E0433`.
error: could not compile `hello` due to previous error

Great! This error tells us we need a ThreadPool type or module, so we’ll build one now. Our ThreadPool implementation will be independent of the kind of work our web server is doing. So, let’s switch the hello crate from a binary crate to a library crate to hold our ThreadPool implementation. After we change to a library crate, we could also use the separate thread pool library for any work we want to do using a thread pool, not just for serving web requests.

Create a src/lib.rs that contains the following, which is the simplest definition of a ThreadPool struct that we can have for now:

Filename: src/lib.rs

pub struct ThreadPool;

Then edit main.rs file to bring ThreadPool into scope from the library crate by adding the following code to the top of src/main.rs:

Filename: src/main.rs

use hello::ThreadPool;
use std::{
    fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}

This code still won’t work, but let’s check it again to get the next error that we need to address:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope
  --> src/bin/main.rs:11:28
   |
11 |     let pool = ThreadPool::new(4);
   |                            ^^^ function or associated item not found in `ThreadPool`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` due to previous error

This error indicates that next we need to create an associated function named new for ThreadPool. We also know that new needs to have one parameter that can accept 4 as an argument and should return a ThreadPool instance. Let’s implement the simplest new function that will have those characteristics:

Filename: src/lib.rs

pub struct ThreadPool;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        ThreadPool
    }
}

We chose usize as the type of the size parameter, because we know that a negative number of threads doesn’t make any sense. We also know we’ll use this 4 as the number of elements in a collection of threads, which is what the usize type is for, as discussed in the “Integer Types” section of Chapter 3.

Let’s check the code again:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope
  --> src/bin/main.rs:16:14
   |
16 |         pool.execute(|| {
   |              ^^^^^^^ method not found in `ThreadPool`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` due to previous error

Now the error occurs because we don’t have an execute method on ThreadPool. Recall from the “Creating a Similar Interface for a Finite Number of Threads” section that we decided our thread pool should have an interface similar to thread::spawn. In addition, we’ll implement the execute function so it takes the closure it’s given and gives it to an idle thread in the pool to run.

We’ll define the execute method on ThreadPool to take a closure as a parameter. Recall from the “Moving Captured Values Out of the Closure and the Fn Traits” section in Chapter 13 that we can take closures as parameters with three different traits: Fn, FnMut, and FnOnce. We need to decide which kind of closure to use here. We know we’ll end up doing something similar to the standard library thread::spawn implementation, so we can look at what bounds the signature of thread::spawn has on its parameter. The documentation shows us the following:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T,
        F: Send + 'static,
        T: Send + 'static,

The F type parameter is the one we’re concerned with here; the T type parameter is related to the return value, and we’re not concerned with that. We can see that spawn uses FnOnce as the trait bound on F. This is probably what we want as well, because we’ll eventually pass the argument we get in execute to spawn. We can be further confident that FnOnce is the trait we want to use because the thread for running a request will only execute that request’s closure one time, which matches the Once in FnOnce.

The F type parameter also has the trait bound Send and the lifetime bound 'static, which are useful in our situation: we need Send to transfer the closure from one thread to another and 'static because we don’t know how long the thread will take to execute. Let’s create an execute method on ThreadPool that will take a generic parameter of type F with these bounds:

Filename: src/lib.rs

pub struct ThreadPool;

impl ThreadPool {
    // --snip--
    pub fn new(size: usize) -> ThreadPool {
        ThreadPool
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

We still use the () after FnOnce because this FnOnce represents a closure that takes no parameters and returns the unit type (). Just like function definitions, the return type can be omitted from the signature, but even if we have no parameters, we still need the parentheses.

Again, this is the simplest implementation of the execute method: it does nothing, but we’re trying only to make our code compile. Let’s check it again:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.24s

It compiles! But note that if you try cargo run and make a request in the browser, you’ll see the errors in the browser that we saw at the beginning of the chapter. Our library isn’t actually calling the closure passed to execute yet!

Note: A saying you might hear about languages with strict compilers, such as Haskell and Rust, is “if the code compiles, it works.” But this saying is not universally true. Our project compiles, but it does absolutely nothing! If we were building a real, complete project, this would be a good time to start writing unit tests to check that the code compiles and has the behavior we want.

Validating the Number of Threads in new

We aren’t doing anything with the parameters to new and execute. Let’s implement the bodies of these functions with the behavior we want. To start, let’s think about new. Earlier we chose an unsigned type for the size parameter, because a pool with a negative number of threads makes no sense. However, a pool with zero threads also makes no sense, yet zero is a perfectly valid usize. We’ll add code to check that size is greater than zero before we return a ThreadPool instance and have the program panic if it receives a zero by using the assert! macro, as shown in Listing 20-13.

Filename: src/lib.rs

pub struct ThreadPool;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        ThreadPool
    }

    // --snip--
    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

Listing 20-13: Implementing ThreadPool::new to panic if size is zero

We’ve also added some documentation for our ThreadPool with doc comments. Note that we followed good documentation practices by adding a section that calls out the situations in which our function can panic, as discussed in Chapter 14. Try running cargo doc --open and clicking the ThreadPool struct to see what the generated docs for new look like!

Instead of adding the assert! macro as we’ve done here, we could make new return a Result like we did with Config::new in the I/O project in Listing 12-9. But we’ve decided in this case that trying to create a thread pool without any threads should be an unrecoverable error. If you’re feeling ambitious, try to write a version of new with the following signature to compare both versions:

pub fn new(size: usize) -> Result<ThreadPool, PoolCreationError> {

Creating Space to Store the Threads

Now that we have a way to know we have a valid number of threads to store in the pool, we can create those threads and store them in the ThreadPool struct before returning it. But how do we “store” a thread? Let’s take another look at the thread::spawn signature:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T,
        F: Send + 'static,
        T: Send + 'static,

The spawn function returns a JoinHandle<T>, where T is the type that the closure returns. Let’s try using JoinHandle too and see what happens. In our case, the closures we’re passing to the thread pool will handle the connection and not return anything, so T will be the unit type ().

The code in Listing 20-14 will compile but doesn’t create any threads yet. We’ve changed the definition of ThreadPool to hold a vector of thread::JoinHandle<()> instances, initialized the vector with a capacity of size, set up a for loop that will run some code to create the threads, and returned a ThreadPool instance containing them.

Filename: src/lib.rs

use std::thread;

pub struct ThreadPool {
    threads: Vec<thread::JoinHandle<()>>,
}

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let mut threads = Vec::with_capacity(size);

        for _ in 0..size {
            // create some threads and store them in the vector
        }

        ThreadPool { threads }
    }
    // --snip--

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

Listing 20-14: Creating a vector for ThreadPool to hold the threads

We’ve brought std::thread into scope in the library crate, because we’re using thread::JoinHandle as the type of the items in the vector in ThreadPool.

Once a valid size is received, our ThreadPool creates a new vector that can hold size items. We haven’t used the with_capacity function in this book yet, which performs the same task as Vec::new but with an important difference: it preallocates space in the vector. Because we know we need to store size elements in the vector, doing this allocation up front is slightly more efficient than using Vec::new, which resizes itself as elements are inserted.

When you run cargo check again, it should succeed.

A Worker Struct Responsible for Sending Code from the ThreadPool to a Thread

We left a comment in the for loop in Listing 20-14 regarding the creation of threads. Here, we’ll look at how we actually create threads. The standard library provides thread::spawn as a way to create threads, and thread::spawn expects to get some code the thread should run as soon as the thread is created. However, in our case, we want to create the threads and have them wait for code that we’ll send later. The standard library’s implementation of threads doesn’t include any way to do that; we have to implement it manually.

We’ll implement this behavior by introducing a new data structure between the ThreadPool and the threads that will manage this new behavior. We’ll call this data structure Worker, which is a common term in pooling implementations. Think of people working in the kitchen at a restaurant: the workers wait until orders come in from customers, and then they’re responsible for taking those orders and filling them.

Instead of storing a vector of JoinHandle<()> instances in the thread pool, we’ll store instances of the Worker struct. Each Worker will store a single JoinHandle<()> instance. Then we’ll implement a method on Worker that will take a closure of code to run and send it to the already running thread for execution. We’ll also give each worker an id so we can distinguish between the different workers in the pool when logging or debugging.

Let’s make the following changes to what happens when we create a ThreadPool. We’ll implement the code that sends the closure to the thread after we have Worker set up in this way:

  1. Define a Worker struct that holds an id and a JoinHandle<()>.
  2. Change ThreadPool to hold a vector of Worker instances.
  3. Define a Worker::new function that takes an id number and returns a Worker instance that holds the id and a thread spawned with an empty closure.
  4. In ThreadPool::new, use the for loop counter to generate an id, create a new Worker with that id, and store the worker in the vector.

If you’re up for a challenge, try implementing these changes on your own before looking at the code in Listing 20-15.

Ready? Here is Listing 20-15 with one way to make the preceding modifications.

Filename: src/lib.rs

use std::thread;

pub struct ThreadPool {
    workers: Vec<Worker>,
}

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id));
        }

        ThreadPool { workers }
    }
    // --snip--

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize) -> Worker {
        let thread = thread::spawn(|| {});

        Worker { id, thread }
    }
}

Listing 20-15: Modifying ThreadPool to hold Worker instances instead of holding threads directly

We’ve changed the name of the field on ThreadPool from threads to workers because it’s now holding Worker instances instead of JoinHandle<()> instances. We use the counter in the for loop as an argument to Worker::new, and we store each new Worker in the vector named workers.

External code (like our server in src/main.rs) doesn’t need to know the implementation details regarding using a Worker struct within ThreadPool, so we make the Worker struct and its new function private. The Worker::new function uses the id we give it and stores a JoinHandle<()> instance that is created by spawning a new thread using an empty closure.

This code will compile and will store the number of Worker instances we specified as an argument to ThreadPool::new. But we’re still not processing the closure that we get in execute. Let’s look at how to do that next.

Sending Requests to Threads via Channels

Now we’ll tackle the problem that the closures given to thread::spawn do absolutely nothing. Currently, we get the closure we want to execute in the execute method. But we need to give thread::spawn a closure to run when we create each Worker during the creation of the ThreadPool.

We want the Worker structs that we just created to fetch code to run from a queue held in the ThreadPool and send that code to its thread to run.

In Chapter 16, you learned about channels—a simple way to communicate between two threads—that would be perfect for this use case. We’ll use a channel to function as the queue of jobs, and execute will send a job from the ThreadPool to the Worker instances, which will send the job to its thread. Here is the plan:

  1. The ThreadPool will create a channel and hold on to the sender.
  2. Each Worker will hold on to the receiver.
  3. We’ll create a new Job struct that will hold the closures we want to send down the channel.
  4. The execute method will send the job it wants to execute through the sender.
  5. In its thread, the Worker will loop over its receiver and execute the closures of any jobs it receives.

Let’s start by creating a channel in ThreadPool::new and holding the sender in the ThreadPool instance, as shown in Listing 20-16. The Job struct doesn’t hold anything for now but will be the type of item we’re sending down the channel.

Filename: src/lib.rs

use std::{sync::mpsc, thread};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

struct Job;

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id));
        }

        ThreadPool { workers, sender }
    }
    // --snip--

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize) -> Worker {
        let thread = thread::spawn(|| {});

        Worker { id, thread }
    }
}

Listing 20-16: Modifying ThreadPool to store the sender of a channel that transmits Job instances

In ThreadPool::new, we create our new channel and have the pool hold the sender. This will successfully compile.

Let’s try passing a receiver of the channel into each worker as the thread pool creates the channel. We know we want to use the receiver in the thread that the workers spawn, so we’ll reference the receiver parameter in the closure. The code in Listing 20-17 won’t quite compile yet.

Filename: src/lib.rs

use std::{sync::mpsc, thread};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

struct Job;

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, receiver));
        }

        ThreadPool { workers, sender }
    }
    // --snip--

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

// --snip--


struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}

Listing 20-17: Passing the receiver to the workers

We’ve made some small and straightforward changes: we pass the receiver into Worker::new, and then we use it inside the closure.

When we try to check this code, we get this error:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0382]: use of moved value: `receiver`
  --> src/lib.rs:27:42
   |
22 |         let (sender, receiver) = mpsc::channel();
   |                      -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
27 |             workers.push(Worker::new(id, receiver));
   |                                          ^^^^^^^^ value moved here, in previous iteration of loop

For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello` due to previous error

The code is trying to pass receiver to multiple Worker instances. This won’t work, as you’ll recall from Chapter 16: the channel implementation that Rust provides is multiple producer, single consumer. This means we can’t just clone the consuming end of the channel to fix this code. We also don’t want to send a message multiple times to multiple consumers; we want one list of messages with multiple workers such that each message gets processed once.

Additionally, taking a job off the channel queue involves mutating the receiver, so the threads need a safe way to share and modify receiver; otherwise, we might get race conditions (as covered in Chapter 16).

Recall the thread-safe smart pointers discussed in Chapter 16: to share ownership across multiple threads and allow the threads to mutate the value, we need to use Arc<Mutex<T>>. The Arc type will let multiple workers own the receiver, and Mutex will ensure that only one worker gets a job from the receiver at a time. Listing 20-18 shows the changes we need to make.

Filename: src/lib.rs

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};
// --snip--

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

struct Job;

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    // --snip--

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

// --snip--

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        // --snip--
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}

Listing 20-18: Sharing the receiver among the workers using Arc and Mutex

In ThreadPool::new, we put the receiver in an Arc and a Mutex. For each new worker, we clone the Arc to bump the reference count so the workers can share ownership of the receiver.

With these changes, the code compiles! We’re getting there!

Implementing the execute Method

Let’s finally implement the execute method on ThreadPool. We’ll also change Job from a struct to a type alias for a trait object that holds the type of closure that execute receives. As discussed in the “Creating Type Synonyms with Type Aliases” section of Chapter 19, type aliases allow us to make long types shorter. Look at Listing 20-19.

Filename: src/lib.rs

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

// --snip--

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

// --snip--

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}

Listing 20-19: Creating a Job type alias for a Box that holds each closure and then sending the job down the channel

After creating a new Job instance using the closure we get in execute, we send that job down the sending end of the channel. We’re calling unwrap on send for the case that sending fails. This might happen if, for example, we stop all our threads from executing, meaning the receiving end has stopped receiving new messages. At the moment, we can’t stop our threads from executing: our threads continue executing as long as the pool exists. The reason we use unwrap is that we know the failure case won’t happen, but the compiler doesn’t know that.

But we’re not quite done yet! In the worker, our closure being passed to thread::spawn still only references the receiving end of the channel. Instead, we need the closure to loop forever, asking the receiving end of the channel for a job and running the job when it gets one. Let’s make the change shown in Listing 20-20 to Worker::new.

Filename: src/lib.rs

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

// --snip--

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let job = receiver.lock().unwrap().recv().unwrap();

            println!("Worker {id} got a job; executing.");

            job();
        });

        Worker { id, thread }
    }
}

Listing 20-20: Receiving and executing the jobs in the worker’s thread

Here, we first call lock on the receiver to acquire the mutex, and then we call unwrap to panic on any errors. Acquiring a lock might fail if the mutex is in a poisoned state, which can happen if some other thread panicked while holding the lock rather than releasing the lock. In this situation, calling unwrap to have this thread panic is the correct action to take. Feel free to change this unwrap to an expect with an error message that is meaningful to you.

If we get the lock on the mutex, we call recv to receive a Job from the channel. A final unwrap moves past any errors here as well, which might occur if the thread holding the sender has shut down, similar to how the send method returns Err if the receiver shuts down.

The call to recv blocks, so if there is no job yet, the current thread will wait until a job becomes available. The Mutex<T> ensures that only one Worker thread at a time is trying to request a job.

Our thread pool is now in a working state! Give it a cargo run and make some requests:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
warning: field is never read: `workers`
 --> src/lib.rs:7:5
  |
7 |     workers: Vec<Worker>,
  |     ^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: field is never read: `id`
  --> src/lib.rs:48:5
   |
48 |     id: usize,
   |     ^^^^^^^^^

warning: field is never read: `thread`
  --> src/lib.rs:49:5
   |
49 |     thread: thread::JoinHandle<()>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: `hello` (lib) generated 3 warnings
    Finished dev [unoptimized + debuginfo] target(s) in 1.40s
     Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.

Success! We now have a thread pool that executes connections asynchronously. There are never more than four threads created, so our system won’t get overloaded if the server receives a lot of requests. If we make a request to /sleep, the server will be able to serve other requests by having another thread run them.

Note: if you open /sleep in multiple browser windows simultaneously, they might load one at a time in 5 second intervals. Some web browsers execute multiple instances of the same request sequentially for caching reasons. This limitation is not caused by our web server.

After learning about the while let loop in Chapter 18, you might be wondering why we didn’t write the worker thread code as shown in Listing 20-21.

Filename: src/lib.rs

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}
// --snip--

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            while let Ok(job) = receiver.lock().unwrap().recv() {
                println!("Worker {id} got a job; executing.");

                job();
            }
        });

        Worker { id, thread }
    }
}

Listing 20-21: An alternative implementation of Worker::new using while let

This code compiles and runs but doesn’t result in the desired threading behavior: a slow request will still cause other requests to wait to be processed. The reason is somewhat subtle: the Mutex struct has no public unlock method because the ownership of the lock is based on the lifetime of the MutexGuard<T> within the LockResult<MutexGuard<T>> that the lock method returns. At compile time, the borrow checker can then enforce the rule that a resource guarded by a Mutex cannot be accessed unless we hold the lock. But this implementation can also result in the lock being held longer than intended if we don’t think carefully about the lifetime of the MutexGuard<T>.

The code in Listing 20-20 that uses let job = receiver.lock().unwrap().recv().unwrap(); works because with let, any temporary values used in the expression on the right hand side of the equals sign are immediately dropped when the let statement ends. However, while let (and if let and match) does not drop temporary values until the end of the associated block. In Listing 20-21, the lock remains held for the duration of the call to job(), meaning other workers cannot receive jobs.

Zarifçe Kapatma ve Temizleme

Liste 20-20'deki kod, amaçladığımız gibi bir iş parçacığı havuzu kullanarak isteklere eşzamansız olarak yanıt veriyor. Doğrudan kullanmadığımız workers, id ve thread alanları hakkında bize hiçbir şeyi temizlemediğimizi hatırlatan bazı uyarılar alıyoruz. Ana iş parçacığını durdurmak için daha az zarif olan ctrl-c yöntemini kullandığımızda, bir isteği sunmanın ortasında olsalar bile diğer tüm iş parçacıkları da hemen durdurulur.

Şimdi, havuzdaki (thread) her bir iş parçacığına katılmak için Drop tanımını uygulayacağız, böylece onlar kapanmadan önce üzerinde çalıştıkları istekleri tamamlayabilirler. Ardından, ileti dizilerine yeni istekleri kabul etmeyi bırakmaları ve kapatmaları gerektiğini söylemenin bir yolunu uygulayacağız. Bu kodu çalışırken görmek için, iş parçacığı havuzunu zarif bir şekilde kapatmadan önce sunucumuzu yalnızca iki isteği kabul edecek şekilde değiştireceğiz.

ThreadPool'da Drop Tanımını Süreklemek

Hadi havuzumuz için Drop tanımını sürekleyelim. Her ne zaman havuz bırakılırsa, iş parçacıklarımızın tamamı işlerini bitirmelidir. Liste 20-22 bize Drop süreklemesinin ilk girişimini gösteriyor. Tabii bu kod henüz tam anlamıyla çalışmıyor.

Dosya adı: src/lib.rs

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let job = receiver.lock().unwrap().recv().unwrap();

            println!("Worker {id} got a job; executing.");

            job();
        });

        Worker { id, thread }
    }
}

Liste 20-22: Havuz alandan ayrıldığında her iş parçacığına girmek

İlk olarak, iş parçacığı havuzunun workers alanı arasında döngü yaparız. Bunun için &mut kullanıyoruz çünkü self değişken bir referanstır ve ayrıca worker'ı değiştirebiliyor olmamız gerekir. Her çalışan için, bu belirli çalışanın kapatıldığını söyleyen bir mesaj yazdırırız ve ardından o çalışanın iş parçacığına katılmayı çağırırız. Katılma çağrısı (join) başarısız olursa, Rust'ı paniğe sürüklemek ve uygunsuz bir kapatmaya gitmek için paketi açarız (unwrap).

Bu kodu derlediğimizde aldığımız hata:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference
  --> src/lib.rs:52:13
   |
52 |             worker.thread.join().unwrap();
   |             ^^^^^^^^^^^^^ move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait

For more information about this error, try `rustc --explain E0507`.
error: could not compile `hello` due to previous error

Hata bize, her bir çalışanın yalnızca değişken bir referansını almaya sahip olduğumuz ve join argümanının sahipliğini üstlendiği için join diyemeyeceğimizi söylüyor. Bu sorunu çözmek için, join'in thread'i tüketebilmesi için thread'i thread'in sahibi olan Worker örneğinin dışına taşımamız gerekiyor. Bunu Liste 17-15'te yaptık: Worker bunun yerine bir Option<thread::JoinHandle<()>> tutarsa, değeri Some değişkeninin dışına taşımak ve içinde bir None değişkeni bırakmak için Option üzerindeki take fonksiyonunu çağırabiliriz. Başka bir deyişle, çalışan bir Worker'ın iş parçacığında Some varyantı olacaktır ve bir Worker'ı temizlemek istediğimizde, Worker'ın çalıştıracak bir iş parçacığı olmaması için Some'yi None ile değiştiririz.

Dolayısıyla, Worker tanımını şu şekilde güncellemek istediğimizi biliyoruz:

Dosya adı: src/lib.rs

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let job = receiver.lock().unwrap().recv().unwrap();

            println!("Worker {id} got a job; executing.");

            job();
        });

        Worker { id, thread }
    }
}

Şimdi değişmesi gereken diğer yerleri bulmak için derleyiciye bakalım. Bu kodu kontrol ederken iki hata alıyoruz:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no method named `join` found for enum `Option` in the current scope
  --> src/lib.rs:52:27
   |
52 |             worker.thread.join().unwrap();
   |                           ^^^^ method not found in `Option<JoinHandle<()>>`

error[E0308]: mismatched types
  --> src/lib.rs:72:22
   |
72 |         Worker { id, thread }
   |                      ^^^^^^ expected enum `Option`, found struct `JoinHandle`
   |
   = note: expected enum `Option<JoinHandle<()>>`
            found struct `JoinHandle<_>`
help: try wrapping the expression in `Some`
   |
72 |         Worker { id, Some(thread) }
   |                      +++++      +

Some errors have detailed explanations: E0308, E0599.
For more information about an error, try `rustc --explain E0308`.
error: could not compile `hello` due to 2 previous errors

Worker::new'in sonundaki koda işaret eden ikinci hatayı ele alalım; yeni bir Worker oluşturduğumuzda, iş parçacığı (thread) değerini Some içine sarmamız gerekir. Bu hatayı düzeltmek için aşağıdaki değişiklikleri yapın:

Dosya adı: src/lib.rs

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        // --snip--

        let thread = thread::spawn(move || loop {
            let job = receiver.lock().unwrap().recv().unwrap();

            println!("Worker {id} got a job; executing.");

            job();
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

İlk hata Drop süreklemesindedir. thread'i worker'ın dışına taşımak için Option değerini alabilmek için take fonksiyonunu çağırmayı amaçladığımızdan daha önce bahsetmiştik.

Aşağıdaki değişiklikler bunu yapacaktır:

Dosya adı: src/lib.rs

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let job = receiver.lock().unwrap().recv().unwrap();

            println!("Worker {id} got a job; executing.");

            job();
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

Bölüm 17'de söylendiği gibi, Option üzerindeki take fonksiyonu Some değişkenini çıkarır ve onun yerine None'u bırakır. Some'ı yok etmek ve ipliği almak için if let'i kullanıyoruz; sonra iplik üzerinde join'i çağırıyoruz. Bir işçinin iş parçacığı zaten None ise, işçinin iş parçacığını zaten temizlediğini biliyoruz, bu nedenle bu durumda hiçbir şey değişmeyecektir.

İşleri Dinlemeyi Durdurmak İçin İpliklere Sinyal Vermek

Yaptığımız tüm değişikliklerle kodumuz herhangi bir uyarı olmadan derleniyor. Ancak kötü haber şu ki, bu kod henüz istediğimiz gibi çalışmıyor. Anahtar, Worker örneklerinin iş parçacıkları tarafından çalıştırılan kapatmalardaki mantıktır: şu anda buna join diyoruz, ancak bu, iş aramak için sonsuza kadar döngü yaptıkları için iş parçacıklarını kapatmaz. Mevcut drop uygulamamızla ThreadPool'umuzu düşürmeye çalışırsak, ana iş parçacığı sonsuza kadar ilk iş parçacığının bitmesini bekleyecek. Bu sorunu çözmek için ThreadPool drop süreklemesinde bir değişikliğe ve ardından Worker döngüsünde (loop) bir değişikliğe ihtiyacımız olacak.

İlk olarak, ThreadPool drop süreklemesini, ileti dizilerinin bitmesini beklemeden önce göndereni açıkça bırakacak şekilde değiştireceğiz.

Liste 20-23, göndereni açıkça bırakmak (drop) için ThreadPool'daki değişiklikleri gösterir. Göndericiyi (sender) ThreadPool'un dışına taşıyabilmek için aynı Optionu kullanıyoruz ve iş parçacığında yaptığımız gibi tekniği alıyoruz:

Dosya adı: src/lib.rs

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}
// --snip--

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        // --snip--

        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let job = receiver.lock().unwrap().recv().unwrap();

            println!("Worker {id} got a job; executing.");

            job();
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

Liste 20-23: Çalışan iş parçacıklarına katılmadan önce sender'ı açıkça bırakın

sender'ı bırakmak, kanalı kapatır, bu da daha fazla mesaj gönderilmeyeceğini gösterir. Bu olduğunda, işçilerin sonsuz döngüde yaptığı tüm recv çağrıları bir hata döndürür. Liste 20-24'te, bu durumda döngüden zarif bir şekilde çıkmak için Worker döngüsünü değiştiriyoruz; bu, ThreadPool drop süreklemesi, join çağrısı yaptığında iş parçacıklarının biteceği anlamına geliyor.

Dosya adı: src/lib.rs

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            match receiver.lock().unwrap().recv() {
                Ok(job) => {
                    println!("Worker {id} got a job; executing.");

                    job();
                }
                Err(_) => {
                    println!("Worker {id} disconnected; shutting down.");
                    break;
                }
            }
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

Liste 20-24: "recv" bir hata döndürdüğünde açıkça döngüden çıkar

Bu kodu çalışırken görmek için, Liste 20-25'te gösterildiği gibi, sunucuyu düzgün bir şekilde kapatmadan önce main'i yalnızca iki isteği kabul edecek şekilde değiştirelim.

Dosya adı: src/main.rs

use hello::ThreadPool;
use std::fs;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
use std::thread;
use std::time::Duration;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();

    let get = b"GET / HTTP/1.1\r\n";
    let sleep = b"GET /sleep HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK", "hello.html")
    } else if buffer.starts_with(sleep) {
        thread::sleep(Duration::from_secs(5));
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();

    let response = format!(
        "{}\r\nContent-Length: {}\r\n\r\n{}",
        status_line,
        contents.len(),
        contents
    );

    stream.write_all(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

Liste 20-25: Döngüden çıkıp iki istek sunduktan sonra sunucuyu kapatır

Gerçek dünya uygulamasında bir web sunucusunun yalnızca iki istek sunduktan sonra kapanmasını istemezsiniz. Bu kod, yalnızca zarif kapatma ve temizlemenin çalışır durumda olduğunu gösterir.

take metodu, Iterator tanımında tanımlanır ve yinelemeyi en fazla ilk iki öğeyle sınırlar. ThreadPool, main sonunda kapsam dışına çıkacak ve drop süreklemesi çalışacaktır.

Sunucuyu cargo run ile başlatın ve üç istekte bulunun. Üçüncü istek hata vermeli ve uçbiriminizde buna benzer bir çıktı görmelisiniz:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 1.0s
     Running `target/debug/hello`
Worker 0 got a job; executing.
Shutting down.
Shutting down worker 0
Worker 3 got a job; executing.
Worker 1 disconnected; shutting down.
Worker 2 disconnected; shutting down.
Worker 3 disconnected; shutting down.
Worker 0 disconnected; shutting down.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3

Farklı bir çalışan sıralaması ve yazdırılan mesajlar görebilirsiniz. Bu kodun nasıl çalıştığını mesajlardan görebiliriz: 0 ve 3 numaralı işçiler ilk iki isteği aldı. Sunucu, ikinci bağlantıdan sonra bağlantıları kabul etmeyi durdurdu ve ThreadPool'daki Drop süreklemesi, işçi 3 daha işine başlamadan önce yürütülmeye başladı. sender'ı bırakmak, tüm çalışanların bağlantısını keser ve onlara kapatmalarını söyler.

Çalışanların her biri, bağlantıyı kestiklerinde bir mesaj yazdırır ve ardından iş parçacığı havuzu, her bir çalışan iş parçacığının bitmesini beklemek için birleştirme çağırır.

Bu uygulamanın ilginç bir yönüne dikkat edin: ThreadPool sender'ı bıraktı (drop) ve herhangi bir çalışan bir hata almadan önce, işçi 0'a girmeye çalıştık.

İşçi 0, recv'den henüz bir hata almamıştı, bu nedenle ana iş parçacığı, işçi 0'ı beklemeyi engelledi. bitirmek için. Bu arada, işçi 3 bir iş aldı ve ardından tüm iş parçacıkları bir hata aldı. İşçi 0 bittiğinde, ana iş parçacığı diğer işçilerin bitirmesini bekledi. Bu noktada, hepsi döngülerinden çıkmış ve durmuşlardı. Tebrikler! Artık projemizi tamamladık; zaman uyumsuz olarak yanıt vermek için bir iş parçacığı havuzu kullanan temel bir web sunucumuz var. Havuzdaki tüm iş parçacıklarını temizleyen sunucunun zarif bir şekilde kapatılmasını gerçekleştirebiliyoruz.

Referans için tam kod:

Dosya adı: src/main.rs

use hello::ThreadPool;
use std::fs;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
use std::thread;
use std::time::Duration;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();

    let get = b"GET / HTTP/1.1\r\n";
    let sleep = b"GET /sleep HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK", "hello.html")
    } else if buffer.starts_with(sleep) {
        thread::sleep(Duration::from_secs(5));
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();

    let response = format!(
        "{}\r\nContent-Length: {}\r\n\r\n{}",
        status_line,
        contents.len(),
        contents
    );

    stream.write_all(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

Dosya adı: src/lib.rs

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender: Some(sender) }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let message = receiver.lock().unwrap().recv();

            match message {
                Ok(job) => {
                    println!("Worker {id} got a job; executing.");

                    job();
                }
                Err(_) => {
                    println!("Worker {id} disconnected; shutting down.");
                    break;
                }
            }
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

Daha fazlasını da yapabilirdik! Eğer bu projeyi geliştirmeye devam etmek istiyorsanız, burada size yardımcı olacak bazı fikirler var:

  • ThreadPool'a ve genel yöntemlerine daha fazla belge ekleyin.
  • Kütüphanenin işlevselliğine ilişkin testler ekleyin
  • Daha kararlı hata işleme için fonksiyon çağrılarına unwrap ekleyin.
  • Web isteklerini sunmaktan başka bir görevi gerçekleştirmek için ThreadPool'u kullanın.
  • crates.io'da iş parçacığı havuzu arayın ve yaptığımız web sunucusunu bulduğunuz kasayda sürekleyin. Daha sonra süreklediğimizle arasındaki API kararlılığını karşılaştırın.

Özet

Bravo! Kitabın sonuna geldiniz! Rust'un bu turuna katıldığınız için size teşekkür etmek istiyoruz. Artık kendi Rust projelerinizi uygulamaya ve diğer insanların projelerine yardım etmeye hazırsınız. Rust yolculuğunuzda karşılaştığınız her türlü zorlukta size yardım etmeyi sevecek diğer Rustseverlerden oluşan hoş bir topluluk olduğunu unutmayın.

Eklemeler

Aşağıdaki bölümlerde Rust yolculuğunuzda işinize yarayabilecek referans materyalleri yer almaktadır.

Ekleme A: Anahtar Sözcükler

Aşağıdaki listede bulunan anahtar sözcükler Rust dilinde şu anda ya da gelecekte kullanılacağı için rezerve edilmiştir. Bu nedenle tanımlayıcı olarak kullanılamazlar. (“Ham Tanımlayıcılar” kısmında tartışacağımız tanımlayıcılar hariç), isimleri dahil olmak üzere fonksiyonlar, değişkenler, parametreler, yapı girdileri, modüller, kasalar, değişmezler, makrolar, statik değerler, öznitelikler, türler, tanımlar ya da ömürlükleri kapsar.

Şu anda Kullanılan Anahtar Sözcükler

Aşağıdaki anahtar sözcükler açıklanan işlevselliklere sahiptir.

  • as - ilkel dönüşüm yapmak, belirli özelliğin türünün belirsizliğini gidermek, ya da kullanımdaki use ve extern crate ifadelerini yeniden adlandırmak için kullanılır
  • async - şu andaki ipliği bloklamak yerine Future döndürür
  • await - Future'ın sonucu hazır olana kadar çağrıları susturur
  • break - döngüden çıkar
  • const - değişmez öğeleri veya sabit işaretçileri tanımlar
  • continue - sonraki döngü yineleyiciyle devam ettirir
  • crate - makroda tanımlanan bir kasa değişkenini ya da harici kasayı bağlar
  • dyn - bir tanım nesnesine dinamik olarak gönderir
  • else - if ya da if let kontrol akış yapılarının B planını uygular
  • enum - numaralandırılmış yapı tanımlar
  • extern - harici bir kasayı, fonksiyonu ya da değişkeni bağlar
  • false - Boole yanlış değişmezi
  • fn - fonksiyon ya da fonksiyon işaretçi türünü tanımlar
  • for - bir yineleyici üzerinde öğeler kadar döngü yapar, tanım uygular ya da yüksek dereceli ömürlüğü belirtir
  • if - koşullu bir ifadenin sonucuna dayalı dal oluşturur
  • impl - var olan veya tanımsal işlevselliğini sürekler
  • in - for döngüsünün söz diziminin bir parçası
  • let - değişken atar
  • loop - koşulsuz döngüye girer
  • match - modelleri kullanarak değer eşleştirir
  • mod - modül tanımlar
  • move - bir kapanış yaparak tüm yakalamalarının sahipliğini alır
  • mut - referanslarda, işaretçilerde ya da model atamalarında değişmemezliği belirtir
  • pub - yapı alanlarında, impl bloklarında ya da modüllerde genel görünümlüğü belirtir
  • ref - referansla atar
  • return - fonksiyondan döndürür
  • Self - tanımladığımız ya da süreklediğimiz bir tür için takma ad
  • self - halihazırdaki modül belirteci
  • static - program çalıştığından beri tutulan genel değişken ya da ömürlük
  • struct - yapı tanımlar
  • super - halihazırdaki modülün bulunduğu ana modül belirteci
  • trait - tanım tanımlar
  • true - Boole doğru değişmezi
  • type - bir tür takma adı veya ilişkili tür tanımlar
  • union - birlik tanımlar ve sadece birlik tanımlarken bir anahtar sözcük görevi görür
  • unsafe - güvensiz kodlar, fonksiyonlar, tanımlar ya da süreklemeleri belirtir
  • use - sembolleri kapsama alır
  • where - bir türü sınırlayan tümceleri belirtir
  • while - bir ifadenin sonucuna göre çalışan koşullu döngü tanımlar

Gelecekte Kullanım için Ayrılmış Anahtar Kelimeler

Aşağıdaki anahtar kelimelerin herhangi bir işlevi yoktur, ancak gelecekteki potansiyel kullanımdan dolayı Rust tarafından ayrılmıştır.

  • abstract
  • become
  • box
  • do
  • final
  • macro
  • override
  • priv
  • try
  • typeof
  • unsized
  • virtual
  • yield

Ham Tanımlayıcılar

Ham tanımlayıcılar normalde kullanılmayacakları yerlerde kullanmanıza izin veren söz dizimidir özelliğidir. Anahtar sözcüğünüzün başına r# koyarak ham işaretçileri kullanabilirsiniz.

Örneğin, match bir anahtar sözcüktür. Eğer fonksiyonunuz ad olarak match kullandığında onu derlemeye çalışırsanız:

Filename: src/main.rs

fn match(needle: &str, haystack: &str) -> bool {
    haystack.contains(needle)
}

şu hatayı alırsınız:

error: expected identifier, found keyword `match`
 --> src/main.rs:4:4
  |
4 | fn match(needle: &str, haystack: &str) -> bool {
  |    ^^^^^ expected identifier, found keyword

Bu hata size match'i fonksiyon adı olarak kullanamayacağınızı gösterir. match'i fonksiyon adı kullanmak için ham tanımlayıcı söz dizimini kullanmanız gerekir, aynı bunun gibi:

Filename: src/main.rs

fn r#match(needle: &str, haystack: &str) -> bool {
    haystack.contains(needle)
}

fn main() {
    assert!(r#match("foo", "foobar"));
}

Bu kod hatasız derlenir. Not olaraktan, r# öneki main adıyla anılan özel fonksiyon için de kullanılabilir.

Ham tanımlayıcılar sizi hangi sözcüğü kullanacağınız konusunda özgür tutar. Ekleme olaraktan, ham işaretçiler kasanızın kullandığından farklı Rust sürümünü kullanan kütüphaneleri kullanmanıza izin verir. Örnek olarak, try 2015 sürümünde bir anahtar sözcük değildi, 2018 sürümünde geldi. Eğer 2015 sürümünü kullanan bir kütüphaneniz varsa ve bu kütüphane try diye bir fonksiyona sahipse, 2018 sürümünde bu fonksiyonu çağırabilmeniz için ham tanımlayıcılar kullanmanız gerekir. Sürümler hakkında daha fazla bilgiye erişmek için Ekleme E bölümüne göz atabilirsiniz.

Ekleme B: Operatörler ve Semboller

Bu ekleme, Rust'ın yaygınlar, tanım sınırları, makrolar, nitelikler, yorum satırları, demetler ve parantezler bağlamında beliren operatörlerini ve diğer sembollerini dahil eden bir sözlüğü içerir.

Operatörler

Tablo B-1 Rust'taki operatörleri, kodda nasıl belireceğini, kısa özetini, nasıl aşırı yüklenilebileceğini gösterir.

Tablo B-1: Operatörler

OperatörÖrnekAçıklamaAşırı Yüklenilebilir mi?
!ident!(...), ident!{...}, ident![...]Makro genişlemesi
!!exprBitsel veya mantıksal tamamlayıcıNot
!=var != exprEşitsizlik karşılaştırıcıPartialEq
%expr % exprModuRem
%=var %= exprModu ve eşitlemesiRemAssign
&&expr, &mut exprReferans
&&type, &mut type, &'a type, &'a mut typeReferans işaretçi türü
&expr & exprBitsel VEBitAnd
&=var &= exprBitsel VE ve eşitlemesiBitAndAssign
&&expr && exprKısa devre mantıksal VE
*expr * exprAritmetik çarpmaMul
*=var *= exprAritmetik çarpma ve eşitlemesiMulAssign
**exprReferansı kaldırmaDeref
**const type, *mut typeHam işaretçi
+trait + trait, 'a + traitBileşik tür kısıtlaması
+expr + exprAritmetik toplamaAdd
+=var += exprAritmetik toplama ve eşitlemesiAddAssign
,expr, exprArgüman ve öğe ayırıcı
-- exprAritmetik olumsuzlamaNeg
-expr - exprAritmetik çıkarmaSub
-=var -= exprAritmetik çıkarma ve eşitlemesiSubAssign
->fn(...) -> type, |...| -> typeFonksiyon ve dönüş tipi
.expr.identÜye erişimi
...., expr.., ..expr, expr..exprSağa özgü aralık değişmeziPartialOrd
..=..=expr, expr..=exprSağ dahil aralık değişmeziPartialOrd
....exprYapı değişmezini güncelleme söz dizimi
..variant(x, ..), struct_type { x, .. }Model atama
...expr...expr(Kaldırıldı, yerine ..= kullanın) Bir modelde: kapsayıcı aralık modeli
/expr / exprAritmetik bölmeDiv
/=var /= exprAritmetik bölme ve eşitlemeDivAssign
:pat: type, ident: typeKısıtlama
:ident: exprYapı alanı oluşturucusu
:'a: loop {...}Döngü adı
;expr;Koşul ve öğe sonlandırıcı
;[...; len]Sabit boyutlu dizi söz diziminin bir parçası
<<expr << exprSola kaydırmaShl
<<=var <<= exprSola kaydırma ve eşitlemeShlAssign
<expr < exprDaha az karşılaştırmasıPartialOrd
<=expr <= exprDaha az ya da eşit karşılaştırmasıPartialOrd
=var = expr, ident = typeAtama/eşitlik
==expr == exprEşitlik karşılaştırmasıPartialEq
=>pat => exprmatch söz diziminin bir parçası
>expr > exprDaha fazla karşılaştırmasıPartialOrd
>=expr >= exprDaha fazla ya da eşit karşılaştırmasıPartialOrd
>>expr >> exprSağa kaydırmaShr
>>=var >>= exprSağa kaydırma ve eşitlemeShrAssign
@ident @ patModel atama
^expr ^ exprBitsel Dışlayıcı VEYA işleviBitXor
^=var ^= exprBitsel Dışlayıcı VEYA işlevi ve eşitlemeBitXorAssign
|pat | patModel alternatifleri
|expr | exprBitsel YA DABitOr
|=var |= exprBitsel YA DA ve eşitlemeBitOrAssign
||expr || exprKısa devre yapan mantıksal YA DA
?expr?Hata yayılımı

Operatör dışı Semboller

Aşağıdaki liste, operatör işlevi görmeyen tüm harfleri içerir. Yani, bir işlev veya yöntem çağrısı gibi davranmazlar

Tablo B-2, kendi başlarına görünen ve çeşitli şekillerde geçerli olan sembolleri göstermektedir.

Tablo B-2: Kendi Başına Söz Dizimi

SembolAçıklama
'identAdlandırılmış ömürlük ya da döngü adı
...u8, ...i32, ...f64, ...usize, vs.Belirli bir türün sayısal değişmezi
"..."Dizgi değişmezi
r"...", r#"..."#, r##"..."##, vs.Ham dizgi değişmezi, kaçış karakterleri işlenmez
b"..."Bitsel dizgi değişmezi; dizgi yerine bitlerden oluşan dizi oluşturur
br"...", br#"..."#, br##"..."##, vs.Ham bitsel dizgi değişmezi, ham ve bitsel dizgi değişmezi kombinasyonu
'...'Karakter değişmezi
b'...'ASCII bit değişmezi
|...| exprKapanış ifadeleri
!Farklı fonksiyonlar için her zaman boş alt tür
_“Ignored” model atama, ayrıca tam sayı değişmezlerini okunuşlu kılmak için de kullanılır

Tablo B-3 modül hiyerarşisinden bir öğeye giden yol bağlamında görünen tüm sembolleri gösterir.

Tablo B-3: Yol Bağlamlı Söz Dizimi

SembolAçıklama
ident::identAd alanı yolu
::pathSandığın köküne giden yol
self::pathHalihazırdaki modüle giden yol
super::pathAna modüle giden yol
type::ident, <type as trait>::identİlişkili sabitler, fonksiyonlar ve türler
<type>::...Doğrudan adlandırılamayan bir tür için ilişkili bir öğe (örneğin, <&T>::..., <[T]>::... vs.)
trait::method(...)Bir metod çağrısını tanımlayan özelliği adlandırarak belirsizliği giderme
type::method(...)Tanımlandığı türü adlandırarak bir metod çağrısının belirsizliğini giderme
<type as trait>::method(...)Tanımını ve türünü adlandırarak bir metod çağrısının belirsizliğini giderme

Tablo B-4 yaygın türü kullanma bağlamında görünen sembolleri gösterir

Tablo B-4: Yaygınlar

SembolAçıklama
path<...>Bir türdeki yaygın tür için parametreleri belirtir (örneğin, Vec<u8>)
path::<...>, method::<...>Bir ifadede yaygın türe, fonksiyona veya metoda ilişkin parametreleri belirtir; genellikle turbofish olarak anılır (örneğin, "42".parse::<i32>())
fn ident<...> ...Yaygın fonksiyon tanımlar
struct ident<...> ...Yaygın yapı tanımlar
enum ident<...> ...Yaygın numaralandırılmış yapı tanımlar
impl<...> ...Yaygın süreklemesini tanımlar
for<...> typeDaha yüksek dereceli şekilde ömürlük sınırlar
type<ident=type>Bir veya daha fazla ilişkili türün belirli atamalara sahip olduğu yaygın bir tür (örneğin, Iterator<Item=T>)

Tablo B-5 tanım sınırlamalarıyla yaygın tür parametrelerinin kısıtlanması bağlamında görünen sembolleri gösterir.

Tablo B-5: Tanıma Bağlı Kısıtlamalar

SembolAçıklama
T: UT yaygın parametresini U'yu uygulayan türlerle sınırlar
T: 'aT yaygın parametresi, a'nın ömründen daha uzun ömürlü olmalıdır
T: 'staticT yaygın parametresi, static olanlar dışında referansı alınmış başka bir referansı içeremez
'b: 'a'b yaygın parametresi, 'a'nın ömründen daha uzun ömürlü olmalıdır
T: ?SizedYaygın tür parametresinin dinamik olarak boyutlandırılmış bir tür olmasına izin ver
'a + trait, trait + traitBileşik tür kısıtlaması

Tablo B-6 makroları çağırma veya tanımlama ve bir öğedeki nitelikleri belirleme bağlamında görünen sembolleri gösterir.

Tablo B-6: Makrolar ve Özellikler

SembolAçıklama
#[meta]Dış özellik
#![meta]İç özellik
$identMakro belirteci
$ident:kindMakro yakalama
$(…)…Makro yineleme
ident!(...), ident!{...}, ident![...]Makro çağırma

Tablo B-7 yorum satırı olarak yorumlanan sembolleri gösterir.

Tablo B-7: Yorum Satırları

SembolAçıklama
//Yorum satırı
//!İç doküman yorum satırı
///Dış doküman yorum satırı
/*...*/Yorum bloğu
/*!...*/İç doküman yorum bloğu
/**...*/Dış doküman yorum bloğu

Tablo B-8 demet yapısı kullanımı bağlamında görünen sembolleri gösterir.

Tablo B-8: Demet Yapısı

BağlamAçıklama
()Boş demet, hem değişmez hem tür
(expr)Parantezli ifade
(expr,)Tek elemanlı demet ifadesi
(type,)Tek elemanlı demet türü
(expr, ...)Demet ifadesi
(type, ...)Demet türü
expr(expr, ...)Fonksiyon çağrı ifadesi; ayrıca struct ve enum varyantlarını çağırmak için kullanılır
expr.0, expr.1, etc.Demet elemanı göstergeci

Tablo B-9 süslü parantezlerin kullanıldığı bağlamları gösterir.

Tablo B-9: Süslü Parantezler

BağlamAçıklama
{...}Blok ifade
Type {...}struct değişmezi

Tablo B-10 köşeli parantezlerin kullanıldığı bağlamları gösterir.

Tablo B-10: Köşeli Parantezler

BağlamAçıklama
[...]Dize değişmezi
[expr; len]expr'i len kadar kopyalayan dize değişmezi
[type; len]typelen kadar tutan dize türü ifadesi
expr[expr]Koleksiyon elemanı göstergeci. (Index, IndexMut) aşırı yüklenebilir
expr[..], expr[a..], expr[..b], expr[a..b]Range, RangeFrom, RangeTo ya da RangeFull kullanarak koleksiyon dilimleyici gibi davranan koleksiyon elemanı göstergeci

Ekleme C: Türetilebilir Tanımlar

Kıtabın farklı bölümlerinde yapıya ya da numaralandırılmış yapıya dahil edebileceğiniz derive tanımını anlatmıştık. derive tanımı, yazdığınız derive süreklemesini sürekleyerek kod üretir.

Bu ekte, derive ile kullanabileceğiniz standart kütüphanedeki tüm tanımların bir referansını sunuyoruz. Her bölüm şunları kapsar:

  • Bu tanımı türeten hangi operatörler ve yöntemler etkinleştirilecek
  • derive tarafından sağlanan tanımın süreklenmesi ne iş yapar
  • Tanımın süreklenmesi, tür için ne anlama gelir
  • Tanımı süreklemenize izin verilen ve verilmeyen koşullar
  • Tanımı gerektirecek operasyonların örnekleri

derive tarafından sağlanandan farklı bir davranış istiyorsanız, bunların manuel olarak nasıl sürekleneceğine ilişkin ayrıntılar için standart kütüphanenin dokümantasyonuna bakın. Standart kütüphanede tanımlanan özelliklerin geri kalanı derive kullanılarak türlerinize uygulanamaz. Bu özelliklerin mantıklı varsayılan davranışları yoktur, bu nedenle bunları başarmaya çalıştığınız şey için anlamlı olacak şekilde süreklemek size kalmıştır. Son kullanıcılar için biçimlendirmeyi yöneten Display, türetilemeyen bir tanıma örnektir. Her zaman bir türü son kullanıcıya göstermenin uygun yolunu düşünmelisiniz. Bir son kullanıcının türün hangi kısımlarını görmesine izin verilmelidir? Hangi kısımları alakalı bulacaklar? Hangi veri formatı onlar için en uygun olur? Rust derleyicisi bu içgörüye sahip değildir, bu nedenle sizin için uygun varsayılan davranışı sağlayamaz. Bu ekte sağlanan türetilebilir tanımların listesi kapsamlı değildir: kütüphaneler, türetmeyi kendi tanımları için sürekleyebilir ve kullanabileceğiniz özelliklerin listesini gerçekten açık uçlu olarak türetebilir. derive'ın süreklemesi, Bölüm 19'un “Makrolar” bölümünde ele alınan prosedürel bir makro kullanmayı içerir.

Programcı Çıktısı için Debug Tanımı

Debug tanımı {}'ın içine :? koyarak kullanabileceğiniz hata ayıklama formatlaması için dizgi düzenlemesini aktifleştirir.

Debug tanımı nesnelerin türlerini hata ayıklamanız için yazdırmaya izin verir.

assert_eq! makrosunun kullanımında Debug tanımını kullanmak gereklidir. Bu makro eğer her iki nesne de birbirine eşit değilse bir hata göndererek yazılımcıların nerede hata olduğunu görmelerine yardımcı olur.

Eşitlik Kıyaslamaları için PartialEq ve Eq Tanımları

PartialEq tanımı == ve != operatörlerini herhangi bir nesne türünde kullanmanız için gerekli olan tanımdır.

PartialEq'ı tanımlamak eq metodunu otomatik olarak sürekler. Her ne zaman PartialEq yapılarda tanımlanırsa, her iki nesnenin her alt üyesi eşitse nesnelerin eşitliğine karar verir. Numaralandırılmış yapılar için her varyant kendi içinde eşittir ve diğer varyantlarla eşitlik söz konusu değildir.

Her iki nesnenin eşitliğinin kontrolü için assert_eq! makrosunda PartialEq'i tanımlamak gereklidir.

Eq tanımı herhangi bir metoda sahip değildir. Asıl amacı verilen türün her değeri için eşitliğinin kontrolüdür. Eq tanımı sadece PartialEq'i de tanımlayan tanımlar için geçerlidir. Ayrıca PartialEq'i sürekleyebilen her tür Eq'i de sürekleyebileceği anlamına gelmez. Bir örnek olarak, kayan nokta sayı türleri örnek verilebilir. Bu türün süreklemesi her iki nesnenin de birbirine eşit olmadığı durumlarda sayı-değili (NaN) döndürür.

Örneğin her ne zaman Eq, HashMap<K, V> için gerekirse bize her iki nesnenin de eşit olup olmadığını söyleyebilecek bir tanımı belirtmiş olur.

Sıralama Kıyaslamaları için PartialOrd ve Ord Tanımları

PartialOrd tanımı, sıralama amaçları için türleri kıyaslamaya izin verir. PartialOrd'ı sürekleyen bir tür, <, >, <=, ve >= operatörlerini de aynı tür için kullanabilir hale gelir. PartialOrd tanımını ancak PartialEq'i de tanımlayan tanımlar için kullanabilirsiniz.

PartialOrd'ı tanımlamak ayrıca eğer verilen değerler herhangi bir sıralamaya tabii tutulamadığı durumlarda None, diğer durumlarda Option<Ordering> döndüren partial_cmp metodunu da sürekler. Bir örnek olaraktan, partial_cmp'ı herhangi bir kayan noktasal sayı ile kullanmak None döndürecektir.

Her ne zaman yapılar için tanımlanmışsa, PartialOrd her iki nesnenin her üye içindeki her değerini kıyaslar. Numaralandırılmış yapılar için tanımlandığında, tüm varyantları önceden tanımlanandan sonradan tanımlanana şeklinde sıralar.

PartialOrd tanımı bu durumlar için gereklidir; örneğin rand kasasındaki gen_range metodu, verilen aralıkta ve ifadede sözde rastgele bir değer üretir.

Ord tanımı ayrıca verilen iki değer arasında doğru bir sıralamanın olup olmadığını da size belirtir. Ord tanımı, Option<Ordering> yerine Ordering tanımını döndüren cmp metodunu sürekler. Ordering tanımını döndürmesinin asıl sebebi, doğru bir sıralamanın her zaman olabileceği ihtimalindendir.

Ord tanımını türlere ancak PartialOrd ve Eq'i (ayrıca Eq de PartialEq'i tanımlamayı zorunlu kılar) de sürekleyen tanımlar için uygulayabilirsiniz. Yapılar ve numaralandırılmış yapılar üzerinde uygulandığında cmp partial_cmp'in PartialOrd'u süreklerken yaptığı gibi aynı yolu uygular.

BTreeSet<T>'de veriler tutmanız gerektiğinde Ord tanımını kullanmanız gerekir çünkü bu veri yapısı verileri sıralanmış olarak tutar.

Değerleri Çoğaltmak için Clone ve Copy Tanımları

Clone tanımı değerin derin bir kopyasını oluşturmanıza izin verir ve bu çoğaltma işlemi rastgele kod çalıştırmayı ve yığın verisi kopyalamasına sebep olabilir. Clone hakkında daha fazla bilgi için “Değişkenler ve Veri Etkileşiminin Yolları: Clone” bölümüne göz atabilirsiniz.

Clone'i tanımlamak ayrıca tüm türler için kullanılabilecek clone metodunu da sürekler.

Örneğin bir dilim üzerinde to_vec metodunu çalıştırmak için Clone'un tanımlanması gerekir.

Copy tanımı yığında tutulan bitleri çoğaltmanıza izin verir. Copy hakkında daha fazla bilgi için “Yığın Öncelikli Veri Tanımı: Copy” bölümüne göz atabilirsiniz.

Copy tanımı herhangi bir rastgele kod çağırma konusunda yapıyı istismar etmeyi önlemek için herhangi bir metod tanımlamaz. Bu yönüyle yazılımcılar bu şekilde veriyi kopyalamanın aşırı hızlı olduğunu düşüneceklerdir.

Copy'i tüm yapı üyeleri Copy'i tanımlayan yapılar için de kullanabilirsiniz. Copy'i tanımlayan bir nesne ayrıca Clone'u da tanımlamak zorundadır çünkü, Copy'i tanımlayan bir tür önemsiz bir Clone tanımlamasına da sahiptir, yani Copy ile benzer görevi yapar.

Copy tanımı çoğu zaman gerekmez; Copy'i tanımlayan türler ayrıca optimizasyonlara da sahiptir yani clone kullanmamanız kodunuzu daha yalın ve öz yapacaktır.

Copy ile mümkün olan her şey Clone ile de mümkündür fakat clone metodunun kullanılması bazı yerlerde kodunuzu yavaşlatabilmektedir.

Değerleri Birleştirmek için Hash Tanımı

Hash tanımı sabit değerli bir türün örneğini almanıza izin verir. Hash'i tanımlamak ayrıca hash metodunu da sürekler.

Varsayılan Değerler için Default Tanımı

Default tanımı bir türe varsayılan bir değer atamanıza izin verir. Default'u tanımlamak default fonksiyonunu da sürekler.

Default::default fonksiyonu çoğunlukla “Yapı Güncelleme Söz Dizimi ile Diğer Örneklerden Örnekler Oluşturma” bölümünde de belirtildiği şekliyle yapı güncelleme söz dizimiyle birlikte kullanılır. Yapının üyelerini düzenleyebilir ve daha sonra geri kalan üyeler için varsayılan tür değerlerini ..Default::default() ile atayabilirsiniz.

Option<T> örnekleri üzerinde unwrap_or_default metodunu kullanabilmeniz için Default tanımınının da tanımlanmış olması gerekir. Örneğin, Option<T> None ise , unwrap_or_default metodu Option<T>'de depolanan T türü için Default::default'un sonucunu döndürecektir.

Ekleme D - Kullanışlı Geliştirme Araçları

Bu eklemede, Rust projesinin bize sağladığı bazı kullanışlı geliştirme araçlarından bahsedeceğiz. Otomatik düzenleyici, hata ve uyarı çözücü, kod düzenleyici ve nasıl TGO'lar (IDE) ile kullanabileceğinizi göstereceğiz.

rustfmt ile Otomatik Düzenleme

rustfmt aracı kodunuzu topluluk kodu stili şeklinde düzenler. Çoğu proje rustfmt'yi hangi Rust stilini kullanmakta kararsız kalındığı vakit kullanır: herkes bu aracı kullanarak kodunu düzenler.

rustfmt'i yüklemek için şu komutu girin:

$ rustup component add rustfmt

Bu komut size rustfmt ve cargo-fmt araçlarını verir, nasıl Rust'ın rustc ve cargo'yu birlikte dağıttığı gibi. Herhangi bir Cargo projesini düzenlemek için, şunu girin:

$ cargo fmt

Bu komudu çalıştırmak halihazırdaki kasada bulunan tüm Rust kodlarınızı düzenler. Kodunuzun çalışma şeklini ve mantığını değiştirmez sadece kod stilini değiştirir. rustfmt hakkında daha fazla bilgi almak için dokümantasyonunu kullanabilirsiniz.

rustfix ile Kodunuzu Çözümleme

rustfix aracı Rust'ın yüklemelerine dahil edilmiş, bazı derleyici hatalarını otomatik olarak çözümleyen bir araçtır. Eğer Rust'ta kod yazmışsanız, büyük ihtimalle derleyici hatalarını ve uyarılarını da görmüşsünüzdür. Örnek olaraktan, şu koda odaklanın:

Dosya: src/main.rs

fn do_something() {}

fn main() {
    for i in 0..100 {
        do_something();
    }
}

Burada, do_something adlı fonksiyonu 100 kez çağırıyoruz ama hiçbir zaman i değerini döngü içinde kullanmıyoruz. İşte bu yüzden Rust bizi bunun hakkında uyaracaktır:

$ cargo build
   Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: unused variable: `i`
 --> src/main.rs:4:9
  |
4 |     for i in 0..100 {
  |         ^ help: consider using `_i` instead
  |
  = note: #[warn(unused_variables)] on by default

    Finished dev [unoptimized + debuginfo] target(s) in 0.50s

Bu uyarı bize i yerine _i kullanmamız gerektiğini sunar: bu alt çizgi ile bu değerin kullanılmayacağını söylemiş oluyoruz. Bu öneriyi otomatik olarak eklemek istiyorsanız cargo fix komutu vasıtasıyla rustfix aracını kullanın:

$ cargo fix
    Checking myprogram v0.1.0 (file:///projects/myprogram)
      Fixing src/main.rs (1 fix)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s

src/main.rs koduna tekrar baktığımızda, cargo fix'in bazı yerleri değiştirdiğini görüyoruz:

Dosya adı: src/main.rs

fn do_something() {}

fn main() {
    for _i in 0..100 {
        do_something();
    }
}

for döngüsü değeri artık _i şekliyle adlandırıldı, artık uyarı çıkmayacak.

cargo fix komutunu ayrıca farklı Rust sürümleri arasında kodunuzu güncelleştirmek için kullanabilirsiniz. Sürümler Ekleme E'de açıklanacak.

Clippy ile Daha Fazla Düzenleme

Clippy aracı düzenleme tavsiyelerinin bir koleksiyonunu tutan ve bunlar vasıtasıyla kodunuzu analiz eden ve size kodunuz hakkında bilgiler ve öneriler veren bir araçtır.

Clippy'i indirmek için, şu komutu girin:

$ rustup component add clippy

Herhangi bir Cargo projesinde Clippy’nin düzenlemelerini kullanmak için, şunu girin:

$ cargo clippy

Örnek olaraktan, diyelim ki bir matematik sabitini yakınsayarak hesaplamak istiyorsunuz, mesela pi olsun, bu program onu yapar:

Dosya adı: src/main.rs

fn main() {
    let x = 3.1415;
    let r = 8.0;
    println!("the area of the circle is {}", x * r * r);
}

cargo clippy'i bu projede çalıştırmak bize şu hatayı verir:

error: approximate value of `f{32, 64}::consts::PI` found. Consider using it directly
 --> src/main.rs:2:13
  |
2 |     let x = 3.1415;
  |             ^^^^^^
  |
  = note: #[deny(clippy::approx_constant)] on by default
  = help: for further information visit https://rust-lang-nursery.github.io/rust-clippy/master/index.html#approx_constant

Bu hata, Rust'ın bu sabiti daha kesin olarak tanımladığını ve bunun yerine sabiti kullanırsanız programınızın daha doğru olacağını belirtir. Rust'ın sunduğu PI sabitini kullandığınızda, aşağıdaki kod herhangi bir hata ya da uyarı vermeden düzenlenir:

Dosya adı: src/main.rs

fn main() {
    let x = std::f64::consts::PI;
    let r = 8.0;
    println!("the area of the circle is {}", x * r * r);
}

Clippy hakkında daha fazla bilgi almak için dokümantasyonunu kullanabilirsiniz.

rust-analyzer Kullanarak TGO (IDE) Entegrasyonu

TGO entegrasyonu için Rust topluluğu rust-analyzer kullanmanızı öneriyor. Bu araç bazı TGO'lar ve dillerin birbirleri arasındaki iletişimi sağlayan Dil Sunucu Protokolü, vasıtasıyla derleyici tabanlı bir iletişim ağı oluşturur. Farklı editörler rust-analyzer'i kullanabilir, mesela Visual Studio Code için Rust analyzer eklentisi kullanılabilir.

Kurulum yönergeleri için rust-analyzer projesinin ana sayfasına gidebilir, daha sonra TGO'nuz için desteklenen dil sunucusunu kurabilirsiniz. TGO'nuz bazı yenilikler edinecektir, mesela otomatik tamamlama, tanıma yönlendirme, satır içi hata ve uyarılar vs.

Ekleme E - Sürümler

Bölüm 1'de cargo new komutunun Cargo.toml dosyasına sürümle alakalı bilgiler eklediğini gördünüz. Bu eklemede bunun ne anlama geldiğini konuşacağız!

Rust dili ve derleyicisi altı-haftalık yeni özellikler içeren değişmez bir sürüm döngüsüne sahiptir. Diğer programlama dilleri yeni sürümleri fazla değişikliklerle seyrek sıklıklarla yayınlarlar, Rust ise küçük değişikliklerle sıklıkla yayınlar. Daha sonra, tüm küçük değişiklikler toplanır ve yayınlandıktan sonra geriye bakıp şunu demek zor olabilir: “Vay be, Rust 1.10 ve Rust 1.31'ındaki farka bak, Rust ne kadar da değişmiş!”

Her iki ya da üç yılda bir Rust takımı yeni Rust sürümü yayınlar. Her sürüm yeni özellikleri tamamıyla dokümantasyonlaşmış ve araç gereçleri tam halde getirir. Yeni özellikler her altı-haftalık yayınlama işleminin bir parçasıdır.

Yeni sürümler farkli kişiler için farklı işlemlere hizmet eder:

  • Aktif Rust kullanıcıları için yeni sürüm kolayca anlaşılabilir paketlere büyük değişiklikler getirir.
  • Kullanıcı olmayanlar için yeni sürüm Rust'ı farklı bir bakışta baktıracak önemli yenilikler getirdiğinin sinyalidir.
  • Rust'ı geliştirenler için yeni sürüm projenin bütünü için bir dönüm noktasıdır.

Bu yazının zamanında, üç Rust sürümü ulaşılabilirdir: Rust 2015, Rust 2018, Rust 2021. Bu kitap Rust 2021 sürümünün kuralları ve deyimleri kullanılarak yazılmıştır.

Cargo.toml'daki edition anahtarı, kodunuzun hangi derleyici sürümüyle derleneceğini belirtir. Eğer anahtar bulunmuyorsa, Rust geriye dönük uyumluluktan dolayı 2015 sürümünü kullanır.

Her proje varsayılan olan 2015 sürümünden başka sürümü kullanabilir. Sürümler uyumsuz değişiklikler barındırabilir, örneğin yeni bir anahtar sözcük kodunuzun derlenememesine yol açabilir. Ancak, bu değişiklikleri seçmediğiniz sürece, kodunuz yeni Rust derleyici sürümlerini kullansanız bile derlenmeye devam edecektir.

Tüm Rust derleyici sürümleri var olan önceki sürümleri destekleyecek şekildedir. Ve farklı sürümdeki kasaları desteklenen herhangi sürümlerle bağlayabilirsiniz. Sürüm değişiklikleri sadece derleyicinin ayrıştırdığı kodu etkiler. Öyleyse, eğer Rust 2015 kullanıyorsanız ve kullandığınız bağımlılık Rust 2018 sürümünü kullanıyorsa, kodunuz bu bağımlılığı kullanarak derlenecektir. Aynı şekilde tam tersi durumda da derlenecektir.

Açık olmak gerekirse: çoğu özellik tüm sürümlerde ulaşılabilecek Geliştiriciler herhangi bir Rust sürümünü kullandıklarında dahi, yeni stabil sürümlerin gelişimlerini göreceklerdir. ANcak, bazı durumlarda, özellikle yeni anahtar sözcükler eklendiğinde bazı yeni özellikler sadece sonraki sürümlerde ulaşılabilir olmaktadır. Eğer özelliklerin avantajlarını edinmek istiyorsanız, yeni sürümlere geçmeniz gerekmektedir.

Daha fazla detay için, Sürüm Kılavuzu kitabını inceleyerek sürümler arasındaki farkı görebilir ve yeni sürümlere kodunuzu nasıl is cargo fix komutuyla güncelleyebileceğinizi öğrenebilirsiniz.

Ekleme F: Kitabın Çevirileri

Şu anda da bulunduğunuz bu çeviri gibi, İngilizceden farklı diller için çevrilmiş halleri de bulunmaktadır. Bunların çoğu halen çevrilme aşamasındadır; Çeviri kısmına giderek bizi yeni bir çeviri hakkında bilgilendirebilirsiniz!

Ekleme G - “Gecelik Rust” ve Rust Nasıl Yapıldı

Bu ek, Rust'ın nasıl yapıldığı ve bunun bir Rust geliştiricisi olarak sizi nasıl etkilediği hakkındadır.

Durgunluk Olmadan Stabilite

Rust, kodunuzun kararlılığına çok önem verir. Rust'ı üzerine inşa edebileceğiniz kaya gibi sağlam bir temel olmasını istiyoruz ve işler sürekli değişiyor olsaydı bu imkansız olurdu. Aynı zamanda, yeni özellikleri deneyemezsek kusurları çözemeyiz. Bu soruna bizim çözümümüz “durgunluk olmadan istikrar” dediğimiz şeydir ve yol gösterici ilkemiz şudur: Asla yeni bir kararlı Rust sürümüne geçmekten korkmamalısınız. Her yükseltme sorunsuz olmalı, ancak size yeni özellikler, daha az hata ve daha hızlı derleme süreleri de getirmelidir.

Çuf çuf! Kanalları Bırakın ve Yeni Trenlere Binin

Rust'ın gelişimi, bir tren tarifesine göre çalışır. Yani, tüm geliştirmeler Rust deposunun ana dalında yapılır. Sürümler, Cisco IOS ve diğer yazılım projeleri tarafından kullanılan bir yazılım sürüm dizisi modelini takip eder.

Rust için üç yayın kanalı vardır:

  • Gecelik
  • Beta
  • Stabil

Çoğu Rust geliştiricisi ana olarak stabil kanalını kullanır fakat deneysel yeni özellikleri denemek isteyen Rustseverler isterlerse gecelik ya da beta kanallarını da kullanabilirler.

Geliştirme ve sürüm sürecinin nasıl çalıştığına dair bir örnek: Rust ekibinin Rust 1.5'in sürümü üzerinde çalıştığını varsayalım. Bu sürüm Aralık 2015'te yayınlandı ve Rust'a yeni bir çok özellik eklendi: bunlar eklenirken ana dalda birçok yeni bir taahhüt (commit) gönderilmiş oluyor. Her gece, Rust'ın yeni bir gecelik versiyonu üretilir. Her gün bir yayın günüdür ve bu yayınlar yayın altyapımız tarafından otomatik olarak oluşturulur. Zaman geçtikçe, yayınlarımız gecede bir kez şöyle görünür:

nightly: * - - * - - *

Her altı haftada bir, bizim için yayın zamanıdır! beta dalı Rust deposundan geceliğin kullandığı master dalını çıkarır. Burada artık iki yayın var olmuş olur:

nightly: * - - * - - *
                     |
beta:                *

Çoğu Rust kullanıcısı beta yayınlarını aktif olarak kullanmaz, ancak Rust'ın olası sorunları keşfetmesine yardımcı olmak için CI sistemlerinde betaya karşı test yapar. Bu arada, her gece gecelik sürümünün yayınlandığını hatırlatmış olalım:

nightly: * - - * - - * - - * - - *
                     |
beta:                *

Diyelim ki bir sorun bulundu. İyi bir şey olacak ki, sorun kararlı bir sürüme girmeden önce beta sürümünü test edebilecek vaktimiz kalıyor. Düzeltme master'a uygulanır, böylece oluşan sorunlar gece düzeltilir ve ardından düzeltme beta dalına geri aktarılır ve yeni bir beta sürümü üretilir:

nightly: * - - * - - * - - * - - * - - *
                     |
beta:                * - - - - - - - - *

İlk beta oluşturulduktan altı hafta sonra, stable sürümünün zamanı geldi! stable dalı beta dalından türetilir:

nightly: * - - * - - * - - * - - * - - * - * - *
                     |
beta:                * - - - - - - - - *
                                       |
stable:                                *

Yaşasın! Rust 1.5 tamam! Ancak bir şeyi unuttuk: altı hafta geçtiği için, Rust'ın bir sonraki sürümü olan 1.6'nın yeni bir beta sürümünün de yayınlanması gerekiyor. Bu nedenle, beta'nın stable'dan dallanmasından sonra, beta'nın bir sonraki sürümü her gece tekrar dallanır ve güncellenir:

nightly: * - - * - - * - - * - - * - - * - * - *
                     |                         |
beta:                * - - - - - - - - *       *
                                       |
stable:                                *

Buna “tren modeli” denir, çünkü her altı haftada bir, bir sürüm ”istasyonu terk eder”, ancak kararlı bir sürüm olarak gelmeden önce yine de beta kanalında bir yolculuğa daha çıkması gerekir. Rust, saat altı gibi haftada bir çıkıyor. Bir Rust sürümünün tarihini biliyorsanız, bir sonrakinin tarihini de bilirsiniz: altı hafta sonra. Her altı haftada bir planlanan yayınlara sahip olmanın güzel bir yönü, bir sonraki trenin yakında gelmesidir. Bir özellik belirli bir sürümü kaçırırsa, endişelenmenize gerek yok: kısa süre içinde bir yenisi daha geliyor! Bu, son teslim tarihine yakın olası cilasız özellikleri gizlice sokma baskısını azaltmaya yardımcı olur. Bu süreç sayesinde, her zaman Rust'ın bir sonraki sürümünü kontrol edebilir ve yükseltmenin kolay olduğunu kendiniz doğrulayabilirsiniz: bir beta sürümü beklendiği gibi çalışmazsa, bunu ekibe bildirebilir ve güncellemeden önce düzeltmesini sağlayabilirsiniz ve sonraki kararlı sürümde çıkabilecek bir sorun çözülmüş olur! Beta sürümünde çökmeler nispeten nadirdir, ancak rustc hala bir yazılım parçasıdır ve sorunlar mevcuttur.

Kararsız Özellikler

Bu sürüm modeliyle ilgili bir sorun daha var: kararsız özellikler.

Rust, belirli bir sürümde hangi özelliklerin etkinleştirildiğini belirlemek için “özellik bayrakları“ adı verilen bir teknik kullanır.

Yeni bir özellik aktif olarak geliştiriliyorsa, o özelliği gerekli özellik bayraklarını kullanarak çağırabilirsiniz. En son değişiklikleri kullanabilmeniz için nightly de olmanız gerekebilir. Bir kullanıcı olarak, devam eden çalışmaları denemek istiyorsanız, deneyebilirsiniz, ancak Rust'ın her gece yayınlanan sürümünü kullanıyor olmanız ve istenilen özelliği kullanabilmek için kaynak kodunuza uygun bayrakla açıklama eklemeniz gerekir. Son teknolojiyi kullanmak isteyenler bunu yapabilir ve kaya gibi sağlam bir deneyim isteyenler kararlı bir şekilde kalabilir ve kodlarının kırılmayacağını bilir. Durgunluk olmadan istikrar. Bu kitap yalnızca kararlı özellikler hakkında bilgi içerir, çünkü devam eden özellikler hala değişmektedir ve kesinlikle bu kitabın yazıldığı zaman ile kararlı yapılarda etkinleştirildikleri zaman arasında farklı olacaktır. Yalnızca gecelik özelliklerle ilgili belgeleri çevrimiçi olarak bulabilirsiniz.

Rustup ve Rust Nightly'nin Rolü

Rustup, farklı yayın kanalları arasındaki dönüşümleri sizin için kolaylaştırır. Varsayılan olarak, stabil Rust sürümünün yüklü olması gerekir. Gecelik sürümünü yükleyebilimeniz için şu komutu girebilirsiniz:

$ rustup toolchain install nightly

Göründüğü gibi tüm araç takımlarını (Rust'ın sürüm yayınlarını ve bileşenlerini) rustup'la da pekala yükleyebilirsiniz. İşte Windows bilgisayarından bir örnek:

> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc

Gördüğünüz gibi, kararlı araç zinciri varsayılandır. Çoğu Rust kullanıcısı çoğu zaman kararlı sürümü kullanır. Kök dizindeyken rustup'un kullanması gereken tek gecelik araç zincirini ayarlamak için o projenin dizininde rustup override komutunu kullanabilirsiniz:

$ cd ~/projects/needs-nightly
$ rustup override set nightly

RFC Süreci ve Ekipler

Peki bu yeni özellikler hakkında nasıl bilgi edineceksiniz? Rust'ın geliştirme modeli, bir Yorum İsteği (RFC) sürecini takip eder. Rust'ta bir iyileştirme istiyorsanız, RFC adlı bir teklif yazabilirsiniz. Rust'ı geliştirmek için herkes RFC'ler yazabilir ve öneriler, birçok konu alt ekibinden oluşan Rust ekibi tarafından incelenir ve tartışılır. Rust'ın web sitesinde, dil tasarımı, derleyici uygulaması, altyapı, dokümantasyon ve daha fazlası gibi projenin her alanı için ekipleri içeren tam bir ekip listesi bulunmaktadır. Uygun ekip teklifi ve yorumları okur, kendi yorumlarını yazar ve sonunda özelliği kabul veya reddetme konusunda fikir birliği sağlanır. Özellik kabul edilirse Rust deposunda bir sorun açılır ve birisi bunu . Bunu çok iyi uygulayan kişi, özelliği ilk etapta öneren kişi olmayabilir! Uygulama hazır olduğunda, “Kararsız Özellikler” bölümünde tartıştığımız gibi, bir özellik kapısının arkasındaki ana dal üzerine iner. Bir süre sonra, her gece yayınlananları kullanan Rust geliştiricileri yeni özelliği deneyebildiklerinde, ekip üyeleri bu özelliği, her gece nasıl çalıştığını tartışacak ve kararlı Rust'a dönüştürüp dönüştürmeyeceğine karar verecek. Bu da gelecek trenin içinde olabileceği anlamına gelebilir!