Rust dasturlash tili
Steve Klabnik va Carol Nicholsning, Rust hamjamiyatining hissasi bilan
Kitobning ushbu versiyasi siz Rust 1.67.1 (2023-02-09-da chiqarilgan) yoki undan keyingi versiyasidan foydalanayotganingizni taxmin qiladi. Rustni o'rnatish yoki yangilash uchun 1-bobning ”O'rnatish” bo'limiga qarang.
HTML formati https://doc.rust-lang.org/stable/book/ saytida va oflayn rejimda rustup
bilan qilingan Rust o'rnatishlari mavjud; ochish uchun rustup docs --book
ni ishga tushiring.
Bir nechta hamjamiyat tarjimalari ham mavjud.
Ushbu kitob qog'oz va elektron kitob formatida No Starch Press nashrida mavjud.
🚨 Ko'proq interaktiv o'rganishni xohlaysizmi? Rust Bookning boshqa versiyasini sinab koʻring: viktorinalar, alohida chizilgan belgilashlar, vizualizatsiya va boshqalar: https://rust-book.cs.brown.edu
Muqaddima
Bu har doim ham unchalik aniq bo'lmagan, lekin Rust dasturlash tili asosan imkoniyatlarni kengaytirishga qaratilgan: hozir qanday kod yozayotganingizdan qat'iy nazar, Rust sizga oldinga qaraganda kengroq domenlarda ishonch bilan dasturlash imkonini beradi.
Masalan, memory managementning low-leveldagi tafsilotlari, ma'lumotlarni taqdim etish va parallellik bilan shug'ullanadigan "systems-leveldagi" ishni olaylik. An'anaga ko'ra, dasturlashning bu sohasi sirli bo'lib ko'rinadi, unga faqat o'zining dahshatli tuzoqlaridan qochish uchun zarur yillarini o'rganishga bag'ishlagan tanlanganlargina foydalanishi mumkin. Va hatto buni amalda qo'llayotganlar ham, ularning kodi ekspluatatsiyalar, buzilishlar yoki korruptsiyaga ochiq bo'lmasligi uchun buni ehtiyotkorlik bilan bajaradilar.
Rust eski tuzoqlarni yo'q qilish va sizga yordam berish uchun do'stona, sayqallangan vositalar to'plamini taqdim etish orqali bu to'siqlarni yo'q qiladi. Low-leveldagi boshqaruvga ”pastga tushishi” kerak bo'lgan dasturchilar buni Rust bilan odatiy holga tushib qolish xavfi yoki xavfsizlik teshiklarisiz va o'zgaruvchan toolchainning nozik tomonlarini o'rganmasdan amalga oshirishlari mumkin. Yaxshisi, bu til sizni tabiiy ravishda tezlik va xotiradan foydalanish jihatidan samarali bo'lgan ishonchli kodga yo'naltirish uchun mo'ljallangan.
Low leveldagi kod bilan ishlayotgan dasturchilar o'zlarining bilimlarini oshirish uchun Rust-dan foydalanishlari mumkin. Masalan, Rust-da parallelizmni joriy qilish nisbatan low-riskli operatsiya: kompilyator siz uchun klassik xatolarni aniqlaydi. Va siz tasodifan nosozliklar yoki zaifliklarni kiritmasligingizga ishonch bilan kodingizdagi yanada tajovuzkor optimallashtirishlarni hal qilishingiz mumkin.
Ammo Rust low-leveldagi tizimlarni dasturlash bilan cheklanmaydi. Bu CLI ilovalari, veb-serverlar va boshqa ko'plab turdagi kodlarni yozishni juda yoqimli qilish uchun yetarlicha ifodali va samarador - siz ikkalasining oddiy misollarini keyinroq kitobda topasiz. Rust bilan ishlash sizga bir domendan boshqasiga o'tadigan ko'nikmalarni shakllantirish imkonini beradi; veb-ilovani yozish orqali Rustni o'rganishingiz mumkin, so'ngra Raspberry Pi-ni nishonga olish uchun xuddi shu ko'nikmalarni qo'llashingiz mumkin.
Ushbu kitob o'z foydalanuvchilarini kuchaytirish uchun Rust imkoniyatlarini to'liq qamrab oladi. Bu sizga nafaqat Rust haqidagi bilimingizni, balki umuman dasturchi sifatidagi ishonchingizni oshirishga yordam beradigan samimiy va qulay kitob. Shunday qilib, sho'ng'ing, o'rganishga tayyor bo'ling va Rust hamjamiyatiga xush kelibsiz!
— Nicholas Matsakis va Aaron Turon
Kirish
Eslatma: Kitobning ushbu nashri No Starch Press-dan bosma va elektron kitob formatida mavjud bo'lgan Rust dasturlash tili bilan bir xil.
Rust dasturlash tiliga xush kelibsiz, Rust haqida kirish kitobi. Rust dasturlash tili tezroq va ishonchli dasturlarni yozishga yordam beradi. Yuqori darajadagi samaradorlik va low-leveldagi boshqaruv ko'pincha dasturlash tilini loyihalashda bir-biriga zid keladi; Rust bu ziddiyatga qarshi turadi. Kuchli texnik imkoniyatlar va ishlab chiquvchilarning ajoyib tajribasini muvozanatlash orqali Rust sizga an'anaviy ravishda bunday nazorat bilan bog'liq bo'lgan barcha qiyinchiliklarsiz low-leveldagi tafsilotlarni (masalan, xotiradan foydalanish) boshqarish imkoniyatini beradi.
Rust kim uchun
Rust turli sabablarga ko'ra ko'p odamlar uchun idealdir. Keling, eng muhim guruhlarning bir nechtasini ko'rib chiqaylik.
Dasturchilar jamoalari
Rust turli darajadagi tizimlarni dasturlash bo'yicha bilimga ega bo'lgan yirik ishlab chiquvchilar guruhlari o'rtasida hamkorlik qilish uchun samarali vosita ekanligini isbotlamoqda. Low-leveldagi kod turli xil nozik xatolarga moyil bo'lib, ko'pchilik boshqa tillarda ularni faqat keng ko'lamli sinov va tajribali ishlab chiquvchilar tomonidan sinchkovlik bilan tekshirish orqali aniqlash mumkin.Rust-da kompilyator ushbu qiyin xatolar, jumladan, parallellik xatolari bilan kodni kompilyatsiya qilishni rad etib, darvozabon rolini o'ynaydi. Kompilyator bilan birga ishlash orqali jamoa xatolarni ta'qib qilishdan ko'ra, vaqtini dastur mantig'iga qaratishga sarflashi mumkin.
Rust shuningdek, tizim dasturlash dunyosiga zamonaviy ishlab chiquvchilar vositalarini olib keladi:
- Cargo dependency menejeri va build toolni o'z ichiga oladi, Rust ekotizimida bog'liqliklarni qo'shish, kompilyatsiya qilish va boshqarishni qiyinchiliksiz va davomli qiladi.
- Rustfmt formatlash vositasi ishlab chiquvchilar orasida barqaror kodlash uslubini ta'minlaydi.
- Rust Language Server kodni toʻldirish va inline xato xabarlari uchun Integrated Development Environment (IDE) integratsiyasini quvvatlaydi.
Rust ekotizimidagi ushbu va boshqa vositalardan foydalangan holda, ishlab chiquvchilar tizim darajasidagi kodni yozishda samarali bo'lishi mumkin.
Talabalar
Rust talabalar va tizim tushunchalarini o'rganishga qiziquvchilar uchun. Rust-dan foydalanib, ko'p odamlar operatsion tizimlarni ishlab chiqish kabi mavzular haqida bilib oldilar. Jamiyat juda mehmondo'st va talabalar savollariga javob berishdan xursand. Ushbu kitob kabi sa'y-harakatlar orqali Rust guruhlari tizim tushunchalarini ko'proq odamlar, ayniqsa dasturlash uchun yangi bo'lganlar uchun qulayroq qilishni xohlashadi.
Kompaniyalar
Yuzlab yirik va kichik kompaniyalar ishlab chiqarishda Rust-dan CLI dasturlar, veb-xizmatlar, DevOps toollari, embedded qurilmalar, audio va video tahlillari va transkodlar, kriptovalyutalar, bioinformatika, qidiruv tizimlari, Internet of Things ilovalari kabi turli vazifalar uchun foydalanadilar. , machine learning va hatto Firefox veb-brauzerining asosiy qismlari.
Open Source dasturchilar
Rust Rust dasturlash tilini, hamjamiyatini, ishlab chiquvchilar vositalarini va kutubxonalarini yaratmoqchi bo'lgan odamlar uchundir. Rust tiliga o'z hissangizni qo'shishingizni istardik.
Tezlik va barqarorlikni qadrlaydigan odamlar
Rust dasturlash tili tezlik va barqarorlikni xohlaydigan odamlar uchundir. Tezlik deganda biz Rust kodi qanchalik tez ishlashini va Rust sizga dasturlar yozish imkonini beradigan tezligini nazarda tutamiz. Rust kompilyatorining tekshiruvlari qo'shimcha funksiyalar va refaktoring orqali barqarorlikni ta'minlaydi. Bu ishlab chiquvchilar ko'pincha o'zgartirishdan qo'rqadigan ushbu tekshiruvlarsiz tillardagi mo'rt eski koddan farqli o'laroq. Nol xarajatli abstraktsiyalarga, qo'lda yozilgan kod kabi tezroq lower-leveldagi kodni kompilyatsiya qiladigan higher-leveldagi funktsiyalarga intilish orqali Rust xavfsiz kodni ham tezkor kod qilishga intiladi.
Rust tili boshqa ko'plab foydalanuvchilarni ham qo'llab-quvvatlashga umid qiladi; Bu yerda tilga olinganlar faqat eng katta manfaatdor tomonlardan biri hisoblanadi. Umuman olganda, Rustning eng katta ambitsiyalari xavfsizlik va unumdorlik, tezlik va samaradorlikni ta'minlash orqali dasturchilar o'nlab yillar davomida qabul qilgan kelishuvlarni yo'q qilishdir. Rust-ni sinab ko'ring va uning tanlovlari sizga mos keladimi yoki yo'qligini tekshiring.
Bu kitob kim uchun
Ushbu kitobda siz boshqa dasturlash tilida kod yozgansiz deb taxmin qilinadi, lekin qaysi biri haqida hech qanday taxminlar yo'q. Biz materialni turli xil dasturlash tajribasiga ega bo'lganlar uchun keng foydalanishga harakat qildik. Biz dasturlash nima ekanligi yoki u haqida qanday fikr yuritish haqida gapirishga ko'p vaqt sarflamaymiz. Agar siz dasturlashda mutlaqo yangi bo'lsangiz, dasturlash bilan tanishishni ta'minlaydigan kitobni o'qisangiz yaxshi bo'lardi.
Ushbu kitobdan qanday foydalanish kerak
Umuman olganda, bu kitob siz uni oldindan orqaga ketma-ket o'qiyotganingizni taxmin qiladi. Keyingi boblar oldingi boblardagi tushunchalarga asoslanadi va oldingi boblar ma'lum bir mavzu bo'yicha tafsilotlarni o'rganmasligi mumkin, lekin keyingi bobda mavzuni qayta ko'rib chiqadi.
Ushbu kitobda siz ikki xil bo'limni topasiz: kontseptsiya bo'limlari va loyiha bo'limlari. Kontseptsiya boblarida siz Rustning bir tomoni haqida bilib olasiz. Loyiha bo'limlarida biz hozirgacha o'rganganlaringizni qo'llagan holda kichik dasturlarni birgalikda tuzamiz. 2, 12 va 20-boblar loyiha boblari; qolganlari kontseptsiya boblari.
1-bobda Rustni qanday o'rnatish, "Hello, world!" dasturi va Cargo, Rust paket menejeri va build tooldan qanday foydalanishni ko'rib chiqamiz. 2-bob Rustda dastur yozish bo'yicha amaliy kirish bo'lib, siz raqamlarni taxmin qilish o'yinini tuzasiz. Bu yerda biz tushunchalarni yuqori darajada yoritamiz va keyingi boblarda qo'shimcha tafsilotlar beriladi. Agar siz darhol qo'llaringizni ifloslantirmoqchi bo'lsangiz, 2-bob buning uchun joy. 3-bobda boshqa dasturlash tillariga oʻxshash Rust funksiyalari yoritilgan va 4-bobda siz Rustning ownershp tizimi haqida bilib olasiz. Agar siz keyingisiga o‘tishdan oldin har bir tafsilotni o‘rganishni ma’qul ko‘radigan, ayniqsa sinchkov o‘quvchi bo‘lsangiz, 2-bobni o‘tkazib yuborib, to‘g‘ridan-to‘g‘ri 3-bobga o‘tishingiz va loyiha ustida ishlashni hohlaganingizda 2-bobga qaytishingiz mumkin. siz o'rgangan tafsilotlar.
5-bobda structlar va metodlar muhokama qilinadi, 6-bob esa enumlar, match
expressionlari va if let
control flow konstruksiyasini qamrab oladi. Rust-da maxsus turlarni yaratish uchun struclar va enumlardan foydalanasiz.
7-bobda siz Rust modul tizimi va kodingizni va uning umumiy amaliy dasturlash interfeysini (API) tashkil qilish uchun maxfiylik qoidalari haqida bilib olasiz. 8-bobda standart kutubxona taqdim etadigan vektorlar, stringlar va hash maplar kabi umumiy yig'ish ma'lumotlar tuzilmalari muhokama qilinadi. 9-bob Rustning xatolarni hal qilish falsafasi va usullarini o'rganadi.
10-bob generiklar, traitlar va lifetimeni o'rganadi, bu sizga bir nechta turlarga tegishli kodni aniqlash imkoniyatini beradi. 11-bob sinovdan o'tadi, bu hatto Rustning xavfsizlik kafolatlari bilan ham dasturingiz mantig'ining to'g'riligini ta'minlash uchun zarurdir. 12-bobda biz fayllar ichidagi matnni qidiradigan grep
buyruq qatori vositasidan o'zimizning funksiyalar to'plamini yaratamiz. Buning uchun biz oldingi boblarda muhokama qilgan ko'plab tushunchalardan foydalanamiz.
13-bob yopilishlar va iteratorlarni o'rganadi: Rustning funktsional dasturlash tillaridan kelib chiqadigan xususiyatlari. 14-bobda biz Cargolarni chuqurroq ko'rib chiqamiz va kutubxonalaringizni boshqalar bilan baham ko'rishning eng yaxshi amaliyotlari haqida gaplashamiz. 15-bobda standart kutubxona taqdim etadigan smart pointerlar va ularning funksionalligini ta'minlaydigan traitlar muhokama qilinadi.
16-bobda biz bir vaqtning o'zida dasturlashning turli modellarini ko'rib chiqamiz va Rust sizga bir nechta mavzularda qo'rqmasdan dasturlashda qanday yordam berishi haqida gaplashamiz. 17-bobda Rust idiomlari sizga tanish bo'lishi mumkin bo'lgan obyektga yo'naltirilgan(OOP) dasturlash tamoyillari bilan qanday taqqoslanishi ko'rib chiqiladi.
18-bobda Rust dasturlari bo'ylab g'oyalarni ifodalashning kuchli usullari bo'lgan patternlar va patternlarni moslashtirish haqida ma'lumot berilgan. 19-bobda ilg'or qiziqarli mavzular, jumladan xavfli Rust, makroslar va boshqa ko'p narsalar mavjud.
20-bobda biz low-leveldagi ko'p tarmoqli veb-serverni amalga oshiradigan loyihani yakunlaymiz!
Va nihoyat, ba'zi qo'shimchalarda til haqida foydali ma'lumotlar ko'proq mos yozuvlar formatida mavjud. A ilovasida Rustning kalit so'zlari, B ilovasida Rust operatorlari va belgilari, C ilovasi standart kutubxona tomonidan taqdim etilgan hosila traitlarini o'z ichiga oladi, D ilovasi ba'zi foydali ishlab chiqish vositalarini qamrab oladi va E ilovasida Rust nashrlari tushuntiriladi. F ilovasida siz kitobning tarjimalarini topishingiz mumkin, G ilovasida esa Rust qanday qilinganligi va nightlyli Rust nima ekanligini ko'rib chiqamiz.
Ushbu kitobni o'qishning noto'g'ri usuli yo'q: agar siz oldinga o'tmoqchi bo'lsangiz, unga boring! Agar chalkashliklarga duch kelsangiz, avvalgi boblarga qaytishingiz kerak bo'lishi mumkin. Lekin siz uchun nima ish qilsa, shuni qiling.
Rustni o'rganish jarayonining muhim qismi kompilyator ko'rsatadigan xato xabarlarini o'qishni o'rganishdir: ular sizni ish kodiga yo'naltiradi. Shunday qilib, biz kompilyator har bir vaziyatda sizga ko'rsatadigan xato xabari bilan birga kompilyatsiya qilinmaydigan ko'plab misollarni keltiramiz. Bilingki, agar siz tasodifiy misol kiritsangiz va ishlatsangiz, u kompilyatsiya qilinmasligi mumkin! Ishlamoqchi bo'lgan misol xato uchun mo'ljallanganligini bilish uchun atrofdagi matnni o'qiganingizga ishonch hosil qiling. Ferris, shuningdek, ishlash uchun mo'ljallanmagan kodni ajratishga yordam beradi:
Ferris | Ma'nosi |
---|---|
Bu kod kompilyatsiya qilinmaydi! | |
Bu kod panic! | |
Ushbu kod kerakli xatti-harakatni keltirib chiqarmaydi. |
Aksariyat hollarda biz sizni kompilyatsiya qilinmagan har qanday kodning to'g'ri versiyasiga olib boramiz.
Manba kodi
Ushbu kitob yaratilgan manba fayllarni GitHubda topish mumkin.
Ishni boshlash
Rust sayohatingizni boshlaymiz! O'rganish uchun ko'p narsa bor, lekin har bir sayohat bir joydan boshlanadi. Ushbu bobda biz muhokama qilamiz:
- Rustni Linux, macOS va Windows-ga o'rnatish
Hello, world!
ni chop etuvchi dasturni yozishcargo
dan foydalanib, Rust paketlar menejeri va build systemdan foydalanish
O'rnatish
Birinchi qadam Rustni o'rnatishdir.Rustni Rust versiyalari va tegishli vositalarni boshqarish uchun buyruq qatori vositasi bo‘lgan rustup
orqali yuklab olamiz. Yuklab olish uchun sizga internet ulanishi kerak bo'ladi.
Eslatma: Agar biron sababga ko'ra
rustup
dan foydalanmaslikni xohlasangiz, boshqa variantlar uchun Rustni o'rnatishning boshqa usullari sahifasiga qarang.
Quyidagi qadamlar Rust kompilyatorining so'nggi barqaror versiyasini o'rnatadi. Rustning barqarorligi kafolati kitobdagi kompilyatsiya qilingan barcha misollar Rustning yangi versiyalari bilan kompilyatsiya qilishda davom etishini ta'minlaydi. Chiqish versiyalar orasida biroz farq qilishi mumkin, chunki Rust ko'pincha xato xabarlari va ogohlantirishlarni yaxshilaydi. Boshqacha qilib aytadigan bo'lsak, ushbu qadamlar yordamida o'rnatgan har qanday yangi, barqaror Rust versiyasi ushbu kitob mazmuni bilan kutilganidek ishlashi kerak.
Buyruqlar qatori yozuvi
Ushbu bobda va butun kitobda biz terminalda ishlatiladigan ba'zi buyruqlarni ko'rsatamiz. Terminalga kiritishingiz kerak bo'lgan barcha qatorlar
$
bilan boshlanadi.$
belgisini kiritishingiz shart emas; bu har bir buyruqning boshlanishini ko'rsatish uchun ko'rsatilgan buyruq qatori.$
bilan boshlanmagan qatorlar odatda oldingi buyruqning natijasini ko'rsatadi. Bundan tashqari, PowerShell-ga xos misollarda$
emas,>
ishlatiladi.
Linux yoki macOS-ga rustup
o'rnatish
Agar siz Linux yoki macOS dan foydalansangiz, terminalni oching va quyidagi buyruqni kiriting:
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
Buyruq skriptni yuklab oladi va Rustning eng so'nggi barqaror versiyasini o'rnatadigan rustup
vositasini o'rnatishni boshlaydi. Sizdan parol so'ralishi mumkin. O'rnatish muvaffaqiyatli bo'lsa, quyidagi qator paydo bo'ladi:
Rust is installed now. Great!
Shuningdek, sizga linker, kerak bo'ladi, ya'ni Rust o'zining kompilyatsiya qilingan natijalarini bitta faylga birlashtirish uchun foydalanadigan dastur. Ehtimol,bu sizda allaqachon mavjud. Agar linker xatolarga duch kelsangiz, odatda linkerni o'z ichiga olgan C kompilyatorini o'rnatishingiz kerak. C kompilyatori ham foydalidir, chunki ba'zi umumiy Rust paketlari C kodiga bog'liq va C kompilyatoriga muhtoj bo'ladi.
MacOS-da siz C kompilyatorini ishga tushirish orqali olishingiz mumkin:
$ xcode-select --install
Linux foydalanuvchilari odatda distributiv texnik hujjatlariga muvofiq GCC yoki Clang o'rnatishlari kerak. Misol uchun, agar siz Ubuntu'dan foydalansangiz, build-essential
paketini o'rnatishingiz mumkin.
Windows-ga rustup
o'rnatish
Windows tizimida https://www.rust-lang.org/tools/install saytiga o'ting va Rustni o'rnatish bo'yicha ko'rsatmalarga amal qiling. O'rnatishning bir nuqtasida sizga Visual Studio 2013 yoki undan keyingi versiyalari uchun MSVC yaratish vositalari kerakligi haqida xabar keladi.
Build toolsini olish uchun Visual Studio 2022 ni o'rnatishingiz kerak bo'ladi. Qaysi ish dasturlarini o'rnatish kerakligi so'ralganda, quyidagilarni kiriting:
- “Desktop Development with C++”
- TWindows 10 yoki 11 SDK
- Ingliz tili to'plami komponenti va siz tanlagan boshqa tillar to'plami
Ushbu kitobning qolgan qismi cmd.exe va PowerShell da ishlaydigan buyruqlardan foydalanadi. Agar aniq farqlar bo'lsa, qaysi birini ishlatishni tushuntiramiz.
Muammolarni bartaraf etish
Rust to'g'ri o'rnatilganligini tekshirish uchun shellni oching va quyidagi qatorni kiriting:
$ rustc --version
Quyidagi formatda chiqarilgan so‘nggi barqaror versiya uchun versiya raqami, xesh va tasdiqlangan sanani ko‘rishingiz kerak:
rustc x.y.z (abcabcabc yyyy-mm-dd)
Agar siz ushbu ma'lumotni ko'rsangiz, Rustni muvaffaqiyatli o'rnatdingiz! Agar siz ushbu ma'lumotni ko'rmasangiz, Rust %PATH%
tizim o'zgaruvchingizda quyidagi tarzda ekanligini tekshiring.
Windows CMD-da quyidagilardan foydalaning:
> echo %PATH%
PowerShell-da foydalaning:
> echo $env:Path
Linux va macOS-da quyidagilardan foydalaning:
$ echo $PATH
Agar hammasi to'g'ri bo'lsa va Rust hali ham ishlamasa, yordam olishingiz mumkin bo'lgan bir qancha joylar mavjud. Boshqa Rustaceanlar (biz o'zimizni chaqiradigan ahmoqona taxallus) bilan qanday bog'lanishni hamjamiyat sahifasida bilib oling.
Yangilash va o'chirish
Rust rustup
orqali o'rnatilgandan so'ng, yangi chiqarilgan versiyaga yangilash oson. Shelldan quyidagi yangilash skriptini ishga tushiring:
$ rustup update
Rust va rustup
-ni o'chirish uchun shelldan quyidagi o'chirish skriptini ishga tushiring:
$ rustup self uninstall
Mahalliy texnik hujjatlar
Rust-ning o'rnatilishi texnik hujjatlarning mahalliy nusxasini ham o'z ichiga oladi, shunda siz uni oflayn rejimda o'qishingiz mumkin. Brauzeringizda mahalliy texnik hujjatlarni ochish uchun rustup doc
dasturini ishga tushiring.
Istalgan vaqtda standart kutubxona tomonidan tur yoki funksiya taqdim etilsa va siz u nima qilishini yoki undan qanday foydalanishni bilmasangiz, bilish uchun amaliy dasturlash interfeysi (API) texnik hujjatlaridan foydalaning!
Hello, World!
Endi siz Rustni o'rnatdingiz, hozir sizning birinchi Rust dasturingizni yozishning ayni vaqti.
Yangi dasturlash tilini o'rganishda Hello, World!
matnini ekranga chop etuvchi kichik va sodda
dastur tuzish an'anaga aylangan, shunday ekan biz ham sinab ko'ramiz!
Eslatma: Bu kitob terminal bilan ishlay olishning boshlang'ich ko'nikmalarini talab qiladi. Rust sizning kod muxarriringiz foydalanadigan asboblaringiz va kodingizni qayerda joylayishi bo'yicha talablar qo'ymaydi, shuning uchun agar siz terminal o'rniga integratsiyalashgan ishlab chiqish muhitidan (IDE) foydalanishni afzal ko'rsangiz, o'zingizning sevimli IDE-dan foydalaning. Ko'pgina IDElar endi ma'lum darajada Rust-ni qo'llab-quvvatlaydi; tafsilotlar uchun IDE hujjatlarini tekshiring. Rust jamoasi
rust-analyzer
orqali ajoyib IDE yordamini ta'minlashga e'tibor qaratdi. Batafsil ma’lumot uchun D ilovasini ko'zdan kechiring.
Loyiha jildini yaratish
Siz ishni Rust kodingizni joylaytirish uchun jild yaratishdan boshlaysiz. Rust uchun sizning kodingiz qayerda joylashining ahamiyati yo'q, lekin biz bu kitobdagi mashq va loyihalarni joylash uchun projects nomli jild yaratishingizni maslahat beramiz.
Terminalni oching va projects jildini yaratish va uning ichidan “Hello, world!” loyihasi jildini yaratish uchun quyidagi buyruqlarni kiriting.
Linux, macOS va Windows Powershell uchun:
$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
Windows CMD uchun:
> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world
Rust dasturi yozish va ishga tushirish.
Endi, main.rs nomli yangi fayl yarating. Rust kodlar har doim .rs kengaytmasi bilan tugaydi. Agar fayl nomida bir nechta so'zlardan foydalansangiz, ularni ajratish uchun pastki chiziqdan foydalanish shart. Masalan, helloworld.rs o'rniga hello_world.rs dan foydalaning.
Endi hozirgina yaratgan main.rs faylingizni kod muharririda oching.
Fayl nomi: main.rs
fn main() { println!("Hello, world!"); }
Faylni saqlang va Terminalda ~/projects/hello_world jildiga qayting. Linux yoki macOS da faylni kompilyatsiya qilish va ishga tushirish uchun quyidagi buyruqlarni kiriting:
$ rustc main.rs
$ ./main
Hello, world!
Windowsda ./main
ning o'rniga .\main.exe
buyrug'ini kiriting:
> rustc main.rs
> .\main.exe
Hello, world!
Operatsion tizimingizdan qat'i nazar, terminalda Hello, world!
qatori chop etilishi kerak.Agar siz ushbu chiqishni ko'rmasangiz, yordam olish usullari uchun O'rnatish bo'limining ”Muammolarni bartaraf etish” bo'limiga qayting.
Agar Hello, world!
chop etilgan bo'lsa, tabriklaymiz! Siz rasmiy ravishda Rust dasturini yozdingiz. Bu sizni Rust dasturchisiga aylantiradi - xush kelibsiz!
Rust dasturining tuzilishi.
Keling "Hello, world!" dasturiga chuqurroq nazar solamiz. Boshqotirmaning 1-qismi:
fn main() { }
Bu qatorlar main
nomli funksiyani e'lon qiladi. main
funksiyasi alohida: u har doim bajariladigan Rust dasturida ishlaydigan birinchi koddir. Bu yerda birinchi satr hech qanday parametrga ega boʻlmagan va hech narsani qaytarmaydigan main
funksiyasini eʼlon qiladi.
Agar parametrlar mavjud bo'lsa, ular ()
qavslar ichiga kiradi.
Funksiyasing tanasi {}
bilan o'ralgan. Rust har bir funksiyalarda e'lon qilishda
{}
dan foydalanishni talab qiladi.
Eslatma: Agar siz Rust loyihalarda standart usulda kod yozmoqchi bo'lsangiz kodingizni maʼlum bir uslubda formatlash uchun
rustfmt
nomli avtomatik formatlash vositasidan foydalanishingiz mumkin (batafsilroqrustfmt
D ilovasi -da) Rust jamoasi ushbu vositani standart Rust distributiviga kiritdi, chunkirustc
kabi, u allaqachon kompyuteringizga o'rnatilgan bo'lishi kerak!
main
funksiyaning tanasi quyidagi kodni o'z ichiga oladi:
#![allow(unused)] fn main() { println!("Hello, world!"); }
Shu bir qator kod shu kichik dasturdagi barcha ishni amalga oshiardi: u matnni ekranga chop etadi.Bu yerda ahamiyat qaratish zarur bo'lgan to'rtta muhim narsalar bor.
Birinchidan, Rust style to'rtta bo'shliqdan iborat tab emas
Ikkinchidan, println!
Rust makrosini chaqiradi. Agar u funktsiyani o'rniga chaqirgan bo'lsa, u println
(!
belgisiz) sifatida kiritiladi. Biz Rust makrolari haqida 19-bobda batafsilroq muhokama qilamiz.Hozircha siz shuni bilishingiz kerakki, !
belgisidan foydalanish oddiy funksiya o‘rniga makrosni chaqirayotganingizni anglatadi va makrolar har doim ham funksiyalar bilan bir xil qoidalarga amal qilmaydi.
Uchinchidan, siz "Hello, world!"
qatorini ko'rasiz. Bu satrni argument sifatida println!
ga uzatamiz va satr ekranga chop etiladi.
To'rtinchidan, satrni nuqtali vergul (;
) bilan tugatamiz, bu esa bu ifoda tugaganligini va keyingisi boshlashga tayyorligini bildiradi. Rust kodining aksariyat satrlari nuqtali vergul bilan tugaydi.
Kompilyatsiya va ishga tushirish alohida bosqichlardir
Siz yangi yaratilgan dasturni ishga tushirdingiz, shuning uchun jarayonning har bir bosqichini ko'rib chiqamiz.
Rust dasturini ishga tushirishdan oldin uni Rust kompilyatoridan foydalanib, rustc
buyrug‘ini kiritib, unga manba faylingiz nomini quyidagi tarzda kiritishingiz kerak:
$ rustc main.rs
Agar siz C yoki C++ bilan ishlagan bo'lsangiz, bu gcc
yoki clang
ga o'xshashligini sezasiz. Muvaffaqiyatli kompilyatsiyadan so'ng Rust binary bajariladigan faylni chiqaradi.
Linux, macOS va Windows-dagi PowerShell-da siz shelldagi ls
buyrug'ini kiritish orqali bajariladigan faylni ko'rishingiz mumkin:
$ ls
main main.rs
Linux va macOS-da siz ikkita faylni ko'rasiz. Windows-dagi PowerShell bilan siz CMD-dan foydalangan holda ko'rgan uchta faylni ko'rasiz. Windows-da CMD bilan siz quyidagilarni kiritasiz:
> dir /B %= the /B faqat fayl nomlarini ko'rsatishni aytadi =%
main.exe
main.pdb
main.rs
Bu sizga .rs kengaytmali kod faylini, bajariluvchi faylni(Windowsda main.exe
boshqa barcha tizimlarda main
), va Windowsdan foydalanayotganingizda, debugging
ma'lumotlarini o'z ichida saqlovchi .pdb kengaytmali faylni ko'rsatadi.
Bu yerdan siz main yoki main.exe faylini ishga tushirasiz, masalan:
$ ./main # or .\Windows-da main.exe
Agar sizning main.rs faylingiz “Hello, world!” dasturi bo'lsa, bu dastur
ekranga Hello, world!
matnini chop etadi.
Agar siz Ruby, Python yoki JavaScript kabi dinamik tilni yaxshi bilsangiz, dasturni alohida bosqichlar sifatida kompilyatsiya qilish va ishga tushirishga odatlanmagan bo'lishingiz mumkin. Rust - bu oldindan tuzilgan kompilyatsiya tili, ya'ni siz dasturni kompilyatsiya qilishingiz va bajariladigan faylni boshqa birovga berishingiz mumkin va ular Rustni o'rnatmasdan ham uni ishga tushirishlari mumkin.Agar siz kimgadir .rb, .py yoki .js faylini bersangiz, ularda Ruby, Python yoki JavaScript ilovasi oʻrnatilgan boʻlishi kerak (mos ravishda). Ammo bu tillarda dasturni kompilyatsiya qilish va ishga tushirish uchun faqat bitta buyruq kerak bo'ladi. Til dizaynida hamma narsa o'zaro kelishuvdir.
Oddiy dasturlar uchun rustc
bilan kompilyatsiya qilish juda mos keladi, lekin loyihangiz o'sib borishi bilan siz barcha variantlarni boshqarishni va kodingizni almashishni osonlashtirishni xohlaysiz.
Endi, biz siz bilan haqiqiy Rust dasturlarini tuzishda qulaylik yaratuvchi
Cargo yordamchisi bilan tanishamiz.
Hello, Cargo!
Cargo - bu Rustning build tizimi va paketlar menejeri. Aksariyat Rustaceanlar o'zlarining Rust loyihalarini boshqarish uchun ushbu vositadan foydalanadilar, chunki Cargo siz uchun kodni yaratish, kodingizga bog'liq kutubxonalarni yuklab olish va ushbu kutubxonalarni yaratish kabi ko'plab vazifalarni bajaradi.(Biz sizning kodingizga kerak bo'lgan kutubxonalarni chaqiramiz dependencies.)
Eng oddiy Rust dasturlari, biz hozirgacha yozganimiz kabi, hech qanday dependencylarga ega emas. Agar biz “Hello, world!” Cargo bilan loyiha bo'lsa, u faqat sizning kodingizni yaratish bilan shug'ullanadigan Cargo qismidan foydalanadi. Murakkab Rust dasturlarini yozganingizda, siz dependencylarni qo'shasiz va agar siz Cargo yordamida loyihani boshlasangiz, dependencylarni qo'shish osonroq bo'ladi.
Rust loyihalarining aksariyati Cargolardan foydalanganligi sababli, ushbu kitobning qolgan qismida siz ham Cargodan foydalanasiz deb taxmin qilinadi. O'rnatish bo'limida muhokama qilingan rasmiy o'rnatuvchilardan foydalansangiz, Cargo Rust bilan birga keladi. Agar siz Rust-ni boshqa vositalar orqali o'rnatgan bo'lsangiz, terminalingizga quyidagilarni kiritish orqali Cargo o'rnatilganligini tekshiring:
$ cargo --version
Agar siz versiya raqamini ko'rsangiz, sizda bor! Agar siz command not found
kabi xatolikni ko'rsangiz, Cargoni qanday qilib alohida o'rnatish bo'yicha texnik hujjatlarni ko'rib chiqing.
Cargo bilan loyiha yaratish
Keling, Cargo-dan foydalanib yangi loyiha yarataylik va u bizning asl “Hello, world!” loyihadan qanday farq qilishini ko'rib chiqaylik. O'zingizning projects jildigiga (yoki kodingizni saqlashga qaror qilgan joyingizga) qayting. Keyin istalgan operatsion tizimda quyidagilarni bajaring:
$ cargo new hello_cargo
$ cd hello_cargo
Birinchi buyruq hello_cargo nomli yangi jild va loyihani yaratadi. Biz loyihamizga hello_cargo deb nom berdik va Cargo o'z fayllarini xuddi shu nomdagi jildda yaratadi.
hello_cargo jildiga o'ting va fayllar ro'yxatini ko'ring.Cargo biz uchun ikkita fayl va bitta jild yaratganini ko'rasiz: Cargo.toml fayli va ichida main.rs fayli bo'lgan src jildi.
Shuningdek, u .gitignore fayli bilan birga yangi Git repositoryni ishga tushirdi. Mavjud Git repositoryda cargo new
ni ishga tushirsangiz, Git fayllari yaratilmaydi; cargo new - vcs=git
yordamida bu xatti-harakatni bekor qilishingiz mumkin.
Eslatma: Git keng tarqalgan versiya boshqaruv tizimidir. Siz
--vcs
buyrugʻi yordamidacargo new
ni boshqa versiyani boshqarish tizimidan foydalanishga yoki versiyani boshqarish tizimisiz foydalanishga oʻzgartirishingiz mumkin. Mavjud variantlarni ko'rish uchuncargo new --help
ni ishga tushiring.
Siz tanlagan matn muharririda Cargo.tomlni oching. U 1-2 ro'yxatdagi kodga o'xshash bo'lishi kerak.
Fayl nomi: Cargo.toml
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
Bu fayl TOML da (Tom’s Obvious, Minimal Language) formati, bu Cargo konfiguratsiya formati.
Birinchi qator, [package]
, bo'lim sarlavhasi bo'lib, quyidagi iboralar paketni sozlayotganligini bildiradi.Ushbu faylga qo'shimcha ma'lumot qo'shsak, biz boshqa bo'limlarni qo'shamiz.
Keyingi uchta qatorda Cargo dasturingizni kompilyatsiya qilish uchun kerak bo'lgan konfiguratsiya ma'lumotlarini o'rnatadi: Rustning nomi, versiyasi va foydalanish uchun nashri.
E ilovasida edition
kaliti haqida gaplashamiz.
Oxirgi qator, [dependencies]
, loyihangizning har qanday dependencylarini ro'yxatlash uchun bo'limning boshlanishi. Rustda kod paketlari crates deb ataladi. Ushbu loyiha uchun bizga boshqa cratelar kerak bo'lmaydi, lekin biz 2-bobdagi birinchi loyihada bo'lamiz, shuning uchun biz ushbu dependencies bo'limidan foydalanamiz.
Endi src/main.rs oching va qarang:
Fayl nomi: src/main.rs
fn main() { println!("Hello, world!"); }
Cargo “Hello, world!” siz uchun dastur, xuddi biz Ro'yxat 1-1 da yozganimiz kabi! Hozircha, bizning loyihamiz va yaratilgan Cargo loyihasi o'rtasidagi farq shundaki, Cargo kodni src jildiga joylashtirgan va bizda yuqori jildda Cargo.toml konfiguratsiya fayli mavjud.
Cargo sizning manba fayllaringiz src jildida turishini kutadi. Yuqori darajadagi loyiha jildi faqat README fayllari, litsenziya maʼlumotlari, konfiguratsiya fayllari va kodingizga aloqador boʻlmagan boshqa narsalar uchun moʻljallangan. Cargo-dan foydalanish loyihalaringizni tartibga solishga yordam beradi. Hamma narsaning o'rni bor va hamma narsa o'z o'rnida.
Agar siz “Hello, world!” bilan qilganimizdek, Cargo-dan foydalanmaydigan loyihani boshlagan bo'lsangiz, uni Cargo-dan foydalanadigan loyihaga aylantirishingiz mumkin. Loyiha kodini src jildiga o'tkazing va tegishli Cargo.toml faylini yarating.
Cargo loyihasini qurish va ishga tushirish
Keling, “Hello, world!” ni qurish va ishga tushirishda nima farq qilishini ko'rib chiqaylik. Cargo bilan dasturni hello_cargo jildidan quyidagi buyruqni kiritish orqali loyihangizni build qiling:
$ cargo build
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs
Ushbu buyruq bajariladigan faylni joriy jildingizda emas, balki target/debug/hello_cargo da (yoki Windowsda target\debug\hello_cargo.exe)da yaratadi. Odatiy tuzilish debug tuzilishi bo'lgani uchun Cargo binary faylni debug nomli jildga joylashtiradi. Ushbu buyruq bilan bajariladigan faylni ishga tushirishingiz mumkin:
$ ./target/debug/hello_cargo # yoki .\target\debug\hello_cargo.exe Windowsda
Hello, world!
Agar hammasi yaxshi bo'lsa, Hello, world!
terminalga chop etilishi kerak.cargo build
ni birinchi marta ishga tushirish ham Cargoning yuqori darajadagi yangi faylni yaratishiga olib keladi: Cargo.lock. Ushbu fayl loyihangizdagi dependencylarning aniq versiyalarini kuzatib boradi. Ushbu loyihada dependencylar yo'q, shuning uchun faylda kod biroz kam. Siz hech qachon ushbu faylni qo'lda o'zgartirishingiz shart emas; Cargo uning tarkibini siz uchun boshqaradi.
Biz hozirgina cargo build
orqali loyihasini build qildik va uni ./target/debug/hello_cargo
bilan ishga tushirdik, lekin kodni kompilyatsiya qilish uchun cargo run
dan ham foydalanishimiz va natijada bajariladigan faylni bitta buyruqda ishga tushirishimiz mumkin:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/hello_cargo`
Hello, world!
cargo run
dan foydalanish cargo build
ni ishga tushirishdan ko'ra qulayroqdir va keyin binary yo'lni to'liq ishlatadi, shuning uchun ko'pchilik ishlab chiquvchilar cargo run
dan foydalanadilar.
E'tibor bering, bu safar biz Hello_cargo
ni kompilyatsiya qilayotganini ko'rsatadigan natijani ko'rmadik. Cargo fayllar o'zgarmaganligini aniqladi, shuning uchun u qayta tiklanmadi, balki binary faylni ishga tushirdi. Agar siz manba kodingizni o'zgartirgan bo'lsangiz, Cargo loyihani ishga tushirishdan oldin uni qayta build qilgan bo'lar edi va siz ushbu natijani ko'rgan bo'lar edingiz:
$ 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 shuningdek, cargo check
deb nomlangan buyruqni taqdim etadi. Bu buyruq kompilyatsiya qilish uchun kodingizni tezda tekshiradi, lekin bajariladigan fayl yaratmaydi:
$ cargo check
Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
Nima uchun bajariladigan faylni xohlamaysiz? Ko'pincha cargo check
cargo build
dan ko'ra tezroq bo'ladi,, chunki u bajariladigan faylni yaratish bosqichini o'tkazib yuboradi. Agar siz kod yozish paytida ishingizni doimiy ravishda tekshirayotgan bo'lsangiz, cargo check
dan foydalanish loyihangiz hali ham kompilyatsiya qilinayotganligini bildirish jarayonini tezlashtiradi! Shunday qilib, ko'plab Rustaceanlar vaqti-vaqti bilan cargo check
ni amalga oshiradilar, chunki ular o'z dasturlarini kompilyatsiya qilishiga ishonch hosil qilish uchun yozadilar. Keyin ular bajariladigan fayldan foydalanishga tayyor bo'lgach, cargo build
ni ishga tushiradilar.
Cargo haqida shu paytgacha o'rganganlarimizni takrorlaymiz:
- Biz
cargo new
yordamida loyiha yaratamiz. cargo build
yordamida loyihani build qilishimiz mumkin.- Biz
cargo run
yordamida bir bosqichda loyiha build qilishimiz va ishga tushirishimiz mumkin. cargo check
yordamida xatolarni tekshirish uchun binary ishlab chiqarmasdan loyihani build qilishimiz mumkin.- Build natijasini bizning kodimiz bilan bir xil jildda saqlash o'rniga, Cargo uni target/debug jildida saqlaydi.
Cargo-dan foydalanishning qo'shimcha afzalligi shundaki, qaysi operatsion tizimda ishlayotganingizdan qat'i nazar, buyruqlar bir xil bo'ladi. Shunday qilib, biz endi Linux va MacOS uchun Windows-ga nisbatan maxsus ko'rsatmalar bermaymiz.
Loyihani Reliz qilish
Loyihangiz nihoyat relizga tayyor bo'lgach, uni optimallashtirish bilan kompilyatsiya qilish uchun cargo build --release
dan foydalanishingiz mumkin. Ushbu buyruq target/debug o'rniga target/release da bajariladigan fayl yaratadi. Optimizatsiya Rust kodingizni tezroq ishga tushiradi, lekin bu kompilyatsiya vaqtini uzaytiradi. Shuning uchun ikkita turli profil mavjud: biri tez va tez-tez qayta tiklamoqchi bo'lganingizda ishlab chiqish uchun, ikkinchisi esa oxirgi dasturni yaratish uchun siz foydalanuvchiga qayta tiklanmaydigan va mkon qadar tez ishlaydigan oxirgi dastur. Agar siz kodingizning ishlash vaqtini solishtirmoqchi bo'lsangiz, cargo build --release
dasturini ishga tushiring va target/release da bajariladigan fayl bilan taqqoslang.
Konventsiya sifatida Cargo
Oddiy loyihalar bilan Cargo rustc
dan foydalanishdan ko'ra unchalik katta foyda keltirmaydi, ammo dasturlaringiz yanada murakkablashgani sayin u o'z qiymatini isbotlaydi.
Dasturlar bir nechta fayllarga ko'payib rivojlanganda yoki ularga dependency kerak bo'lsa, Cargo-ga buildni muvofiqlashtirishga ruxsat berish ancha oson bo'ladi.
hello_cargo
loyihasi oddiy bo'lsa ham, u endi Rust karyerangizning qolgan qismida foydalanadigan haqiqiy asboblarning ko'p qismini ishlatadi. Haqiqatan ham, mavjud loyihalar ustida ishlash uchun siz Git yordamida kodni tekshirish, ushbu loyiha jildiga oʻzgartirish va build qilish uchun quyidagi buyruqlardan foydalanishingiz mumkin:
$ git clone github.com/birorta-loyiha
$ cd birorta-loyiha
$ cargo build
Cargo haqida ko'proq ma'lumot olish uchun uning texnik hujjatlarini tekshiring.
Xulosa
Siz allaqachon Rust sayohatingizni ajoyib boshladingiz! Ushbu bobda siz quyidagilarni o'rgandingiz:
- Rust-ning so'nggi barqaror versiyasini
rustup
yordamida o'rnatish - Rustning yangi versiyasiga yangilash
- Mahalliy o'rnatilgan texnik hujjatlarni ochish
- “Hello, world!” deb yozing va ishga tushiring. to'g'ridan-to'g'ri
rustc
dan foydalangan holda dastur - Cargo konventsiyalaridan foydalangan holda yangi loyiha yaratish va ishga tushirish
Bu Rust kodini o'qish va yozishga odatlanish uchun yanada muhimroq dastur yaratish uchun ajoyib vaqt. Shunday qilib, 2-bobda biz taxminiy o'yin dasturini tuzamiz. Agar siz Rust-da umumiy dasturlash tushunchalari qanday ishlashini o'rganishni afzal ko'rsangiz, 3-bobga qarang va keyin 2-bobga qayting.
Taxmin qilish o'yinini dasturlash
Keling, birgalikda amaliy loyiha orqali Rustga o'taylik! Ushbu bob sizni bir nechta umumiy Rust tushunchalari bilan tanishtirib, ulardan haqiqiy dasturda qanday foydalanishni ko'rsatib beradi. Siz let
, match
, metodlari, bog'langan funksiyalar, external cratelardan foydalanish va boshqalar haqida bilib olasiz! Keyingi boblarda biz ushbu fikrlarni batafsilroq ko'rib chiqamiz. Ushbu bobda siz faqat asoslarni mashq qilasiz.
Biz klassik boshlang'ich dasturlash muammosini amalga oshiramiz: taxmin qilish o'yini. Bu qanday ishlaydi: dastur 1 dan 100 gacha tasodifiy butun son hosil qiladi. Keyin u o'yinchini taxmin qilishni taklif qiladi.Tahmin kiritilgandan so'ng, dastur taxmin kichik yoki katta ekanligini ko'rsatadi. Agar taxmin to'g'ri bo'lsa, o'yin tabrik xabarini chop etadi va chiqadi.
Yangi loyiha yaratish
Yangi loyihani o'rnatish uchun 1-bobda yaratgan projects jildiga o'ting va Cargo-dan foydalanib yangi loyiha yarating, masalan:
$ cargo new taxminiy_raqam
$ cd taxminiy_raqam
Birinchi cargo new
buyrug'i birinchi argument sifatida loyiha nomini (taxminiy_raqam
)ni oladi. Ikkinchi buyruq yangi loyiha jildiga kiradi.
Yaratilgan Cargo.toml fayliga qarang:
Fayl nomi: Cargo.toml
[package]
name = "taxminiy_raqam"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
1-bobda ko'rganingizdek, cargo new
siz uchun “Hello, world!” so'zini chop etadigan dastur yaratadi. src/main.rs faylini tekshiring:
Fayl nomi: src/main.rs
fn main() { println!("Hello, world!"); }
Keling, ushbu "Hello, world!" dasturni yarating va cargo run
buyrug'i yordamida ishga tushiring :
$ cargo run
Compiling taxminiy_raqam v0.1.0 (file:///projects/taxminiy_raqam)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/taxminiy_raqam`
Hello, world!
run
buyrug‘i loyihani tezda takrorlash kerak bo‘lganda foydali bo‘ladi, biz bu o‘yinda qilganimizdek, keyingisiga o‘tishdan oldin har bir iteratsiyani tezda sinab ko‘ramiz.
src/main.rs faylini qayta oching. Siz ushbu fayldagi barcha kodlarni yozasiz.
Taxmin qilish o'yiniga ishlov berish
Taxmin qilish o'yini dasturining birinchi qismi foydalanuvchi kiritishini so'raydi, ushbu kiritishni qayta ishlaydi va kirish kutilgan shaklda ekanligini tekshiradi. Boshlash uchun biz o'yinchiga taxmin kiritishga ruxsat beramiz. 2-1 ro'yxatdagi kodni src/main.rs ichiga kiriting.
Fayl nomi: src/main.rs
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
println!("Sizning taxminingiz: {taxmin}");
}
Ushbu kod juda ko'p ma'lumotlarni o'z ichiga oladi, shuning uchun uni satrga o'tkazamiz. Foydalanuvchi kiritishini olish va natijani chiqish sifatida chop etish uchun biz io
input/output kutubxonasini qamrab olishimiz kerak. io
kutubxonasi std
deb nomlanuvchi standart kutubxonadan keladi:
use std::io;
Odatda, Rust standart kutubxonada belgilangan elementlar to'plamiga ega bo'lib, u har bir dastur doirasiga kiradi. Ushbu to'plam prelude deb ataladi va siz undagi hamma narsani standart kutubxona texnik hujjatlarida ko'rishingiz mumkin.
Agar siz foydalanmoqchi bo'lgan tur preludeda bo'lmasa, siz ushbu turni use
iborasi bilan aniq kiritishingiz kerak. std::io
kutubxonasidan foydalanish sizga bir qator foydali xususiyatlarni, jumladan, foydalanuvchi kiritishini qabul qilish imkoniyatini beradi.
1-bobda ko'rganingizdek, main
funksiya dasturga kirish nuqtasidir:
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
println!("Sizning taxminingiz: {taxmin}");
}
fn
sintaksisi yangi funktsiyani e'lon qiladi; Qavslar, ()
, hech qanday parametr yo'qligini bildiradi; va jingalak qavs, {
, funksiyaning asosiy qismini boshlaydi.
1-bobda ham bilib olganingizdek, println!
bu ekranga satrni chop etuvchi makros:
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
println!("Sizning taxminingiz: {taxmin}");
}
Ushbu kod o'yin nima ekanligini ko'rsatuvchi va foydalanuvchidan ma'lumot so'rashni chop etadi.
O'zgaruvchilar bilan qiymatlarni saqlash
Keyinchalik, foydalanuvchi ma'lumotlarini saqlash uchun o'zgaruvchi yaratamiz, masalan:
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
println!("Sizning taxminingiz: {taxmin}");
}
Endi dastur qiziqarli bo'lib bormoqda! Bu kichik satrda juda ko'p narsa bor. O'zgaruvchini yaratish uchun let
iborasidan foydalanamiz. Mana yana bir misol:
let olmalar = 5;
Bu qator olmalar
nomli yangi o‘zgaruvchini yaratadi va uni 5 qiymatiga bog‘laydi. Rustda o'zgaruvchilar standard bo'yicha o'zgarmasdir, ya'ni o'zgaruvchiga qiymat berganimizdan keyin qiymat o'zgarmaydi.Biz ushbu kontseptsiyani 3-bobdagi ”O'zgaruvchilar va O'zgaruvchanlik” bo'limida batafsil muhokama qilamiz. Oʻzgaruvchini oʻzgaruvchan qilish uchun oʻzgaruvchi nomidan oldin mut
qoʻshamiz:
let olmalar = 5; // o'zgarmas
let mut bananlar = 5; // o'zgaruvchan
Eslatma:
//
sintaksisi satr oxirigacha davom etadigan izohni boshlaydi. Rust izohlarda hamma narsani e'tiborsiz qoldiradi. Izohlarni 3-bobda batafsilroq muhokama qilamiz.
Taxmin qilish o'yin dasturiga qaytsak, endi bilasizki, let mut taxmin
taxmin
nomli o'zgaruvchan o'zgaruvchini kiritadi. Teng belgisi (=
) Rustga biz hozir biror narsani oʻzgaruvchiga bogʻlamoqchi ekanligimizni bildiradi. Tenglik belgisining o'ng tomonida taxmin
bog'langan qiymat joylashgan bo'lib, u String::new
funksiyasini chaqirish natijasidir, bu String
ning yangi nusxasini qaytaradi.
String standart kutubxona tomonidan taqdim etilgan string turi bo'lib, u rivojlantirib boriladigan, UTF-8 kodlangan matn bitidir.
::new
qatoridagi ::
sintaksisi new
String
tipidagi bog'langan funksiya ekanligini bildiradi. Assosiatsiyalangan funksiya bu funksiya
turida amalga oshiriladi, bu holda String
. Ushbu new
funksiya yangi, bo'sh qatorni yaratadi. Siz ko'p turdagi new
funksiyani topasiz, chunki u qandaydir yangi qiymatni yaratadigan funksiyaning umumiy nomi.
To'liq let mut taxmin = String::new();
qatori hozirda String
ning yangi, bo'sh nusxasiga bog'langan o'zgaruvchan o'zgaruvchini yaratadi.
Foydalanuvchi ma'lumotlarini qabul qilish
Eslatib o'tamiz, biz dasturning birinchi qatoriga use std::io;
bilan standart kutubxonadan input/output funksiyasini kiritgan edik. Endi biz io
modulidan stdin
funksiyasini chaqiramiz, bu bizga foydalanuvchi kiritishini boshqarish imkonini beradi:
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
println!("Sizning taxminingiz: {taxmin}");
}
Agar biz dasturning boshida use std::io;
bilan io
kutubxonasini import qilmagan bo'lsak, biz ushbu funktsiya chaqiruvini std::io::stdin
sifatida yozish orqali funksiyadan foydalanishimiz xam mumkin. stdin
funksiyasi std::io::Stdin
misolini qaytaradi, bu sizning terminalingiz uchun standart kirish uchun asosni ifodalovchi tur.
Keyinchalik, .read_line(&mut taxmin)
qatori foydalanuvchidan ma'lumot olish uchun standart kiritish nuqtasidagi read_line
metodini chaqiradi.
Shuningdek, foydalanuvchi kiritgan maʼlumotlarni qaysi qatorda saqlash kerakligini aytish uchun read_line
ga argument sifatida &mut taxmin
ni oʻtkazamiz. read_line
ning toʻliq vazifasi foydalanuvchi nima yozganidan qatʼiy nazar standart kiritishga olish va uni satrga qoʻshishdir (uning mazmunini qayta yozmasdan), shuning uchun biz bu qatorni argument sifatida beramiz. String argumenti o'zgaruvchan bo'lishi kerak, shuning uchun metod string tarkibini o'zgartirishi mumkin.
&
bu argument reference(havola) ekanligini bildiradi, bu sizga kodingizning bir nechta qismlariga ushbu ma'lumotni xotiraga bir necha marta nusxalash kerak bo'lmasdan bitta ma'lumotga kirish imkonini beradi. Referencelar murakkab xususiyat bo'lib, Rustning asosiy afzalliklaridan biri havolalardan foydalanish qanchalik xavfsiz va oson ekanligidir. Ushbu dasturni tugatish uchun ko'p bilimlrga ega bo'lishingiz shart emas. Hozircha siz bilishingiz kerak bo'lgan narsa shundaki, o'zgaruvchilar singari, havolalar ham standard bo'yicha o'zgarmasdir. Demak, uni oʻzgaruvchan qilish uchun &taxmin
oʻrniga &mut taxmin
yozish kerak. (4-bobda havolalar ko'proq va yaxshiroq tushuntiriladi)
Potensial nosozlikni Result
turi bilan hal qilish
Biz hali ham ushbu kod qatori ustida ishlayapmiz. Biz hozir matnning uchinchi qatorini muhokama qilmoqdamiz, lekin u hali ham bitta mantiqiy kod qatorining bir qismi ekanligini unutmang. Keyingi qism bu metod:
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
println!("Sizning taxminingiz: {taxmin}");
}
Biz ushbu kodni quyidagicha yozishimiz mumkin edi:
io::stdin().read_line(&mut taxmin).expect("Satrni o‘qib bo‘lmadi");
Biroq, bitta uzun qatorni o'qish qiyin, shuning uchun uni bo'lish yaxshidir. .method_name()
sintaksisi bilan metodni chaqirganda uzun qatorlarni ajratishga yordam berish uchun yangi qator va boshqa bo'shliqlarni kiritish ko'pincha oqilona. Endi bu kod nima qilishini muhokama qilaylik.
Yuqorida aytib o'tilganidek, read_line
foydalanuvchi kiritgan narsani biz unga o'tkazadigan qatorga qo'yadi, lekin u Result
qiymatini ham qaytaradi. Result
- ko'pincha enum deb ataladigan enumeration, bu bir nechta mumkin bo'lgan holatlardan birida bo'lishi mumkin bo'lgan tur. Har bir mumkin bo'lgan holatni variant deb ataymiz.
6-bobda enumlar batafsilroq yoritiladi. Ushbu Result
turlarining maqsadi xatolarni qayta ishlash ma'lumotlarini kodlashdir.
Result
variantlari Ok
va Err
. Ok
varianti operatsiya muvaffaqiyatli bo'lganligini bildiradi va Ok
ichida muvaffaqiyatli yaratilgan qiymat.
Err
varianti operatsiya bajarilmaganligini bildiradi va Err
operatsiya qanday yoki nima uchun bajarilmagani haqida maʼlumotni oʻz ichiga oladi.
Result
turidagi qiymatlar, har qanday turdagi qiymatlar kabi, ularda aniqlangan metodlarga ega. Result
misolida siz murojat qilishingiz mumkin bo'lgan expect
metodi mavjud. Agar Result
ning ushbu namunasi Err
qiymati bo'lsa, expect
dasturning ishlamay qolishiga olib keladi va expect
ga argument sifatida siz uzatgan xabarni ko'rsatadi. Agar read_line
metodi Err
ni qaytarsa, bu asosiy operatsion tizimdan kelgan xato natijasi bo'lishi mumkin.
Agar Result
ning ushbu namunasi Ok
qiymati bo‘lsa, expect
Ok
ushlab turgan qaytarish qiymatini oladi va siz undan foydalanishingiz uchun aynan shu qiymatni sizga qaytaradi.
Bunday holda, bu qiymat foydalanuvchi kiritishidagi baytlar soni.
Agar siz expect
ga murojat qilmasangiz, dastur kompilyatsiya qilinadi, lekin siz ogohlantirish olasiz:
$ cargo build
Compiling taxminiy_raqam v0.1.0 (file:///projects/taxminiy_raqam)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut taxmin);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
warning: `taxminiy_raqam` (bin "taxminiy_raqam") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Rust read_line
dan qaytarilgan Result
qiymatini ishlatmaganligingiz haqida ogohlantiradi, bu dastur mumkin bo'lgan xatoni hal qilmaganligini ko'rsatadi.
Ogohlantirishni yo'qotishning to'g'ri yo'li aslida xatolarni qayta ishlash kodini yozishdir, ammo bizning holatlarimizda muammo yuzaga kelganda biz ushbu dasturni ishdan chiqarishni xohlaymiz, shuning uchun biz expect
dan foydalanishimiz mumkin. Xatolarni tiklash haqida [9-bobda]recover] bilib olasiz.
Qiymatlarni println!
bilan chop etish
Yopuvchi jingalak qavsdan tashqari, kodda hozirgacha muhokama qilinadigan yana bitta satr mavjud:
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
println!("Sizning taxminingiz: {taxmin}");
}
Ushbu satr foydalanuvchi kiritishini o'z ichiga olgan qatorni chop etadi. {}
jingalak qavslar to'plami o'rnini egallaydi: {}
qiymatini joyida ushlab turadigan qisqichbaqa qisqichlari deb tasavvur qiling. O'zgaruvchining qiymatini chop etishda o'zgaruvchi nomi jingalak qavslar ichiga kirishi mumkin. Ifodani baholash natijasini chop etishda format satriga bo'sh jingalak qavslarni joylashtiring, so'ngra har bir bo'sh jingalak qavs o'rnini egallagan holda bir xil tartibda chop etish uchun vergul bilan ajratilgan iboralar ro'yxati bilan format qatoriga amal qiling. O‘zgaruvchini va ifoda natijasini println!
ga bitta chaqiruvda chop etish quyidagicha ko‘rinadi:
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {x} va y + 2 = {}", y + 2); }
Bu kod x = 5 va y + 2 = 12
ni chop etadi.
Birinchi qismni sinovdan o'tkazish
Keling, taxmin qilish o'yinining birinchi qismini sinab ko'raylik. Uni cargo run
yordamida ishga tushiring:
$ cargo run
Compiling taxminiy_raqam v0.1.0 (file:///projects/taxminiy_raqam)
Finished dev [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/taxminiy_raqam`
Raqamni topish o'yini!
Iltimos, taxminingizni kiriting.
6
Sizni taxminingiz: 6
Shu nuqtada, o'yinning birinchi qismi tugadi: biz klaviaturadan ma'lumotlarni olamiz va keyin uni chop etamiz.
Yashirin raqam yaratish
Keyinchalik, foydalanuvchi taxmin qilishga harakat qiladigan maxfiy raqamni yaratishimiz kerak. Yashirin raqam har safar boshqacha bo'lishi kerak, shuning uchun o'yinni bir necha marta o'ynash qiziqarli bo'ladi. O'yin juda qiyin bo'lmasligi uchun biz 1 dan 100 gacha bo'lgan tasodifiy raqamdan foydalanamiz. Rust hali o'zining standart kutubxonasida tasodifiy raqamlar funksiyasini o'z ichiga olmaydi. Biroq, Rust jamoasi ushbu funksiyaga rand
cratei taqdim etadi.
Ko'proq funksionallikka ega bo'lish uchun Cratedan foydalanish
Esda tutingki, crate Rust manba kodi fayllari to'plamidir. Biz qurayotgan loyiha binary crate bo'lib, u bajariladigan. rand
crate boshqa dasturlarda foydalanish uchun moʻljallangan va mustaqil ravishda bajarib boʻlmaydigan kodni oʻz ichiga olgan library crate.
Cargoning tashqi cratelarni muvofiqlashtirishi bu erda Cargp haqiqatan ham ishlaydi. rand
dan foydalanadigan kodni yozishdan oldin, biz Cargo.toml faylini rand
cratesini dependency sifatida qo‘shish uchun o‘zgartirishimiz kerak. Hozir o‘sha faylni oching va Cargo siz uchun yaratgan[dependencies]
bo‘limi sarlavhasi ostiga quyidagi qatorni qo‘shing.rand
ni aynan bizda boʻlganidek, ushbu versiya raqami bilan belgilaganingizga ishonch hosil qiling, aks holda ushbu qoʻllanmadagi kod misollari ishlamasligi mumkin:
Fayl nomi: Cargo.toml
[dependencies]
rand = "0.8.5"
Cargo.toml faylida sarlavhadan keyingi hamma narsa boshqa bo'lim boshlanmaguncha davom etadigan bo'limning bir qismidir. [dependencies]
da siz Cargo loyihangiz qaysi tashqi cratelarga bog'liqligini va bu cratelarning qaysi versiyalari kerakligini aytasiz. Bunday holda, biz rand
crateni 0.8.5
semantik versiya spetsifikatsiyasi bilan belgilaymiz. Cargo versiya raqamlarini yozish uchun standart bo'lgan Semantic Versioningni (ba'zan SemVer deb ataladi) tushunadi. 0.8.5
spetsifikatsiyasi aslida ^0.8.5
ning qisqartmasi boʻlib, kamida 0.8.5, lekin 0.9.0 dan past boʻlgan har qanday versiyani bildiradi.
Cargo ushbu versiyalarni 0.8.5 versiyasiga mos keladigan umumiy API-larga ega deb hisoblaydi va bu spetsifikatsiya sizga ushbu bobdagi kod bilan tuziladigan so‘nggi patch versiyasini olishingizni kafolatlaydi. 0.9.0 yoki undan kattaroq versiyalar quyidagi misollar ishlatadigan API bilan bir xil bo'lishi kafolatlanmaydi.
Endi, hech qanday kodni o'zgartirmasdan, 2-2 ro'yxatda ko'rsatilganidek, loyihani build qilaylik.
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
Downloaded libc v0.2.127
Downloaded getrandom v0.2.7
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.16
Downloaded rand_chacha v0.3.1
Downloaded rand_core v0.6.3
Compiling libc v0.2.127
Compiling getrandom v0.2.7
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling taxminiy_raqam v0.1.0 (file:///projects/taxminiy_raqam)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Siz turli xil versiya raqamlarini (lekin ularning barchasi SemVer tufayli kod bilan mos keladi!) va turli xil satrlarni (operatsion tizimga qarab) ko'rishingiz mumkin va satrlar boshqa tartibda bo'lishi mumkin.
Biz tashqi dependency qo'shganimizda, Cargo Crates.io ma'lumotlarining nusxasi bo'lgan registry dan dependency uchun zarur bo'lgan barcha narsalarning so'nggi versiyalarini oladi.Crates.io - bu Rust ekotizimidagi odamlar o'zlarining ochiq manbali Rust loyihalarini boshqalar foydalanishi uchun joylashtiradigan joy.
registrni yangilagandan so'ng, Cargo [dependencies]
bo'limini tekshiradi va ro'yxatda hali yuklab olinmagan cratelarni yuklab oladi. Bu holatda, garchi biz faqat rand
ni dependency sifatida ko'rsatgan bo'lsak-da, Cargo rand
ishlashga bog'liq bo'lgan boshqa cratelarni ham oldi. Cratelarni yuklab olgandan so'ng, Rust ularni kompilyatsiya qiladi va keyin mavjud bo'lgan dependency bilan loyihani tuzadi.
Agar siz hech qanday o'zgartirishlarsiz darhol cargo build
ni qayta ishga tushirsangiz, Finished
qatoridan boshqa hech qanday natija olmaysiz. Cargo allaqachon dependencylarni yuklab olganini va kompilyatsiya qilganini biladi va siz Cargo.toml faylida ular haqida hech narsani o'zgartirmagansiz. Cargo, shuningdek, kodingiz haqida hech narsani o'zgartirmaganligingizni biladi, shuning uchun u ham uni qayta kompilyatsiya qilmaydi. Hech narsa qilmasdan, u shunchaki chiqib ketadi.
Agar siz src/main.rs faylini ochsangiz, ahamiyatsiz o'zgarishlarni amalga oshirsangiz va keyin uni saqlab va qayta build qilsangiz, siz faqat ikkita chiqish qatorini ko'rasiz:
$ cargo build
Compiling taxminiy_raqam v0.1.0 (file:///projects/taxminiy_raqam)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
Bu satrlar shuni ko'rsatadiki, Cargo faqat src/main.rs fayliga kichik o'zgartirishingiz bilan buildni yangilaydi. Sizning dependencylaringiz o'zgarmadi, shuning uchun Cargo allaqachon yuklab olingan va ular uchun tuzilgan narsadan qayta foydalanishi mumkinligini biladi..
Cargo.lock fayli bilan qayta tiklanadigan tuzilmalarni ta'minlash
Cargoda siz yoki boshqa birov kodingizni har safar yaratganingizda bir xil artefaktni qayta tiklashingiz mumkinligini ta'minlaydigan mexanizm mavjud: Siz aksini ko'rsatmaguningizcha, cargo faqat siz ko'rsatgan dependency versiyalaridan foydalanadi. Masalan, kelasi hafta rand
cratening 0.8.6 versiyasi chiqadi va bu versiyada muhim xatoliklar tuzatilgan, lekin u sizning kodingizni buzadigan regressiyani ham o‘z ichiga oladi. Buni hal qilish uchun Rust birinchi marta cargo build
dasturini ishga tushirganingizda Cargo.lock faylini yaratadi, shuning uchun biz endi bu guessing_game jildida mavjud.
Loyihani birinchi marta yaratganingizda, Cargo mezonlarga mos keladigan dependencylarning barcha versiyalarini aniqlaydi va keyin ularni Cargo.lock fayliga yozadi. Keyingi loyihangizni yaratganingizda, Cargo Cargo.lock fayli mavjudligini ko'radi va versiyalarni qayta aniqlash uchun barcha ishlarni bajarishdan ko'ra, u erda ko'rsatilgan versiyalardan foydalanadi. Bu sizga avtomatik ravishda takrorlanadigan tuzilishga ega bo'lish imkonini beradi. Boshqacha qilib aytganda, Cargo.lock fayli tufayli loyihangiz aniq yangilanmaguningizcha 0.8.5 da qoladi. Cargo.lock fayli qayta tiklanadigan tuzilmalar uchun muhim bo'lgani uchun u ko'pincha loyihangizdagi kodning qolgan qismi bilan manba nazoratida tekshiriladi.
Yangi versiyani olish uchun Crateni yangilash
Crateni yangilamoqchi bo'lsangiz, Cargo update
buyrug'ini beradi, bu buyruq Cargo.lock faylini e'tiborsiz qoldiradi va Cargo.toml dagi texnik xususiyatlaringizga mos keladigan barcha so'nggi versiyalarni aniqlaydi. Keyin Cargo ushbu versiyalarni Cargo.lock fayliga yozadi. Aks holda, standart bo'yicha, Cargo faqat 0.8.5 dan katta va 0.9.0 dan kichik versiyalarni qidiradi. Agar rand
cratesi ikkita yangi 0.8.6 va 0.9.0 versiyalarini chiqargan bo'lsa, cargo update
ni ishga tushirgan bo'lsangiz, quyidagilarni ko'rasiz:
$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
Cargo 0.9.0 versiyasiga e'tibor bermaydi. Bu vaqtda siz Cargo.lock faylingizda oʻzgarishlarni ham sezasiz, bunda siz hozir foydalanayotgan rand
cratesi versiyasi 0.8.6. rand
0.9.0 versiyasidan yoki 0.9.x seriyasining istalgan versiyasidan foydalanish uchun Cargo.toml faylini quyidagi koʻrinishda yangilashingiz kerak boʻladi:
[dependencies]
rand = "0.9.0"
Keyingi safar cargo build
ni ishga tushirganingizda, Cargo mavjud cratelar reestrini yangilaydi va siz ko‘rsatgan yangi versiyaga muvofiq rand
talablaringizni qayta baholaydi.
Cargo va uning ekotizimlari haqida ko'p gapirish mumkin, biz ularni 14-bobda muhokama qilamiz, ammo hozircha bilishingiz kerak bo'lgan narsa shu. Cargo kutubxonalarni qayta ishlatishni juda osonlashtiradi, shuning uchun Rustaceans bir nechta paketlardan yig'ilgan kichikroq loyihalarni yozishga qodir.
Tasodifiy raqamni yaratish
Keling, taxmin qilish uchun raqam yaratishda rand
dan foydalanishni boshlaylik. Keyingi qadam 2-3 ro'yxatda ko'rsatilganidek src/main.rs ni yangilashdir.
Fayl nomi: src/main.rs
use std::io;
use rand::Rng;
fn main() {
println!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
println!("Yashirin raqam: {yashirin_raqam}");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
println!("Sizning taxminingiz: {taxmin}");
}
Avval use rand::Rng;
qatorini qo'shamiz. Rng
traiti tasodifiy sonlar generatorlari qo'llaydigan metodlarni belgilaydi va biz ushbu usullardan foydalanishimiz uchun bu trait mos bo'lishi kerak. 10-bobda traitlar batafsil yoritiladi.
Keyin o'rtada ikkita qator qo'shamiz. Birinchi qatorda biz rand::thread_rng
funksiyasini chaqiramiz, bu bizga biz foydalanmoqchi bo'lgan tasodifiy sonlar generatorini beradi: joriy bajarilish oqimi uchun mahalliy bo'lgan va operatsion tizim tomonidan ekilgan. Keyin tasodifiy sonlar generatorida gen_range
metodini chaqiramiz. Bu metod biz use rand::Rng;
iborasi bilan qamrab olgan Rng
traiti bilan aniqlanadi. gen_range
metodi argument sifatida diapazon ifodasini oladi va diapazonda tasodifiy son hosil qiladi. Biz bu yerda foydalanayotgan diapazon ifodasi turi start..=end
shaklini oladi va pastki va yuqori chegaralarni qamrab oladi, shuning uchun biz 1 va 100 oralig‘idagi raqamni so‘rash uchun 1..=100
ni belgilashimiz kerak. .
Eslatma: Siz faqat qaysi traitlardan foydalanishni va qaysi metodlar va funktsiyalarni cratedan chaqirishni bila olmaysiz, shuning uchun har bir crateda foydalanish bo'yicha ko'rsatmalar mavjud. Cargo-ning yana bir qulay xususiyati shundaki,
cargo doc --open
buyrug'ini ishga tushirish sizning barcha dependencylar tomonidan taqdim etilgan texnik hujjatlarni mahalliy sifatida tuzadi va uni brauzeringizda ochadi. Agar sizrand
cratedagi boshqa funksiyalarga qiziqsangiz, masalan,cargo doc --open
ni ishga tushiring va chap tomondagi yon paneldagirand
tugmasini bosing.
Ikkinchi yangi qator maxfiy raqamni chop etadi. Bu dasturni ishlab chiqishda uni sinab ko'rishimiz uchun foydalidir, lekin biz uni oxirgi versiyadan o'chirib tashlaymiz. Agar dastur boshlanishi bilanoq javobni chop etsa, bu unchalik o'yin emas!
Dasturni bir necha marta ishga tushirishga harakat qiling:
$ cargo run
Compiling taxminiy_raqam v0.1.0 (file:///projects/taxminiy_raqam)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Running `target/debug/taxminiy_raqam`
Raqamni topish o'yini!
Yashirin raqam: 7
Iltimos, taxminingizni kiriting.
4
Siznig taxminingiz: 4
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/taxminiy_raqam`
Raqamni topish o'yini!
Yashirin raqam: 83
Iltimos, taxminingizni kiriting.
5
Siznig taxminingiz: 5
Siz turli xil tasodifiy raqamlarni olishingiz kerak va ularning barchasi 1 dan 100 gacha raqamlar bo'lishi kerak. Ajoyib ish!
Taxminni maxfiy raqam bilan solishtirish
Endi bizda foydalanuvchi kiritishi va tasodifiy raqam bor, biz ularni solishtirishimiz mumkin. Ushbu qadam 2-4 ro'yxatda ko'rsatilgan. E'tibor bering, bu kod hozircha kompilatsiya bo'lmaydi, biz tushuntiramiz.
Fayl nomi: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
println!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
println!("Yashirin raqam: {yashirin_raqam}");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
println!("Sizning taxminingiz: {taxmin}");
match taxmin.cmp(&yashirin_raqam) {
Ordering::Less => println!("Raqam Kichik!"),
Ordering::Greater => println!("Raqam katta!"),
Ordering::Equal => println!("Siz yutdingiz!"),
}
}
Avval biz standart kutubxonadan std::cmp::Ording
deb nomlangan turni olib keladigan yana bir use
iborasini qo'shamiz. Ordering
turi boshqa raqam boʻlib, Less
, Greater
va Equal
variantlariga ega. Bu ikkita qiymatni solishtirganda mumkin bo'lgan uchta natijadir.
Keyin pastki qismida Ordering
turidan foydalanadigan beshta yangi qator qo'shamiz. cmp
metodi ikkita qiymatni solishtiradi va uni solishtirish mumkin bo'lgan har qanday narsani chaqirish mumkin. Siz solishtirmoqchi bo'lgan narsaga reference kerak: bu erda taxmin
bilan yashirin_raqam
solishtiriladi. Keyin u biz use
iborasi bilan qamrab olgan Ordering
raqamining variantini qaytaradi. Biz taxmin
va yashirin_raqam
qiymatlari bilan cmp
ga murojatdan Ordering
ning qaysi varianti qaytarilganiga qarab, keyin nima qilish kerakligini hal qilish uchun match
ifodasidan foydalanamiz.
Match
ifodasi arms dan tuzilgan. Arm mos keladigan pattern va agar match
ga berilgan qiymat armning patterniga mos kelsa, bajarilishi kerak bo'lgan koddan iborat. Rust match
ga berilgan qiymatni oladi va har bir armning patternini o'z navbatida ko'rib chiqadi. Patternlar va match
konstruksiyasi Rust-ning kuchli xususiyatlari hisoblanadi: ular sizning kodingiz duch kelishi mumkin bo'lgan turli vaziyatlarni ifodalash imkonini beradi va ularning barchasini boshqarishingizga ishonch hosil qiladi. Bu xususiyatlar mos ravishda 6-bobda va 18-bobda batafsil yoritiladi.
Keling, bu yerda ishlatadigan match
iborasi bilan bir misolni ko'rib chiqaylik. Aytaylik, foydalanuvchi 50 ni taxmin qilgan va bu safar tasodifiy yaratilgan maxfiy raqam 38 ni tashkil qiladi.
Kod 50 ni 38 ga solishtirganda, cmp
metodi Ordering::Greater
ni qaytaradi, chunki 50 38 dan katta. match
ifodasi Ordering::Greater
qiymatini oladi va har bir armning patternini tekshirishni boshlaydi. U birinchi armning Ordering::Less
patternini koʻrib chiqadi va Ordering::Greater
qiymati Ordering::Less
qiymatiga mos kelmasligini koʻradi, shuning uchun u armdagi kodga eʼtibor bermaydi va keyingi armga oʻtadi. Keyingi armning namunasi Ordering::Greater
boʻlib, Ordering::Greater
bilan does match keladi! Oʻsha armdagi bogʻlangan kod ishga tushadi va ekranga Raqam katta!
deb chop etiladi. match
iborasi birinchi muvaffaqiyatli o'yindan keyin tugaydi, shuning uchun bu senariydagi oxirgi armni ko'rib chiqmaydi.
Biroq, 2-4 ro'yxatdagi kod hali kompilyatsiya qilinmaydi. Keling, sinab ko'raylik:
$ 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.5
Compiling taxminiy_raqam v0.1.0 (file:///projects/taxminiy_raqam)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match taxmin.cmp(&yashirin_raqam) {
| --- ^^^^^^^^^^^^^^ expected struct `String`, found integer
| |
| arguments to this function are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: associated function defined here
--> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/cmp.rs:783:8
For more information about this error, try `rustc --explain E0308`.
error: could not compile `taxminiy_raqame` due to previous error
Xatoning asosi mos kelmaydigan turlar mavjudligini bildiradi. Rust kuchli, statik turdagi tizimga ega. Biroq, u ham turdagi inference ega. Biz let mut taxmin = String::new()
deb yozganimizda, Rust taxmin
String
bo'lishi kerak degan xulosaga keldi va bizni turni yozishga majburlamadi. Boshqa tomondan, yashirin_raqam
raqam turidir. Rust raqamlarining bir nechta turlari 1 dan 100 gacha qiymatga ega bo'lishi mumkin: i32
, 32 bitli raqam; u32
, unsigned 32-bitli raqam; i64
, 64-bitli raqam; boshqalar kabi. Agar boshqacha koʻrsatilmagan boʻlsa, Rust standart boʻyicha i32
ga oʻrnatiladi, bu yashirin_raqam
turiga, agar siz Rustning boshqa raqamli turini chiqarishiga olib keladigan turdagi maʼlumotlarni boshqa joyga qoʻshmasangiz. Xatoning sababi shundaki, Rust string va raqam turini taqqoslay olmaydi.
Oxir-oqibat, biz dastur tomonidan kiritilgan String
ni haqiqiy son turiga aylantirmoqchimiz, shuning uchun uni raqamli raqam bilan yashirin raqam bilan solishtirishimiz mumkin.Buni main
funksiya tanasiga ushbu qatorni qo'shish orqali qilamiz:
Fayl nomi: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
println!("Yashirin raqam: {yashirin_raqam}");
println!("Iltimos, taxminingizni kiriting.");
// --snip--
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
let taxmin: u32 = taxmin.trim().parse().expect("Iltimos, raqam yozing!");
println!("Sizning taxminingiz: {taxmin}");
match taxmin.cmp(&yashirin_raqam) {
Ordering::Less => println!("Raqam Kichik!"),
Ordering::Greater => println!("Raqam katta!"),
Ordering::Equal => println!("Siz yutdingiz!"),
}
}
Satr
let taxmin: u32 = taxmin.trim().parse().expect("Iltimos, raqam yozing!");
Biz taxmin
nomli o'zgaruvchini yaratamiz. Ammo shoshilmang, dasturda allaqachon taxmin
nomli o'zgaruvchi mavjud emasmi? Bu shunday, lekin foydali Rust bizga taxmin
ning oldingi qiymatini yangisi bilan ergashtirish imkonini beradi. Shadowing bizga ikkita noyob oʻzgaruvchini yaratish oʻrniga, taxmin
oʻzgaruvchi nomidan qayta foydalanish imkonini beradi, masalan, taxmin_str
va taxmin
. Biz buni 3-bobda batafsil ko'rib chiqamiz, ammo hozircha shuni bilingki, bu xususiyat ko'pincha qiymatni bir turdan boshqa turga aylantirmoqchi bo'lganingizda ishlatiladi.
Biz bu yangi o'zgaruvchini taxmin.trim().parse()
ifodasiga bog'laymiz. Ifodadagi taxmin
matni qator sifatida kiritilgan asl taxmin
o'zgaruvchisiga ishora qiladi. String
misolidagi trim
metodi boshida va oxiridagi har qanday bo‘shliqni yo‘q qiladi, bu qatorni faqat raqamli ma’lumotlarni o‘z ichiga olishi mumkin bo‘lgan u32
bilan solishtirishimiz uchun buni qilishimiz kerak. Foydalanuvchi read_line
ni to'ldirish uchun entertugmasini bosib, ularni kiritishi kerak
satrga yangi satr belgisini qo'shadigan taxmin. Masalan, agar foydalanuvchi 5 raqamini kiritsa va va enter tugmasini bossa taxmin
shunday ko'rinadi: 5\n
.
\n
“yangi qator”ni bildiradi. (Windows tizimida enter tugmasini bosish natijasida carriage qaytariladi va yangi qator \r\n
chiqadi.)
trim
metodi \n
yoki \r\n
ni yo'q qiladi, natijada atigi 5
bo`ladi.
Satrlardagi parse
metodi qatorni boshqa turga aylantiradi.
Bu yerda biz uni stringdan raqamga aylantirish uchun foydalanamiz. Biz Rustga let taxmin: u32
yordamida kerakli raqam turini aytishimiz kerak. taxmin
dan keyin ikki nuqta (:
) Rustga o'zgaruvchining turiga izoh berishimizni aytadi. Rust bir nechta o'rnatilgan raqam turlariga ega; Bu yerda koʻrilgan u32
unsigned, 32-bitli butun son.
Bu kichik ijobiy raqam uchun yaxshi standart tanlovdir. Boshqa raqamlar turlari haqida 3-bobda bilib olasiz.
Bundan tashqari, ushbu misol dasturidagi u32
annotation va yashirin_raqam
bilan taqqoslash Rust yashirin_raqam
ham u32
bo'lishi kerak degan xulosaga keladi. Shunday qilib, endi taqqoslash bir xil turdagi ikkita qiymat o'rtasida bo'ladi!
parse
metodii faqat mantiqiy ravishda raqamlarga aylantirilishi mumkin bo'lgan belgilarda ishlaydi va shuning uchun osongina xatolarga olib kelishi mumkin. Agar, masalan, satrda A👍%
bo'lsa, uni raqamga aylantirishning hech qanday metodi bo'lmaydi. Muvaffaqiyatsiz bo'lishi mumkinligi sababli, parse
metodii read_line
metodi kabi Result
turini qaytaradi (oldingi ["Result
bilan potentsial muvaffaqiyatsizlikni ko'rib chiqish"] bo'limida muhokama qilingan)(#handling-potential-failure-with-result)). Biz ushbu Result
ga yana expect
metodini qo'llash orqali xuddi shunday munosabatda bo'lamiz. Agar parse
qatordan raqam yarata olmagani uchun Err
Result
variantini qaytarsa, expect
chaqiruvi o‘yinni buzadi va biz bergan xabarni chop etadi.
Agar parse
qatorni raqamga muvaffaqiyatli aylantira olsa, u Result
ning Ok
variantini qaytaradi va expect
biz xohlagan raqamni Ok
qiymatidan qaytaradi.
Endi dasturni ishga tushiramiz:
$ cargo run
Compiling taxminiy_raqam v0.1.0 (file:///projects/taxminiy_raqam)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/taxminiy_raqam`
Raqamni topish o'yini!
Yashirin raqam: 58
Iltimos, taxminingizni kiriting.
76
Sizning taxminingiz: 76
Raqam katta!
Yaxshi! Tahmindan oldin bo'shliqlar qo'shilgan bo'lsa ham, dastur foydalanuvchi 76 ni taxmin qilganini aniqladi. Har xil turdagi kirishlar bilan turli xatti-harakatlarni tekshirish uchun dasturni bir necha marta ishga tushiring: raqamni to'g'ri taxmin qiling, katta raqamni taxmin qiling va kichik raqamni taxmin qiling.
Hozir bizda o'yinning ko'p qismi ishlayapti, lekin foydalanuvchi faqat bitta taxmin qila oladi. Keling, buni loop qo'shish orqali o'zgartiraylik!
Loop bilan bir nechta taxminlarga ruxsat berish
loop
kalit so'zi cheksiz tsiklni yaratadi. Biz foydalanuvchilarga raqamni taxmin qilishda ko'proq imkoniyat berish uchun tsikl qo'shamiz:
Fayl nomi: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
// --snip--
println!("Yashirin raqam: {yashirin_raqam}");
loop {
println!("Iltimos, taxminingizni kiriting.");
// --snip--
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
let taxmin: u32 = taxmin.trim().parse().expect("Iltimos, raqam yozing!");
println!("Sizning taxminingiz: {taxmin}");
match taxmin.cmp(&yashirin_raqam) {
Ordering::Less => println!("Raqam Kichik!"),
Ordering::Greater => println!("Raqam katta!"),
Ordering::Equal => println!("Siz yutdingiz!"),
}
}
}
Ko'rib turganingizdek, biz hamma narsani taxminiy kiritish so'rovidan boshlab tsiklga o'tkazdik. Ilova ichidagi satrlarni har birida yana to'rtta bo'sh joydan o'tkazganingizga ishonch hosil qiling va dasturni qayta ishga tushiring. Dastur endi boshqa bir taxminni abadiy yani har doim so'raydi, bu aslida yangi muammoni keltirib chiqaradi. Foydalanuvchi chiqa olmaydiganga o'xshaydi!
Foydalanuvchi har doim ctrl-c klaviatura yorlig'i yordamida dasturni to'xtatishi mumkin. Ammo bu to'yib bo'lmaydigan yirtqich hayvondan qochishning yana bir yo'li bor, “Taxminni maxfiy raqam bilan solishtirish“: mavzusidagi parse
muhokamasida aytib o'tilganidek, agar foydalanuvchi raqam bo'lmagan javobni kiritsa, dastur buziladi. Bu yerda ko'rsatilganidek, foydalanuvchiga chiqishga ruxsat berish uchun undan foydalanishimiz mumkin
$ cargo run
Compiling taxminiy_raqam v0.1.0 (file:///projects/taxminiy_raqam)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/taxminiy_raqam`
Raqamni topish o'yini!
Yashirin raqam: 59
Iltimos, taxminingizni kiriting.
45
Sizning taxminingiz: 45
Raqam Kichik!
Iltimos, taxminingizni kiriting.
60
Sizning taxminingiz: 60
Raqam katta!
Iltimos, taxminingizni kiriting.
59
Sizning taxminingiz: 59
Siz yutdingiz!
Iltimos, taxminingizni kiriting.
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
quit
deb yozsangiz, o‘yin tugaydi, lekin siz ko‘rganingizdek, boshqa raqam bo‘lmagan ma’lumotlarni kiritish ham shunday bo‘ladi. Bu, eng kamida, suboptimaldir; Biz to'g'ri raqam taxmin qilinganda ham o'yin to'xtashini xohlaymiz.
To'g'ri taxmindan keyin chiqish
Keling, foydalanuvchi g'alaba qozonganida break
iborasini qo'shish orqali o'yinni to'xtatish uchun dasturlashtiramiz:
Fayl nomi: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
println!("Yashirin raqam: {yashirin_raqam}");
loop {
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
let taxmin: u32 = taxmin.trim().parse().expect("Iltimos, raqam yozing!");
println!("Sizning taxminingiz: {taxmin}");
// --snip--
match taxmin.cmp(&yashirin_raqam) {
Ordering::Less => println!("Raqam Kichik!"),
Ordering::Greater => println!("Raqam katta!"),
Ordering::Equal => {
println!("Siz yutdingiz!");
break;
}
}
}
}
Siz yutdingiz!
so‘ng break
qatorini qo‘shish foydalanuvchi maxfiy raqamni to‘g‘ri taxmin qilganda dasturni tsikldan chiqadi. Loopdan chiqish dasturdan chiqishni ham anglatadi, chunki sikl main
ning oxirgi qismidir.
Noto'g'ri kiritish
O'yinning xatti-harakatlarini yanada yaxshilash uchun, foydalanuvchi raqamlardan boshqa belgilar kiritganda dasturni ishdan chiqargandan ko'ra, foydalanuvchi taxmin qilishni davom ettirishi uchun o'yinni raqam bo'lmagan belgilarga e'tibor bermaslikka harakat qildiraylik. Buni 2-5 roʻyxatda koʻrsatilganidek, taxmin
satrdan u32
ga aylantirilgan qatorni oʻzgartirish orqali amalga oshirishimiz mumkin.
Fayl nomi: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
println!("Yashirin raqam: {yashirin_raqam}");
loop {
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
// --snip--
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
let taxmin: u32 = match taxmin.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("Sizning taxminingiz: {taxmin}");
// --snip--
match taxmin.cmp(&yashirin_raqam) {
Ordering::Less => println!("Raqam Kichik!"),
Ordering::Greater => println!("Raqam katta!"),
Ordering::Equal => {
println!("Siz yutdingiz!");
break;
}
}
}
}
Xato ustida ishlamay qolishdan xatoni hal qilishga o‘tish uchun biz expect
chaqiruvidan match
ifodasiga o‘tamiz. Esda tutingki, parse
Result
turini qaytaradi, Result
esa Ok
va Err
variantlariga ega bo'lgan raqamdir. Biz bu yerda cmp
metodining Ordering
natijasi bilan bo‘lgani kabi match
ifodasidan foydalanmoqdamiz.
Agar parse
qatorni raqamga muvaffaqiyatli aylantira olsa, natijada olingan raqamni o'z ichiga olgan Ok
qiymatini qaytaradi. Bu Ok
qiymati birinchi armning patterniga mos keladi va match
ifodasi parse
ishlab chiqarilgan va Ok
qiymatiga qo'ygan num
qiymatini qaytaradi. Bu raqam biz yaratayotgan yangi taxmin
o'zgaruvchisida biz xohlagan joyda tugaydi
Agar parse
satrni raqamga aylantira olmasa xato haqida qo'shimcha ma'lumotni o'z ichiga olgan Err
qiymatini qaytaradi. Err
qiymati birinchi match
bo‘limidagi Ok(num)
patterniga mos kelmaydi, lekin ikkinchi armdagi Err(_)
patterniga mos keladi. Pastki chiziq, _
, diqqatga sazovor qiymatdir; bu misolda biz barcha Err
qiymatlariga, ular ichida qanday ma'lumotlar bo'lishidan qat'iy nazar, mos kelmoqchimiz deymiz. Shunday qilib, dastur ikkinchi armning continue
kodini bajaradi, bu dasturga loop
ning keyingi iteratsiyasiga o'tishni va boshqa taxminni so'rashni aytadi. Shunday qilib, dastur parse
duch kelishi mumkin bo'lgan barcha xatolarga e'tibor bermaydi!
Endi dasturdagi hamma narsa kutilganidek ishlashi kerak. Keling, sinab ko'raylik:
$ cargo run
Compiling taxminiy_raqam v0.1.0 (file:///projects/taxminiy_raqam)
Finished dev [unoptimized + debuginfo] target(s) in 4.45s
Running `target/debug/taxminiy_raqam`
Raqamni topish o'yini!
Yashirin raqam: 61
Iltimos, taxminingizni kiriting.
10
Sizning taxminingiz: 10
Raqam Kichik!
Iltimos, taxminingizni kiriting.
99
Sizning taxminingiz: 99
Raqam katta!
Iltimos, taxminingizni kiriting.
foo
Iltimos, taxminingizni kiriting.
61
Sizning taxminingiz: 61
Siz yutdingiz!
Ajoyib! Bitta kichik so'nggi tweak bilan biz taxmin qilish o'yinini tugatamiz. Eslatib o'tamiz, dastur hali ham maxfiy raqamni chop etmoqda. Bu sinov uchun yaxshi ishladi, lekin o'yinni buzadi. Maxfiy raqamni chiqaradigan println!
ni o'chirib tashlaymiz. 2-6 ro'yxat yakuniy kodni ko'rishingiz mumkin.
Fayl nomi: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
loop {
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
let taxmin: u32 = match taxmin.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("Sizning taxminingiz: {taxmin}");
match taxmin.cmp(&yashirin_raqam) {
Ordering::Less => println!("Raqam Kichik!"),
Ordering::Greater => println!("Raqam katta!"),
Ordering::Equal => {
println!("Siz yutdingiz!");
break;
}
}
}
}
Shu nuqtada, siz taxmin qilish o'yinini muvaffaqiyatli yaratdingiz. Tabriklaymiz!
Xulosa
Ushbu loyiha sizni Rustning ko'plab yangi tushunchalari bilan tanishtirishning amaliy usuli bo'ldi: let
, match
, funktsiyalar, tashqi cratelardan foydalanish va boshqalar. Keyingi bir necha boblarda siz ushbu tushunchalar haqida batafsilroq bilib olasiz. 3-bob ko'pchilik dasturlash tillarida mavjud bo'lgan o'zgaruvchilar, ma'lumotlar turlari va funktsiyalari kabi tushunchalarni qamrab oladi va ulardan Rustda qanday foydalanishni ko'rsatadi. 4-bobda Rust tilini boshqa tillardan ajratib turadigan egalik huquqi o‘rganiladi. 5-bobda tuzilmalar va metodlar sintaksisi muhokama qilinadi va 6-bobda enumlar qanday ishlashi tushuntiriladi.
Umumiy dasturlash tushunchalari
Ushbu bob deyarli barcha dasturlash tillarida paydo bo'ladigan tushunchalarni va ular Rustda qanday ishlashini o'z ichiga oladi. Ko'pgina dasturlash tillari o'rtasida juda ko'p umumiylik mavjud. Ushbu bobda keltirilgan tushunchalarning hech biri Rustga xos emas, lekin biz ularni Rust kontekstida ko'rib chiqamiz va ushbu tushunchalardan foydalanish bo'yicha konventsiyalarni tushuntiramiz.
Xususan, siz o'zgaruvchilar, asosiy turlar, funktsiyalar, izohlar va control flow haqida bilib olasiz. Ushbu asoslar har bir Rust dasturida bo'ladi va ularni erta o'rganish sizga boshlash uchun kuchli asos bo'ladi.
Kalit so'zlar
Rust dasturlash tilida boshqa tillardagi kabi faqat til tomonidan foydalanish uchun ajratilgan kalit so'zlar to'plami mavjud. Shuni yodda tutingki, siz ushbu so'zlarni o'zgaruvchilar yoki funksiyalar nomi sifatida ishlata olmaysiz. Kalit so'zlarning aksariyati maxsus ma'noga ega va siz ulardan Rust dasturlarida turli vazifalarni bajarish uchun foydalanasiz; ba'zilarida ular bilan bog'liq bo'lgan joriy funksionallik yo'q, lekin kelajakda Rustga qo'shilishi mumkin bo'lgan funksiyalar uchun ajratilgan. Kalit so'zlar ro'yxatini A ilovasida. topishingiz mumkin.
O'zgaruvchilar va o'zgaruvchanlik
”O'zgaruvchilar bilan qiymatlarni saqlash” bo'limida aytib o'tilganidek, standart bo'yicha o'zgaruvchilar o'zgarmasdir.Rust sizga o'z kodingizni Rust taqdim etgan xavfsizlik va qulay parallellikdan foydalanadigan tarzda yozish uchun beradigan ko'plab qulayliklardan biridir. Biroq, siz hali ham o'zgaruvchilaringizni o'zgaruvchan qilish imkoniyatiga egasiz. Keling, Rust sizni qanday qilib va nima uchun o'zgarmaslikni afzal ko'rishga undashini va nega ba'zan siz undan voz kechishingiz mumkinligini bilib olaylik.
Agar o'zgaruvchi o'zgarmas bo'lsa, qiymat nomga bog'langandan keyin siz bu qiymatni o'zgartira olmaysiz. Buni ko'rsatish uchun cargo new variables
yordamida projects jildingizda variables nomli yangi loyihani yarating.
Keyin, yangi variables jildida src/main.rs ni oching va uning kodini quyidagi kod bilan almashtiring. Bu kod hozircha kompilyatsiya qilinmaydi, biz avval o'zgarmaslik xatosini ko'rib chiqamiz.
Fayl nomi: src/main.rs
fn main() {
let x = 5;
println!("x qiymati: {x}");
x = 6;
println!("x qiymati: {x}");
}
Kodni saqlang va dasturni cargo run
yordamida ishga tushiring. Ushbu chiqishda ko'rsatilganidek, o'zgarmaslik xatosi haqida xato xabarini olishingiz kerak:
$ 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!("x qiymati: {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
Ushbu misol kompilyator sizning dasturlaringizdagi xatolarni topishga qanday yordam berishini ko'rsatadi. Kompilyatordagi xatolar sizni asabiylashtirishi mumkin, lekin aslida ular sizning dasturingiz hali siz xohlagan narsani xavfsiz bajarmayotganligini anglatadi; ular sizning yaxshi dasturchi emasligingizni bildirmaydi! Tajribali Rustaceanlar hali ham kompilyator xatolariga duch kelishadi.
Siz oʻzgarmas x
oʻzgaruvchisiga ikkinchi qiymatni belgilashga harakat qilganingiz uchun ````x` oʻzgaruvchisiga ikki marta tayinlab boʻlmaydi``` xato xabarini oldingiz.
O'zgarmas deb belgilangan qiymatni o'zgartirishga urinayotganda kompilyatsiya vaqtida xatolarga duch kelishimiz muhim, chunki bu holat xatolarga olib kelishi mumkin.Agar bizning kodimizning bir qismi qiymat hech qachon o'zgarmasligi haqidagi faraz asosida ishlayotgan bo'lsa va kodimizning boshqa qismi bu qiymatni o'zgartirsa, kodning birinchi qismi uni bajarish uchun mo'ljallangan narsani qilmasligi mumkin. Bunday xatoning sababini aniqlash qiyin bo'lishi mumkin, ayniqsa kodning ikkinchi qismi faqat ba'zan qiymatini o'zgartirganda. Rust kompilyatori qiymat o'zgarmasligini bildirganingizda, u haqiqatan ham o'zgarmasligini kafolatlaydi, shuning uchun uni o'zingiz kuzatib borishingiz shart emas. Shunday qilib, kodingizni tushunish osonroq.
Ammo o'zgaruvchanlik juda foydali bo'lishi mumkin va kodni yozishni qulayroq qilishi mumkin.
Garchi oʻzgaruvchilar standart boʻyicha oʻzgarmas boʻlsa-da, 2-bobda boʻlgani kabi oʻzgaruvchi nomi oldiga mut
qoʻshish orqali ularni oʻzgaruvchan qilish mumkin. mut
qo'shilishi, shuningdek, kodning boshqa qismlari ushbu o'zgaruvchining qiymatini o'zgartirishini ko'rsatib, kelajakdagi kod o'quvchilariga niyatni bildiradi.
Masalan, src/main.rs ni quyidagiga o'zgartiramiz:
Fayl nomi: src/main.rs
fn main() { let mut x = 5; println!("x qiymati: {x}"); x = 6; println!("x qiymati: {x}"); }
Dasturni hozir ishga tushirganimizda, biz quyidagilarni olamiz:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
x qiymati: 5
x qiymati: 6
mut
ishlatilganda x
ga bog‘langan qiymatni 5
dan 6
ga o‘zgartirishga ruxsat beriladi. Oxir oqibat, o'zgaruvchanlikni qo'llash yoki qilmaslikni hal qilish sizga bog'liq va bu vaziyatda eng aniq deb o'ylagan narsangizga bog'liq.
Konstantalar
O'zgarmas o'zgaruvchilar singari, konstantalar nomga bog'langan va o'zgarishi mumkin bo'lmagan qiymatlardir, lekin konstantalar va o'zgaruvchilar o'rtasida bir nechta farqlar mavjud.
Birinchidan, mut
dan konstantalar bilan foydalanishga ruxsat berilmagan. Konstantalar standart bo'yicha shunchaki o'zgarmas emas - ular har doim o'zgarmasdir.Siz konstantalarni let
kalit so'zi o'rniga const
kalit so'zidan foydalanib e'lon qilasiz va qiymat turiga annotatsiya qilinishi kerak. Biz turlar va izohlarni keyingi "Ma'lumotlar turlari" bo'limida ko'rib chiqamiz, shuning uchun hozir tafsilotlar haqida qayg'urmang. Bilingki, siz har doim turga annotate qo'yishingiz kerak.
Konstantalar har qanday miqyosda, shu jumladan global miqyosda e'lon qilinishi mumkin, bu ularni kodning ko'p qismlari bilishi kerak bo'lgan qiymatlar uchun foydali qiladi.
Oxirgi farq shundaki, konstantalar faqat ish vaqtida hisoblanishi mumkin bo'lgan qiymatning natijasi emas, balki faqat konstanta ifodaga o'rnatilishi mumkin.
Mana konstanta deklaratsiyaga misol:
#![allow(unused)] fn main() { const SONIYADA_UCH_SOAT: u32 = 60 * 60 * 3; }
Konstanta nomi SONIYADA_UCH_SOAT
va uning qiymati 60 ni (bir daqiqadagi soniyalar soni) 60 ga (bir soatdagi daqiqalar soni) 3 ga (biz hisoblamoqchi bo'lgan soatlar soni) ko'paytirish natijasiga o'rnatiladi. Rustning konstantalar uchun nomlash konventsiyasi so'zlar orasida barcha bosh harflarni pastki chiziq bilan ishlatishdir. Kompilyator kompilyatsiya vaqtida cheklangan operatsiyalar to'plamini baholashga qodir, bu bizga ushbu qiymatni 10,800 qiymatiga o'rnatmasdan, tushunish va tekshirish osonroq bo'lgan tarzda yozishni tanlash imkonini beradi.
Konstantalarni e'lon qilishda qanday operatsiyalardan foydalanish mumkinligi haqida qo'shimcha ma'lumot olish
Rust Referencening konstantalar bo'limiga qarang
Konstantalar dastur ishlayotgan butun vaqt davomida, ular e'lon qilingan doirada amal qiladi. Bu xususiyat dasturning bir nechta qismlari bilishi kerak bo'lgan, masalan, o'yinning har qanday o'yinchisi olishi mumkin bo'lgan maksimal ball soni yoki yorug'lik tezligi kabi, ilova domeningizdagi qiymatlar uchun foydali konstantalarni qiladi.
Dasturingiz davomida ishlatiladigan qattiq kodlangan qiymatlarni konstantalar sifatida nomlash ushbu qiymatning ma'nosini kodning kelajakdagi maintainerlariga yetkazishda foydalidir. Bu, shuningdek, kodingizda faqat bitta joyga ega bo'lishga yordam beradi, agar kelajakda qattiq kodlangan qiymat yangilanishi kerak bo'lsa, o'zgartirishingiz kerak bo'ladi.
Shadowing
2-bobdagi Taxmin qilish oʻyini boʻyicha qoʻllanmada koʻrganingizdek, oldingi oʻzgaruvchi bilan bir xil nomli yangi oʻzgaruvchini eʼlon qilishingiz mumkin.Rustaceanlarning aytishicha, birinchi o'zgaruvchi ikkinchi o'zgaruvchi tomonidan shadow qilingan ya'ni ikkinchi o'zgaruvchi o'zgaruvchi nomidan foydalanganda kompilyator ko'radigan narsadir.
Darhaqiqat, ikkinchi o'zgaruvchi birinchisiga shadow qilib, o'zgaruvchi nomidan har qanday foydalanishni uning o'zi shadowli bo'lmaguncha yoki doirasi tugaguncha oladi.
Biz bir xil oʻzgaruvchining nomidan foydalanib, let
kalit soʻzidan foydalanishni quyidagi tarzda takrorlash orqali oʻzgaruvchini shadow qilishimiz mumkin:
Fayl nomi: src/main.rs
fn main() { let x = 5; let x = x + 1; { let x = x * 2; println!("x ning ichki doiradagi qiymati: {x}"); } println!("x qiymati: {x}"); }
Bu dastur avval x
ni 5
qiymatiga bog'laydi. Keyin u let x =
ni takrorlab, asl qiymatni olib, 1
qo'shish orqali yangi x
o'zgaruvchisini yaratadi, shunda x
qiymati 6
bo'ladi. Keyin, jingalak qavslar bilan yaratilgan ichki doirada uchinchi let
iborasi ham x
ga shadow qiladi va yangi o'zgaruvchini yaratadi va oldingi qiymatni 2
ga ko'paytirib, x
ga 12
qiymatini beradi.
Bu doira tugagach, ichki shadow tugaydi va x
6
ga qaytadi.
Ushbu dasturni ishga tushirganimizda, u quyidagilarni chiqaradi:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
x ning ichki doiradagi qiymati: 12
x qiymati: 6
Shadowing o‘zgaruvchini mut
deb belgilashdan farq qiladi, chunki let
kalit so‘zidan foydalanmasdan tasodifan ushbu o‘zgaruvchiga qayta tayinlashga harakat qilsak, kompilyatsiya vaqtida xatolikka yo‘l qo‘yamiz. let
dan foydalanib, biz qiymat bo'yicha bir nechta o'zgarishlarni amalga oshirishimiz mumkin, lekin bu o'zgarishlar tugagandan so'ng o'zgaruvchi o'zgarmas bo'lishi mumkin.
Mut
va shadow o'rtasidagi boshqa farq shundaki, biz let
kalit so'zini qayta ishlatganimizda yangi o'zgaruvchini samarali yaratayotganimiz sababli, qiymat turini o`zgartirishimiz mumkin, lekin bir xil nomni qayta ishlatishimiz ham mumkin. Misol uchun, bizning dasturimiz foydalanuvchidan bo'sh joy belgilarini kiritish orqali ba'zi matnlar orasida qancha bo'sh joy bo'lishini ko'rsatishni so'raydi va biz ushbu kiritishni raqam sifatida saqlamoqchimiz:
fn main() { let joylar = " "; let joylar = joylar.len(); }
Birinchi joylar
o'zgaruvchisi satr turi, ikkinchi joylar
o'zgaruvchisi esa raqam turi. Shadowing shu tariqa bizni turli nomlar bilan chiqishdan saqlaydi, masalan, joylar_str
va joylar_num
; Buning o'rniga biz oddiyroq joylar
nomini qayta ishlatishimiz mumkin. Biroq, bu erda ko'rsatilganidek, buning uchun mut
dan foydalanmoqchi bo'lsak, kompilyatsiya vaqtida xatoga duch kelamiz:
fn main() {
let mut joylar = " ";
joylar = joylar.len();
}
Xato bizga o'zgaruvchining turini mutatsiyaga o'tkazishga ruxsat yo'qligini aytadi:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut joylar = " ";
| ----- expected due to this value
3 | joylar = joylar.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error
Endi biz o'zgaruvchilar qanday ishlashini o'rganib chiqdik, keling, ular bo'lishi mumkin bo'lgan ko'proq ma'lumotlar turlarini ko'rib chiqaylik.
Ma'lumotlar turlari
Rust-dagi har bir qiymat ma'lum bir ma'lumot turiga tegishli bo'lib, Rustga qanday ma'lumotlar ko'rsatilayotganligini bildiradi, shuning uchun u ushbu ma'lumotlar bilan qanday ishlashni biladi. Biz ikkita ma'lumotlar turini ko'rib chiqamiz: skalyar va birikma.
Esda tutingki, Rust statik tarzda yozilgan tildir, ya'ni kompilyatsiya vaqtida barcha o'zgaruvchilarning turlarini bilishi kerak. Kompilyator odatda qiymat va uni qanday ishlatishimiz asosida biz qaysi turdan foydalanmoqchi ekanligimiz haqida xulosa chiqarishi mumkin.
Ko‘p turlar mumkin bo‘lgan hollarda, masalan, 2-bobdagi “Tahminni maxfiy raqam bilan solishtirish” bo‘limidagi parse
yordamida String
ni raqamli turga o‘zgartirganimizda, quyidagi turdagi izohni qo‘shishimiz kerak:
#![allow(unused)] fn main() { let taxmin: u32 = "42".parse().expect("Raqam emas!"); }
Oldingi kodda ko'rsatilgan : u32
turidagi izohni qo'shmasak, Rust quyidagi xatoni ko'rsatadi, ya'ni kompilyator bizdan qaysi turdan foydalanishni xohlayotganimizni bilish uchun qo'shimcha ma'lumotga muhtoj:
$ 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 taxmin = "42".parse().expect("Raqam emas!");
| ^^^^^
|
help: consider giving `taxmin` an explicit type
|
2 | let taxmin: _ = "42".parse().expect("Raqam emas!");
| +++
For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations` due to previous error
Boshqa ma'lumotlar turlari uchun turli turdagi izohlarni ko'rasiz.
Skalyar Turlar
Skalyar turi bitta qiymatni ifodalaydi. Rust to'rtta asosiy skalyar turga ega: integerlar, floating-point number, boolean va belgilar. Siz ularni boshqa dasturlash tillaridan bilishingiz mumkin. Keling, ularning Rustda qanday ishlashini ko'rib chiqaylik.
Integer Turlari
Integer kasr komponenti bo‘lmagan sondir. Biz 2-bobda u32
tipidagi bitta integer sonni ishlatdik. Ushbu turdagi deklaratsiya u bilan bog'langan qiymat 32 bit bo'sh joyni egallagan belgisiz butun son bo'lishi kerakligini bildiradi (Signed integer sonlar u
o'rniga i
bilan boshlanadi). 3-1-jadvalda Rust-da o'rnatilgan integer son turlari ko'rsatilgan. Integer son qiymatining turini e'lon qilish uchun biz ushbu variantlardan foydalanishimiz mumkin.
Uzunlik | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
Signedlar kichkina i
harfi bilan boshlanadi, Unsigned esa kichik u
harfi bilan boshlanadi.
Har bir variant signed yoki unsigned bo'lishi mumkin va aniq o'lchamga ega. Signed va Unsigned raqam manfiy boʻlishi mumkinmi yoki yoʻqligini anglatadi, boshqacha qilib aytganda, raqam u bilan birga belgiga ega boʻlishi (signed) boʻlishi kerakmi yoki u faqat ijobiy bo'ladimi va shuning uchun belgisiz (unsigned) ifodalanishi mumkinmi. Bu raqamlarni qog'ozga yozishga o'xshaydi: belgi muhim bo'lsa, raqam ortiqcha yoki minus belgisi bilan ko'rsatiladi; ammo, agar raqamni ijobiy deb hisoblash xavfsiz bo'lsa, u hech qanday belgisiz ko'rsatiladi. Signed raqamlar ikkita to'ldiruvchi ko'rinish yordamida saqlanadi.
Har bir signed variant -(2n - 1) dan 2n -
1 -1 gacha bo'lgan raqamlarni saqlashi mumkin, bu erda n variant foydalanadigan bitlar soni.
Shunday qilib, i8
-(27) dan 27 - 1, gacha bo'lgan raqamlarni saqlashi mumkin, bu tengdir -128 dan 127 gacha.
Unsigned variantlar 0 dan 2n - 1 gacha raqamlarni saqlashi mumkin, shuning uchun u8
0 dan 28 - 1 gacha bo'lgan raqamlarni saqlashi mumkin, bu 0 dan 255 gacha.
Bundan tashqari, isize
va usize
turlari dasturingiz ishlayotgan kompyuterning arxitekturasiga bog'liq bo'lib, u jadvalda “arch” sifatida ko'rsatilgan: agar siz 64 bitli arxitekturada bo'lsangiz 64 bit va 32 bitli arxitekturada bo'lsangiz 32 bit.
Integer sonlarni 3-2-jadvalda ko'rsatilgan istalgan shaklda yozishingiz mumkin. E'tibor bering, bir nechta raqamli turlar bo'lishi mumkin bo'lgan son harflari turni belgilash uchun 57u8
kabi tur qo'shimchasiga ruxsat beradi. Raqamni o'qishni osonlashtirish uchun _
dan raqamli harflar ham foydalanishi mumkin, masalan, 1_000
, siz 1000
ni ko'rsatganingizdek bir xil qiymatga ega bo'ladi.
Raqamli harflar | Misol |
---|---|
O'nlik | 98_222 |
O'n oltilik | 0xff |
Sakkizlik | 0o77 |
Ikkilik | 0b1111_0000 |
Bayt (faqat "u8") | b'A' |
Xo'sh, qaysi turdagi integer sonni ishlatishni qanday bilasiz? Agar ishonchingiz komil bo'lmasa, Rustning standart sozlamalari odatda boshlash uchun yaxshi joylardir: integer son turlari standart bo'yicha i32
dir. isize
yoki usize
dan foydalanadigan asosiy holat to'plamning bir turini indekslashdir.
Integer Overflow
Aytaylik, sizda 0 dan 255 gacha bo'lgan qiymatlarni ushlab turadigan
u8
tipidagi o'zgaruvchi bor. Agar siz o'zgaruvchini ushbu diapazondan tashqaridagi qiymatga o'zgartirishga harakat qilsangiz, masalan, 256, integer overflow sodir bo'ladi, bu ikki xatti-harakatdan biriga olib kelishi mumkin. Debug mode rejimida kompilyatsiya qilayotganingizda, Rust butun sonlarning to'lib ketishini tekshirishni o'z ichiga oladi, bu esa dasturni ishga tushirish vaqtida panic chiqaradi. Rust dastur xato bilan chiqqanda panicking atamasini ishlatadi; Biz panic haqida 9-bobdagi “panic
bilan tuzatib bo'lmaydigan xatolar” bo'limda batafsil ko'rib chiqamiz
--release
buyrug'i bilan reliz rejimida kompilyatsiya qilayotganingizda, Rust panic keltirib chiqaradigan butun sonlarni tekshirishni o'z ichiga olmaydi. overflow occur sodir bo'ladi Rust ikkitasini to'ldiruvchi wrapni bajaradi. Qisqa qilib aytganda, turdagi maksimal qiymatdan kattaroq qiymatlar, tur ushlab turishi mumkin bo'lgan minimal qiymatlargacha "wrap" ni tashkil qiladi.u8
holatida 256 qiymati 0 ga, 257 qiymati 1 ga aylanadi va hokazo. Dastur panic qo'ymaydi, lekin o'zgaruvchi siz kutgan qiymatga ega bo'lmaydi. Butun sonlarni wrapga tayanish xato hisoblanadi. Owerflow ehtimolini aniq ko'rib chiqish uchun siz prime sonlar uchun standart kutubxona tomonidan taqdim etilgan ushbu metodlar oilalaridan foydalanishingiz mumkin:
- Barcha modelarni
wrapping_*
metodlari bilan oʻrash, masalan,wrapping_add
.- Agar
checked_*
metodlari owerflow boʻlsa,None
qiymatini qaytaring.- Qiymat va boolean qiymatni qaytaring, bu
overflowing_*
metodlari bilan overflow bo'lganini ko'rsatadi.- Qiymatning minimal yoki maksimal qiymatlarida
saturating_*
metodllari bilan saturate bo'lgan.
Floating-Point Turlari
Rust shuningdek floating-point raqamlar uchun ikkita primitive turga ega, ular kasrli raqamlardir.
Rust-ning floating-point turlari f32
va f64
bo'lib, ular mos ravishda 32 bit va 64 bit o'lchamga ega.
Standart tur f64
dir, chunki zamonaviy protsessorlarda u f32
bilan bir xil tezlikda, lekin aniqroq bo'lishga qodir.
Barcha floating-point turlari signeddir.
Bu yerda harakatdagi floating-point raqamlarni ko'rsatadigan misol:
Fayl nomi: src/main.rs
fn main() { let x = 2.0; // f64 let y: f32 = 3.0; // f32 }
Floating-point raqamlari IEEE-754 standartiga muvofiq taqdim etiladi. f32
turi bitta aniqlikdagi floatdir va f64
ikki tomonlama aniqlikka ega.
Raqamli operatsiyalar
Rust barcha turdagi raqamlar uchun kutilgan asosiy matematik operatsiyalarni qo'llab-quvvatlaydi: qo'shish, ayirish, ko'paytirish, bo'lish va qoldiq. Butun sonni bo'lish noldan eng yaqin butun songa qisqaradi. Quyidagi kod let
iborasida har bir raqamli operatsiyadan qanday foydalanishni ko'rsatadi:
Fayl nomi: src/main.rs
fn main() { // qo'shish let qoshish = 5 + 10; // ayirish let ayrish = 95.5 - 4.3; // ko'paytirish let kopaytirish = 4 * 30; // bo'lish let bolish = 56.7 / 32.2; let manfiy = -5 / 3; // Natijalar -1 // qoldiq let qoldiq = 43 % 5; }
Ushbu bayonotlardagi har bir ifoda matematik operatordan foydalanadi va bitta qiymatga baholanadi, keyin esa o'zgaruvchiga bog'lanadi. B ilovasi da Rust taqdim etgan barcha operatorlar ro'yxati mavjud.
Boolean turi
Ko'pgina boshqa dasturlash tillarida bo'lgani kabi, Rust-da ham Boolean turi ikkita mumkin bo'lgan qiymatga ega: true
va false
. Boolean hajmi bir baytga teng.
Rustdagi boolean turi bool
yordamida belgilanadi. Misol uchun:
Fayl nomi: src/main.rs
fn main() { let t = true; let f: bool = false; // aniq turdagi izoh bilan }
Boolean qiymatlardan foydalanishning asosiy metodi shartlardir, masalan, if
ifodasidir. Rustda if
iboralari qanday ishlashini “Control Flow” bo‘limida ko‘rib chiqamiz.
Belgilar(Character) turi
Rustning char
turi tilning eng primitive alifbo turidir. Mana char
qiymatlarini e'lon qilishning ba`zi misollari:
Fayl nomi: src/main.rs
fn main() { let c = 'z'; let z: char = 'ℤ'; // aniq turdagi izoh bilan let yurak_kozli_mushuk = '😻'; }
E'tibor bering, biz qo'sh tirnoq ishlatadigan satr harflaridan farqli o'laroq, char
harflarini bitta tirnoq bilan belgilaymiz. Rustning char
turi to'rt bayt o'lchamga ega va Unicode Scalar qiymatini ifodalaydi, ya'ni u ASCIIdan ko'ra ko'proq narsani anglatishi mumkin.
Urg'uli harflar; Xitoy, yapon va koreys belgilar; emoji; va nol kenglikdagi boʻshliqlar Rust-dagi barcha haqiqiy char
qiymatlaridir. Unicode Scalar qiymatlari U+0000
dan U+D7FF
gacha va U+E000
dan U+10FFFF
gacha.
Biroq, “character” aslida Unicode-da tushuncha emas, shuning uchun “character” nima ekanligi haqidagi Rustdagi char
bilan mos kelmasligi mumkin. Biz ushbu mavzuni 8-bobdagi “UTF-8 kodlangan matnni satrlar bilan saqlash” bo'limida batafsil muhokama qilamiz.
Murakkab turlar
Murakkab turlar bir nechta qiymatlarni bir turga to'plashi mumkin.Rust ikkita primitive birikma turiga ega: tuplelar va arraylar.
Tuple turi
tuple - bu turli xil turlarga ega bo'lgan bir qator qiymatlarni bitta qo'shma turga birlashtirishning umumiy metodi.Tuplelar belgilangan uzunlikka ega: bir marta e'lon qilingandan so'ng, ular o'sishi yoki kichrayishi mumkin emas.
Qavslar ichida vergul bilan ajratilgan qiymatlar ro'yxatini yozish orqali tuple yaratamiz. Tupledagi har bir pozitsiya o'z turiga ega va tupledagi turli qiymatlarning turlari bir xil bo'lishi shart emas. Ushbu misolda biz ixtiyoriy turdagi izohlarni qo'shdik:
Fayl nomi: src/main.rs
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }
tup
o'zgaruvchisi butun tuplega bog'lanadi, chunki tuple bitta birikma element hisoblanadi. Tupledan individual qiymatlarni olish uchun biz tuple qiymatini buzish uchun pattern moslashuvidan foydalanishimiz mumkin, masalan:
Fayl nomi: src/main.rs
fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; println!("y qiymati: {y}"); }
Bu dastur avval tuple yaratadi va uni tup
o'zgaruvchisiga bog'laydi.Keyin u tup
ni olish va uni uchta alohida o‘zgaruvchiga, x
, y
va z
ga aylantirish uchun let
bilan pattern ishlatadi. Bu destruktura deb ataladi, chunki u bitta tupleni uch qismga ajratadi. Nihoyat, dastur y
qiymatini chop etadi, bu 6,4
.
Shuningdek, biz to'g'ridan-to'g'ri nuqta (.
) va undan keyin kirishni xohlagan qiymat indeksidan foydalanib, tuple elementiga kirishimiz mumkin. Misol uchun:
Fayl nomi: src/main.rs
fn main() { let x: (i32, f64, u8) = (500, 6.4, 1); let besh_yuz = x.0; let olti_butun_tort= x.1; let bir = x.2; }
Bu dastur x
tuplesini yaratadi va so'ngra o'z indekslari yordamida tuplening har bir elementiga kiradi. Ko'pgina dasturlash tillarida bo'lgani kabi, tupledagi birinchi indeks 0 ga teng.
Hech qanday qiymatsiz tuple maxsus nomga, unit ega. Bu qiymat va unga mos keladigan tur ()
yoziladi va bo'sh qiymat yoki bo'sh qaytish turini ifodalaydi. Ifodalar, agar ular boshqa qiymatni qaytarmasa, bilvosita birlik qiymatini qaytaradi.
Array Turi
Bir nechta qiymatlar to'plamiga ega bo'lishning yana bir usuli arraydir. Tupledan farqli o'laroq, arrayning har bir elementi bir xil turdagi bo'lishi kerak. Ba'zi boshqa tillardagi arraylardan farqli o'laroq, Rustdagi arraylar belgilangan uzunlikka ega.
Biz arraydagi qiymatlarni kvadrat qavslar ichida vergul bilan ajratilgan ro'yxat sifatida yozamiz:
Fayl nomi: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; }
Arraylar maʼlumotlaringizni toʻplamga emas, balki stekga ajratishni istasangiz foydali boʻladi (biz 4-bobda) stek va toʻplam haqida koʻproq gaplashamiz yoki sizda har doim maʼlum miqdordagi elementlar mavjudligini taʼminlashni istasangiz). Array vektor turi kabi moslashuvchan emas. Vektor standart kutubxona tomonidan taqdim etilgan o'xshash to'plam turi bo'lib, uning hajmini o'stirish yoki kichraytirishi mumkin. Agar array yoki vektordan foydalanishga ishonchingiz komil bo'lmasa, vektordan foydalanishingiz mumkin. 8-bobda vektorlar batafsilroq muhokama qilinadi.
Biroq, agar elementlar sonini o'zgartirish kerak bo'lmasligini bilsangiz, arraylar foydaliroq bo'ladi. Misol uchun, agar siz dasturda oy nomlaridan foydalansangiz, vektordan ko'ra massivdan foydalanar edingiz, chunki u har doim 12 ta elementdan iborat bo'lishini bilasiz:
#![allow(unused)] fn main() { let oylar = ["Yanvar", "Fevral", "Mart", "Aprel", "May", "Iyun", "Iyul", "Avgust", "Setabr", "Oktabr", "Noyabr", "Dekabr"]; }
Siz har bir element turi, nuqta-vergul va arraydagi elementlar soni bilan kvadrat qavslar yordamida array turini yozasiz, masalan:
#![allow(unused)] fn main() { let a: [i32; 5] = [1, 2, 3, 4, 5]; }
Bu erda i32
har bir elementning turi. Nuqtali verguldan keyin 5
raqami array beshta elementdan iboratligini bildiradi.
Bundan tashqari, har bir element uchun bir xil qiymatni o'z ichiga olgan arrayni boshlang'ich qiymatdan keyin nuqta-vergul qo'yib, so'ngra bu yerda ko'rsatilgandek kvadrat qavs ichida array uzunligini belgilash orqali ishga tushirishingiz mumkin:
#![allow(unused)] fn main() { let a = [3; 5]; }
a
nomli array dastlab 3
qiymatiga o'rnatiladigan 5
elementni o'z ichiga oladi. Bu let a = [3, 3, 3, 3, 3];
yozish bilan bir xil, ammo qisqaroq tarzda.
Array elementlariga kirish
Array - bu stekda taqsimlanishi mumkin bo'lgan ma'lum, qat'iy o'lchamdagi xotiraning bitta bo'lagi. Siz indekslash yordamida array elementlariga kirishingiz mumkin, masalan:
Fayl nomi: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; let birinchi = a[0]; let ikkinchi = a[1]; }
Bu misolda birinchi
deb nomlangan o‘zgaruvchi 1
qiymatini oladi, chunki bu arraydagi [0]
indeksidagi qiymatdir. ikkinchi
deb nomlangan ozgaruvchi arraydagi [1]
indeksidan 2
qiymatini oladi.
Yaroqsiz Array elementlariga kirish
Keling, array oxiridan o‘tgan array elementiga kirishga harakat qilsangiz nima bo‘lishini ko‘rib chiqamiz. Aytaylik, foydalanuvchidan array indeksini olish uchun 2-bobdagi taxminiy o‘yinga o‘xshash ushbu kodni ishlatasiz:
Fayl nomi: src/main.rs
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Iltimos, array indeksini kiriting.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Satrni o‘qib bo‘lmadi");
let index: usize = index
.trim()
.parse()
.expect("Kiritilgan indeks raqam emas");
let element = a[index];
println!("{index} indeksidagi elementning qiymati: {element}");
}
Ushbu kod muvaffaqiyatli kompilyatsiya qilinadi.Agar siz ushbu kodni cargo run
yordamida ishga tushirsangiz va 0
, 1
, 2
, 3
yoki 4
kiritsangiz, dastur arraydagi ushbu indeksdagi mos qiymatni chop etadi. Buning o'rniga array oxiridan o'tgan raqamni kiritsangiz, masalan, 10
, siz shunday chiqishni ko'rasiz:
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
Dastur indekslash operatsiyasida yaroqsiz qiymatdan foydalanish nuqtasida runtime xatosiga olib keldi. Dastur xato xabari bilan chiqdi va yakuniy println!
bayonotini bajarmadi. Indekslash yordamida elementga kirishga harakat qilganingizda, Rust siz ko'rsatgan indeks array uzunligidan kamroq ekanligini tekshiradi. Agar indeks uzunlikdan kattaroq yoki unga teng bo'lsa, Rust panic chiqaradi. Bu tekshirish runtimeda amalga oshirilishi kerak, ayniqsa bu holatda, chunki kompilyator foydalanuvchi kodni keyinroq ishga tushirganda qanday qiymat kiritishini bila olmaydi.
Bu Rustning xotira xavfsizligi tamoyillarining amaldagi namunasidir. Ko'pgina low-leveldagi tillarda bunday tekshirish amalga oshirilmaydi va noto'g'ri indeksni taqdim etganingizda, yaroqsiz xotiraga kirish mumkin. Rust xotiraga kirishga ruxsat berish va davom ettirish o'rniga darhol chiqish orqali sizni bunday xatolardan himoya qiladi. 9-bobda Rust-ning xatolarini qanday hal qilish va siz panic qo'ymaydigan va yaroqsiz xotiraga kirishga ruxsat bermaydigan o'qilishi mumkin bo'lgan xavfsiz kodni qanday yozishingiz mumkinligi muhokama qilinadi.
Funksiyalar
Funksiyalar Rust kodida keng tarqalgan. Siz allaqachon tildagi eng muhim funksiyalardan birini ko'rgansiz: ko'plab dasturlarning kirish nuqtasi bo'lgan main
funksiya. Siz yangi funksiyalarni e'lon qilish imkonini beruvchi fn
kalit so'zini ham ko'rdingiz.
Rust kodi funksiya va oʻzgaruvchilar nomlari uchun anʼanaviy uslub sifatida snake case dan foydalanadi, unda barcha harflar kichik va alohida soʻzlarning tagiga chiziladi. Mana, misol funksiya ta'rifini o'z ichiga olgan dastur:
Fayl nomi: src/main.rs
fn main() { println!("Hello, world!"); boshqa_funksiya(); } fn boshqa_funksiya() { println!("Boshqa funksiya."); }
Rust-da funksiyani fn
so'ng funksiya nomi va qavslar to'plamini kiritish orqali aniqlaymiz. Jingalak qavslar kompilyatorga funksiya tanasi qayerda boshlanishi va tugashini bildiradi.
Biz belgilagan har qanday funksiyani uning nomidan keyin qavslar to'plamini kiritish orqali chaqirishimiz mumkin. Dasturda boshqa_funksiya
ni aniqlanganligi sababli uni main
funksiya ichidan chaqirish mumkin. E'tibor bering, biz boshqa_funksiya
ni manba kodidagi main
funksiyadan keyin belgilaganmiz; uni avval ham belgilashimiz mumkin edi. Rust sizning funksiyalaringizni qayerda belgilashingizning ahamiyati yo'q, faqat ular so'rov yuboruvchi tomonidan ko'rinadigan doirada aniqlangan.
Keling, funksiyalarni ko'proq o'rganish uchun funksiyalar nomli yangi binary loyihani boshlaylik. boshqa_funksiya
misolini src/main.rs ga joylashtiring va uni ishga tushiring.Quyidagi chiqishni ko'rishingiz kerak:
$ 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!
Boshqa funksiya.
Qatorlar main
funksiyada paydo bo'ladigan tartibda bajariladi.
Avvaliga "Hello, world!" xabar chop etiladi, keyin boshqa_funksiya
chaqiriladi va uning xabari chop etiladi.
Parametrlar
Biz funksiyalarni parametrlari bo'lishi uchun belgilashimiz mumkin, ular funksiya imzosining bir qismi bo'lgan maxsus o'zgaruvchilardir. Agar funksiya parametrlarga ega bo'lsa, siz unga ushbu parametrlar uchun aniq qiymatlarni berishingiz mumkin. Texnik jihatdan aniq qiymatlar argumentlar deb ataladi, ammo tasodifiy suhbatda odamlar funksiya taʼrifidagi oʻzgaruvchilar yoki funksiyani chaqirganingizda qabul qilingan aniq qiymatlar uchun parametr va argument soʻzlarini bir-birining oʻrniga ishlatishga moyildirlar.
boshqa_funksiya
ning ushbu versiyasida biz parametr qo'shamiz:
Fayl nomi: src/main.rs
fn main() { boshqa_funksiya(5); } fn boshqa_funksiya(x: i32) { println!("x qiymati: {x}"); }
Ushbu dasturni ishga tushirishga harakat qiling; quyidagi chiqishni olishingiz kerak:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
x qiymati: 5
boshqa_funksiya
deklaratsiyasi x
nomli bitta parametrga ega. x
turi i32
sifatida belgilangan. Biz 5
ni boshqa_funksiya
ga o‘tkazganimizda, println!
makros 5
ni x
ni o‘z ichiga olgan jingalak qavslar juftligi format satrida joylashgan joyga qo‘yadi.
Funksiya signaturelarda siz har bir parametr turini e'lon qilishingiz kerak. Bu Rust dizaynidagi ataylab qabul qilingan qaror: funksiya taʼriflarida turdagi izohlarni talab qilish kompilyatorga qaysi turni nazarda tutayotganingizni tushunish uchun ularni kodning boshqa joylarida ishlatishingizga deyarli hech qachon ehtiyoj sezmasligini anglatadi. Kompilyator, shuningdek, funksiya qanday turlarni kutayotganini bilsa, yanada foydali xato xabarlarini berishi mumkin.
Bir nechta parametrlarni belgilashda parametr deklaratsiyasini vergul bilan ajrating, masalan:
Fayl nomi: src/main.rs
fn main() { belgilangan_vaqt(5, 'h'); } fn belgilangan_vaqt(value: i32, unit_label: char) { println!("Belgilangan Vaqt: {value}{unit_label}"); }
Ushbu misol ikkita parametrli belgilangan_vaqt
nomli funksiyani yaratadi. Birinchi parametr value
deb nomlangan va i32
dir. Ikkinchisi unit_label
deb nomlanadi va char
turidir. Keyin funksiya value
va ``unit_label` ni o‘z ichiga olgan matnni chop etadi.
Keling, ushbu kodni ishga tushirishga harakat qilaylik. Hozirda funksiyalar loyihangizning src/main.rs faylidagi dasturni oldingi misol bilan almashtiring va uni cargo run
yordamida ishga tushiring:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
Belgilangan Vaqt: 5h
Biz funksiyani value
qiymati sifatida 5
va unit_label
qiymati sifatida 'h'
deb ataganimiz sababli, dastur chiqishi ushbu qiymatlarni o`z ichiga oladi.
Statementlar va Expressionlar
Funksiya qismlari ixtiyoriy ravishda statement bilan tugaydigan bir qator expressionlardan iborat. Hozircha biz ko'rib chiqqan funksiyalar yakuniy expressionni o'z ichiga olmagan, lekin siz expressionni statementning bir qismi sifatida ko'rdingiz. Rust expressionga asoslangan til bo'lganligi sababli, bu tushunish uchun muhim farqdir. Boshqa tillar bir xil farqlarga ega emas, shuning uchun keling, qanday statementlar va expressionlar ekanligini va ularning farqlari funksiyalar tanasiga qanday ta'sir qilishini ko'rib chiqaylik.
- Statementlar ba'zi amallarni bajaradigan va qiymat qaytarmaydigan ko'rsatmalardir.
- Expressionlar qiymatga baholanadi. Keling, ba'zi misollarni ko'rib chiqaylik.
Biz allaqachon statementlar va expressionlarni ishlatganmiz. O'zgaruvchini yaratish va unga let
kalit so'zi bilan qiymat berish - bu statement. 3-1 ro'yxatda let y = 6;
- bu statement.
Fayl nomi: src/main.rs
fn main() { let y = 6; }
Funksiya definitionlari ham statementlardir; oldingi misol o'z-o'zidan bir statementdir.
Statementlar qiymatlarni qaytarmaydi. Shuning uchun siz boshqa o'zgaruvchiga let
iborasini tayinlay olmaysiz, chunki quyidagi kod bunga harakat qiladi; siz xatoga duch kelasiz:
Fayl nomi: src/main.rs
fn main() {
let x = (let y = 6);
}
Ushbu dasturni ishga tushirganingizda, sizda paydo bo'ladigan xato quyidagicha ko'rinadi:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
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 unstable
--> 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
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 3 previous errors; 1 warning emitted
let y = 6
statementi qiymat qaytarmaydi, shuning uchun x
bog'lanishi uchun hech narsa yo'q. Bu boshqa tillarda sodir bo'ladigan narsadan farq qiladi, masalan, C va Ruby, bu yerda assignment assignmentning qiymatini qaytaradi. Bu tillarda siz x = y = 6
yozishingiz mumkin va x
va y
ham 6
qiymatiga ega; Rustda bunday emas.
Expressionlar qiymatga baholanadi va siz Rust-da yozadigan kodning qolgan qismini tashkil qiladi. 5 + 6
kabi matematik amalni ko'rib chiqing, bu 11
qiymatini beruvchi expressiondir. Expressionlar statementlarning bir qismi bo'lishi mumkin: 3-1 ro'yxatdagi let y = 6;
ifodasidagi 6
, 6
qiymatini beruvchi expressiondir. Funksiyani chaqirish expressiondir. Makroni chaqirish expressiondir. Jingalak qavslar bilan yaratilgan yangi qamrov bloki expressiondir, masalan:
Fayl nomi: src/main.rs
fn main() { let y = { let x = 3; x + 1 }; println!("y qiymati: {y}"); }
Bu expression:
{
let x = 3;
x + 1
}
blok bo'lib, bu holda 4
ga evaluate bo'ladi. Bu qiymat let
statementining bir qismi sifatida y
ga bog'lanadi. E'tibor bering, "x + 1
qatorining oxirida nuqta-vergul yo'q, bu siz ko'rgan ko'pgina qatorlardan farqli o'laroq. Expressionlar yakuniy nuqtali vergullarni o'z ichiga olmaydi. Ifodaning oxiriga nuqtali vergul qo'shsangiz, uni statementga aylantirasiz va u keyinchalik qiymatni qaytarmaydi. Keyingi funksiyani qaytarish qiymatlari va expressionlarini o'rganayotganda buni yodda tuting.
Return qiymatlari bilan funksiyalar
Funksiyalar qiymatlarni ularni chaqiradigan kodga return qaytarishi mumkin. Return qiymatlarini nomlamaymiz, lekin ularning turini o'qdan keyin e'lon qilishimiz kerak (->
). Rustda funksiyaning return qiymati funksiya tanasi blokidagi yakuniy ifodaning qiymati bilan sinonimdir. Siz return
kalit so'zidan foydalanib va qiymatni belgilash orqali funksiyadan erta qaytishingiz mumkin, lekin ko'pchilik funksiyalar oxirgi expressionni bevosita qaytaradi. Mana qiymatni return qiladigan funksiyaga misol:
Fayl nomi: src/main.rs
fn besh() -> i32 { 5 } fn main() { let x = besh(); println!("x qiymati: {x}"); }
besh
funksiyasida funksiya chaqiruvlari, makroslar va hatto let
iboralari ham yo‘q – faqat 5
raqamining o‘zi. Bu Rust-da juda to'g'ri funksiya. Funksiyaning return turi ham -> i32
sifatida ko'rsatilganligini unutmang.Ushbu kodni ishga tushirishga harakat qiling; chiqish quyidagicha ko'rinishi kerak:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
x qiymati: 5
besh
dagi 5
funksiyaning return qiymatidir, shuning uchun return turi i32
dir. Keling, buni batafsilroq ko'rib chiqaylik. Ikkita muhim bit mavjud: birinchidan, let x = besh();
qatori biz o'zgaruvchini ishga tushirish uchun funksiyaning return qiymatidan foydalanayotganimizni ko'rsatadi. Chunki besh
funksiyasi 5
ni qaytaradi, bu qator quyidagi bilan bir xil:
#![allow(unused)] fn main() { let x = 5; }
Ikkinchidan, besh
funksiyasi hech qanday parametrga ega emas va return qiladigan qiymat turini belgilaydi, lekin funksiyaning tanasi nuqta-vergulsiz yolg‘iz 5
bo‘ladi, chunki bu biz qiymatini qaytarmoqchi bo‘lgan ifodadir.
Keling, yana bir misolni ko'rib chiqaylik:
Fayl nomi: src/main.rs
fn main() { let x = qoshilgan_bir(5); println!("x qiymati: {x}"); } fn qoshilgan_bir(x: i32) -> i32 { x + 1 }
Ushbu kodni ishga tushirish x qiymati: 6
ni chop etadi. Ammo, agar biz x + 1
bo'lgan satr oxiriga nuqta-vergul qo'ysak, uni expressiondan statementga o'zgartirsak, xatoga yo'l qo'yamiz:
Fayl nomi: src/main.rs
fn main() {
let x = qoshilgan_bir(5);
println!("x qiymati: {x}");
}
fn qoshilgan_bir(x: i32) -> i32 {
x + 1;
}
Ushbu kodni kompilyatsiya qilish quyidagi kabi xatoga olib keladi:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn qoshilgan_bir(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` due to previous error
Asosiy xato xabari, mismatched types
(mos kelmaydigan turlar) ushbu kod bilan bog'liq asosiy muammoni ochib beradi. qoshilgan_bir
funksiyasining taʼrifida aytilishicha, u i32
ni qaytaradi, lekin statementlar birlik turi boʻlgan ()
bilan expression bo'lgan qiymatga evaluate bo'lmaydi. Shuning uchun, hech narsa return qilinmaydi, bu funksiya definitioniga zid keladi va xatolikka olib keladi. Ushbu chiqishda Rust bu muammoni tuzatishga yordam beradigan xabarni taqdim etadi: u nuqta-vergulni olib tashlashni taklif qiladi, bu xatoni tuzatadi.
Izohlar
Barcha dasturchilar o'z kodlarini tushunishni osonlashtirishga harakat qilishadi, lekin ba'zida qo'shimcha tushuntirish kerak. Bunday hollarda dasturchilar o'zlarining manba kodlarida izohlar qoldiradilar, ularni kompilyator e'tiborsiz qoldiradi, ammo manba kodini o'qiyotgan odamlar uchun foydali bo'lishi mumkin.
Mana oddiy izoh:
#![allow(unused)] fn main() { // hello, world }
Rustda idiomatik izoh uslubi izohni ikki qiyshiq chiziq bilan boshlaydi va izoh satr oxirigacha davom etadi. Bitta satrdan tashqariga chiqadigan izohlar uchun har bir satrga //
qo'shishingiz kerak bo'ladi, masalan:
#![allow(unused)] fn main() { // Shunday qilib, biz bu erda murakkab ish qilyapmiz, // bizga bir nechta izohlar kerak bo'ladi! Vou! Umid qilamanki, // bu izoh nima bo'layotganini tushuntiradi. }
Izohlar, shuningdek, kodni o'z ichiga olgan qatorlar oxirida joylashtirilishi mumkin:
Fayl nomi: src/main.rs
fn main() { let omadli_raqam = 7; // Bugun o'zimni omadli his qilyapman }
Ammo siz ularni ushbu formatda ko'proq ko'rasiz, izohli kod ustidagi alohida satrda izoh bilan:
Fayl nomi: src/main.rs
fn main() { // Bugun o'zimni omadli his qilyapman let omadli_raqam = 7; }
Rustda yana bir turdagi izohlar, hujjatlar izohlari mavjud, biz ularni 14-bobning “Crates.io-ga crateni nashr qilish“ bo'limida muhokama qilamiz.
Control Flow
Shartning true
yoki yo'qligiga qarab ba'zi kodlarni ishga tushirish va shart true
bo'lganda ba'zi kodlarni qayta-qayta ishga tushirish qobiliyati ko'pchilik dasturlash tillarida asosiy building bloklari hisoblanadi. Rust kodining bajarilishini nazorat qilish imkonini beruvchi eng keng tarqalgan konstruksiyalar if
expressionlari va looplaridir.
if
ifodalari
if
ifodasi shartlarga qarab kodingizni branchga ajratish imkonini beradi. Siz shartni taqdim etasiz va keyin shunday deb aytasiz: “Agar bu shart bajarilsa, ushbu kod blokini ishga tushiring. Agar shart bajarilmasa, ushbu kod blokini ishga tushirmang."
If
ifodasini oʻrganish uchun loyihalar jildingizda branchlar nomli yangi loyiha yarating. src/main.rs faylida quyidagilarni kiriting:
Fayl nomi: src/main.rs
fn main() { let raqam = 3; if raqam < 5 { println!("shart true edi"); } else { println!("shart true edi"); } }
Barcha if
expressionlari if
kalit so‘zidan boshlanadi, undan keyin shart keladi. Bunday holda, shart raqam
o'zgaruvchisi 5 dan kichik qiymatga ega yoki yo'qligini tekshiradi. Agar shart true
bo'lsa, biz kod blokini shartdan keyin darhol jingalak qavslar ichiga joylashtiramiz.
if
expressionlaridagi shartlar bilan bog‘langan kod bloklari ba’zan arms deb ataladi, xuddi biz 2-bobning “Tahminni maxfiy raqam bilan solishtirish” bo‘limida muhokama qilgan match
expressionlaridagi qurollar kabi.
Ixtiyoriy ravishda, agar shart false
deb baholansa, dasturga bajarilishi uchun muqobil kod blokini berish uchun biz tanlagan else
expressionini ham kiritishimiz mumkin. Agar else
ifodasini bermasangiz va shart false
bo‘lsa, dastur shunchaki if
blokini o‘tkazib yuboradi va kodning keyingi bitiga o‘tadi.
Ushbu kodni ishga tushirishga harakat qiling; quyidagi chiqishni ko'rishingiz kerak:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branchlar)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branchlar`
shart true edi
Keling, nima sodir bo'lishini ko'rish uchun raqam
qiymatini shartni false
qiladigan qiymatga o'zgartirib ko'raylik:
fn main() {
let raqam = 7;
if raqam < 5 {
println!("shart true edi");
} else {
println!("shart false edi");
}
}
Dasturni qayta ishga tushiring va natijaga qarang:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
shart false edi
Shuni ham ta'kidlash kerakki, ushbu koddagi shart bool
bo'lishi kerak. Agar shart bool
bo'lmasa, biz xatoga yo'l qo'yamiz. Masalan, quyidagi kodni ishga tushirishga harakat qiling:
Fayl nomi: src/main.rs
fn main() {
let raqam = 3;
if raqam {
println!("raqam uchta edi");
}
}
if
sharti bu safar 3
qiymatiga teng bo'ladi va Rust xato qiladi:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if raqam {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error
Xato shuni ko'rsatadiki, Rust bool
kutgan, lekin integer(butun) son olgan. Ruby va JavaScript kabi tillardan farqli o'laroq, Rust boolean bo'lmagan turlarni boolean tilga o'zgartirishga avtomatik ravishda urinmaydi. Siz aniq bo'lishingiz va har doim if
ni mantiqiy shart sifatida ko'rsatishingiz kerak. Agar biz if
kod bloki faqat raqam 0
ga teng bo‘lmaganda ishlashini istasak, masalan, if
ifodasini quyidagiga o‘zgartirishimiz mumkin:
Fayl nomi: src/main.rs
fn main() { let raqam = 3; if raqam != 0 { println!("raqam noldan boshqa narsa edi"); } }
Ushbu kodni ishga tushirish raqam noldan boshqa narsa edi
chop etiladi.
else if
bilan bir nechta shartlarni boshqarish
if
va else
ni else if
ifodasida birlashtirib, bir nechta shartlardan foydalanishingiz mumkin.Misol uchun:
Fayl nomi: src/main.rs
fn main() { let raqam = 6; if raqam % 4 == 0 { println!("raqam 4 ga bo'linadi"); } else if raqam % 3 == 0 { println!("raqam 3 ga bo'linadi"); } else if raqam % 2 == 0 { println!("raqam 2 ga bo'linadi"); } else { println!("raqam 4, 3 yoki 2 ga bo'linmaydi"); } }
Ushbu dasturda to'rtta yo'l bor. Uni ishga tushirgandan so'ng siz quyidagi chiqishni ko'rishingiz kerak:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
raqam 3 ga bo'linadi
Ushbu dastur bajarilganda, u har bir if
expressionni navbatma-navbat tekshiradi va shart true
deb baholanadigan birinchi tanani bajaradi. E'tibor bering 6, 2 ga bo'linsa ham, biz son 2 ga bo'linmaydi
chiqishini ko'rmayapmiz va else
blokidagi raqam 4, 3 yoki 2 ga bo'linmaydi
matnini ko'rmaymiz.Buning sababi, Rust faqat birinchi true
shart uchun blokni bajaradi va bir marta topilsa, qolganlarini ham tekshirmaydi.
Juda ko'p else if
expressionlaridan foydalanish kodingizni buzishi mumkin, shuning uchun sizda bir nechta bo'lsa, kodingizni qayta tahrirlashni xohlashingiz mumkin. 6-bobda bu holatlar uchun match
deb nomlangan kuchli Rust tarmoqli konstruksiyasi tasvirlangan.
let
statementida if
dan foydalanish
if
expression bo‘lganligi sababli, biz 3-2-listdagi kabi natijani o‘zgaruvchiga belgilash uchun let
statementining o‘ng tomonida foydalanishimiz mumkin.
Fayl nomi: src/main.rs
fn main() { let shart = true; let raqam = if shart { 5 } else { 6 }; println!("Raqamning qiymati: {raqam}"); }
raqam
o'zgaruvchisi if
expressioni natijasiga asoslangan qiymatga bog'lanadi. Nima sodir bo'lishini ko'rish uchun ushbu kodni ishga tushiring:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
Raqamning qiymati: 5
Esda tutingki, kod bloklari ulardagi oxirgi expressiongacha evaluate qilianadi va raqamlar o'zlari ham expressionlardir. Bu holda butun if
expressionning qiymati qaysi kod bloki bajarilishiga bog'liq. Bu if
ning har bir armidan result bo'lish potentsialiga ega bo'lgan qiymatlar bir xil turdagi bo'lishi kerakligini anglatadi; 3-2 ro'yxatda if
va else
armllarining natijalari i32
butun sonlari edi. Agar turlar mos kelmasa(mismatched), quyidagi misolda bo'lgani kabi, biz xatoga duch kelamiz:
Fayl nomi: src/main.rs
fn main() {
let shart = true;
let raqam = if shart { 5 } else { "olti" };
println!("Raqamning qiymati: {raqam}");
}
Ushbu kodni kompilyatsiya qilmoqchi bo'lganimizda, biz xatoga duch kelamiz. if
va else
armllari mos kelmaydigan qiymat turlariga ega va Rust muammoni dasturda qayerdan topish mumkinligini aniq ko'rsatadi:
$ 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 raqam = if shart { 5 } else { "olti" };
| - ^^^^^ 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
blokidagi expression butun songa, else
blokidagi expression esa satrga baholanadi. Bu ishlamaydi, chunki oʻzgaruvchilar bitta turga ega boʻlishi kerak va Rust kompilyatsiya vaqtida raqam
oʻzgaruvchisi qaysi turini aniq bilishi kerak. raqam
turini bilish kompilyatorga ushbu tur biz raqam
ishlatadigan hamma joyda yaroqliligini tekshirish imkonini beradi. Agar raqam
turi faqat runtimeda aniqlangan bo'lsa, Rust buni qila olmaydi; kompilyator murakkabroq bo'lar edi va agar u har qanday o'zgaruvchi uchun bir nechta gipotetik turlarni kuzatib borishi kerak bo'lsa, kod haqida kamroq kafolatlar beradi.
Looplar bilan takrorlash
Ko'pincha kod blokini bir necha marta bajarish foydali bo'ladi. Ushbu vazifani bajarish uchun Rust bir nechta looplarni taqdim etadi, ular sikl tanasi ichidagi kod orqali oxirigacha ishlaydi va keyin darhol boshida boshlanadi. Looplar bilan tajriba o'tkazish uchun keling, looplar deb nomlangan yangi loyiha yarataylik.
Rustda uch xil looplar mavjud: loop
, while
va for
. Keling, har birini sinab ko'raylik.
Kodni loop
bilan takrorlash
loop
kalit so'zi Rustga kod blokini abadiy qayta-qayta bajarishni yoki uni to'xtatishni aniq aytmaguningizcha bajarishni aytadi.
Misol tariqasida, looplar jildingizdagi src/main.rs faylini quyidagicha o'zgartiring:
Fayl nomi: src/main.rs
fn main() {
loop {
println!("yana!");
}
}
Ushbu dasturni ishga tushirganimizda, dasturni qo'lda to'xtatmagunimizcha, yana!
so'zi doimiy ravishda chop etilishini ko'ramiz.Aksariyat terminallar uzluksiz siklda ishlab qolgan dasturni to'xtatish uchun ctrl-c klaviatura yorliqlarini qo'llab-quvvatlaydi.
Sinab ko'ring:
$ cargo run
Compiling loops v0.1.0 (file:///projects/looplar)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/looplar`
yana!
yana!
yana!
yana!
^Cyana!
^C
belgisi ctrl-c tugmalarini bosgan joyni bildiradi. Kod uzilish signalini qabul qilganda siklning qayerda bo'lganiga qarab, ^C
dan keyin chop etilgan yana!
so'zini ko'rishingiz yoki ko'rmasligingiz mumkin.
Yaxshiyamki, Rust kod yordamida loopdan chiqish yo'lini ham taqdim etadi. Siz dasturga siklni bajarishni qachon to'xtatish kerakligini aytish uchun break
kalit so'zini siklga qo'yishingiz mumkin.
Eslatib o'tamiz, biz buni 2-bobning ”To'g'ri taxmindan keyin chiqish” bo'limidagi taxminiy o'yinda, foydalanuvchi to'g'ri raqamni taxmin qilish orqali o'yinda g'alaba qozonganida dasturdan chiqish uchun qilganmiz.
Shuningdek, biz taxmin qilish o'yinida continue
dan foydalandik, bu siklda dasturga siklning ushbu iteratsiyasida qolgan har qanday kodni o'tkazib yuborish va keyingi iteratsiyaga o'tishni aytadi.
Looplardan qiymatlarni qaytarish(return)
loop
dan foydalanishdan biri bu ish bajarilmasligi mumkin bo'lgan operatsiyani qaytadan urinish, masalan, thread o'z ishini tugatganligini tekshirish. Bundan tashqari, ushbu operatsiya natijasini kodingizning qolgan qismiga sikldan o'tkazishingiz kerak bo'lishi mumkin. Buning uchun siklni toʻxtatish uchun foydalanadigan break
ifodasidan keyin return qilinishi kerak boʻlgan qiymatni qoʻshishingiz mumkin; bu qiymat loopdan qaytariladi, shuning uchun uni bu yerda ko'rsatilganidek ishlatishingiz mumkin:
fn main() { let mut hisoblagich = 0; let natija = loop { hisoblagich += 1; if hisoblagich == 10 { break hisoblagich * 2; } }; println!("Natija: {natija}"); }
Loopdan oldin biz hisoblagich
nomli o‘zgaruvchini e’lon qilamiz va uni 0
ga ishga tushiramiz. Keyin sikldan qaytarilgan qiymatni ushlab turish uchun natija
nomli o'zgaruvchini e'lon qilamiz. Loopning har bir iteratsiyasida biz hisoblagich
o‘zgaruvchisiga 1
qo‘shamiz va keyin hisoblagich
10 ga teng yoki yo‘qligini tekshiramiz. Bu bo'lganda, biz hisoblagich * 2
qiymati bilan break
kalit so'zidan foydalanamiz. Loopdan so'ng biz natija
qiymatini belgilaydigan statementni tugatish uchun nuqta-verguldan foydalanamiz. Nihoyat, biz qiymatni natija
da chop qilamiz, bu holda 20
.
Bir nechta looplar orasidagi farqni ajratish uchun loop labellari
Agar sizda looplar ichida looplaringiz bo'lsa, o'sha nuqtada eng ichki loopga break
va continue
amallari qo'llaniladi. Siz ixtiyoriy ravishda siklda loop label belgilashingiz mumkin, undan so‘ng break
yoki continue
bilan o‘sha kalit so‘zlar eng ichki loop o‘rniga belgilangan loopga qo‘llanilishini belgilashingiz mumkin. Loop labellari bitta tirnoqcha bilan boshlanishi kerak. Mana ikkita ichki loop bilan bir misol:
fn main() { let mut hisob = 0; 'hisoblash: loop { println!("hisob = {hisob}"); let mut qolgan = 10; loop { println!("qolgan = {qolgan}"); if qolgan == 9 { break; } if hisob == 2 { break 'hisoblash; } qolgan -= 1; } hisob += 1; } println!("Yakuniy hisob = {hisob}"); }
Tashqi loopda 'hisoblash
labeli bor va u 0 dan 2 gacha hisoblanadi.
Labelsiz ichki loop 10 dan 9 gacha hisoblanadi. Label ko'rsatilmagan birinchi break
faqat ichki sikldan chiqadi. break 'hisoblash;
statementi tashqi sikldan chiqadi. Keling kodni run qilib ko'ramiz:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
hisob = 0
qolgan = 10
qolgan = 9
hisob = 1
qolgan = 10
qolgan = 9
hisob = 2
qolgan = 10
Yakuniy hisob = 2
while
bilan shartli looplar
Dastur ko'pincha loop ichidagi shartni evaluate qilishi kerak bo'ladi. Shart true
bo'lsa-da, loop ishlaydi. Shart true
bo'lishni to'xtatganda, dastur loopni to'xtatib, break
ni chaqiradi. Bu kabi xatti-harakatlarni loop
, if
, else
va break
kombinatsiyasidan foydalanib amalga oshirish mumkin; Agar xohlasangiz, buni hozir dasturda sinab ko'rishingiz mumkin. Biroq, bu pattern shunchalik keng tarqalganki, Rustda buning uchun while
sikli deb ataladigan o'rnatilgan til konstruktsiyasi mavjud. 3-3 ro'yxatda biz dasturni uch marta aylanish uchun while
dan foydalanamiz, har safar sanab chiqamiz, so'ngra sikldan so'ng xabarni chop etamiz va chiqamiz.
Fayl nomi: src/main.rs
fn main() { let mut raqam = 3; while raqam != 0 { println!("{raqam}!"); raqam -= 1; } println!("LIFTOFF!!!"); }
Bu konstruksiya loop
, if
, else
va break
dan foydalansangiz, zarur bo'ladigan ko'plab joylashtirishlarni yo'q qiladi va bu aniqroq bo'ladi. Shart true
deb baholansa, kod ishlaydi; aks holda, u loopdan chiqadi.
for
bilan to'plam bo'ylab aylanish
Siz while
konstruksiyasidan array kabi to‘plam elementlari ustidan aylanishni tanlashingiz mumkin. Masalan, 3-4 ro'yxatdagi sikl a
arrayidagi har bir elementni chop etadi.
Fayl nomi: src/main.rs
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("qiymati: {}", a[index]); index += 1; } }
Bu erda kod arraydagi elementlar orqali hisoblanadi. U 0
indeksidan boshlanadi va keyin arraydagi yakuniy indeksga yetguncha (ya'ni, index < 5
endi true
bo`lmaganda) sikl davom etadi. Ushbu kodni ishga tushirish arraydagi har bir elementni chop etadi:
$ cargo run
Compiling looplar v0.1.0 (file:///projects/looplar)
Finished dev [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/looplar`
qiymati: 10
qiymati: 20
qiymati: 30
qiymati: 40
qiymati: 50
Barcha besh array qiymatlari kutilganidek terminalda paydo bo'ladi. Garchi index
bir nuqtada 5
qiymatiga yetsa ham, arraydan oltinchi qiymatni olishga urinishdan oldin sikl ishlashni to‘xtatadi.
Biroq, bu yondashuv xatoga moyil; Agar indeks qiymati yoki test holati noto'g'ri bo'lsa, biz dasturni panic qo'yishimiz mumkin. Misol uchun, agar siz a
arrayining ta'rifini to'rtta elementga o'zgartirsangiz, lekin shartni while index < 4
bo'lganda yangilashni unutgan bo'lsangiz, kod panic qo'zg'atadi. Bu ham sekin, chunki kompilyator sikl orqali har bir iteratsiyada indeks array chegaralarida ekanligini shartli tekshirish uchun runtime kodini qo‘shadi.
Aniqroq variant sifatida, siz for
siklidan foydalanishingiz va to'plamdagi har bir element uchun ba'zi kodlarni bajarishingiz mumkin. for
sikli 3-5-ro'yxatdagi kodga o'xshaydi.
Fayl nomi: src/main.rs
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("qiymati: {element}"); } }
Ushbu kodni ishga tushirganimizda, biz 3-4 ro'yxatdagi kabi natijani ko'ramiz. Eng muhimi, biz kodning xavfsizligini oshirdik va arrayning oxiridan tashqariga chiqish yoki yetarlicha uzoqqa bormaslik va ba'zi elementlarni yetishmayotganligi sababli paydo bo'lishi mumkin bo'lgan xatolar ehtimolini yo'q qildik.
for
siklidan foydalanib, agar siz 3-4 roʻyxatda qoʻllanilgan metodda boʻlgani kabi arraydagi qiymatlar sonini oʻzgartirsangiz, boshqa kodni oʻzgartirishni eslab qolishingiz shart emas.
for
looplarining xavfsizligi va ixchamligi ularni Rustda eng ko‘p ishlatiladigan loop konstruksiyasiga aylantiradi. 3-3 ro'yxatdagi while
siklidan foydalanilgan ortga hisoblash misolida bo'lgani kabi, ma'lum bir necha marta kodni ishlatmoqchi bo'lgan vaziyatlarda ham ko'pchilik Rustaceanlar for
siklidan foydalanadilar. Buning yo'li standart kutubxona tomonidan taqdim etilgan Range
dan foydalanish bo'lib, bir raqamdan boshlanib, boshqa raqamdan oldin tugaydigan barcha raqamlarni ketma-ketlikda hosil qiladi.
Ortga hisoblash for
sikli va biz hali u to‘g‘risida gapirmagan boshqa metod – rev
yordamida diapazonni teskari tomonga o‘zgartirishga o‘xshaydi:
Fayl nomi: src/main.rs
fn main() { for raqam in (1..4).rev() { println!("{raqam}!"); } println!("LIFTOFF!!!"); }
Bu kod biroz chiroyliroq, shunday emasmi?
Xulosa
Siz erishdingiz! Bu juda katta bob bo'ldi: siz o'zgaruvchilar, skalyar va compound ma'lumotlar turlari, funksiyalar, izohlar, if
expressionlari va sikllar haqida bilib oldingiz! Ushbu bobda muhokama qilingan tushunchalar bilan mashq qilish uchun quyidagilarni amalga oshirish uchun dasturlar yaratishga harakat qiling:
- Haroratni Farengeyt va Selsiy o'rtasida o'zgartiring.
- nta Fibonachchi raqamini yarating.
- Qo'shiqning takrorlanishidan foydalanib, “Rojdestvoning o'n ikki kuni“ Rojdestvo qo'shig'ining so'zlarini chop eting.
Davom etishga tayyor bo'lganingizda, Rustda boshqa dasturlash tillarida odatda mavjud bo'lmagan ownership(egalik) tushunchasi haqida gaplashamiz.
Ownershipni tushunish (Egalik)
Ownership Rustning eng noyob xususiyati bo'lib, tilning qolgan qismiga chuqur ta'sir ko'rsatadi. Bu Rust-ga garbage collectorga muhtoj bo'lmasdan xotira xavfsizligini kafolatlash imkonini beradi, shuning uchun ownership qanday ishlashini tushunish muhimdir. Ushbu bobda biz ownership huquqi, shuningdek, bir nechta tegishli xususiyatlar haqida gapiramiz: borrowing, slices va Rust ma'lumotlarni xotirada qanday joylashtirishi.
Ownership Nima?
Ownership(Egalik) bu Rust dasturi xotirani qanday boshqarishini boshqaradigan qoidalar to'plami. Barcha dasturlar ishlayotgan vaqtda kompyuter xotirasidan qanday foydalanishini boshqarishi kerak. Ba'zi tillarda axlat yig'ish(garbage collection) mavjud bo'lib, ular dastur ishlayotgan paytda ishlatilmaydigan xotirani muntazam ravishda qidiradi; boshqa tillarda dasturchi xotirani aniq ajratishi va bo'shatishi kerak. Rust uchinchi yondashuvdan foydalanadi: xotira kompilyator tekshiradigan qoidalar to'plamiga ownership tizimi orqali boshqariladi. Agar biron bir qoidalar buzilgan bo'lsa, dastur kompilatsiya qilinmaydi. Ownership xususiyatlarining hech biri dasturingiz ishlayotgan vaqtda sekinlashtirmaydi.
Ownership ko'plab dasturchilar uchun yangi tushuncha bo'lganligi sababli, unga ko'nikish uchun biroz vaqt kerak bo'ladi. Yaxshi xabar shundaki, siz Rust va ownership tizimi qoidalari bilan qanchalik tajribali bo'lsangiz, xavfsiz va samarali kodni tabiiy ravishda ishlab chiqish osonroq bo'ladi. Unda davom etamiz!
Ownershipni tushunganingizda, Rustni noyob qiladigan xususiyatlarni tushunish uchun mustahkam asosga ega bo'lasiz. Ushbu bobda, siz juda keng tarqalgan ma'lumotlar tuzilishiga qaratilgan ba'zi misollarni orqali ownershipni ishlashini o'rganasiz: string.
Stack va Heap
Ko'pgina dasturlash tillari stack va heap haqida tez-tez o'ylashingizni talab qilmaydi. Ammo Rust kabi tizim dasturlash tilida qiymat stackda yoki heapda bo'ladimi, til o'zini qanday tutishiga ta'sir qiladi va nima uchun siz ma'lum qarorlar qabul qilishingiz kerak. Ownershipning qismlari stack va heapga nisbatan keyinchalik ushbu bobda tasvirlanadi, shuning uchun bu yerda tayyorgarlik jarayonida qisqacha tushuntirish berilgan.
Stack ham, heap ham runtimeda foydalanish uchun kodingiz uchun mavjud bo'lgan xotira qismlaridir, lekin ular turli yo'llar bilan tuzilgan. Stack qiymatlarni ularni olgan tartibda saqlaydi va qiymatlarni teskari tartibda o'chiradi Bu oxirgi kelgan, birinchi chiqqan deb ataladi. Plitalar stackini o'ylab ko'ring: ko'proq plastinka qo'shsangiz, ularni qoziqning ustiga qo'yasiz va plastinka kerak bo'lganda, siz yuqoridan birini olib qo'yasiz. Plitalarni o'rtadan yoki pastdan qo'shish yoki olib tashlash ham ishlamaydi! Ma'lumotlarni qo'shish stackga qo'shish, ma'lumotlarni olib tashlash esa stackdan o'chirish deb ataladi. Stackda saqlangan barcha ma'lumotlar ma'lum, qat'iy belgilangan hajmga ega bo'lishi kerak. Kompilyatsiya vaqtida noma'lum o'lchamli yoki o'zgarishi mumkin bo'lgan o'lchamdagi ma'lumotlar esa heapda saqlanishi kerak.
heap kamroq tartibga solingan: ma'lumotlarni heapga qo'yganingizda, ma'lum miqdorda bo'sh joy talab qilasiz. Xotira ajratuvchisi(memory allocator) heapda yetarlicha katta bo'lgan bo'sh joyni topadi, uni ishlatilayotgan deb belgilaydi va o'sha joyning manzili bo'lgan pointerni ni qaytaradi. Bu jarayon heap allocating deb ataladi va ba'zan faqat allocating deb qisqartiriladi (qiymatlarni stackga qo'shish ajratish hisoblanmaydi). Heapga pointer ma'lum, qat'iy o'lcham bo'lgani uchun siz pointerni stackda saqlashingiz mumkin, lekin yaroqli ma'lumotlarni olishni istasangiz, pointergaga amal qilishingiz kerak. Restoranda o'tirganingizni o'ylab ko'ring. Kirish paytida siz guruhingizdagi odamlar sonini bildirasiz va uy egasi hammaga mos keladigan bo'sh stol topadi va sizni u yerga olib boradi. Agar guruhingizdagi kimdir kechikib kelsa, sizni topish uchun qayerda o'tirganingizni so'rashi mumkin.
stackga qo'shish heapda allocating qilishdan tezroq bo'ladi, chunki allacator hech qachon yangi ma'lumotlarni saqlash uchun joy izlamasligi kerak; bu joy har doim stackning yuqori qismida joylashgan. Nisbatan, heapda bo'sh joy ajratish ko'proq mehnat talab qiladi, chunki allacator avval ma'lumotlarni saqlash uchun yetarlicha katta joy topishi va keyingi allocatinga tayyorgarlik ko'rish uchun buxgalteriya hisobini amalga oshirishi kerak.
Heapdagi ma'lumotlarga kirish stackdagi ma'lumotlarga kirishdan ko'ra sekinroq, chunki u yerga borish uchun pointerga amal qilishingiz kerak. Zamonaviy protsessorlar xotirada kamroq o'tishsa, tezroq ishlaydi. O'xshashlikni davom ettirib, ko'plab jadvallardan buyurtmalarni qabul qiladigan restoran serverini ko'rib chiqing. Keyingi stolga o'tishdan oldin barcha buyurtmalarni bitta stolda olish eng samarali hisoblanadi. A jadvalidan buyurtma olish, keyin B jadvalidan buyurtma olish, keyin yana A dan va yana B dan bitta buyurtma olish ancha sekinroq jarayon bo'ladi. Xuddi shu qoidaga ko'ra, protsessor uzoqroqda emas (u heapda bo'lishi mumkin) emas, balki boshqa ma'lumotlarga yaqin (stackdagi kabi) ma'lumotlarda ishlasa, o'z ishini yaxshiroq bajarishi mumkin.
Sizning kodingiz funksiyani chaqirganda, funksiyaga o'tgan qiymatlar (shu jumladan, potentsial, heapdagi ma'lumotlarga pointerlar) va funksiyaning mahalliy o'zgaruvchilari stackga qo'shiladi. Funktsiya tugagach, bu qiymatlar stackdan chiqariladi.
Kodning qaysi qismlari heapda qaysi ma'lumotlardan foydalanayotganini kuzatib borish, heapdagi takroriy ma'lumotlar miqdorini minimallashtirish va bo'sh joy qolmasligi uchun heapdagi foydalanilmagan ma'lumotlarni tozalash - bularning barchasi ownership hal qiladigan muammolardir. Ownershipni tushunganingizdan so'ng, stack va heap haqida tez-tez o'ylashingiz shart emas, lekin ownership qilishning asosiy maqsadi heap ma'lumotlarni boshqarish ekanligini bilish uning nima uchun shunday ishlashini tushuntirishga yordam beradi.
Ownership qoidalari
Birinchidan, ownership qoidalarini ko'rib chiqaylik.Biz ularni ko'rsatadigan misollar bilan ishlashda ushbu qoidalarni yodda tuting:
- Rust-dagi har bir qiymat ownerga ega.
- Bir vaqtning o'zida faqat bitta owneri bo'lishi mumkin.
- Owneri amaldan tashqariga chiqsa, qiymat o'chiriladi.
O'zgaruvchan Scope
Endi biz Rustning asosiy sintaksisidan o‘tganimiz uchun, biz barcha fn main() {
kodini misollarga kiritmaymiz, shuning uchun agar kuzatib boradigan bo‘lsangiz, quyidagi misollarni main
funksiyasiga qo‘lda kiritganingizga ishonch hosil qiling. Natijada, bizning misollarimiz biroz ixchamroq bo'ladi, bu bizga boilerplate kodiga emas, balki haqiqiy tafsilotlarga e'tibor berishga imkon beradi.
Ownershipning birinchi misoli sifatida biz ba'zi o'zgaruvchilarning scopeni ko'rib chiqamiz. Scope - dastur doirasidagi element amal qiladigan diapazon. Quyidagi o'zgaruvchini oling:
#![allow(unused)] fn main() { let s = "salom"; }
s
o'zgaruvchisi satr literaliga ishora qiladi, bu yerda satr qiymati dasturimiz matniga qattiq kodlangan. O'zgaruvchi e'lon qilingan paytdan boshlab joriy scopning oxirigacha amal qiladi. 4-1 ro'yxatida s
o'zgaruvchisi qayerda to'g'ri bo'lishini izohlovchi izohlar bilan dastur ko'rsatilgan.
fn main() { { // s bu erda haqiqiy emas, u hali e'lon qilinmagan let s = "salom"; // s shu nuqtadan boshlab amal qiladi // s bilan ish bajaring } // bu scope endi tugadi va s endi haqiqiy emas }
Boshqacha qilib aytganda, bu yerda ikkita muhim nuqta bor:
- Qachonki
s
scopega kirsa, u amal qiladi. - U scopedan tashqariga chiqmaguncha amal qiladi.
Ushbu nuqtada, scopelar va o'zgaruvchilarning yaroqliligi o'rtasidagi munosabatlar boshqa dasturlash tillaridagiga o'xshaydi. Endi biz String
turini joriy qilish orqali ushbu tushunchaga asoslanamiz.
String
turi
Ownership qoidalarini tasvirlash uchun bizga 3-bobning ”Ma'lumotlar turlari”
bo'limida ko'rib chiqilganlarga qaraganda murakkabroq ma'lumotlar turi kerak. Oldin ko'rib chiqilgan turlar ma'lum o'lchamga ega bo'lib, ular stackda saqlanishi va qo'llanilish doirasi tugagach, stackdan o'chirilishi mumkin va agar kodning boshqa qismi foydalanishi kerak bo'lsa yangi, mustaqil misol yaratish uchun tez va ahamiyatsiz nusxa ko'chirilishi mumkin kodning boshqa qismi bir xil qiymatni boshqa doirada ishlatishi kerak. Ammo biz heapda saqlangan ma'lumotlarni ko'rib chiqmoqchimiz va Rust bu ma'lumotlarni qachon tozalashni bilishini o'rganmoqchimiz va String
turi ajoyib misoldir.
Biz String
ning ownership bilan bog'liq qismlariga e'tibor qaratamiz. Ushbu jihatlar standart kutubxona tomonidan taqdim etilganmi yoki siz yaratganmi, boshqa murakkab ma'lumotlar turlariga ham tegishli.
Biz 8-bobda String
ni chuqurroq muhokama qilamiz.
Biz allaqachon string literallarini ko'rdik, bu yerda string qiymati bizning dasturimizga qattiq kodlangan. String literallari qulay, ammo ular biz matndan foydalanmoqchi bo'lgan har qanday vaziyatga mos kelmaydi. Buning sabablaridan biri shundaki, ular o'zgarmasdir. Yana bir narsa shundaki, biz kodni yozganimizda har bir satr qiymatini bilish mumkin emas: masalan, agar biz foydalanuvchi ma'lumotlarini olib, uni saqlamoqchi bo'lsak-chi? Bunday holatlar uchun Rust ikkinchi string turiga ega, String
. Bu tur heapda ajratilgan ma'lumotlarni boshqaradi va shuning uchun kompilyatsiya vaqtida bizga noma'lum bo'lgan matn miqdorini saqlashi mumkin. Siz from
funksiyasidan foydalanib satr literalidan String
yaratishingiz mumkin, masalan:
#![allow(unused)] fn main() { let s = String::from("salom"); }
Ikki nuqtali ::
operatori bizga string_from
kabi qandaydir nomdan foydalanish o'rniga String
turi ostida ushbu from
funksiyasini nom maydoniga qo`yish imkonini beradi.
Biz ushbu sintaksisni 5-bobning ”Metod sintaksisi” bo'limida ko'proq muhokama qilamiz va 7-bobdagi ”Modul treedagi elementga murojaat qilish yo'llari” da modullar bilan nomlar oralig'i haqida gapiramiz.
Ushbu turdagi string mutatsiyaga uchragan bo'lishi mumkin:
fn main() { let mut s = String::from("salom"); s.push_str(", dunyo!"); // push_str() satrga literal qo'shadi println!("{}", s); // Bu “salom, dunyo!” deb chop etiladi }
Xo'sh, bu yerda qanday farq bor? Nima uchun String
ni mutatsiyaga solish mumkin, lekin harflarni o'zgartirish mumkin emas? Farqi bu ikki turning xotira bilan qanday munosabatda bo'lishida.
Xotira va Taqsimlash(Allocation)
String literalida biz kompilyatsiya vaqtida tarkibni bilamiz, shuning uchun matn to'g'ridan-to'g'ri yakuniy bajariladigan faylga qattiq kodlangan.Shuning uchun string literallari tez va samarali. Ammo bu xususiyatlar faqat satr literalining o'zgarmasligidan kelib chiqadi. Afsuski, kompilyatsiya vaqtida hajmi noma'lum bo'lgan va dasturni ishga tushirishda hajmi o'zgarishi mumkin bo'lgan har bir matn bo'lagi uchun biz binary faylga bir bo'lak xotira qo'ya olmaymiz.
String
turida o'zgaruvchan, o'sib boradigan matn qismini qo'llab-quvvatlash uchun tarkibni saqlash uchun kompilyatsiya vaqtida noma'lum bo'lgan xotira hajmini yig'ishda ajratishimiz kerak. Buning ma'nosi:
- Xotira runtimeda xotira allactoridan so'ralishi kerak.
String
bilan ishlashni tugatgandan so'ng, bizga ushbu xotirani allacatoriga qaytarish usuli kerak.
Bu birinchi qism biz tomonimizdan amalga oshiriladi: biz String::from
deb chaqirganimizda, uni implementi kerakli xotirani talab qiladi. Bu dasturlash tillarida deyarli universaldir.
Biroq, ikkinchi qism boshqacha. Garbage Collector (GC) bo'lgan tillarda GC endi ishlatilmayotgan xotirani kuzatib boradi va tozalaydi va bu haqda o'ylashimiz shart emas. Ko'pgina tillarda GC bo'lmaganda, xotiradan qachon foydalanilmayotganini aniqlash va uni aniq bo'shatish uchun kodni chaqirish, xuddi biz so'raganimizdek, bizning burchimizdir. Buni to'g'ri bajarish tarixan qiyin dasturlash muammosi bo'lgan. Agar unutsak, xotirani behuda sarflaymiz. Agar biz buni juda erta qilsak, bizda noto'g'ri o'zgaruvchi bo'ladi. Agar buni ikki marta qilsak, bu ham xato. Aynan bitta allocate
ni bitta bo'sh
bilan birlashtirishimiz kerak.
Rust boshqa yo'lni egallaydi: unga ega bo'lgan o'zgaruvchi amaldan tashqariga chiqqandan so'ng xotira avtomatik ravishda qaytariladi. Bu yerda 4-1 roʻyxatdagi misolimiz satr harfi oʻrniga String
yordamida berilgan:
fn main() { { let s = String::from("salom"); // s shu nuqtadan boshlab amal qiladi // s bilan ish bajaring } // bu scope endi tugadi va s yo'q // uzoqroq amal qiladi }
Biz String
kerak bo'lgan xotirani ajratuvchiga qaytarishimiz mumkin bo'lgan tabiiy nuqta bor: s
scopedan chiqib ketganda. O'zgaruvchi scopedan chiqib ketganda, Rust biz uchun maxsus funksiyani chaqiradi.Ushbu funktsiya drop
deb ataladi va u yerda String
muallifi xotirani qaytarish uchun kodni qo'yishi mumkin. Rust yopilgan jingalak qavsda avtomatik ravishda drop
ni chaqiradi.
Eslatma: C++ da, elementning ishlash muddati oxirida resurslarni taqsimlashning bunday sxemasi ba'zan Resource Acquisition Is Initialization (RAII)(Resurslarni yig'ish - ishga tushirish (RAII) deb ataladi. Agar siz RAII patternlaridan foydalangan bo'lsangiz, Rust-dagi
drop
funksiyasi sizga tanish bo'ladi.
Ushbu pattern Rust kodini yozish usuliga chuqur ta'sir qiladi. Bu hozir oddiy bo'lib tuyulishi mumkin, ammo biz bir nechta o'zgaruvchilar biz yig'ilgan ma'lumotlardan foydalanishni xohlayotganimizda, murakkabroq holatlarda kodning harakati kutilmagan bo'lishi mumkin. Keling, ushbu vaziyatlarning ba'zilarini ko'rib chiqaylik.
Move bilan o'zaro ta'sir qiluvchi o'zgaruvchilar va ma'lumotlar
Rustda bir nechta o'zgaruvchilar bir xil ma'lumotlar bilan turli yo'llar bilan o'zaro ta'sir qilishi mumkin. 4-2 ro'yxatda integer sondan foydalanish misolini ko'rib chiqaylik.
fn main() { let x = 5; let y = x; }
Bu nima qilayotganini taxmin qilishimiz mumkin: 5
qiymatini x
ga bog‘lang; keyin x
dagi qiymatdan nusxa oling va uni y
ga bog'lang. Endi bizda ikkita o'zgaruvchi bor, x
va y
va ikkalasi ham 5
ga teng. Bu haqiqatan ham sodir bo'lmoqda, chunki butun sonlar ma'lum, qat'iy o'lchamga ega oddiy qiymatlardir va bu ikkita 5
qiymat stackga qo'shiladi.
Endi String
versiyasini ko'rib chiqamiz:
fn main() { let s1 = String::from("salom"); let s2 = s1; }
Bu juda o'xshash ko'rinadi, shuning uchun biz uning ishlash metodi bir xil bo'ladi deb taxmin qilishimiz mumkin: ya'ni ikkinchi qator s1
qiymatining nusxasini yaratadi va uni s2
bilan bog'laydi. Ammo bu sodir bo'ladigan narsa emas.
Qopqoq ostidagi String
bilan nima sodir bo'layotganini ko'rish uchun 4-1-rasmga qarang. String
chap tomonda ko'rsatilgan uchta qismdan iborat: satr tarkibini saqlaydigan xotiraga ko'rsatgich, uzunlik(len) va sig'im(capacity).
Ushbu ma'lumotlar guruhi stackda saqlanadi. O'ng tomonda tarkibni saqlaydigan heap xotira joylashgan.
Uzunlik - String
mazmuni hozirda qancha xotira, baytlarda foydalanayotganligi. Sig'im(capacity) - bu String
allacatordan olgan xotiraning umumiy hajmi, baytlarda. Uzunlik va si'gimlar o'rtasidagi farq muhim, ammo bu kontekstda emas, shuning uchun hozircha si'gimlarni e'tiborsiz qoldirish yaxshi.
s1
ni s2
ga belgilaganimizda, String
ma'lumotlari nusxalanadi, ya'ni biz stackdagi pointer, uzunlik va sig`imdan nusxa olamiz. Biz pointer(ko'rsatkich) ko'rsatgan to'plamdagi ma'lumotlarni ko'chirmaymiz. Boshqacha qilib aytganda, ma'lumotlarning xotirada ko'rinishi 4-2-rasmga o'xshaydi.
Tasvir 4-3-rasmga o'xshamaydi, agar Rust o'rniga heap ma'lumotlarni ko'chirsa, xotira qanday ko'rinishga ega bo'lardi. Agar Rust buni amalga oshirgan bo'lsa, s2 = s1
operatsiyasi, agar heapdagi ma'lumotlar katta bo'lsa, runtimening ishlashi nuqtai nazaridan juda qimmat bo'lishi mumkin.
Avvalroq biz aytgan edikki, o‘zgaruvchi qo‘llanish doirasidan chiqib ketganda, Rust avtomatik ravishda drop
funksiyasini chaqiradi va bu o‘zgaruvchi uchun heap xotirani tozalaydi. Ammo 4-2-rasmda ikkala ma'lumot pointeri bir xil joyga ishora qiladi. Bu muammo: s2
va s1
scopedan chiqib ketganda, ikkalasi ham bir xil xotirani bo'shatishga harakat qiladi. Bu double free(ikki marta bo'sh)xato sifatida tanilgan va biz avval aytib o'tgan xotira xavfsizligi xatolaridan biridir. Xotirani ikki marta bo'shatish xotira buzilishiga olib kelishi mumkin, bu esa xavfsizlik zaifliklariga olib kelishi mumkin.
Xotira xavfsizligini ta'minlash uchun let s2 = s1;
qatoridan keyin Rust s1
ni endi yaroqsiz deb hisoblaydi. Shuning uchun, s1
qo'llanilgandan tashqariga chiqqanda Rust hech narsani bo'shatishi shart emas. s2
yaratilgandan keyin s1
dan foydalanmoqchi bo'lganingizda nima sodir bo`lishini tekshiring; u ishlamaydi:
fn main() {
let s1 = String::from("salom");
let s2 = s1;
println!("{}, dunyo!", s1);
}
Siz shunday xatoga yo'l qo'yasiz, chunki Rust bekor qilingan havoladan foydalanishga to'sqinlik qiladi:
$ 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("salom");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, dunyo!", s1);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error
Agar siz boshqa tillar bilan ishlashda shallow copy va deep copy so‘zlarini eshitgan bo‘lsangiz, pointerni nusxalash tushunchasi, ma'lumotlardan nusxa ko'chirmasdan uzunligi va sig'imi olish, ehtimol shallow copy kabi eshitiladi. Ammo Rust birinchi o'zgaruvchini ham bekor qilganligi sababli, shallow copy deb nomlanish o'rniga u move(ko'chirish) deb nomlanadi. Bu misolda s1
s2
ga ko'chirilgan deb aytamiz. Shunday qilib, aslida nima sodir bo'lishi 4-4-rasmda ko'rsatilgan.
Bu bizning muammomizni hal qiladi! Faqatgina s2
amal qilganda, u scopedan tashqariga chiqsa, u faqat xotirani bo'shatadi va biz tugatdik.
Bundan tashqari, dizayn tanlovi ham mavjud: Rust hech qachon avtomatik ravishda ma'lumotlaringizning "deep copyni" yaratmaydi. Shuning uchun, har qanday avtomatik nusxa ko'chirish runtimening ishlashi nuqtai nazaridan arzon deb taxmin qilish mumkin.
Clone bilan o'zaro ta'sir qiluvchi o'zgaruvchilar va ma'lumotlar
Agar biz faqat stack ma'lumotlarini emas, balki String
ning heap ma'lumotlarini deeply copyni istasak, clone
deb nomlangan umumiy metoddan foydalanishimiz mumkin. Metod sintaksisini 5-bobda muhokama qilamiz, lekin metodlar ko‘p dasturlash tillarida umumiy xususiyat bo‘lgani uchun siz ularni avval ko‘rgan bo‘lsangiz kerak.
Mana amaldagi clone
metodiga misol:
fn main() { let s1 = String::from("salom"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); }
Bu juda yaxshi ishlaydi va 4-3-rasmda ko'rsatilgan xatti-harakatni aniq ishlab chiqaradi, bu yerda heap ma'lumotlar nusxalanadi.
clone
ga murojatni ko'rsangiz, ba'zi bir ixtiyoriy kod bajarilayotganini va bu kod qimmat bo'lishi mumkinligini bilasiz. Bu boshqa narsa sodir bo'layotganining vizual ko'rsatkichidir.
Faqat stack ma'lumotlari: nusxalash
Biz hali gapirmagan yana bir narsa bor. Integer sonlardan foydalanadigan ushbu kod - bir qismi 4-2 ro'yxatda ko'rsatilgan - ishlaydi va amal qiladi:
fn main() { let x = 5; let y = x; println!("x = {}, y = {}", x, y); }
Ammo bu kod biz bilib olgan narsaga zid ko'rinadi: bizda clone
uchun murojat yo'q, lekin x
hali ham amal qiladi va y
ga o'tkazilmagan.
Sababi, kompilyatsiya vaqtida ma'lum o'lchamga ega bo'lgan integer sonlar kabi turlar to'liq stackda saqlanadi, shuning uchun haqiqiy qiymatlarning nusxalari tezda tayyorlanadi. Bu shuni anglatadiki, biz y
o'zgaruvchisini yaratganimizdan keyin x
ning haqiqiy bo'lishiga to'sqinlik qilish uchun hech qanday sabab yo'q. Boshqacha qilib aytadigan bo'lsak, bu yerda deep va shallow nusxa ko'chirish o'rtasida farq yo'q, shuning uchun clone
ni chaqirish odatdagi shallow copydan farq qilmaydi va biz uni tark etishimiz mumkin.
Rust Copy
traiti deb nomlangan maxsus annotationga ega bo'lib, uni butun sonlar kabi stackda saqlanadigan turlarga joylashtirishimiz mumkin (biz 10-bobda traitlar haqida ko'proq gaplashamiz). Agar tur Copy
traitini amalga oshirsa, undan foydalanadigan o‘zgaruvchilar harakatlanmaydi, aksincha, ahamiyatsiz tarzda ko‘chiriladi, bu esa boshqa o‘zgaruvchiga tayinlangandan keyin ham amal qiladi.
Rust turi yoki uning biron bir qismi Drop
traitini qo‘llagan bo‘lsa, Copy
bilan turga annotation qo‘yishimizga ruxsat bermaydi. Qiymat doirasidan chiqib ketganda turga maxsus biror narsa kerak bo'lsa va biz ushbu turga Copy
annotationni qo'shsak, biz kompilyatsiya vaqtida xatolikni olamiz. Traitni amalga oshirish uchun turingizga Copy
annotationni qanday qo‘shish haqida bilish uchun C ilovasidagi “Derivable Traitlar”ga qarang.
Xo'sh, Copy
traitini qaysi turlar amalga oshiradi? Ishonch hosil qilish uchun berilgan tur uchun texnik hujjatlarni tekshirishingiz mumkin, lekin umumiy qoida sifatida har qanday oddiy skalyar qiymatlar guruhi Copy
ni amalga oshirishi mumkin va ajratishni talab qiladigan yoki biron bir manba shakli bo‘lgan hech narsa Copy
ni amalga oshira olmaydi. Copy
ni amalga oshiradigan ba'zi turlar:
u32
kabi barcha integer turlari.- Boolean turi,
bool
,true
vafalse
qiymatlari bilan. - Barcha floating-point turlari, masalan,
f64
. - Belgi turi,
char
. - Tuplelar, agar ular faqat
Copy
ni ham implement qiladigan turlarni o'z ichiga olsa. Masalan,(i32, i32)
Copy
ni implement qiladi, lekin(i32, String)
bajarmaydi.
Ownership va Funksiyalar
Funksiyaga qiymat berish mexanikasi o'zgaruvchiga qiymat berish mexanikasiga o'xshaydi. O'zgaruvchini funksiyaga o'tkazish, xuddi assignment kabi ko'chiriladi yoki nusxalanadi. 4-3 ro'yxatda o'zgaruvchilarning qayerga kirishi va tashqariga chiqishini ko'rsatadigan ba'zi izohlar bilan misol mavjud.
Fayl nomi: src/main.rs
fn main() { let s = String::from("salom"); // s scopega kiradi ownershiplik_qiladi(s); // s qiymati funksiyaga o'tadi ... // ... va shuning uchun bu yerda endi amal qilmaydi let x = 5; // x scopega kiradi nusxasini_yaratadi(x); // x funksiyaga o'tadi, // lekin i32 nusxa ko'chirish, shuning uchun tinch qo'yish yaxshidir // keyin x dan foydalaning } // Bu erda x scopedan chiqib ketadi, keyin s. Lekin s qiymati ko'chirilganligi sababli, hech // qanday maxsus narsa sodir bo'lmaydi. fn ownershiplik_qiladi(some_string: String) { // some_string scopega kiradi println!("{}", some_string); } // Bu yerda some_string scopedan chiqib ketadi va `drop` deb ataladi. Qo'llab-quvvatlovchi // xotira bo'shatiladi. fn nusxasini_yaratadi(some_integer: i32) { // some_integer scopega kiradi println!("{}", some_integer); } // Bu erda some_integer scopedan tashqariga chiqadi. Hech qanday maxsus narsa bo'lmaydi.
Agar biz ownershiplik_qiladi
chaqiruvidan keyin s
dan foydalanmoqchi bo'lsak, Rust kompilyatsiya vaqtida xatolikka yo'l qo'yadi. Ushbu statik tekshiruvlar bizni xatolardan himoya qiladi. s
va x
dan foydalanadigan main
ga kod qo‘shib ko‘ring va ulardan qayerda foydalanishingiz mumkinligini va ownership qoidalari bunga xalaqit beradigan joyni ko‘ring.
Return qiymatlari va Scope
Return qilingan qiymatlar ownershipni ham o'tkazishi mumkin. 4-4 ro'yxatda 4-3 ro'yxatdagi kabi izohlar bilan ba'zi qiymatlarni qaytaradigan funksiya misoli ko'rsatilgan.
Fayl nomi: src/main.rs
fn main() { let s1 = egalik_beradi(); // egalik_beradi o'zining return qiymatini // s1 ga o'tkazadi let s2 = String::from("salom"); // s2 scopega kiradi let s3 = oladi_va_qaytaradi(s2); // s2 oladi_va_qaytaradi ichiga // ko'chiriladi, u ham o'zining return // qiymatini s3 ga o'tkazadi } // Bu erda s3 scopedan chiqib ketadi va o'chiriladi. s2 ko'chirildi, shuning uchun // hech narsa sodir bo'lmaydi. s1 scopedan chiqib ketadi va o'chiriladi. fn egalik_beradi() -> String { // egalik_beradi o'zining return // qiymatini uni chaqiradigan // funksiyaga o'tkazadi let some_string = String::from("rust"); // some_string scopea kiradi some_string // some_string return qilinadi va // chaqiruv funksiyasiga // o'tadi } // Bu funksiya Stringni oladi va bittasini qaytaradi fn oladi_va_qaytaradi(a_string: String) -> String { // a_string scopega // kiradi a_string // a_string qaytariladi va chaqiruv funksiyasiga o'tadi }
O'zgaruvchiga ownership har safar bir xil patternga amal qiladi: boshqa o'zgaruvchiga qiymat berish uni ko'chiradi. Heapdagi maʼlumotlarni oʻz ichiga olgan oʻzgaruvchi scopedan tashqariga chiqsa, agar maʼlumotlarga ownership boshqa oʻzgaruvchiga oʻtkazilmagan boʻlsa, qiymat drop
orqali tozalanadi.
Bu ishlayotganda, ownership va keyin har bir funksiyaga ownershipini qaytarish biroz zerikarli. Agar funksiyaga qiymatdan foydalanishiga ruxsat bermoqchi bo'lsak, lekin ownershiplik qilmasak nima bo'ladi? Bu juda zerikarli, agar biz uni qayta ishlatmoqchi bo'lsak, biz kiritgan har qanday narsa, shuningdek, biz qaytarishni xohlashimiz mumkin bo'lgan funktsiya tanasidan kelib chiqadigan har qanday ma'lumotlarga qo'shimcha ravishda qaytarib berilishi kerak.
Rust 4-5 ro'yxatda ko'rsatilganidek, tuple yordamida bir nechta qiymatlarni return qilish imkon beradi.
Fayl nomi: src/main.rs
fn main() { let s1 = String::from("salom"); let (s2, len) = uzunlikni_hisoblash(s1); println!("'{}' uzunligi {}.", s2, len); } fn uzunlikni_hisoblash(s: String) -> (String, usize) { let length = s.len(); // len() string uzunligini qaytaradi (s, length) }
Ammo bu umumiy bo'lishi kerak bo'lgan kontseptsiya uchun juda ko'p funksiya va juda ko'p ish. Yaxshiyamki, Rustda qiymatni ownershipni o'tkazmasdan ishlatish xususiyati mavjud, uni reference deb atashadi.
Reference va Borrowing
Ro'yxat 4-5dagi tuple kodi bilan bog'liq muammo shundaki, biz String
ni chaqiruvchi funksiyaga qaytarishimiz kerak, shunda biz uzunlikni_hisoblash
ga chaqiruvdan keyin ham String
dan foydalanishimiz mumkin, chunki String
uzunlikni_hisoblash
ga ko'chirildi. Buning o'rniga biz String
qiymatiga reference(havola) berishimiz mumkin.
Reference pointerga o'xshaydi, chunki u biz ushbu manzilda saqlangan ma'lumotlarga kirish uchun amal qilishimiz mumkin bo'lgan manzildir; bu ma'lumotlar boshqa o'zgaruvchilarga tegishli. Pointerdan farqli o'laroq, reference ma'lumotnomaning amal qilish muddati davomida ma'lum turdagi yaroqli qiymatni ko'rsatishi kafolatlanadi.
Qiymatga egalik qilish o'rniga parametr sifatida obyektga referencega ega bo'lgan uzunlikni_hisoblash
funksiyasini qanday aniqlash va ishlatishingiz mumkin:
Fayl nomi: src/main.rs
fn main() { let s1 = String::from("salom"); let len = uzunlikni_hisoblash(&s1); println!("'{}' uzunligi {}.", s1, len); } fn uzunlikni_hisoblash(s: &String) -> usize { s.len() }
Birinchidan, o'zgaruvchilar deklaratsiyasidagi barcha tuple kodi va funksiyani qaytarish qiymati yo'qolganiga e'tibor bering. Ikkinchidan, &s1
ni uzunlikni_hisoblash
ga o'tkazamiz va uning definitionida biz String
emas, &String
ni olamiz. Ushbu ampersandlar reference ni ifodalaydi va ular sizga biron bir qiymatga ownershiplik qilmasdan murojaat qilish imkonini beradi. 4-5-rasmda ushbu tushuncha tasvirlangan.
Eslatma:
&
yordamida reference qilishning teskarisi dereferencing bo'lib, u*
dereference operatori yordamida amalga oshiriladi. Biz 8-bobda dereference operatoridan baʼzi foydalanishni koʻrib chiqamiz va 15-bobda dereference tafsilotlarini muhokama qilamiz.
Keling, bu yerda funksiya chaqiruvini batafsil ko'rib chiqaylik:
fn main() { let s1 = String::from("salom"); let len = uzunlikni_hisoblash(&s1); println!("'{}' uzunligi {}.", s1, len); } fn uzunlikni_hisoblash(s: &String) -> usize { s.len() }
&s1
sintaksisi bizga s1
qiymatiga refers qiluvchi, lekin unga tegishli bo`lmagan reference yaratish imkonini beradi. Unga egalik qilmaganligi sababli, reference foydalanishni to'xtatganda, u ko'rsatgan qiymat o'chirilmaydi.
Xuddi shunday, funksiya imzosi s
parametrining turi reference ekanligini ko'rsatish uchun &
dan foydalanadi. Keling, ba'zi tushuntirish izohlarini qo'shamiz:
fn main() { let s1 = String::from("salom"); let len = uzunlikni_hisoblash(&s1); println!("'{}' uzunligi {}.", s1, len); } fn uzunlikni_hisoblash(s: &String) -> usize { // s - Stringga reference(havola) s.len() } // Bu yerda s scopedan chiqib ketadi. Lekin u nazarda tutgan itemga ownership qilmagani // uchun u tashlanmaydi.
s
o'zgaruvchisi amal qiladigan doirasi har qanday funksiya parametrining qamrovi bilan bir xil bo'ladi, lekin s
foydalanishni to'xtatganda reference ko'rsatilgan qiymat o'chirilmaydi, chunki s
ownershipga ega emas. Funksiya referencelarni yaroqli qiymatlar o'rniga parametr sifatida ko'rsatsa, biz ownershipni qaytarish uchun qiymatlarni qaytarishimiz shart emas, chunki bizda hech qachon ownership bo'lmagan.
Malumot yaratish harakatini borrowing(qarz olish) deb ataymiz. Haqiqiy hayotda bo'lgani kabi, agar biror kishi biror narsaga ega bo'lsa, siz undan qarz olishingiz mumkin. Ishingiz tugagach, uni qaytarib berishingiz kerak. Siz unga egalik qilmaysiz.
Xo'sh, agar biz borrowing qilgan narsani o'zgartirishga harakat qilsak nima bo'ladi? 4-6 ro'yxatdagi kodni sinab ko'ring. Spoiler ogohlantirish: bu ishlamaydi!
Fayl nomi: src/main.rs
fn main() {
let s = String::from("salom");
almashtirish(&s);
}
fn almashtirish(some_string: &String) {
some_string.push_str(", rust");
}
Mana xato:
$ 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 almashtirish(some_string: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
8 | some_string.push_str(", rust");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `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
O'zgaruvchilar standart bo'yicha o'zgarmas bo'lganidek, referencelar ham shunday. Bizga reference biror narsani o'zgartirishga ruxsat berilmagan.
O'zgaruvchan Referencelar
Biz 4-6 ro'yxatdagi kodni tuzatishimiz mumkin, buning o'rniga o'zgaruvchan reference
dan foydalanadigan bir nechta kichik sozlashlar bilan borrow qilingan qiymatni o'zgartirishimiz mumkin:
Fayl nomi: src/main.rs
fn main() { let mut s = String::from("salom"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", dunyo"); }
Avval s
ni mut
qilib o'zgartiramiz. Keyin biz &mut s
bilan o'zgaruvchan reference yaratamiz, bu yerda biz change
funksiyasini chaqiramiz va some_string: &mut String
bilan o'zgaruvchan referencei qabul qilish uchun funksiya signatureni yangilaymiz. Bu change
funksiyasi olingan qiymatni o'zgartirishini aniq ko'rsatadi.
O'zgaruvchan referencelar bitta katta cheklovga ega: agar sizda qiymatga o'zgaruvchan reference bo'lsa, sizda bu qiymatga boshqa referencelar bo'lishi mumkin emas. s
ga ikkita o'zgaruvchan reference yaratishga urinayotgan ushbu kod muvaffaqiyatsiz bo'ladi:
Fayl nomi: src/main.rs
fn main() {
let mut s = String::from("salom");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
Mana xato:
$ 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 xatolik bu kodning yaroqsiz ekanligini bildiradi, chunki biz bir vaqtning o'zida bir necha marta o'zgaruvchan s
ni borrow qila ololmaymiz. Birinchi o'zgaruvchan borrow r1
da bo'lib, u println!
da ishlatilgunga qadar davom etishi kerak, lekin bu o'zgaruvchan referenceni yaratish va undan foydalanish o'rtasida, biz r2
da r1
bilan bir xil ma'lumotlarni olgan boshqa o`zgaruvchan reference yaratishga harakat qildik.
Bir vaqtning o'zida bir xil ma'lumotlarga bir nechta o'zgaruvchan referencelarni oldini oluvchi cheklov mutatsiyaga imkon beradi, lekin juda nazorat ostida. Bu yangi Rustaceanlar bilan kurashadigan narsa, chunki aksariyat tillar xohlagan vaqtda mutatsiyaga o'tishga imkon beradi. Ushbu cheklovning afzalligi shundaki, Rust kompilyatsiya vaqtida data raceni oldini oladi. Data race poyga holatiga o'xshaydi va bu uchta xatti-harakatlar sodir bo'lganda sodir bo'ladi:
- Ikki yoki undan ortiq pointerlar bir vaqtning o'zida bir xil ma'lumotlarga kirishadi.
- Pointerlardan kamida bittasi ma'lumotlarga yozish uchun ishlatiladi.
- Ma'lumotlarga kirishni sinxronlashtirish uchun hech qanday mexanizm ishlatilmaydi.
Data race aniqlanmagan xatti-harakatlarga olib keladi va ularni runtimeda kuzatib borishga harakat qilayotganingizda tashxis qo'yish va tuzatish qiyin bo'lishi mumkin; Rust data racelari bilan kodni kompilyatsiya qilishni rad etish orqali bu muammoni oldini oladi!
Har doimgidek, biz bir vaqtning o'zida emas, balki bir nechta o'zgaruvchan referencelarga ruxsat beruvchi yangi scope yaratish uchun jingalak qavslardan foydalanishimiz mumkin:
fn main() { let mut s = String::from("salom"); { let r1 = &mut s; } // r1 bu yerda scopedan chiqib ketadi, shuning uchun biz hech //qanday muammosiz yangi reference qilishimiz mumkin. let r2 = &mut s; }
Rust o'zgaruvchan va o'zgarmas referencelarni birlashtirish uchun shunga o'xshash qoidani qo'llaydi.
fn main() {
let mut s = String::from("salom");
let r1 = &s; // muammo yo'q
let r2 = &s; // muammo yo'q
let r3 = &mut s; // KATTA MUAMMO
println!("{}, {}, va {}", r1, r2, r3);
}
Mana xato:
$ 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; // muammo yo'q
| -- immutable borrow occurs here
5 | let r2 = &s; // muammo yo'q
6 | let r3 = &mut s; // KATTA MUAMMO
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}, {}, va {}", 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
Voy! Bizda shuningdek o'zgaruvchan referencelar bo'lishi mumkin emas, bizda bir xil qiymatga o'zgarmas reference mavjud.
O'zgarmas reference foydalanuvchilari qiymat birdaniga ularning ostidan o'zgarishini kutishmaydi! Biroq, bir nechta o'zgarmas referencelarga ruxsat beriladi, chunki faqat ma'lumotlarni o'qiyotgan hech kim boshqa hech kimning ma'lumotni o'qishiga ta'sir qilish qobiliyatiga ega emas.
E'tibor bering, referencening ko'lami u kiritilgan joydan boshlanadi va oxirgi ishlatilgan vaqtgacha davom etadi. Masalan, ushbu kod kompilyatsiya qilinadi, chunki o'zgarmas referencelarning oxirgi ishlatilishi, println!
, o'zgaruvchan reference kiritilishidan oldin sodir bo'ladi:
fn main() { let mut s = String::from("salom"); let r1 = &s; // muammo yo'q let r2 = &s; // muammo yo'q println!("{} va {}", r1, r2); // r1 va r2 o'zgaruvchilari bu nuqtadan keyin ishlatilmaydi let r3 = &mut s; // muammo yo'q println!("{}", r3); }
r1
va r2
o'zgarmas referencelar doirasi println
dan keyin tugaydi! ular oxirgi marta ishlatiladigan joy, ya'ni o'zgaruvchan referencelar r3
yaratilishidan oldin. Ushbu doiralar bir-biriga mos kelmaydi, shuning uchun bu kodga ruxsat beriladi: kompilyator reference doirasi tugashidan bir nuqtada endi foydalanilmayotganini aytishi mumkin.
Borrowingdagi xatolar ba'zida asabiylashsa ham, Rust kompilyatori potentsial xatoni erta (runtimeda emas, balki kompilyatsiya vaqtida) ko'rsatib beradi va muammo qayerda ekanligini aniq ko'rsatadi. Keyin nima uchun ma'lumotlaringiz siz o'ylagandek emasligini kuzatishingiz shart emas.
Dangling Referencelar
Pointerlari bo'lgan tillarda, dangling pointerni - xotiradagi boshqa birovga berilgan bo'lishi mumkin bo'lgan joyga reference qiluvchi pointerni - bu xotiraga pointerni saqlab qolgan holda, xotirani biroz bo'shatish orqali yaratish oson. Rust-da, aksincha, kompilyator referencelar hech qachon dangling referencelar bo'lmasligini kafolatlaydi: agar sizda ba'zi ma'lumotlarga reference bo'lsa, kompilyator ma'lumotlarga referencedan oldin ma'lumotlar doirasi tashqariga chiqmasligini ta'minlaydi.
Keling, Rust ularni kompilyatsiya vaqtida xatosi bilan qanday oldini olishini ko'rish uchun dangling reference yaratishga harakat qilaylik:
Fayl nomi: src/main.rs
fn main() {
let dangle_reference = dangle();
}
fn dangle() -> &String {
let s = String::from("salom");
&s
}
Mana xato:
$ 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
Ushbu xato xabari biz hali ko'rib chiqmagan xususiyatga ishora qiladi: lifetime. Biz 10-bobda lifetime batafsil muhokama qilamiz. Ammo, agar siz lifetime haqidagi qismlarga e'tibor bermasangiz, xabarda ushbu kod nima uchun muammo ekanligining kaliti mavjud:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
Keling, dangle
kodimizning har bir bosqichida nima sodir bo'layotganini batafsil ko'rib chiqaylik:
Fayl nomi: src/main.rs
fn main() {
let dangle_reference = dangle();
}
fn dangle() -> &String { // dangle Stringga referencei qaytaradi
let s = String::from("salom"); // s - yangi String
&s // biz Stringga referenceni return qilamiz, s
} // Bu yerda s scopedan chiqib ketadi va drop qilinadi. Uning xotirasi yo'qoladi.
// Xavf!
s
dangle
ichida yaratilganligi sababli, dangle
kodi tugagach, s
ajratiladi. Ammo biz unga referenceni qaytarishga harakat qildik. Bu shuni anglatadiki, bu reference yaroqsiz String
ga ishora qiladi. Bu yaxshi emas! Rust bizga buni qilishga ruxsat bermaydi.
Bu yerda yechim to'g'ridan-to'g'ri String
ni return qilishdir:
fn main() { let string = dangle_yoq(); } fn dangle_yoq() -> String { let s = String::from("salom"); s }
Bu hech qanday muammosiz ishlaydi. Ownership boshqa joyga ko'chiriladi va hech narsa ajratilmaydi.
Reference Qoidalari
Keling, referencelar haqida nimalarni muhokama qilganimizni takrorlaymiz:
- Istalgan vaqtda siz yoki bitta oʻzgaruvchan referencega yoki istalgan miqdordagi oʻzgarmas referencelarga ega boʻlishingiz mumkin.
- Referencelar har doim yaroqli bo'lishi kerak.
Keyinchalik, biz boshqa turdagi referenceni ko'rib chiqamiz: slicelar.
Slice turi
Slicelar butun to'plamga emas, balki to'plamdagi elementlarning qo'shni ketma-ketligiga murojaat qilish imkonini beradi. Slice bir xil referencedir, shuning uchun u ownershipga ega emas.
Bu erda kichik dasturlash muammosi: bo'shliqlar bilan ajratilgan so'zlar qatorini oladigan va shu qatorda topilgan birinchi so'zni qaytaradigan funksiya yozing. Agar funksiya satrda bo'sh joy topmasa, butun satr bitta so'zdan iborat bo'lishi kerak, shuning uchun butun satr qaytarilishi kerak.
Keling, slicelar hal qiladigan muammoni tushunish uchun ushbu funksiyaning imzosini slicelardan foydalanmasdan qanday yozishni ko'rib chiqaylik:
fn birinchi_soz(s: &String) -> ?
birinchi_soz
funksiyasi parametr sifatida &String
ga ega. Biz ownershiplik qilishni xohlamaymiz, shuning uchun bu yaxshi. Ammo biz nimani return qilishimiz kerak? Bizda satrning qismi haqida gapirishning metodi yo'q. Biroq, biz bo'sh joy bilan ko'rsatilgan so'z oxiri indeksini qaytarishimiz mumkin. 4-7 ro'yxatda ko'rsatilganidek, buni sinab ko'raylik.
Fayl nomi: src/main.rs
fn birinchi_soz(s: &String) -> usize { let bayt = s.as_bytes(); for (i, &item) in bayt.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() {}
Biz String
elementini element bo'yicha ko'rib chiqishimiz va qiymat bo'sh joy yoki yo'qligini tekshirishimiz kerakligi sababli, as_bytes
metodi yordamida String
ni baytlar arrayiga aylantiramiz.
fn birinchi_soz(s: &String) -> usize {
let bayt = s.as_bytes();
for (i, &item) in bayt.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
Keyinchalik, iter
metodi yordamida baytlar arrayida iterator yaratamiz:
fn birinchi_soz(s: &String) -> usize {
let bayt = s.as_bytes();
for (i, &item) in bayt.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
Biz iteratorlarni 13-bobda batafsilroq muhokama qilamiz.
Hozircha bilingki, iter
to‘plamdagi har bir elementni return qiladigan va enumerate
iter
natijasini o‘rab, har bir elementni tuplening bir qismi sifatida return qiladigan metoddir. enumerate
dan qaytarilgan tuplening birinchi elementi indeks, ikkinchi element esa elementga referencedir.
Bu indeksni o'zimiz hisoblashdan ko'ra biroz qulayroqdir.
enumerate
metodi tupleni qaytarganligi sababli, biz ushbu tupleni destructure qilish uchun patternlardan foydalanishimiz mumkin. Biz 6-bobda patternlarni ko'proq muhokama qilamiz. for
siklida biz tupledagi indeks uchun i
va bitta bayt uchun &element
ga ega bo‘lgan patternni belgilaymiz.
Biz .iter().enumerate()
dan elementga referenceni olganimiz uchun biz patternda &
dan foydalanamiz.
for
sikli ichida biz bayt literal sintaksisidan foydalanib, bo'sh joyni ifodalovchi baytni qidiramiz. Agar bo'sh joy topsak, biz pozitsiyani return qilamiz.
Aks holda, s.len()
yordamida satr uzunligini qaytaramiz.
fn birinchi_soz(s: &String) -> usize {
let bayt = s.as_bytes();
for (i, &item) in bayt.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
Endi bizda satrdagi birinchi so'zning oxirgi indeksini aniqlashning metodi bor, ammo muammo bor. Biz usize
ni o'z-o'zidan qaytarmoqdamiz, lekin bu &String
kontekstida faqat meaningful raqam. Boshqacha qilib aytadigan bo'lsak, bu String
dan alohida qiymat bo'lganligi sababli, uning kelajakda ham amal qilishiga kafolat yo'q. Ro'yxat 4-8da 4-7 ro'yxatdagi birinchi_soz
funksiyasidan foydalanadigan dasturni ko'rib chiqing.
Fayl nomi: src/main.rs
fn birinchi_soz(s: &String) -> usize { let baytlar = s.as_bytes(); for (i, &item) in baytlar.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() { let mut s = String::from("hello world"); let soz = birinchi_soz(&s); // soz 5 qiymatini oladi s.clear(); // bu Stringni bo'shatib, uni "" ga tenglashtiradi // soz hali ham bu erda 5 qiymatiga ega, ammo biz 5 qiymatini meaningfull ishlatishimiz // mumkin bo'lgan boshqa qator yo'q. soz endi mutlaqo yaroqsiz! }
Bu dastur hech qanday xatosiz kompilyatsiya qiladi va agar biz s.clear()
ga murojat qilgandan keyin soz
ishlatgan bo'lsak ham shunday bo'lardi. Chunki soz
s
holatiga umuman bog‘lanmagan, soz
hali ham 5
qiymatini o‘z ichiga oladi. Birinchi so‘zni chiqarish uchun biz ushbu 5
qiymatini s
o‘zgaruvchisi bilan ishlatishimiz mumkin, ammo bu xato bo‘lishi mumkin, chunki soz
da 5
ni saqlaganimizdan so‘ng s
tarkibi o‘zgargan.
Having to worry about the index in word
getting out of sync with the data in
s
is tedious and error prone! Agar biz ikkinchi_soz
funksiyasini yozsak, bu indekslarni boshqarish yanada mo'rt bo'ladi. Uning signaturesi quyidagicha ko'rinishi kerak:
fn ikkinchi_soz(s: &String) -> (usize, usize) {
Endi biz boshlang'ich va tugash indeksini kuzatmoqdamiz va bizda ma'lum bir holatdagi ma'lumotlardan hisoblangan, ammo bu holatga umuman bog'liq bo'lmagan ko'proq qiymatlar mavjud. Bizda bir-biriga bog'liq bo'lmagan uchta o'zgaruvchi mavjud bo'lib, ular sinxronlashtirilishi kerak.
Yaxshiyamki, Rust bu muammoni hal qildi: string slicelar.
String Slicelar
string slice String
qismiga reference boʻlib, u quyidagicha koʻrinadi:
fn main() { let s = String::from("salom duno"); let salom = &s[0..5]; let dunyo = &s[6..11]; }
Butun String
ga reference oʻrniga salom
qoʻshimcha [0..5]
bitida koʻrsatilgan String
qismiga referencedir. Biz [starting_index..ending_index]
ni belgilash orqali qavslar ichidagi diapazondan foydalangan holda slicelarni yaratamiz, bu yerda starting_index
bo'limdagi birinchi pozitsiyadir va ending_index
slicedagi oxirgi pozitsiyadan bittaga ko'p. Ichkarida, slice ma'lumotlar tuzilmasi ending_index
minus starting_index
ga mos keladigan boshlang'ich pozitsiyasini va slice uzunligini saqlaydi. Demak, let dunyo = &s[6..11];
holatida dunyo so'zi s
ning 6 indeksidagi baytga ko‘rsatgichni o‘z ichiga olgan bo‘lak bo‘lib, uzunligi 5 ga teng bo‘ladi.
4-6-rasmda bu diagrammada ko'rsatilgan.
Rustning ..
diapazoni sintaksisi bilan, agar siz 0 indeksidan boshlamoqchi bo'lsangiz, qiymatni ikki davr oldidan tushirishingiz mumkin. Boshqacha qilib aytganda, ular tengdir:
#![allow(unused)] fn main() { let s = String::from("salom"); let slice = &s[0..2]; let slice = &s[..2]; }
Xuddi shu qoidaga ko'ra, agar sizning slicesingiz String
ning oxirgi baytini o'z ichiga olgan bo'lsa, siz keyingi raqamni qo'yishingiz mumkin. Bu shuni anglatadiki, ular tengdir:
#![allow(unused)] fn main() { let s = String::from("salom"); let uzunlik = s.len(); let slice = &s[3..uzunlik]; let slice = &s[3..]; }
Shuningdek, butun satrning bir qismini olish uchun ikkala qiymatni ham tashlab qo'yishingiz mumkin. Shunday qilib, ular teng:
#![allow(unused)] fn main() { let s = String::from("salom"); let uzunlik = s.len(); let slice = &s[0..uzunlik]; let slice = &s[..]; }
Eslatma: String diapazoni indekslari yaroqli UTF-8 belgilar chegaralarida bo'lishi kerak. Agar siz ko'p baytli belgi o'rtasida string slice yaratishga harakat qilsangiz, dasturingiz xato bilan chiqadi. String slicelarini kiritish uchun biz faqat ushbu bo'limda ASCII ni qabul qilamiz; UTF-8 bilan ishlash bo'yicha batafsilroq muhokama 8-bobning ”UTF-8 kodlangan matnni satrlar bilan saqlash” bo'limida keltirilgan.
Ushbu ma'lumotlarning barchasini hisobga olgan holda, sliceni qaytarish uchun birinchi_soz
ni qayta yozamiz. “String slice”ni bildiruvchi tur &str
sifatida yoziladi:
Fayl nomi: src/main.rs
fn birinchi_soz(s: &String) -> &str { let baytlar = s.as_bytes(); for (i, &item) in baytlar.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() {}
Biz so'z oxiri indeksini 4-7 ro'yxatdagi kabi bo'shliqning birinchi marta paydo bo'lishini qidirib olamiz. Bo'shliqni topganimizda, satrning boshi va bo'sh joy indeksidan boshlang'ich va yakuniy indekslar sifatida foydalanib, satr bo'lagini qaytaramiz.
Endi biz birinchi_soz
ni chaqirganimizda, biz asosiy ma'lumotlarga bog'langan bitta qiymatni olamiz. Qiymat slicening boshlang'ich nuqtasiga va bo'limdagi elementlar soniga referencedan iborat.
Sliceni return qilish ikkinchi_soz
funksiyasi uchun ham ishlaydi:
fn ikkinchi_soz(s: &String) -> &str {
Endi bizda oddiy API mavjud, uni buzish ancha qiyin, chunki kompilyator String
ga referencelar haqiqiyligini ta'minlaydi. 4-8 ro'yxatdagi dasturdagi xatoni eslaysizmi, biz indeksni birinchi so'zning oxirigacha olib, keyin qatorni o'chirib tashlaganimizda, indeksimiz yaroqsiz edi? Bu kod mantiqan noto'g'ri edi, lekin darhol xatoliklarni ko'rsatmadi. Agar biz birinchi so'z indeksini bo'shatilgan qator bilan ishlatishga harakat qilsak, muammolar keyinroq paydo bo'ladi. Slicelar bu xatoni imkonsiz qiladi va kodimiz bilan bog'liq muammo borligini bizga tezroq bildiradi. birinchi_soz
slice versiyasidan foydalanish kompilyatsiya vaqtida xatolikka olib keladi:
Fayl nomi: src/main.rs
fn birinchi_soz(s: &String) -> &str {
let baytlar = s.as_bytes();
for (i, &item) in baytlar.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("salom dunyo");
let soz = birinchi_soz(&s);
s.clear(); // error!
println!("birinchi so'z: {}", soz);
}
Mana kompilyator xatosi:
$ 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 soz = birinchi_soz(&s);
| -- immutable borrow occurs here
17 |
18 | s.clear(); // error!
| ^^^^^^^^^ mutable borrow occurs here
19 |
20 | println!("birinchi so'z: {}", soz);
| ---- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error
Borrowing qoidalarini eslang, agar bizda biror narsaga o'zgarmas reference bo'lsa, biz o'zgaruvchan referenceni ham qabul qila olmaymiz. Chunki clear
String
ni qisqartirishi kerak, u o'zgaruvchan referenceni olishi kerak. clear
chaqiruvidan keyingi println!
soz
dagi referencedan foydalanadi, shuning uchun o‘zgarmas reference shu nuqtada faol bo‘lishi kerak. Rust bir vaqtning o'zida clear
va soz
dagi o'zgarmas referenceni bir vaqtning o'zida mavjud bo'lishiga yo'l qo'ymaydi va kompilyatsiya bajarilmaydi. Rust nafaqat API-dan foydalanishni osonlashtirdi, balki kompilyatsiya vaqtidagi xatolarning butun sinfini ham yo'q qildi!
String literallar Slice sifatida
Eslatib o'tamiz, biz binary tizimda saqlanadigan string literallari haqida gapirgan edik. Endi biz slicelar haqida bilganimizdan so'ng, biz string literallarini to'g'ri tushunishimiz mumkin:
#![allow(unused)] fn main() { let s = "Hello, world!"; }
Bu erda s
turi &str
: bu binary faylning o'ziga xos nuqtasiga ishora qiluvchi slice. Shu sababli ham string literallari o'zgarmasdir; &str
- o'zgarmas reference.
Parametrlar sifatida String Slicelar
Siz literal va String
qiymatlarini olish mumkinligini bilish bizni birinchi_soz
ni yana bir takomillashtirishga olib keladi va bu uning signaturesi:
fn birinchi_soz(s: &String) -> &str {
Tajribali Rustacean buni o'rniga 4-9 ro'yxatda ko'rsatilgan signatureni yozadi, chunki bu bizga &String
qiymatlari va &str
qiymatlarida bir xil funksiyadan foydalanishga imkon beradi.
fn birinchi_soz(s: &str) -> &str {
let baytlar = s.as_bytes();
for (i, &item) in baytlar.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mening_stringim = String::from("hello world");
// `birinchi_soz` `String` ning qisman yoki to'liq slicelarida ishlaydi
let soz = birinchi_soz(&mening_stringim[0..6]);
let soz = birinchi_soz(&mening_stringim[..]);
// `birinchi_soz`, shuningdek, `String` ning butun slicelariga ekvivalent bo`lgan
// `String`-ga referencelar ustida ham ishlaydi.
let soz = birinchi_soz(&mening_stringim);
let mening_literal_stringim = "hello world";
// `birinchi_soz` qisman yoki to'liq bo'lgan string literallari slicelarida ishlaydi
let soz = birinchi_soz(&mening_literal_stringim [0..6]);
let soz = birinchi_soz(&mening_literal_stringim [..]);
// String literallari allaqachon string slicelari bo'lganligi sababli,
// bu slice sintaksisisiz ham ishlaydi!
let soz = birinchi_soz(mening_literal_stringim );
}
Agar bizda string slice bo'lsa, biz uni to'g'ridan-to'g'ri o'tkazishimiz mumkin. Agar bizda String
bo'lsa, biz String
slicesini yoki String
ga referenceni o'tkazishimiz mumkin. Ushbu moslashuvchanlik deref coercionlari dan foydalanadi, bu xususiyatni biz 15-bobning “Funktsiyalar va metodlar bilan Implicit Deref Coercionlari” da ko‘rib chiqamiz.
String
ga reference o‘rniga string sliceni olish funksiyasini belgilash bizning API’ni hech qanday funksionallikni yo‘qotmasdan umumiyroq va foydali qiladi:
Fayl nomi: src/main.rs
fn birinchi_soz(s: &str) -> &str { let baytlar = s.as_bytes(); for (i, &item) in baytlar.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let mening_stringim = String::from("hello world"); // `birinchi_soz` `String` ning qisman yoki to'liq slicelarida ishlaydi let soz = birinchi_soz(&mening_stringim[0..6]); let soz = birinchi_soz(&mening_stringim[..]); // `birinchi_soz`, shuningdek, `String` ning butun slicelariga ekvivalent bo`lgan // `String`-ga referencelar ustida ham ishlaydi. let soz = birinchi_soz(&mening_stringim); let mening_literal_stringim = "hello world"; // `birinchi_soz` qisman yoki to'liq bo'lgan string literallari slicelarida ishlaydi let soz = birinchi_soz(&mening_literal_stringim [0..6]); let soz = birinchi_soz(&mening_literal_stringim [..]); // String literallari allaqachon string slicelari bo'lganligi sababli, // bu slice sintaksisisiz ham ishlaydi! let soz = birinchi_soz(mening_literal_stringim ); }
Boshqa Slicelar
String slicelari, siz tasavvur qilganingizdek, stringlarga xosdir. Ammo yana umumiy slice turi ham bor. Ushbu arrayni ko'rib chiqing:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; }
Xuddi biz satrning bir qismiga murojaat qilishni xohlaganimizdek, arrayning bir qismiga murojaat qilishni xohlashimiz mumkin. Biz shunday qilamiz:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); }
Ushbu slice &[i32]
turiga ega. U birinchi element va uzunlikka referenceni saqlash orqali string slicelari kabi ishlaydi. Siz boshqa barcha turdagi to'plamlar uchun bunday slicedan foydalanasiz. 8-bobda vektorlar haqida gapirganda, biz ushbu to'plamlarni batafsil muhokama qilamiz.
Xulosa
Ownership, borrowing va slicelar tushunchalari kompilyatsiya vaqtida Rust dasturlarida xotira xavfsizligini ta'minlaydi. Rust tili sizga boshqa tizim dasturlash tillari kabi xotiradan foydalanishni boshqarish imkonini beradi, lekin egasi amal qilish doirasidan chiqib ketganda maʼlumotlar egasi avtomatik ravishda ushbu maʼlumotlarni tozalaydi, bu boshqaruvni olish uchun qoʻshimcha kod yozish va debug qilish shart emasligini anglatadi.
Ownership Rustning boshqa ko'plab qismlari qanday ishlashiga ta'sir qiladi, shuning uchun biz bu tushunchalar haqida kitobning qolgan qismida batafsilroq gaplashamiz. Keling, 5-bobga o'tamiz va ma'lumotlar bo'laklarini struct
da birga guruhlashni ko'rib chiqamiz.
Tegishli ma'lumotlarni tuzish uchun Structlardan foydalanish
struct
- foydalanuvchi tomonidan belgilangan ma'lumotlar turi bo'lib, u sizga mantiqiy va mantiqiy guruhni tashkil etuvchi bir nechta tegishli qiymatlarni nomlash va to'plash imkonini beradi. Agar siz obyektga yo'naltirilgan til bilan tanish bo'lsangiz, struct obyektning ma'lumotlar atributlariga o'xshaydi. Ushbu bobda biz tuplelarni structlar bilan solishtiramiz va o'zingiz bilgan narsalarga asoslaymiz va qachon structlar ma'lumotlarni guruhlashning eng yaxshi usuli ekanligini ko'rsatamiz.
Biz structlarni qanday aniqlash va yaratishni ko'rsatamiz. Struct turi bilan bog'liq xatti-harakatni belgilash uchun bog'langan funksiyalarni, ayniqsa methodlar deb ataladigan bog'langan funksiyalarni qanday aniqlashni muhokama qilamiz. Structlar va enumlar (6-bobda muhokama qilingan) dastur domenida yangi turlarni yaratish uchun building block hisoblanadi. Ular sizga Rustning kompilyatsiya vaqti turini tekshirish imkoniyatlaridan to'liq foydalanish imkonini beradi.
Structlarni aniqlash va yaratish
Structlar “Tuple turi“ bo'limida muhokama qilingan tuplelarga o'xshaydi, chunki ikkalasi ham bir-biriga bog'liq bo'lgan bir nechta qiymatlarga ega. Tuplelar singari, structning qismlari ham har xil turdagi bo'lishi mumkin. Tuplelardan farqli o'laroq, structda siz har bir ma'lumot qismini nomlaysiz, shunda qiymatlar nimani anglatishini tushunasiz. Ushbu nomlarni qo'shish structlar tuplelardan ko'ra moslashuvchanroq ekanligini anglatadi: misol qiymatlarini belgilash yoki ularga kirish uchun ma'lumotlar tartibiga ishonishingiz shart emas.
Structni aniqlash uchun biz struct
kalit so`zini kiritamiz va butun tuzilishga nom beramiz. Struct nomi birgalikda guruhlangan ma'lumotlar bo'laklarining ahamiyatini tavsiflashi kerak. Keyin, jingalak qavslar ichida biz maydonlar deb ataydigan ma'lumotlar qismlarining nomlari va turlarini aniqlaymiz. Masalan, 5-1 ro'yxati foydalanuvchi hisobi haqidagi ma'lumotlarni saqlaydigan structni ko'rsatadi.
Fayl nomi: src/main.rs
struct Foydalanuvchi { faollik: bool, foydalanuvchi: String, email: String, kirish_hisobi: u64, } fn main() {}
Struct aniqlangandan so'ng, tegishli ma'lumotlar turiga ega bo'lgan har bir maydonga ma'lum bir qiymat berish orqali uni yaratish mumkin. Biz structning nomini ko'rsatish orqali misol yaratamiz va keyin kalit: qiymat(key: value) juftlarini o'z ichiga olgan jingalak qavslarni qo'shamiz, bu erda kalitlar maydonlarning nomlari va qiymatlar biz o'sha maydonlarda saqlamoqchi bo'lgan ma'lumotlardir.Biz maydonlarni structda e'lon qilgan tartibda ko'rsatishimiz shart emas. Boshqacha qilib aytganda, structning ta'rifi tur uchun umumiy shablonga o'xshaydi va misollar tur qiymatlarini yaratish uchun ushbu shablonni ma'lum ma'lumotlar bilan to'ldiradi. Misol uchun, biz 5-2 ro'yxatda ko'rsatilganidek, ma'lum bir foydalanuvchini e'lon qilishimiz mumkin.
Fayl nomi: src/main.rs
struct Foydalanuvchi { faollik: bool, foydalanuvchi: String, email: String, kirish_hisobi: u64, } fn main() { let foydalanuvchi1 = Foydalanuvchi { faollik: true, foydalanuvchi: String::from("ismoilovdev"), email: String::from("ismoilovdev@example.com"), kirish_hisobi: 1, }; }
Structdan ma'lum bir qiymat olish uchun biz nuqta belgilaridan foydalanamiz. Masalan, ushbu foydalanuvchining elektron pochta manziliga kirish uchun biz foydalanuvchi1.email
dan foydalanamiz. Agar misol o'zgaruvchan bo'lsa, biz nuqta belgisi yordamida qiymatni o'zgartirishimiz va ma'lum bir maydonga belgilashimiz mumkin. 5-3 ro'yxatda o'zgaruvchan Foydalanuvchi
misolining email
maydonidagi qiymatni qanday o'zgartirish mumkinligi ko'rsatilgan.
Fayl nomi: src/main.rs
struct Foydalanuvchi { faollik: bool, foydalanuvchi: String, email: String, kirish_hisobi: u64, } fn main() { let foydalanuvchi1 = Foydalanuvchi { faollik: true, foydalanuvchi: String::from("ismoilovdev"), email: String::from("ismoilovdev@example.com"), kirish_hisobi: 1, }; foydalanuvchi1.email = String::from("asilbek@example.com"); }
E'tibor bering, butun misol o'zgaruvchan bo'lishi kerak; Rust bizga faqat ma'lum maydonlarni o'zgaruvchan deb belgilashga ruxsat bermaydi. Har qanday ifodada bo'lgani kabi, biz ushbu yangi misolni bilvosita qaytarish uchun funksiya tanasidagi oxirgi ifoda sifatida structning yangi nusxasini qurishimiz mumkin.
5-4 roʻyxatda berilgan email va foydalanuvchi nomi bilan Foydalanuvchi
misolini qaytaruvchi foydalanuvchi_yaratish
funksiyasi koʻrsatilgan. faollik
maydoni true
qiymatini, kirish_hisobi
esa 1
qiymatini oladi.
Fayl nomi: src/main.rs
struct Foydalanuvchi { faollik: bool, foydalanuvchi: String, email: String, kirish_hisobi: u64, } fn foydalanuvchi_yaratish(email: String, foydalanuvchi: String) -> Foydalanuvchi { Foydalanuvchi { faollik: true, foydalanuvchi: foydalanuvchi, email: email, kirish_hisobi: 1, } } fn main() { let foydalanuvchi1 = foydalanuvchi_yaratish( String::from("ismoilovdev@example.com"), String::from("ismoilovdev"), ); }
Funksiya parametrlarini struct maydonlari bilan bir xil nom bilan nomlash mantiqan to‘g‘ri keladi, lekin email
va foydalanuvchi
maydon nomlari va o‘zgaruvchilarini takrorlash biroz zerikarli. Agar structda ko'proq maydonlar bo'lsa, har bir nomni takrorlash yanada zerikarli bo'ladi. Yaxshiyamki, qulay Shorthand bor!
Field Init Shorthandan foydalanish
Parametr nomlari va struct maydonlarining nomlari 5-4 ro'yxatda aynan bir xil bo'lgani uchun, foydalanuvchi_yaratish
ni qayta yozish uchun init shorthand sintaksisidan foydalanishimiz mumkin, shuning uchun u xuddi shunday ishlaydi, lekin foydalanuvchi
va email
ni takrorlamaydi, 5-5 ro'yxatda ko'rsatilganidek.
Fayl nomi: src/main.rs
struct Foydalanuvchi { faollik: bool, foydalanuvchi: String, email: String, kirish_hisobi: u64, } fn foydalanuvchi_yaratish(email: String, foydalanuvchi: String) -> Foydalanuvchi { Foydalanuvchi { faollik: true, foydalanuvchi, email, kirish_hisobi: 1, } } fn main() { let foydalanuvchi1 = foydalanuvchi_yaratish( String::from("ismoilovdev@example.com"), String::from("ismoilovdev"), ); }
Bu yerda biz Foydalanuvchi
structining yangi nusxasini yaratmoqdamiz, unda email
deb nomlangan maydon mavjud. Biz email
maydonining qiymatini foydalanuvchi_yaratish
funksiyasining email
parametridagi qiymatga o‘rnatmoqchimiz. email
maydoni va email
parametri bir xil nomga ega bo'lgani uchun biz email: email
emas, balki email
yozishimiz kerak.
Structni update sintaksisidan foydalanib, boshqa tuzilma nusxasidan tuzilma namunasini yaratish
Ko'pincha boshqa namunadagi qiymatlarning ko'pini o'z ichiga olgan, lekin ularning ba'zilarini o'zgartiradigan structning yangi nusxasini yaratish foydali bo'ladi. Buni struct update sintaksisi yordamida amalga oshirishingiz mumkin.
Birinchidan, 5-6 ro'yxatda biz yangilanish sintaksisisiz muntazam ravishda foydalanuvchi2
da yangi Foydalanuvchi
misolini qanday yaratishni ko'rsatamiz. Biz email
uchun yangi qiymat o‘rnatdik, lekin aks holda 5-2 ro‘yxatda yaratgan foydalanuvchi1
qiymatidan foydalanamiz.
Fayl nomi: src/main.rs
struct Foydalanuvchi { faollik: bool, foydalanuvchi: String, email: String, kirish_hisobi: u64, } fn main() { // --snip-- let foydalanuvchi1 = Foydalanuvchi { email: String::from("ismoilovdev@example.com"), foydalanuvchi: String::from("ismoilovdev"), faollik: true, kirish_hisobi: 1, }; let foydalanuvchi2 = Foydalanuvchi { faollik: foydalanuvchi1.faollik, foydalanuvchi: foydalanuvchi1.foydalanuvchi, email: String::from("asilbek@example.com"), kirish_hisobi: foydalanuvchi1.kirish_hisobi, }; }
Strukturani yangilash sintaksisidan foydalanib, biz 5-7 ro'yxatda ko'rsatilganidek, kamroq kod bilan bir xil effektga erishishimiz mumkin. ..
sintaksisi qolgan maydonlar aniq o'rnatilganligini va belgilangan namunadagi qiymatlarga ega bo'lishi kerakligini ko'rsatadi.
Fayl nomi: src/main.rs
struct Foydalanuvchi { faollik: bool, foydalanuvchi: String, email: String, kirish_hisobi: u64, } fn main() { // --snip-- let foydalanuvchi1 = Foydalanuvchi { email: String::from("ismoilovdev@example.com"), foydalanuvchi: String::from("ismoilovdev"), faollik: true, kirish_hisobi: 1, }; let foydalanuvchi2 = Foydalanuvchi { email: String::from("asilbek@example.com"), ..foydalanuvchi1 }; }
5-7 roʻyxatdagi kod foydalanuvchi2
da email
uchun boshqa qiymatga ega, lekin foydalanuvchi1
dan foydalanuvchi
, faollik
va kirish_hisobi
maydonlari uchun bir xil qiymatlarga ega boʻlgan misol yaratadi. ..foydalanuvchi1
qolgan maydonlar o‘z qiymatlarini foydalanuvchi1
dagi tegishli maydonlardan olishi kerakligini ko‘rsatish uchun oxirgi o‘rinda turishi kerak, lekin biz istalgan tartibda xohlagancha ko'p maydonlar uchun qiymatlarni belgilashni tanlashimiz mumkin, strukturaning ta'rifidagi maydonlar tartibidan qat'i nazar.
E'tibor bering, strukturani yangilash sintaksisi topshiriq kabi =
dan foydalanadi; Buning sababi, biz ”O'zgaruvchilar va ma'lumotlarni ko'chirish bilan o'zaro ta'sir qilish” bo'limida ko'rganimizdek, u ma'lumotlarni harakatlantiradi. Ushbu misolda biz foydalanuvchi2
ni yaratganimizdan keyin foydalanuvchi1
dan bir butun sifatida foydalana olmaymiz, chunki foydalanuvchi1
ning foydalanuvchi
maydonidagi String
foydalanuvchi2
ga koʻchirilgan. Agar biz foydalanuvchi2
ga email
va foydalanuvchi
uchun yangi String
qiymatlarini bergan bo‘lsak va shuning uchun foydalanuvchi1
dan faqat faollik
va kirish_hisobi
qiymatlarini ishlatgan bo‘lsak, keyin foydalanuvchi1
foydalanuvchi2
yaratilgandan keyin ham amal qiladi. faollik
va kirish_hisobi
turlari nusxa ko'chirish xususiyatini amalga oshiradigan turlardir, shuning uchun biz ”Stek ma'lumotlari: Nusxalash” bo'limida muhokama qilgan xatti-harakatlar qo'llaniladi.
Har xil turlarni yaratish uchun nomli maydonlarsiz tuplelardan foydalanish
Rust shuningdek, tuple structlar deb ataladigan tuplelarga o'xshash structlarni qo'llab-quvvatlaydi. Tuple structlari struct nomi taqdim etadigan qo'shimcha ma'noga ega, ammo ularning maydonlari bilan bog'langan nomlari yo'q; aksincha, ular faqat maydonlarning turlariga ega. Tuple structlari butun tuplega nom berish va tupleni boshqa tuplelardan farqli turga aylantirish zarur bo‘lganda foydali bo‘ladi va har bir maydonni oddiy structdagi kabi nomlashda batafsil yoki ortiqcha bo‘ladi.
Tuple structini aniqlash uchun struct
kalit so‘zi va struct nomidan keyin tupledagi turlardan boshlang. Masalan, bu yerda biz Rang
va Nuqta
nomli ikkita tuple structini aniqlaymiz va foydalanamiz:
Fayl nomi: src/main.rs
struct Rang(i32, i32, i32); struct Nuqta(i32, i32, i32); fn main() { let qora = Rang(0, 0, 0); let kelib_chiqishi = Nuqta(0, 0, 0); }
Esda tutingki, qora
va kelib_chiqishi
qiymatlari har xil turdagi, chunki ular turli xil tuple structlarining namunalaridir. Structdagi maydonlar bir xil turlarga ega bo'lishi mumkin bo'lsa ham, siz belgilagan har bir struct o'z turiga ega. Masalan, Rang
turidagi parametrni qabul qiladigan funksiya, har ikkala tur ham uchta i32
qiymatidan iborat bo‘lsa ham, Nuqta
ni argument sifatida qabul qila olmaydi. Aks holda, tuple structi namunalari tupelarga o'xshaydi, chunki siz ularni alohida qismlarga ajratishingiz mumkin va individual qiymatga kirish uchun .
va keyin indeksdan foydalanishingiz mumkin.
Hech qanday maydonsiz unit kabi structlar
Shuningdek, siz hech qanday maydonga ega bo'lmagan structlarni belgilashingiz mumkin! Ular unita o'xshash structlar deb ataladi, chunki ular biz ”Tuple turi” bo'limida aytib o'tgan unit turiga ()
o'xshash harakat qiladilar. Unitga o'xshash structlar qaysidir turdagi traitni qo'llash kerak bo'lganda foydali bo'lishi mumkin, ammo sizda turning o'zida saqlash uchun ma'lumotlaringiz yo'q. Traitlarni 10-bobda muhokama qilamiz. Mana AlwaysEqual
nomli unit structini e’lon qilish va instantsiyalash misoli:
Fayl nomi: src/main.rs
struct AlwaysEqual; fn main() { let subject = AlwaysEqual; }
AlwaysEqual
ni aniqlash uchun biz struct
kalit so'zidan, kerakli nomdan va keyin nuqta-verguldan foydalanamiz. Jingalak qavslar yoki qavslar kerak emas! Shunda biz subject
o'zgaruvchisida AlwaysEqual
misolini xuddi shunday tarzda olishimiz mumkin: biz belgilagan nomdan foydalanib, hech qanday jingalak qavs yoki qavslarsiz. Tasavvur qiling-a, keyinchalik biz ushbu turdagi xatti-harakatlarni shunday amalga oshiramizki, AlwaysEqual
ning har bir nusxasi har doim boshqa turdagi har bir misolga teng bo'ladi, ehtimol sinov uchun ma'lum natijaga ega bo'lishi mumkin. Ushbu xatti-harakatni amalga oshirish uchun bizga hech qanday ma'lumot kerak emas! Traitlarni qanday aniqlash va ularni har qanday turdagi, shu jumladan unitga o'xshash structlarda amalga oshirishni 10-bobda ko'rasiz.
Strukturaviy ma'lumotlarga ownershiplik qilish
5-1 roʻyxatdagi
Foydalanuvchi
structi taʼrifida biz&str
string slice turidan koʻra tegishliString
turidan foydalandik. Bu ataylab qilingan tanlov, chunki biz ushbu structning har bir nusxasi uning barcha maʼlumotlariga ega boʻlishini va bu maʼlumotlar butun struct amalda boʻlgunga qadar amal qilishini istaymiz.Structlar boshqa narsaga tegishli maʼlumotlarga referencelarni saqlashi ham mumkin, ammo buning uchun biz 10-bobda muhokama qiladigan Rust xususiyatidan lifetimelar foydalanishni talab qiladi. Lifetime struct tomonidan reference qilingan ma'lumotlar struct mavjud bo'lgunga qadar amal qilishini ta'minlaydi. Aytaylik, siz ma'lumotnomani lifetimeni ko'rsatmasdan structda saqlashga harakat qildingiz, quyidagi kabi; bu ishlamaydi:
Fayl nomi: src/main.rs
struct Foydalanuvchi { faollik: bool, foydalanuvchi: &str, email: &str, kirish_hisobi: u64, } fn main() { let foydalanuvchi1 = Foydalanuvchi { faollik: true, foydalanuvchi: "ismoilovdev", email: "ismoilovdev@example.com", kirish_hisobi: 1, }; }
Kompilyator referencelarning lifetimeni aniqlash zarurati haqida shikoyat qiladi:
$ cargo run Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | foydalanuvchi: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct Foydalanuvchi<'a> { 2 | faollik: bool, 3 ~ foydalanuvchi: &'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 Foydalanuvchi<'a> { 2 | faollik: bool, 3 | foydalanuvchi: &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
10-bobda biz ushbu xatolarni qanday tuzatishni muhokama qilamiz, shunda siz referencelarni structlarda saqlashingiz mumkin, ammo hozircha biz bu kabi xatolarni
&str
kabi referencelar oʻrnigaString
kabi tegishli turlardan foydalanib tuzatamiz.
Structlar yordamida namunaviy dastur
Structlarni qachon ishlatishimiz mumkinligini tushunish uchun to'rtburchakning maydonini hisoblaydigan dastur yozaylik. Biz bitta o'zgaruvchilardan foydalanishni boshlaymiz, so'ngra uning o'rniga structlardan foydalanmagunimizcha dasturni qayta yaxshilab boramiz.
Keling, cargo bilan kvadratlar deb nomlangan yangi binary loyihani yarataylik, u piksellarda ko'rsatilgan to'rtburchakning kengligi va balandligini oladi va to'rtburchakning maydonini hisoblaydi. 5-8 ro'yxatda loyihaning src/main.rs faylida nima qilishimiz kerakligini aniq bajarishga imkon beradigan bitta qisqa kod ko'rsatilgan.
Fayl nomi: src/main.rs
fn main() { let kenglik1 = 30; let balandlik1 = 50; println!( "To'rtburchakning maydoni {} kvadrat piksel.", area(kenglik1, balandlik1) ); } fn area(kenglik: u32, balandlik: u32) -> u32 { kenglik * balandlik }
Endi ushbu dasturni cargo run
yordamida ishga tushiring:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
To'rtburchakning maydoni 1500 kvadrat piksel.
Ushbu kod har bir o'lcham bilan area
funksiyasini chaqirish orqali to'rtburchakning maydonini aniqlashga muvaffaq bo'ladi, ammo biz ushbu kodni aniq va o'qilishi uchun ko'proq narsani qilishimiz mumkin.
Ushbu kod bilan bog'liq muammo area
signaturesida aniq ko'rinadi:
fn main() {
let kenglik1 = 30;
let balandlik1 = 50;
println!(
"To'rtburchakning maydoni {} kvadrat piksel.",
area(kenglik1, balandlik1)
);
}
fn area(kenglik: u32, balandlik: u32) -> u32 {
kenglik * balandlik
}
area
funksiyasi bitta to'rtburchakning maydonini hisoblashi kerak, lekin biz yozgan funksiya ikkita parametrga ega va bizning dasturimizning hech bir joyida parametrlar o'zaro bog'liqligi aniq emas. Kenglik va balandlikni birgalikda guruhlash yanada o'qilishi va boshqarilishi oson bo'lishi mumkin edi.3-bobning ”Tuple Turi” bo'limida biz buni amalga oshirishning bir usulini, ya'ni tuplelardan foydalanishni muhokama qildik.
Tuplelar yordamida Refaktoring
5-9 ro'yxatda tuplelardan foydalanadigan dasturimizning boshqa versiyasi ko'rsatilgan.
Fayl nomi: src/main.rs
fn main() { let kvadrat1 = (30, 50); println!( "To'rtburchakning maydoni {} kvadrat pikselga teng.", area(kvadrat1) ); } fn area(olchamlari: (u32, u32)) -> u32 { olchamlari.0 * olchamlari.1 }
Bir tomondan, bu dastur yaxshiroq. Tuplar bizga biroz struct qo'shishga imkon beradi va biz hozir faqat bitta argumentni keltiramiz. Ammo boshqa yo'l bilan, bu versiya unchalik aniq emas: tuplelar o'z elementlarini nomlamaydi, shuning uchun biz hisob-kitobimizni kamroq aniq qilib, tuple qismlariga indeks qilishimiz kerak.
Kenglik va balandlikni aralashtirish maydonni hisoblash uchun muhim emas, lekin agar biz ekranda to'rtburchak chizmoqchi bo'lsak, bu muhim bo'ladi! Shuni yodda tutishimiz kerakki, kenglik
indeks 0
da, balandlik
esa 1
indeksda. Agar kimdir bizning kodimizdan foydalansa, buni tushunish va yodda tutish qiyinroq bo'ladi. Kodimizda ma'lumotlarimizning ma'nosini etkazmaganimiz sababli, endi xatolarni kiritish osonroq.
Struktuctlar bilan Refaktoring: ko'proq ma'no qo'shish
Biz ma'lumotlarni etiketlash orqali ma'no qo'shish uchun structlardan foydalanamiz. Biz foydalanayotgan tupleni 5-10 ro'yxatda ko'rsatilganidek, butun nomi bilan bir qatorda qismlar nomlari bilan tuzilishga aylantirishimiz mumkin.
Fayl nomi: src/main.rs
struct Kvadrat { kenglik: u32, balandlik: u32, } fn main() { let kvadrat1 = Kvadrat { kenglik: 30, balandlik: 50, }; println!( "To'rtburchakning maydoni {} kvadrat pikselga teng.", area(&kvadrat1) ); } fn area(kvadrat: &Kvadrat) -> u32 { kvadrat.kenglik * kvadrat.balandlik }
Bu yerda biz structni aniqladik va uni Kvadrat
deb nomladik. Jingalak qavslar ichida biz maydonlarni kenglik
va balandlik
sifatida belgiladik, ularning ikkalasi ham u32
turiga ega. Keyin, main
da biz Kvadrat
ning ma'lum bir misolini yaratdik, uning kengligi 30
va balandligi 50
.
Bizning area
funksiyamiz endi biz kvadrat
deb nomlagan bitta parametr bilan aniqlanadi, uning turi Kvadrat
structi misolining o‘zgarmas borrowidir. 4-bobda aytib o'tilganidek, biz unga ownershiplik qilishdan ko'ra, structi borrow qilishni xohlaymiz. Shunday qilib, main
o'z ownershipini saqlab qoladi va kvadrat1
dan foydalanishni davom ettirishi mumkin, shuning uchun biz funktsiya signaturesida &
dan foydalanamiz va biz funktiyani chaqiramiz.
area
funksiyasi Kvadrat
misolining kenglik
va balandlik
maydonlariga kiradi (esda tutingki, borrow qilingan struct misolining maydonlariga kirish maydon qiymatlarini ko'chirmaydi, shuning uchun siz ko'pincha structlarning borrowlarini ko'rasiz). Endi area
funksiyasi signaturesi biz nimani nazarda tutayotganimizni aniq aytadi: Kvadrat
maydonini uning kenglik
va balandlik
maydonlaridan foydalanib hisoblang. Bu kenglik va balandlik bir-biri bilan bog'liqligini bildiradi va 0
va 1
qator indeks qiymatlarini ishlatishdan ko'ra, qiymatlarga tavsiflovchi nomlar beradi. Bu aniqlik uchun g'alaba.
Olingan Traitlar bilan foydali funksionallikni qo'shish
Dasturimizni debug qilish va uning barcha maydonlari uchun qiymatlarni ko'rish paytida Kvadrat
misolini chop etish foydali bo'lar edi. 5-11 ro'yxatda biz avvalgi boblarda foydalanganimizdek println!
makrosidan foydalanishga harakat qiladi. Biroq, bu ishlamaydi.
Fayl nomi: src/main.rs
struct Kvadrat {
kenglik: u32,
balandlik: u32,
}
fn main() {
let kvadrat1 = Kvadrat {
kenglik: 30,
balandlik: 50,
};
println!("kvadrat1 - {}", kvadrat1);
}
Ushbu kodni kompilyatsiya qilishda, biz ushbu asosiy xabar bilan xatoga duch kelamiz:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println!
makrosi ko'plab formatlash turlarini amalga oshirishi mumkin va standart bo'yicha jingalak qavslar println!
ga Display
deb nomlanuvchi formatlashdan foydalanishni bildiradi: to'g'ridan-to'g'ri oxirgi foydalanuvchi iste'moli uchun mo'ljallangan chiqish. Biz hozirgacha ko'rgan primitiv turlar standart bo'yicha Display
ni qo'llaydi, chunki foydalanuvchiga 1
yoki boshqa primitiv turni ko'rsatishning faqat bitta usuli bor. Lekin structlar bilan println!
ning chiqishni formatlash metodi unchalik aniq emas, chunki koʻproq koʻrsatish imkoniyatlari mavjud: vergul qoʻyishni xohlaysizmi yoki yoʻqmi? Jingalak qavslarni chop qilmoqchimisiz? Barcha maydonlar ko'rsatilishi kerakmi? Ushbu noaniqlik tufayli Rust biz xohlagan narsani taxmin qilishga urinmaydi va structlarda println!
va {}
to'ldiruvchisi bilan foydalanish uchun Display
ning taqdim etilgan ilovasi yo'q.
Agar xatolarni o'qishda davom etsak, biz ushbu foydali eslatmani topamiz:
= help: the trait `std::fmt::Display` is not implemented for `Kvadrat`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
Keling, sinab ko'raylik! println!
macro chaqiruvi endi println!("kvadrat1 bu {}", kvadrat1);
kabi ko'rinadi. :?
spetsifikatsiyasini jingalak qavslar ichiga qo'yish println!
biz Debug
deb nomlangan chiqish formatidan foydalanmoqchi ekanligimizni bildiradi. Debug
traiti bizga sturctni ishlab chiquvchilar uchun foydali bo'lgan tarzda chop etish imkonini beradi, shuning uchun biz kodimizni tuzatish paytida uning qiymatini ko'rishimiz mumkin.
Keling, ushbu o'zgarishlar bilan kodni kompilyatsiya qilaylik. Ehh! Biz hali ham xatoni olamiz:
error[E0277]: `Rectangle` doesn't implement `Debug`
Ammo yana, kompilyator bizga foydali eslatma beradi:
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
Rust debug ma'lumotlarini chop etish funksiyasini o'z ichiga oladi, lekin biz ushbu funksiyani structimiz uchun mavjud qilish uchun ochiqdan-ochiq rozi bo'lishimiz kerak.
Buni amalga oshirish uchun 5-12 ro'yxatda ko'rsatilganidek, struct ta'rifidan oldin #[derive(Debug)]
tashqi atributini qo'shamiz.
Fayl nomi: src/main.rs
#[derive(Debug)] struct Kvadrat { kenglik: u32, balandlik: u32, } fn main() { let kvadrat1 = Kvadrat { kenglik: 30, balandlik: 50, }; println!("kvadrat1 - {}", kvadrat1); }
Endi dasturni ishga tushirganimizda, biz hech qanday xatolikka yo'l qo'ymaymiz va biz quyidagi natijani ko'ramiz:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
kvadrat1 - Kvadrat { kenglik: 30, balandlik: 50 }
Yaxshi! Bu eng yaxshi natija emas, lekin u ushbu misol uchun barcha maydonlarning qiymatlarini ko'rsatadi, bu disk raskadrovka paytida albatta yordam beradi. Kattaroq structlarga ega bo'lsak, o'qishni biroz osonlashtiradigan chiqishga ega bo'lish foydalidir; bunday hollarda println!
qatoridagi {:?}
o'rniga {:#?}
dan foydalanishimiz mumkin. Ushbu misolda {:#?}
uslubidan foydalanish quyidagi natijalarni beradi:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
kvadrat1 - Kvadrat {
kenglik: 30,
balandlik: 50
}
Debug
formati yordamida qiymatni chop etishning yana bir usuli dbg!
macro Ifodaga egalik qiluvchi macro (mos referencelar oladigan println! dan farqli o'laroq) o'sha dbg!
qayerda fayl va satr raqamini chop etadi! macro murojati sizning kodingizda ushbu ifodaning natijaviy qiymati bilan birga sodir bo'ladi va qiymatga egalik huquqini qaytaradi.
Eslatma:
dbg!
makrosini chaqirish standart chiqish konsoli stremiga (stdout
) chop qiluvchiprintln!
dan farqli ravishda standart xato konsoli stremiga (stderr
) chop etadi. Biz 12-bobdagi ”Xato xabarlarini standart chiqish o‘rniga standart xatoga yozish” bo‘limidastderr
vastdout
haqida ko‘proq gaplashamiz.
Mana bizni kenglik
maydoniga tayinlanadigan qiymat, shuningdek kvadrat1
dagi butun structning qiymati qiziqtiradigan misol:
#[derive(Debug)] struct Kvadrat { kenglik: u32, balandlik: u32, } fn main() { let masshtab = 2; let kvadrat1 = Kvadrat { kenglik: dbg!(30 * masshtab), balandlik: 50, }; dbg!(&kvadrat1); }
Biz 30 * masshtab
iborasi atrofida dbg!
qo'yishimiz mumkin va dbg!
ifoda qiymatiga egalik huquqini qaytargani uchun, kenglik
maydoni bizda dbg!
chaqiruvi bo'lmagani kabi bir xil qiymatga ega bo'ladi. Biz dbg!
kvadrat1
ga egalik qilishini istamaymiz, shuning uchun keyingi chaqiruvda kvadrat1
ga referencedan foydalanamiz.
Ushbu misolning natijasi quyidagicha ko'rinadi:
$ 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 * masshtab = 60
[src/main.rs:14] &kvadrat1 = Kvadrat {
kenglik: 30,
balandlik: 50
}
Biz birinchi debuggingda src/main.rs ning 10-qatoridan kelganini ko'rishimiz mumkin, bu erda biz 30 * masshtab ifodani debugging qilamiz va uning natijaviy qiymati 60
(butun sonlar uchun Debug
formati faqat ularning qiymatini chop etish uchun ishlatiladi). src/main.rs ning 14-qatoridagi dbg!
chaqiruvi &kvadrat1
qiymatini chiqaradi, bu Kvadrat
structidir. Ushbu chiqishda Kvadrat
turidagi chiroyli Debug
formatlash qo'llaniladi. dbg!
makrosi sizning kodingiz nima qilayotganini aniqlashga harakat qilayotganingizda juda foydali bo'lishi mumkin!
Rust Debug
traitiga qo‘shimcha ravishda derive
atributi bilan foydalanishimiz uchun bir qancha taritlarni taqdim etdi, ular bizning odatiy turlarimizga foydali xatti-harakatlar qo‘shishi mumkin.Ushbu traitlar va ularning xatti-harakatlari C ilovasida keltirilgan. Biz 10-bobda ushbu traittlarni odatiy xatti-harakatlar bilan qanday implement qilishni, shuningdek, o'z traitlaringizni qanday yaratishni ko'rib chiqamiz.Bundan tashqari, 'derive' dan boshqa ko'plab atributlar mavjud; qo'shimcha ma'lumot olish uchun Rust Referencening "Atributlar" bo'limiga qarang.
Bizning area
funksiyamiz juda aniq: u faqat to'rtburchaklar maydonini hisoblaydi. Ushbu xatti-harakatni Kvadrat
structimiz bilan yaqinroq bog'lash foydali bo'ladi, chunki u boshqa turlar bilan ishlamaydi. Keling, ushbu kodni qanday qilib qayta ishlashni davom ettirishimiz mumkinligini ko'rib chiqaylik, bu area
funksiyasini Kvadrat
turida aniqlangan area
metod ga aylantiradi.
Metod Sintaksisi
Metodlar funksiyalarga oʻxshaydi: biz ularni fn
kalit soʻzi va nomi bilan eʼlon qilamiz, ular parametrlari va qaytish qiymatiga ega boʻlishi mumkin va ular boshqa joydan metod chaqirilganda ishga tushadigan kodni oʻz ichiga oladi. Funktsiyalardan farqli o'laroq, metodlar struct (yoki biz mos ravishda 6-bob va 17-bobda ko'rib chiqiladigan enum yoki trait obyekti) kontekstida aniqlanadi va ularning birinchi parametri har doim self
dir metod chaqirilayotgan structning namunasini ifodalaydi.
Metodlarni aniqlash
Parametr sifatida Kvadrat
misoliga ega bo‘lgan area
funksiyasini o‘zgartiramiz va uning o‘rniga 5-13 ro‘yxatda ko'rsatilganidek, Kvadrat
structida belgilangan area
metodini yaratamiz.
Fayl nomi: src/main.rs
#[derive(Debug)] struct Kvadrat { kenglik: u32, balandlik: u32, } impl Kvadrat { fn area(&self) -> u32 { self.kenglik * self.balandlik } } fn main() { let kvadrat1 = Kvadrat { kenglik: 30, balandlik: 50, }; println!( "To'rtburchakning maydoni {} kvadrat pikselga teng.", kvadrat1.area() ); }
Kvadrat
kontekstida funksiyani aniqlash uchun Kvadrat
uchun impl
(implementation) blokini ishga tushiramiz. Ushbu impl
blokidagi hamma narsa Kvadrat
turi bilan bog'lanadi. Keyin biz area
funksiyasini impl
jingalak qavslar ichida harakatlantiramiz va birinchi (va bu holda, faqat) parametrni signatureda va tananing hamma joyida self
o‘zgartiramiz. main
da, biz area
funksiyasini chaqirib, argument sifatida kvadrat1
ni topshirgan bo‘lsak, o‘rniga Kvadrat
misolida area
metodini chaqirish uchun metod sintaksisi dan foydalanishimiz mumkin. Metod sintaksisi misoldan keyin keladi: biz nuqta qo'shamiz, undan keyin metod nomi, qavslar va har qanday argumentlar qo'shiladi.
area
uchun signatureda kvadrat: &Kvadrat
o‘rniga &self
dan foydalanamiz. &self
aslida self: &Self
ning qisqartmasi. impl
blokida Self
turi impl
bloki uchun bo'lgan turdagi taxallusdir. Metodlar birinchi parametri uchun Self
turidagi self
deb nomlangan parametrga ega bo'lishi kerak, shuning uchun Rust birinchi parametr joyida faqat self
nomi bilan qisqartirish imkonini beradi.
Esda tutingki, biz hali ham kvadrat: &Kvadrat
da qilganimizdek, bu metod Self
misolini olishini koʻrsatish uchun Self
stenografiyasi oldida &
dan foydalanishimiz kerak. Boshqa har qanday parametr singari, metodlar self
egallashi, o'zgarmas self
borrow qilishi mumkin, xuddi biz bu yerda qilganimizdek yoki o'zgaruvchan self
ni borrow qilishi mumkin.
Biz bu yerda funksiya versiyasida &Kvadrat
dan foydalanganimiz uchun xuddi shu sababga ko‘ra &self
tanladik: biz ownershiplik qilishni istamaymiz va faqat structdagi ma’lumotlarni o‘qishni istaymiz, unga yozishni emas. Agar biz ushbu metodning bir qismi sifatida chaqirgan misolni o'zgartirmoqchi bo'lsak, birinchi parametr sifatida &mut self
dan foydalanamiz. Birinchi parametr sifatida faqat self
ni ishlatib, misolga ownershiplik qiladigan metod kamdan-kam uchraydi; bu metod odatda self
ni boshqa narsaga aylantirganda va siz murojat qiluvchiga transformatsiyadan keyin asl nusxadan foydalanishiga yo'l qo'ymaslikni istasangiz ishlatiladi.
Funktsiyalar o'rniga metodlardan foydalanishning asosiy sababi, har bir metod signaturesida self
turini takrorlashning hojati bo'lmagan metod sintaksisidan tashqari, kodni tashkil qilishdir. Biz kelajakdagi kod foydalanuvchilarini biz taqdim etayotgan kutubxonaning turli joylarida Kvadrat
imkoniyatlarini izlashga majburlashdan ko‘ra, biz tur namunasi bilan qila oladigan barcha narsalarni bitta impl
blokiga joylashtirdik.
E'tibor bering, biz metodga structning maydonlaridan biri bilan bir xil nom berishni tanlashimiz mumkin. Misol uchun, biz Kvadrat
da kenglik
deb nomlangan metodni belgilashimiz mumkin:
Fayl nomi: src/main.rs
#[derive(Debug)] struct Kvadrat { kenglik: u32, balandlik: u32, } impl Kvadrat { fn kenglik(&self) -> bool { self.kenglik > 0 } } fn main() { let kvadrat1 = Kvadrat { kenglik: 30, balandlik: 50, }; if kvadrat1.kenglik() { println!("To'rtburchakning kengligi nolga teng bo'lmagan; bu {}", kvadrat1.kenglik); } }
Bu yerda, agar misolning kenglik
maydonidagi qiymat 0
dan katta bo‘lsa, kenglik
metodi true
qiymatini qaytaradi, agar qiymat 0
bo'lsa, false
bo‘lishini tanlaymiz: biz bir xil nomdagi metod ichidagi maydonni istalgan maqsadda ishlatishimiz mumkin. main
da, biz qavslar bilan kvadrat1.kenglik
ga amal qilsak, Rust kenglik
metodini nazarda tutayotganimizni biladi. Qavslardan foydalanmasak, Rust kenglik
maydonini nazarda tutayotganimizni biladi.
Ko'pincha, lekin har doim emas, biz metodga maydon bilan bir xil nom berganimizda, biz u faqat maydondagi qiymatni qaytarishini va boshqa hech narsa qilmasligini xohlaymiz. Shunga o'xshash metodlar getters deb ataladi va Rust ularni boshqa tillarda bo'lgani kabi tizim maydonlari uchun avtomatik ravishda amalga oshirmaydi. Getterslar foydalidir, chunki siz maydonni shaxsiy(private), lekin metodni hammaga ochiq(public) qilib qo'yishingiz mumkin va shu tariqa ushbu maydonga umumiy API ning bir qismi sifatida faqat o'qish uchun ruxsatni yoqishingiz mumkin. Biz 7-bobda public va private nima ekanligini va qanday qilib maydon yoki metodni public yoki private deb belgilashni muhokama qilamiz.
->
operatori qayerda ishlatiladi?C va C++ tillarida metodlarni chaqirish uchun ikki xil operator qo'llaniladi: obyektdagi metodni to'g'ridan-to'g'ri chaqirayotgan bo'lsangiz
.
va agar siz ko'rsatgichdagi metodni obyektga chaqirayotgan bo'lsangiz va avval ko'rsatgichni yo'qotishingiz kerak bo'lsa->
dan foydalanasiz. Boshqacha qilib aytganda, agarobject
havola bo'lsa, u holdaobject->something()
va(*object).something()
metodi chaqiruvlari bir xil bo'ladi.Rust
->
operatoriga ekvivalentga ega emas; Buning o'rniga Rustda avtomatik reference va dereferencing deb nomlangan xususiyat mavjud. Metodni chaqirish Rustda bunday xatti-harakatlarga ega bo'lgan kam sonli joylardan biridir.Bu shunday ishlaydi:
object.something()
bilan metodni chaqirganingizda, Rust avtomatik ravishda&
,&mut
yoki*
ni qo'shadi, shuning uchunobject
metod signaturega mos keladi. Boshqacha qilib aytganda, quyidagilar bir xil:#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn masofa(&self, other: &Point) -> f64 { let x_kvadrat = f64::powi(other.x - self.x, 2); let y_kvadrat = f64::powi(other.y - self.y, 2); f64::sqrt(x_kvadrat + y_kvadrat) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.masofa(&p2); (&p1).masofa(&p2); }
Birinchisi ancha toza ko'rinadi. Ushbu avtomatik reference qilish harakati, metodlar aniq qabul qiluvchiga ega bo'lganligi sababli ishlaydi -
self
turi. Qabul qiluvchi va metod nomini hisobga olgan holda, Rust ma'lum bir holatda kod nima qilayotganini aniq aniqlashi mumkin: o'qish(&self)
, o'zgartirish (&mut self
) yoki iste'mol qilish (self
). Rust metodi qabul qiluvchilar uchun borrow qilishni yashirin qilib qo'yganligi amalda ownershipni ergonomik qilishning katta qismidir.
Ko'proq parametrlarga ega metodlar
Kvadrat
structida ikkinchi metodni implement qilish orqali metodlardan foydalanishni mashq qilaylik. Bu safar biz Kvadrat
misoli Kvadrat
ning boshqa nusxasini olishini va agar ikkinchi Kvadrat
to'liq o'ziga (birinchi Kvadrat
) sig'ishi mumkin bo'lsa, true
qiymatini qaytarishini istaymiz; aks holda u false
ni qaytarishi kerak.
Ya'ni, ushlab_tur
metodini aniqlaganimizdan so'ng, biz 5-14 ro'yxatda ko'rsatilgan dasturni yozish imkoniyatiga ega bo'lishni xohlaymiz.
Fayl nomi: src/main.rs
fn main() {
let kvadrat1 = Kvadrat {
kenglik: 30,
balandlik: 50,
};
let kvadrat2 = Kvadrat {
kenglik: 10,
balandlik: 40,
};
let kvadrat3 = Kvadrat {
kenglik: 60,
balandlik: 45,
};
println!("kvadrat1 kvadrat2ni ushlab turadimi? {}", kvadrat1.ushlab_tur(&kvadrat2));
println!("kvadrat1 kvadrat3ni ushlab turadimi? {}", kvadrat1.ushlab_tur(&kvadrat3));
}
Kutilgan natija quyidagicha ko‘rinadi, chunki kvadrat2
ning ikkala o‘lchami kvadrat1
o‘lchamidan kichikroq, lekin kvadrat3
kvadrat1
dan kengroq:
kvadrat1 kvadrat2ni ushlab turadimi? true
kvadrat1 kvadrat3ni ushlab turadimi? false
Biz metodni aniqlamoqchi ekanligimizni bilamiz, shuning uchun u impl Kvadrat
blokida bo'ladi. Metod nomi ushlab_tur
bo'ladi va u parametr sifatida boshqa Kvadrat
ning o'zgarmas borrowini oladi. Parametrning turi qanday bo'lishini metodni chaqiruvchi kodga qarab aniqlashimiz mumkin: kvadrat1.ushlab_tur(&kvadrat2)
&kvadrat2
da o'tadi, bu kvadrat2
ga o'zgarmas borrow, Kvadrat
misoli. Bu mantiqqa to'g'ri keladi, chunki biz faqat kvadrat2
ni o'qishimiz kerak (yozishdan ko'ra, bu bizga o'zgaruvchan borrow kerak degan ma'noni anglatadi), va biz main
kvadrat2
ownershipligini saqlab qolishini istaymiz, shuning uchun ushlab_tur
metodini chaqirganimizdan keyin uni qayta ishlatishimiz mumkin. ushlab_tur
ning return qiymati mantiqiy qiymat bo'ladi va implement self
ning kengligi va balandligi mos ravishda boshqa Kvadrat
ning kengligi va balandligidan katta ekanligini tekshiradi. Keling, 5-15 ro'yxatda ko'rsatilgan 5-13 ro'yxatdagi impl
blokiga yangi ushlab_tur
metodini qo'shamiz.
Fayl nomi: src/main.rs
#[derive(Debug)] struct Kvadrat { kenglik: u32, balandlik: u32, } impl Kvadrat { fn area(&self) -> u32 { self.kenglik * self.balandlik } fn ushlab_tur(&self, other: &Kvadrat) -> bool { self.kenglik > other.kenglik && self.balandlik > other.balandlik } } fn main() { let kvadrat1 = Kvadrat { kenglik: 30, balandlik: 50, }; let kvadrat2 = Kvadrat { kenglik: 10, balandlik: 40, }; let kvadrat3 = Kvadrat { kenglik: 60, balandlik: 45, }; println!("kvadrat1 kvadrat2ni ushlab turadimi? {}", kvadrat1.ushlab_tur(&kvadrat2)); println!("kvadrat1 kvadrat3ni ushlab turadimi? {}", kvadrat1.ushlab_tur(&kvadrat3)); }
Ushbu kodni 5-14 ro'yxatdagi main
funksiya bilan ishga tushirganimizda, biz kerakli natijani olamiz. Metodlar biz signaturega self
parametridan keyin qo'shadigan bir nechta parametrlarni olishi mumkin va bu parametrlar funksiyalardagi parametrlar kabi ishlaydi.
Associate Funksiyalar
Associate Funksiyalar (Bog'langan Funktsiyalar).impl
blokida aniqlangan barcha funksiyalar associated funksiyalar deb ataladi, chunki ular impl
nomi bilan atalgan tur bilan bog‘langan. Biz birinchi parametr sifatida self
ega bo'lmagan associated funksiyalarni belgilashimiz mumkin (va shuning uchun metodlar emas), chunki ular bilan ishlash uchun turdagi namuna kerak emas.
Biz allaqachon shunday funksiyadan foydalanganmiz: String
turida aniqlangan String::from
funksiyasi.
Metod bo'lmagan associated funktsiyalar ko'pincha structning yangi nusxasini qaytaradigan konstruktorlar uchun ishlatiladi. Ular ko'pincha new
deb ataladi, ammo new
maxsus nom emas va tilga kiritilmagan. Masalan, biz bir o‘lchamli parametrga ega bo‘lgan kvadrat
nomli associated funksiyani taqdim etishimiz va undan kenglik va balandlik sifatida foydalanishimiz mumkin, bu esa bir xil qiymatni ikki marta belgilashdan ko‘ra Kvadrat
kvadratini yaratishni osonlashtiradi. :
Fayl nomi: src/main.rs
#[derive(Debug)] struct Kvadrat { kenglik: u32, balandlik: u32, } impl Kvadrat { fn kvadrat(size: u32) -> Self { Self { kenglik: size, balandlik: size, } } } fn main() { let kv = Kvadrat::kvadrat(3); }
Return turidagi va funksiya tanasidagi Self
kalit so'zlari impl
kalit so'zidan keyin paydo bo'ladigan turning taxalluslari bo'lib, bu holda Kvadrat
bo'ladi.We’ll discuss modules in Chapter 7.
Ushbu associated funktsiyani chaqirish uchun biz struct nomi bilan ::
sintaksisidan foydalanamiz; let kv = Kvadrat::kvadrat(3);
misol bo'la oladi. Bu funksiya struct tomonidan nom maydoniga ega: ::
sintaksisi ham associated funksiyalar, ham modullar tomonidan yaratilgan nomlar bo'shliqlari uchun ishlatiladi. Biz modullarni 7-bobda muhokama qilamiz.
Bir nechta impl
bloklari
Har bir structga bir nechta impl
bloklari ruxsat etiladi. Masalan, 5-15 ro'yxati 5-16 ro'yxatida ko'rsatilgan kodga ekvivalent bo'lib, har bir metod o'zining impl
blokiga ega yani har bir metod o'z impl
blokida.
#[derive(Debug)] struct Kvadrat { kenglik: u32, balandlik: u32, } impl Kvadrat { fn area(&self) -> u32 { self.kenglik * self.balandlik } } impl Kvadrat { fn ushlab_tur(&self, other: &Kvadrat) -> bool { self.kenglik > other.kenglik && self.balandlik > other.balandlik } } fn main() { let kvadrat1 = Kvadrat { kenglik: 30, balandlik: 50, }; let kvadrat2 = Kvadrat { kenglik: 10, balandlik: 40, }; let kvadrat3 = Kvadrat { kenglik: 60, balandlik: 45, }; println!("kvadrat1 kvadrat2ni ushlab turadimi? {}", kvadrat1.ushlab_tur(&kvadrat2)); println!("kvadrat1 kvadrat3ni ushlab turadimi? {}", kvadrat1.ushlab_tur(&kvadrat3)); }
Bu metodlarni bir nechta impl
bloklariga ajratish uchun hech qanday sabab yo'q, lekin bu to'g'ri sintaksis. Biz 10-bobda bir nechta impl
bloklari foydali bo'lgan holatni ko'rib chiqamiz, bu yerda biz umumiy turlar va taritlarni muhokama qilamiz.
Xulosa
Structlar sizning domeningiz uchun mazmunli bo'lgan maxsus turlarni yaratishga imkon beradi. Structlardan foydalanib, siz bog'langan ma'lumotlar qismlarini bir-biriga bog'lab qo'yishingiz va kodingizni aniq qilish uchun har bir qismga nom berishingiz mumkin. impl
bloklarida siz o'zingizning turingiz bilan bog'liq bo'lgan funksiyalarni belgilashingiz mumkin va metodlar - bu sizning structlaringiz misollarining xatti-harakatlarini belgilashga imkon beruvchi associated funksiyaning bir turi.
Ammo structlar maxsus turlarni yaratishning yagona usuli emas: toolboxga boshqa toolni qo'shish uchun Rust enum xususiyatiga murojaat qilaylik.
Enumlar va Pattern Match
Ushbu bobda biz enumlar deb ham ataladigan enumerationsni ko'rib chiqamiz.
Enumlar sizga turni uning mumkin bo'lgan variantlarini sanab aniqlash imkonini beradi. Avval enum ma'lumotarni qanday birlashtirishi mumkinligini ko'rsatish uchun enumni aniqlaymiz va ishlatamiz. Keyinchalik, qiymatning biror narsa yoki hech narsa bo'lishi mumkinligini ifodalovchi Option
deb nomlangan juda foydali enumni o'rganamiz. Keyin biz match
iborasida pattern matching enumning turli qiymatlari uchun turli kodlarni ishga tushirishni qanday osonlashtirishini ko'rib chiqamiz. Nihoyat, if let
konstruksiyasi sizning kodingizdagi enumlar bilan ishlash uchun qanday qulay va ixcham idioma ekanligini ko'rib chiqamiz.
Enumni aniqlash
Structlar sizga tegishli maydonlar va ma'lumotlarni, masalan, kenglik
va balandlik
bilan Kvadrat
ni guruhlash usulini beradigan bo'lsa, enumlar qiymatni mumkin bo'lgan qiymatlar to'plamidan biri deb aytish metodini beradi. Masalan, Kvadrat
bu mumkin bo‘lgan shakllar to‘plamidan biri bo‘lib, Doira
va Uchburchak
ni ham o‘z ichiga oladi, demoqchimiz. Buning uchun Rust bizga ushbu imkoniyatlarni enum sifatida kodlash imkonini beradi.
Keling, kodda ifodalashni xohlashimiz mumkin bo'lgan vaziyatni ko'rib chiqaylik va bu holda nima uchun enumlar foydali va structlardan ko'ra mosroq ekanligini bilib olaylik. Aytaylik, biz IP manzillar bilan ishlashimiz kerak. Hozirgi vaqtda IP manzillar uchun ikkita asosiy standart qo'llaniladi: to'rtinchi versiya va oltinchi versiya. Bular bizning dasturimiz duch keladigan IP-manzilning yagona imkoniyatlari bo'lgani uchun biz barcha mumkin bo'lgan variantlarni enumerate qilishimiz mumkin, bu yerda enumeration o'z nomini oladi.
Har qanday IP manzil to'rtinchi versiya yoki oltinchi versiya manzili bo'lishi mumkin, lekin ikkalasi bir vaqtning o'zida emas. IP-manzillarning bu xususiyati enum ma'lumotlar structini mos qiladi, chunki enum qiymati faqat uning variantlaridan biri bo'lishi mumkin. To'rtinchi versiya va oltinchi versiya manzillari hali ham IP-manzillardir, shuning uchun kod har qanday IP-manzilga tegishli vaziyatlarni ko'rib chiqayotganda ular bir xil turdagi sifatida ko'rib chiqilishi kerak.
Biz ushbu kontseptsiyani kodda IpAddrKind
ro'yxatini belgilash va IP-manzil bo'lishi mumkin bo'lgan V4
va V6
turlarini enumeration qilish orqali ifodalashimiz mumkin. Bular enumning variantlari:
enum IpAddrKind { V4, V6, } fn main() { let tort = IpAddrKind::V4; let olti = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_turi: IpAddrKind) {}
IpAddrKind
endi biz kodimizning boshqa joylarida foydalanishimiz mumkin bo'lgan maxsus ma'lumotlar turidir.
Enum qiymatlari
Biz IpAddrKind
ning ikkita variantining har birining misollarini quyidagicha yaratishimiz mumkin:
enum IpAddrKind { V4, V6, } fn main() { let tort = IpAddrKind::V4; let olti = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_turi: IpAddrKind) {}
E'tibor bering, enumning variantlari uning identifikatori ostida nom maydonida joylashgan va biz ikkalasini ajratish uchun qo'sh nuqtadan foydalanamiz. Bu foydali, chunki endi ikkala IpAddrKind::V4
va IpAddrKind::V6
qiymatlari bir xil turdagi: IpAddrKind
. Masalan, biz har qanday IpAddrKind
ni qabul qiladigan funksiyani aniqlashimiz mumkin:
enum IpAddrKind { V4, V6, } fn main() { let tort = IpAddrKind::V4; let olti = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_turi: IpAddrKind) {}
Va biz bu funktsiyani ikkala variant bilan chaqirishimiz mumkin:
enum IpAddrKind { V4, V6, } fn main() { let tort = IpAddrKind::V4; let olti = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_turi: IpAddrKind) {}
Enumlardan foydalanish yanada ko'proq afzalliklarga ega. Bizning IP manzilimiz turi haqida ko'proq o'ylab ko'rsak, hozirda bizda haqiqiy IP-manzilni ma'lumotlarni saqlash imkoni yo'q; biz faqat qanday turdagi ekanligini bilamiz. 5-bobda structlar haqida hozirgina bilib olganingizni hisobga olsak, 6-1 ro'yxatda ko'rsatilganidek, bu muammoni structlar yordamida hal qilish istagi paydo bo'lishi mumkin.
fn main() { enum IpAddrKind { V4, V6, } struct IpAddr { tur: IpAddrKind, manzil: String, } let asosiy = IpAddr { tur: IpAddrKind::V4, manzil: String::from("127.0.0.1"), }; let orqaga_qaytish = IpAddr { tur: IpAddrKind::V6, manzil: String::from("::1"), }; }
Bu yerda biz ikkita maydonga ega boʻlgan IpAddr
structini aniqladik: IpAddrKind
turidagi tur
maydoni (biz avvalroq belgilagan raqam) va String
tipidagi manzil
maydoni. Bizda bu structning ikkita misoli bor. Birinchisi asosiy
boʻlib, u 127.0.0.1
bogʻlangan manzil maʼlumotlari bilan tur
sifatida IpAddrKind::V4
qiymatiga ega. Ikkinchi misol - orqaga_qaytish
. U tur
qiymati sifatida IpAddrKind
ning boshqa variantiga ega, V6
va u bilan bog'langan ::1
manzili mavjud. Biz tur
va manzil
qiymatlarini birlashtirish uchun structdan foydalanganmiz, shuning uchun endi variant qiymat bilan bog'langan.
Shu bilan birga, bir xil kontseptsiyani faqat enum yordamida ifodalash yanada ixchamroqdir: struct ichidagi enum o'rniga, biz ma'lumotlarni to'g'ridan-to'g'ri har bir enum variantiga qo'yishimiz mumkin. IpAddr
enumining ushbu yangi ta'rifida aytilishicha, V4
va V6
variantlari ham associated String
qiymatlariga ega bo'ladi:
fn main() { enum IpAddr { V4(String), V6(String), } let asosiy = IpAddr::V4(String::from("127.0.0.1")); let orqaga_qaytish = IpAddr::V6(String::from("::1")); }
Biz to'g'ridan-to'g'ri enumning har bir variantiga ma'lumotlarni biriktiramiz, shuning uchun qo'shimcha structga ehtiyoj qolmaydi. Bu yerda, shuningdek, enumlar qanday ishlashining yana bir tafsilotini ko'rish osonroq bo'ladi: biz belgilagan har bir enum variantining nomi, shuningdek, enum nusxasini yaratuvchi funktsiyaga aylanadi. Ya'ni, IpAddr::V4()
funksiya chaqiruvi bo'lib, u String
argumentini oladi va IpAddr
tipidagi misolni qaytaradi. Enumni aniqlash natijasida aniqlangan ushbu konstruktor funksiyasini avtomatik ravishda olamiz.
Structdan ko'ra enumdan foydalanishning yana bir afzalligi bor: har bir variantda bog'langan ma'lumotlarning har xil turlari va miqdori bo'lishi mumkin. To'rtinchi versiyada IP-manzillar har doim 0 dan 255 gacha bo'lgan qiymatlarga ega bo'lgan to'rtta raqamli komponentga ega bo'ladi. Agar biz V4
manzillarini to‘rtta u8
qiymati sifatida saqlamoqchi bo‘lsak-da, V6
manzillarini bitta String
qiymati sifatida ifodalasak, biz struct bilan buni qila olmaymiz. Enumlar bu ishni osonlik bilan hal qiladi:
fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let asosiy = IpAddr::V4(127, 0, 0, 1); let orqaga_qaytish = IpAddr::V6(String::from("::1")); }
Biz to'rtinchi versiya va oltinchi versiya IP manzillarini saqlash uchun ma'lumotlar tuzilmalarini aniqlashning bir necha xil usullarini ko'rsatdik. Biroq, ma'lum bo'lishicha, IP-manzillarni saqlash va ularning qaysi turini kodlash istagi shunchalik keng tarqalganki, standart kutubxonada biz foydalanishimiz mumkin bo'lgan defination mavjud! . Keling, standart kutubxona IpAddr
ni qanday aniqlashini ko'rib chiqaylik: u biz aniqlagan va ishlatgan aniq enum va variantlarga ega, lekin u manzil ma'lumotlarini variantlar ichida ikki xil struct shaklida joylashtiradi, har bir variant uchun turlicha belgilanadi:
#![allow(unused)] fn main() { struct Ipv4Addr { // --snip-- } struct Ipv6Addr { // --snip-- } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), } }
Ushbu kod har qanday turdagi ma'lumotlarni enum variantiga qo'yish mumkinligini ko'rsatadi: masalan, stringlar, raqamli turlar yoki structlar. Siz hatto boshqa raqamni ham qo'shishingiz mumkin! Bundan tashqari, standart kutubxona turlari ko'pincha siz o'ylab topganingizdan ancha murakkab emas.
E'tibor bering, standart kutubxonada IpAddr
uchun definition mavjud bo'lsa ham, biz o'z definitionimizni ziddiyatli holda yaratishimiz va foydalanishimiz mumkin, chunki biz standart kutubxonaning definitionini o'z doiramizga kiritmaganmiz. Biz 7-bobda turlarni qamrab olish haqida ko'proq gaplashamiz.
Keling, 6-2 ro'yxatdagi enumning yana bir misolini ko'rib chiqaylik: bu o'z variantlarida ko'p turdagi turlarga ega.
enum Xabar { Chiqish, Kochirish { x: i32, y: i32 }, Yozish(String), RangTanlash(i32, i32, i32), } fn main() {}
Ushbu enum har xil turdagi to'rtta variantga ega:
Chiqish
da u bilan bogʻliq hech qanday maʼlumot yoʻq.Kochirish
da struct kabi maydonlarni nomlagan.Yozish
bittaString
ni o'z ichiga oladi.RangTanla
uchtai32
qiymatini o'z ichiga oladi.
Enumni 6-2-roʻyxatdagi kabi variantlar bilan belgilash strukturaviy definitionlarning har xil turlarini aniqlashga oʻxshaydi, faqat enum struct
kalit soʻzidan foydalanmaydi va barcha variantlar Xabar
turi ostida birlashtiriladi. Quyidagi structlar oldingi enum variantlari bilan bir xil ma'lumotlarni saqlashi mumkin:
struct ChiqishXabar; // unit struct struct KochirishXabar { x: i32, y: i32, } struct YozishXabar(String); // tuple struct struct RangTanshlaXabar(i32, i32, i32); // tuple struct fn main() {}
Lekin biz o'z turlariga ega bo'lgan turli structlardan foydalanganimizda, biz har qanday xabar turini qabul qiladigan funksiyalarni osonlikcha aniqlay olmadik, buni bitta tur bo'lgan 6-2 ro'yxatda e'lon qilingan Xabar
turini enum bilan amalga oshirish mumkin.
Enumlar va structlar o'rtasida yana bir o'xshashlik bor: biz impl
yordamida structlarda metodlarni aniqlay olganimizdek, enumlarda ham metodlarni belgilashimiz mumkin. Bu yerda biz Xabar
enumimizda aniqlashimiz mumkin bo'lgan chaqiruv
deb nomlangan metod:
fn main() { enum Xabar { Chiqish, Kochirish { x: i32, y: i32 }, Yozish(String), RangTanlash(i32, i32, i32), } impl Xabar { fn chaqiruv(&self) { // metod tanasi bu yerda aniqlanadi } } let m = Xabar::Yozish(String::from("salom")); m.chaqiruv(); }
Metod tanasi biz metod deb atagan qiymatni olish uchun self
ishlatadi. Ushbu misolda biz Xabar::Yozish(String::from("salom"))
qiymatini o'z ichiga olgan m
o'zgaruvchisini yaratdik va m.chaqiruv()
ishga tushganda chaqiruv
metodining tanasida aynan shunday boladi. Keling, standart kutubxonadagi juda keng tarqalgan va foydali bo'lgan yana bir enumni ko'rib chiqaylik:
Option`.
Option
Enum va uning null qiymatlardan ustunligi
Ushbu bo'lim standart kutubxona tomonidan aniqlangan yana bir enum bo'lgan Option
ning misolini o'rganadi.Option
turi qiymat nimadir yoki hech narsa bo'lmasligi mumkin bo'lgan juda keng tarqalgan senariyni kodlaydi.
Misol uchun, agar siz bo'sh bo'lmagan ro'yxatdagi birinchi elementni so'rasangiz, qiymat olasiz. Agar siz bo'sh ro'yxatdagi birinchi elementni so'rasangiz, hech narsa olmaysiz. Ushbu kontseptsiyani turdagi tizim nuqtai nazaridan ifodalash kompilyator siz ko'rib chiqishingiz kerak bo'lgan barcha ishlarni ko'rib chiqqaningizni tekshirishi mumkinligini anglatadi; bu funksiya boshqa dasturlash tillarida juda keng tarqalgan xatolarni oldini oladi.
Dasturlash tilining dizayni ko'pincha siz qaysi xususiyatlarni o'z ichiga olganligingiz nuqtai nazaridan o'ylanadi, ammo siz chiqarib tashlagan xususiyatlar ham muhimdir. Rust ko'plab boshqa tillarda mavjud bo'lgan null xususiyatiga ega emas. Null - bu qiymat yo'qligini bildiradi. Null bo'lgan tillarda o'zgaruvchilar har doim ikkita holatdan birida bo'lishi mumkin: null yoki null emas.
2009 yilgi "Null References: The Million Dollar Mistake" taqdimotida null ixtirochisi Tony Hoare shunday deydi:
Men buni milliard dollarlik xatoyim deb atayman. O'sha paytda men object-oriented language referencelar uchun birinchi keng qamrovli turdagi tizimni loyihalashtirgan edim. Mening maqsadim referencelardan foydalanishning mutlaqo xavfsiz bo'lishini ta'minlash edi, tekshirish kompilyator tomonidan avtomatik ravishda amalga oshiriladi. Lekin men null reference qo'yish vasvasasiga qarshi tura olmadim, chunki uni amalga oshirish juda oson edi. Bu so'nggi qirq yil ichida bir milliard dollar og'riq va zarar keltirgan son-sanoqsiz xatolar, zaifliklar va tizimning ishdan chiqishiga olib keldi.
Null qiymatlari bilan bog'liq muammo shundaki, agar siz null qiymatdan a sifatida foydalanishga harakat qilsangiz no-null qiymat bo'lsa, siz qandaydir xatoga duch kelasiz. Ushbu null yoki not-null xususiyat keng tarqalganligi sababli, bunday xatoga yo'l qo'yish juda oson.
Biroq, null ifodalamoqchi bo'lgan kontseptsiya hali ham foydalidir: null hozirda yaroqsiz yoki biron sababga ko'ra mavjud bo'lmagan qiymatdir.
Muammo aslida kontseptsiyada emas, balki muayyan amalga oshirishda. Shunday qilib, Rust nulllarga ega emas, lekin u mavjud yoki yo'q qiymat tushunchasini kodlay oladigan enumga ega. Bu enum Option<T>
bo'lib, u standart kutubxona tomonidan quyidagicha aniqlanadi:
#![allow(unused)] fn main() { enum Option<T> { None, Some(T), } }
Option<T>
enumi shunchalik foydaliki, u hatto muqaddimaga ham kiritilgan; uni aniq doiraga kiritishingiz shart emas. Uning variantlari ham muqaddima tarkibiga kiritilgan: Some
va None
dan Option::
prefiksisiz bevosita foydalanishingiz mumkin. Option<T>
enum hali ham oddiy enum bo'lib, Some(T)
va None
hali ham Option<T>
turidagi variantlardir.
<T>
sintaksisi Rustning oʻziga xos xususiyati boʻlib, biz hali gaplashmaganmiz. Bu umumiy turdagi parametr va biz 10-bobda genericlarni batafsil ko'rib chiqamiz.
Hozircha siz bilishingiz kerak bo'lgan narsa shuki, <T>
Option
enumining Some
varianti har qanday turdagi ma'lumotlarning bir qismini saqlashi mumkinligini va o'rniga qo'llaniladigan har bir konkret turni bildiradi. T
umumiy Option<T>
turini boshqa turga aylantiradi. Raqam turlari va qator turlarini saqlash uchun Option
qiymatlaridan foydalanishga misollar keltiramiz:
fn main() { let raqam = Some(5); let belgi = Some('e'); let yoq_raqam: Option<i32> = None; }
raqam
turi - Option<i32>
. belgi
turi Option<char>
bo'lib, u boshqa tur. Rust bu turlarni aniqlashi mumkin, chunki biz Some
variantida qiymat belgilaganmiz. yoq_raqam
uchun Rust bizdan umumiy Option
turiga izoh berishimizni talab qiladi: kompilyator faqat None
qiymatiga qarab mos keladigan Some
varianti qanday turga ega bo'lishini aniqlay olmaydi. Bu yerda biz Rustga aytamizki, biz yoq_raqam
Option<i32>
turida bo'lishini nazarda tutamiz.
Agar bizda Some
qiymati bo'lsa, biz qiymat mavjud ekanligini va qiymat Some
ichida saqlanishini bilamiz. Agar bizda None
qiymati bo'lsa, u qaysidir ma'noda null bilan bir xil narsani anglatadi: bizda haqiqiy qiymat yo'q. Xo'sh, nega Option<T>
nullga ega bo'lishdan yaxshiroq?
Xulosa qilib aytganda, Option<T>
va T
(bu erda T
har qanday tur bo'lishi mumkin) har xil turdagi, chunki kompilyator bizga Option<T>
qiymatidan foydalanishga ruxsat bermaydi, go'yo bu haqiqiy qiymat. Masalan, bu kod kompilyatsiya qilinmaydi, chunki u Option<i8>
ga i8
qo`shishga harakat qilmoqda:
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let natija = x + y;
}
Agar biz ushbu kodni ishlatsak, biz quyidagi kabi xato xabarini olamiz:
$ 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 natija = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
<&'a i8 as Add<i8>>
<&i8 as Add<&i8>>
<i8 as Add<&i8>>
<i8 as Add>
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` due to previous error
Kuchli! Aslida, bu xato xabari Rust i8
va Option<i8>
ni qanday qo'shishni tushunmasligini anglatadi, chunki ular turli xil. Rustda i8
kabi turdagi qiymatga ega bo'lsak, kompilyator bizda har doim haqiqiy qiymatga ega bo'lishini ta'minlaydi. Ushbu qiymatdan foydalanishdan oldin nullni tekshirmasdan ishonch bilan davom etishimiz mumkin. Faqat bizda Option<i8>
(yoki qanday turdagi qiymat bilan ishlayotgan bo'lishimizdan qat'iy nazar) mavjud bo'lganda, biz qiymatga ega bo'lmasligimizdan xavotirlanishimiz kerak va kompilyator qiymatdan foydalanishdan oldin bu holatni hal qilishimizga ishonch hosil qiladi.
Boshqacha qilib aytganda, T
amallarini bajarishdan oldin Option<T>
ni T
ga aylantirishingiz kerak. Umuman olganda, bu null bilan bog'liq eng keng tarqalgan muammolardan birini hal qilishga yordam beradi: agar biror narsa bo'lsa, u null emas deb taxmin qilish.
Null bo'lmagan qiymatni noto'g'ri qabul qilish xavfini yo'q qilish kodingizga ko'proq ishonch hosil qilishingizga yordam beradi. Null bo'lishi mumkin bo'lgan qiymatga ega bo'lish uchun, siz ushbu qiymatning turini Option<T>
qilib aniq belgilashingiz kerak.
Keyin, ushbu qiymatdan foydalanganda, qiymat null bo'lsa, ishni aniq ko'rib chiqishingiz talab qilinadi. Qiymat Option<T>
bo'lmagan turga ega bo'lgan har bir joyda, qiymat null emas deb ishonch bilan taxmin qilishingiz mumkin. Bu Rust uchun nullning tarqalishini cheklash va Rust kodining xavfsizligini oshirish uchun ataylab qilingan dizayn qarori edi.
Xo'sh, Option<T>
turidagi qiymatga ega bo'lganingizda, Some
variantidan T
qiymatini qanday qilib olish mumkin, shunda siz ushbu qiymatdan foydalanishingiz mumkin? Option<T>
enumi turli vaziyatlarda foydali boʻlgan koʻp sonli usullarga ega; siz ularni uning hujjatlarida tekshirishingiz mumkin. Option<T>
dagi metodlar bilan tanishish Rust bilan sayohatingizda juda foydali bo`ladi.
Umuman olganda, Option<T>
qiymatidan foydalanish uchun siz har bir variantni boshqaradigan kodga ega bo'lishni xohlaysiz. Siz faqat Some(T)
qiymatiga ega bo'lganingizda ishlaydigan ba'zi kodni xohlaysiz va bu kod ichki T
dan foydalanishga ruxsat etiladi. Agar sizda None
qiymati bo'lsa va bu kodda T
qiymati bo'lmasa, ishlaydigan boshqa kod ham kerak bo'ladi. match
ifodasi control flow konstruksiyasi bo‘lib, u enumlar bilan foydalanilganda aynan shunday qiladi: u enumning qaysi variantiga ega bo‘lishiga qarab turli xil kodlarni ishga tushiradi va bu kod mos keladigan qiymat ichidagi ma’lumotlardan foydalanishi mumkin.
match
Control Flow konstruksiyasi
Rust match
deb nomlangan juda kuchli control flow konstruksiyasiga ega, bu sizga qiymatni bir qator patternlar bilan solishtirish va keyin qaysi pattern mos kelishiga qarab kodni bajarish imkonini beradi. Patternlar literal qiymatlar, o'zgaruvchilar nomlari, wildcardlar va boshqa ko'plab narsalardan iborat bo'lishi mumkin; 18-bobda har xil turdagi patternlar va ular bajaradigan ishlar yoritilgan. match
ning kuchi patternlarning ifodaliligidan va kompilyator barcha mumkin bo'lgan holatlar ko'rib chiqilishini tasdiqlashidan kelib chiqadi.
match
iborasini tanga saralash mashinasiga o'xshatib tasavvur qiling: tangalar bo'ylab turli o'lchamdagi teshiklari bo'lgan yo'ldan pastga siljiydi va har bir tanga o'zi mos keladigan birinchi teshikdan tushadi. Xuddi shu tarzda, qiymatlar match
dagi har bir patterndan o'tadi va birinchi patternda qiymat “fits,”, qiymat bajarish paytida ishlatiladigan tegishli kod blokiga tushadi.
Tangalar haqida gap ketganda, keling, ularni match
yordamida misol qilib olaylik! Biz noma'lum AQSH tangasini oladigan funksiyani yozishimiz mumkin va xuddi sanash mashinasiga o'xshab uning qaysi tanga ekanligini aniqlaydi va 6-3 ro'yxatda ko'rsatilganidek, uning qiymatini sentlarda qaytaradi.
enum Tanga { Penny, Nickel, Dime, Quarter, } fn sentdagi_qiymat(tanga: Tanga) -> u8 { match tanga { Tanga::Penny => 1, Tanga::Nickel => 5, Tanga::Dime => 10, Tanga::Quarter => 25, } } fn main() {}
Keling, sentdagi_qiymat
funksiyasidagi match
ni ajratamiz. Avval biz match
kalit so'zidan keyin ifodani keltiramiz, bu holda bu qiymat tanga
bo'ladi. Bu if
bilan ishlatiladigan shartli ifodaga juda o'xshaydi, lekin
katta farq bor: if
bilan shart mantiqiy qiymatga baholanishi kerak, ammo bu yerda u har qanday turdagi bo'lishi mumkin. Ushbu misoldagi tanga
turi biz birinchi qatorda belgilagan Tanga
enumidir.
Keyingi match
armlari. Arm ikki qismdan iborat: pattern va ba'zi kod. Bu yerdagi birinchi arm Tanga::Penny
qiymati boʻlgan patternga ega, soʻngra ishlash uchun pattern va kodni ajratuvchi =>
operatori. Bu holatda kod faqat 1
qiymatidan iborat. Har bir arm keyingisidan vergul bilan ajratiladi.
match
ifodasi bajarilganda, natijaviy qiymatni har bir armning patterniga solishtiradi. Agar pattern qiymatga mos kelsa, ushbu pattern bilan bog'langan kod bajariladi. Agar bu pattern qiymatga mos kelmasa, ijro tanga saralash mashinasida bo'lgani kabi keyingi armda davom etadi.
Bizda qancha arm kerak bo'lsa, shuncha arm bo'lishi mumkin: 6-3 ro'yxatda bizning match
imizda to'rtta arm bor.
Har bir arm bilan bog'langan kod ifodadir va mos keladigan qismdagi ifodaning natijaviy qiymati butun match
ifodasi uchun qaytariladigan qiymatdir.
Agar mos keladigan arm kodi qisqa bo'lsa, biz odatda jingalak qavslardan foydalanmaymiz, chunki bu ro'yxat 6-3da bo'lgani kabi, har bir arm shunchaki qiymat qaytaradi. Agar siz mos keladigan chiziqda bir nechta kod qatorlarini ishlatmoqchi bo'lsangiz, jingalak qavslardan foydalaning va armdan keyingi vergul ixtiyoriy bo'ladi. Masalan, quyidagi kodda Omadli tanga!
metod har safar Tanga::Penny
bilan chaqirilganda, lekin baribir blokning oxirgi qiymatini qaytaradi, 1
:
enum Tanga { Penny, Nickel, Dime, Quarter, } fn sentdagi_qiymat(tanga: Tanga) -> u8 { match tanga { Tanga::Penny => { println!("Omadli tanga!"); 1 } Tanga::Nickel => 5, Tanga::Dime => 10, Tanga::Quarter => 25, } } fn main() {}
Qiymatlarni bog'laydigan patternlar
match armlarining yana bir foydali xususiyati shundaki, ular patternga mos keladigan qiymatlarning qismlarini bog'lashlari mumkin. Enum variantlaridan qiymatlarni shunday chiqarishimiz mumkin.
Misol tariqasida, uning ichida ma'lumotlarni saqlash uchun enum variantlarimizdan birini o'zgartiraylik.
1999 yildan 2008 yilgacha Qo'shma Shtatlar bir tomondan 50 shtatning har biri uchun turli dizayndagi tangalarni bosib chiqardi. Boshqa hech qanday tangalar davlat dizayniga ega emas, shuning uchun faqat quarterlarda bunday qo'shimcha qiymat mavjud. Biz ushbu maʼlumotni Quarter
variantini uning ichida saqlangan UsState
qiymatini kiritish uchun oʻzgartirish orqali enum
ga qoʻshishimiz mumkin, biz buni 6-4 roʻyxatda qilganmiz.
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } enum Tanga { Penny, Nickel, Dime, Quarter(UsState), } fn main() {}
Tasavvur qiling-a, sizning do'stingiz barcha 50 shtatdan quarter yig'ishga harakat qilmoqda. Biz tangalar turi bo'yicha saralashimiz bilan birga, agar do'stimizda yo'q bo'lsa, ular uni o'z kollektsiyasiga qo'shishlari uchun har quarter bilan bog'liq shtat nomini ham chaqiramiz.
Ushbu kod uchun match ifodasida biz Tanga::Quarter
varianti qiymatlariga mos keladigan patternga shtat
deb nomlangan o'zgaruvchini qo‘shamiz. Tanga::Quarter
mos kelganda, shtat
o'zgaruvchisi o'sha quarter holati qiymatiga bog'lanadi. Keyin biz ushbu arm uchun kodda shtat
dan foydalanishimiz mumkin, masalan:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Tanga { Penny, Nickel, Dime, Quarter(UsState), } fn sentdagi_qiymat(tanga: Tanga) -> u8 { match tanga { Tanga::Penny => 1, Tanga::Nickel => 5, Tanga::Dime => 10, Tanga::Quarter(shtat) => { println!("{:?} dan shtat quarter!", shtat); 25 } } } fn main() { sentdagi_qiymat(Tanga::Quarter(UsState::Alaska)); }
Agar biz sentdagi_qiymat(Tanga::Quarter(UsState::Alaska))
deb ataydigan bo'lsak, tanga
Tanga::Quarter(UsState::Alaska)
bo'ladi. Ushbu qiymatni har bir match armi bilan solishtirganda, biz Tanga::Quarter(shtat)
ga yetguncha ularning hech biri mos kelmaydi. O'sha paytda shtat
uchun majburiy UsState::Alaska
qiymati bo'ladi. Keyin biz bu bog'lanishni println!
ifodasida qo'llashimiz mumkin, shu bilan Quarter
uchun Tanga
enum variantidan ichki holat qiymatini olamiz.
Option<T>
uchun Match
Oldingi bo'limda biz Option<T>
dan foydalanilganda Some
holatidan ichki T
qiymatini olishni xohladik; Biz, shuningdek, Tanga
enum bilan qilganimizdek, match
yordamida Option<T>
ni boshqarishimiz mumkin! Tangalarni solishtirish o'rniga, biz Option<T>
variantlarini solishtiramiz, lekin match
ifodasining ishlash usuli bir xil bo'lib qoladi.
Aytaylik, biz Option<i32>
ni oladigan funksiya yozmoqchimiz va agar ichida qiymat bo'lsa, bu qiymatga 1 qo'shiladi. Agar ichida qiymat bo'lmasa, funktsiya None
qiymatini qaytarishi va hech qanday operatsiyani bajarishga urinmasligi kerak.
Ushbu funktsiyani yozish juda oson, match
tufayli va 6-5-Ro'yxatga o'xshaydi.
fn main() { fn bir_qoshish(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } let besh = Some(5); let olti = bir_qoshish(besh); let yoq = bir_qoshish(None); }
Keling, bir_qoshish
ning birinchi bajarilishini batafsilroq ko'rib chiqamiz. Biz bir_qoshish(besh)
ni chaqirganimizda, bir_qoshish
tanasidagi x
o'zgaruvchisi Some(5)
qiymatiga ega bo'ladi. Keyin biz buni har bir matchning armi bilan taqqoslaymiz:
fn main() {
fn bir_qoshish(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let besh = Some(5);
let olti = bir_qoshish(besh);
let yoq = bir_qoshish(None);
}
Some(5)
qiymati None
patterniga mos kelmaydi, shuning uchun keyingi armga o'tamiz:
fn main() {
fn bir_qoshish(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let besh = Some(5);
let olti = bir_qoshish(besh);
let yoq = bir_qoshish(None);
}
Some(5)
ga Some(i)
pattern mos keladimi? Ha bu shunday! Bizda ham xuddi shunday variant bor. Keyin i
o'zgaruvchisi Some
ichidagi qiymatga bog'lanadi, shuning uchun i
5
qiymatini oladi. Shundan so'ng match armidagi kod bajariladi, shuning uchun biz i
qiymatiga 1 qo'shamiz va ichida jami 6
bo'lgan yangi Some
qiymatini yaratamiz.
Keling, 6-5-Ro'yxatdagi bir_qoshish
ning ikkinchi chaqiruvini ko'rib chiqaylik, bunda x
None
. Biz match
ga kiramiz va birinchi arm bilan taqqoslaymiz:
fn main() {
fn bir_qoshish(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let besh = Some(5);
let olti = bir_qoshish(besh);
let yoq = bir_qoshish(None);
}
Bu mos keladi! Qo'shiladigan qiymat yo'q, shuning uchun dastur to'xtaydi va =>
o'ng tomonidagi None
qiymatini qaytaradi. Birinchi arm mos kelganligi sababli, boshqa armlar taqqoslanmaydi.
match
va enumlarni birlashtirish ko'p holatlarda foydalidir. Rust kodida siz ushbu patterni juda ko'p ko'rasiz: enum bilan match
, o'zgaruvchini ichidagi ma'lumotlarga bog'lang va keyin unga asoslangan kodni bajaring. Avvaliga bu biroz qiyin, lekin ko'nikkaningizdan so'ng uni barcha tillarda bo'lishini xohlaysiz. Bu har doim foydalanuvchilarning sevimli texnikasi.
Match barcha qiymat variantlarini qamrab oladi
Biz muhokama qilishimiz kerak bo'lgan match
ning yana bir jihati bor: arm patterlari barcha imkoniyatlarni qamrab olishi kerak. Xatoga ega va kompilyatsiya qilinmaydigan bir_qoshish
funksiyamizning ushbu versiyasini ko'rib chiqing:
fn main() {
fn bir_qoshish(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let besh = Some(5);
let olti = bir_qoshish(besh);
let yoq = bir_qoshish(None);
}
Biz None
holatini ko‘rib chiqmadik, shuning uchun bu kod xatolikka olib keladi. Yaxshiyamki, bu xato Rust qanday tutishni biladi. Agar biz ushbu kodni kompilyatsiya qilishga harakat qilsak, biz ushbu xatoni olamiz:
$ 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
|
note: `Option<i32>` defined here
--> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:518:1
|
= note:
/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:522:5: not covered
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` due to previous error
Rust biz barcha mumkin bo'lgan holatlarni qamrab olmaganimizni biladi va hatto qaysi patterni unutganimizni biladi! Rust-da matchlar to'liq: kod to'g'ri bo'lishi uchun biz barcha mumkin bo'lgan holatlarni qamrab olishimiz kerak. Ayniqsa, Option<T>
holatida, Rust bizni None
holatini aniq ko'rib chiqishni unutib qo'yishimizga to'sqinlik qilsa, bizni null bo'lishi mumkin bo'lgan qiymatga ega bo`lishimizdan himoya qiladi, shunday qilib, ilgari muhokama qilingan milliard dollarlik xatoni imkonsiz qiladi.
Hammasini ushlash patternlari va _
placeholder
Enumlardan foydalanib, biz bir nechta ma'lum qiymatlar uchun maxsus harakatlarni amalga oshirishimiz mumkin, ammo boshqa barcha qiymatlar uchun bitta standart amalni bajaramiz. Tasavvur qiling-a, biz o'yinni amalga oshirmoqdamiz, unda 3 ta o'yinda o'yinchi qimirlamaydi, aksincha, chiroyli yangi shlyapa oladi. Agar siz 7 ni aylantirsangiz, o'yinchingiz chiroyli shlyapasini yo'qotadi. Boshqa barcha qiymatlar uchun o'yinchi o'yin taxtasida shuncha bo'sh joyni siljitadi. Mana, bu mantiqni amalga oshiradigan match
, bu erda narda toshlarni o'rash natijasi tasodifiy qiymat emas, balki qattiq kodlangan va mantiqning qolgan qismi jismlarsiz funktsiyalar bilan ifodalanadi, chunki ularni amalga oshirish ushbu doiradan tashqarida. misol:
fn main() { let narda_toshi = 9; match narda_toshi { 3 => chiroyli_shlyapa_qoshish(), 7 => chiroyli_shlyapani_ochirish(), boshqa => player_harakati(boshqa), } fn chiroyli_shlyapa_qoshish() {} fn chiroyli_shlyapani_ochirish() {} fn player_harakati(bosh_joylar: u8) {} }
Dastlabki ikkita arm uchun patternlar 3
va 7
harfli qiymatlardir. Boshqa barcha mumkin bo'lgan qiymatlarni qamrab oladigan oxirgi arm uchun pattern biz boshqa
deb nomlash uchun tanlagan o'zgaruvchidir. boshqa
arm uchun ishlaydigan kod o'zgaruvchini player_harakati
funksiyasiga o'tkazish orqali ishlatadi.
Ushbu kod kompilatsiya qilinadi, garchi biz u8
ga ega bo'lishi mumkin bo'lgan barcha qiymatlarni sanab o'tmagan bo'lsak ham, chunki oxirgi pattern maxsus sanab o'tilmagan barcha qiymatlarga mos keladi. Bu match
toʻliq boʻlishi kerakligi haqidagi talabga javob beradi. E'tibor bering, biz armni eng oxirgi qo'yishimiz kerak, chunki patternlar tartibda baholanadi. Agar biz ushlovchi armni oldinroq qo'ysak, boshqa armlar hech qachon run bo'lmaydi, shuning uchun biz hammamizni tutgandan keyin arm qo'shsak, Rust bizni ogohlantiradi!
Rustda umumiy patterda qiymatdan foydalanishni istamaganimizda foydalanish mumkin bo'lgan pattern ham mavjud: _
- har qanday qiymatga mos keladigan va bu qiymatga bog'lanmaydigan maxsus pattern. Bu Rustga biz qiymatdan foydalanmasligimizni bildiradi, shuning uchun Rust bizni foydalanilmagan o'zgaruvchi haqida ogohlantirmaydi.
Keling, o'yin qoidalarini shunday o'zgartiraylik: agar 3 yoki 7 dan boshqa narda toshi paydo bo'lsa, siz yana boshqatdan aylantirib tashlashingiz kerak. Biz endi catch-all qiymatidan foydalanishimiz shart emas, shuning uchun biz kodimizni boshqa
deb nomlangan o‘zgaruvchi o‘rniga _
ishlatish uchun o‘zgartirishimiz mumkin:
fn main() { let narda_toshi = 9; match narda_toshi { 3 => chiroyli_shlyapa_qoshish(), 7 => chiroyli_shlyapani_ochirish(), _ => qaytadan(), } fn chiroyli_shlyapa_qoshish() {} fn chiroyli_shlyapani_ochirish() {} fn qaytadan() {} }
Bu misol, shuningdek, to'liqlik talabiga javob beradi, chunki biz oxirgi qismdagi barcha boshqa qiymatlarni e'tiborsiz qoldiramiz; biz hech narsani unutmadik.
Nihoyat, biz o'yin qoidalarini yana bir bor o'zgartiramiz, shunda siz 3 yoki 7 ni o'tkazmaguningizcha sizning navbatingizda hech narsa sodir bo'lmaydi. Biz buni birlik qiymatidan (biz "Tuple turi" section da aytib o'tgan bo'sh tuple turi) _
armi bilan birga keladigan kod sifatida ifodalashimiz mumkin:
fn main() { let narda_toshi = 9; match narda_toshi { 3 => chiroyli_shlyapa_qoshish(), 7 => chiroyli_shlyapani_ochirish(), _ => (), } fn chiroyli_shlyapa_qoshish() {} fn chiroyli_shlyapani_ochirish() {} }
Bu yerda biz Rustga aniq aytamizki, biz avvalgi armdagi patternga mos kelmaydigan boshqa qiymatdan foydalanmaymiz va bu holda hech qanday kodni ishga tushirishni xohlamaymiz.
18-bobda biz ko'rib chiqadigan patternlar va match haqida ko'proq ma'lumot bor.
Hozircha biz if let
sintaksisiga o‘tamiz, bu match
ifodasi juda batafsil bo'lgan holatlarda foydali bo'lishi mumkin.
if let
bilan Control Flow
if let
sintaksisi sizga if
va let
ni birlashtirib, qolganlarini e'tiborsiz qoldirib, bitta patternga mos keladigan qiymatlarni boshqarishning kamroq batafsil metodiga imkon beradi. 6-6 ro'yxatdagi dasturni ko'rib chiqaylik, u max_sozlama
o'zgaruvchisidagi Variant<u8>
qiymatiga mos keladigan, lekin Some
varianti boʻlsagina kodni bajarishni xohlaydigan dasturni koʻrib chiqamiz.
fn main() { let max_sozlama = Some(3u8); match max_sozlama { Some(max) => println!("Maksimal {} qilib sozlangan", max), _ => (), } }
Agar qiymat Some
bo'lsa, biz qiymatni patterndagi max
o'zgaruvchisiga bog'lash orqali Some
variantidagi qiymatni chop qilamiz. Biz None
qiymati bilan hech narsa qilishni xohlamaymiz. match
ifodasini qondirish uchun faqat bitta variantni qayta ishlagandan so‘ng _ => ()
qo‘shishimiz kerak, bu esa qo‘shish uchun zerikarli boilerplate koddir.
Buning o'rniga, biz buni qisqaroq qilib if let
yordamida yozishimiz mumkin. Quyidagi kod 6-6 ro'yxatdagi match
bilan bir xil ishlaydi:
fn main() { let max_sozlama = Some(3u8); if let Some(max) = max_sozlama { println!("Maksimal {} qilib sozlangan", max); } }
if let
sintaksisi teng belgisi bilan ajratilgan pattern va ifodani oladi. U xuddi match
bilan ishlaydi, bunda ifoda match
ga beriladi va pattern uning birinchi armi hisoblanadi. Bunday holda, pattern Some(max)
bo'lib, max
Some
ichidagi qiymatga bog'lanadi. Shundan so'ng biz if let
blokining tanasida max
dan xuddi mos keladigan match
armida max
dan foydalanganimiz kabi foydalanishimiz mumkin. Qiymat patternga mos kelmasa, if let
blokidagi kod ishga tushmaydi.
if let
dan foydalanish kamroq yozish, kamroq chekinish va kamroq kodli kodni bildiradi.
Biroq, siz match
amal qiladigan to'liq tekshirishni yo'qotasiz. match
va if let
o‘rtasida tanlov qilish sizning muayyan vaziyatingizda nima qilayotganingizga va ixchamlikka ega bo‘lish to‘liq tekshirishni yo‘qotish uchun to‘g‘ri kelishilganligiga bog‘liq.
Boshqacha qilib aytganda, siz if let
konstruktsiyasini match
uchun sintaktik shakar sifatida o'ylab ko'rishingiz mumkin, agar kiritilgan qiymat bitta patterga mos kelsa va boshqa barcha qiymatlarga e'tibor bermasa, kodni bajaradi.
Biz else
ni if let
bilan kiritishimiz mumkin. else
bilan birlashtirilgan kod bloki if let
va else
ga ekvivalent bo‘lgan match
ifodasidagi _
registriga mos keladigan kod bloki bilan bir xil. 6-4 roʻyxatdagi Tanga
definitionni eslang, bunda Quarter
varianti ham UsState
qiymatiga ega edi. Agar biz quarterlarning holatini e'lon qilishda ko'rgan barcha quarter bo'lmagan tangalarni sanashni istasak, buni quyidagi kabi match
ifodasi bilan qilishimiz mumkin:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Tanga { Penny, Nickel, Dime, Quarter(UsState), } fn main() { let tanga = Tanga::Penny; let mut hisobchi = 0; match tanga { Tanga::Quarter(shtat) => println!("{:?} dan shtat kvartal!", shtat), _ => hisobchi += 1, } }
Yoki if let
va else
ifodalaridan foydalanishimiz mumkin, masalan:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Tanga { Penny, Nickel, Dime, Quarter(UsState), } fn main() { let tanga = Tanga::Penny; let mut hisobchi = 0; if let Tanga::Quarter(shtat) = tanga { println!("{:?} dan shtat kvartal!", shtat); } else { hisobchi += 1; } }
Agar dasturingizda match
yordamida ifodalash uchun juda batafsil mantiq mavjud bo'lsa, Rust toolboxda if let
ham mavjudligini unutmang.
Xulosa
Endi biz sanab o'tilgan qiymatlar to'plamidan biri bo'lishi mumkin bo'lgan maxsus turlarni yaratish uchun enumlardan qanday foydalanishni ko'rib chiqdik. Biz standart kutubxonaning Option<T>
turi xatolarni oldini olish uchun type tizimidan qanday foydalanishni ko'rsatdik. Enum qiymatlari ichida ma'lumotlar mavjud bo'lsa, siz qancha holatlarni ko'rib chiqishingiz kerakligiga qarab, ushbu qiymatlarni ajratib olish va ishlatish uchun match
yoki if let
dan foydalanishingiz mumkin.
Rust dasturlaringiz endi structlar va enumlar yordamida domeningizdagi tushunchalarni ifodalashi mumkin. API-da foydalanish uchun maxsus turlarni yaratish turdagi xavfsizligini ta'minlaydi: kompilyator sizning funksiyalaringiz faqat har bir funktsiya kutgan turdagi qiymatlarni olishiga ishonch hosil qiladi.
Foydalanuvchilaringizga foydalanish uchun qulay va faqat sizning foydalanuvchilarga nima kerakligini aniq ko'rsatadigan yaxshi tashkil etilgan API taqdim etish uchun endi Rust modullariga murojaat qilaylik.
O'sib borayotgan loyihalarni paketlar, cratelar va modullar bilan boshqarish
Katta dasturlarni yozganingizda, kodingizni tartibga solish tobora muhim ahamiyat kasb etadi. Tegishli funksiyalarni guruhlash va kodni alohida xususiyatlar bilan ajratish orqali siz ma'lum bir xususiyatni amalga oshiradigan kodni qayerdan topish va funksiya qanday ishlashini o'zgartirish uchun qayerga borish kerakligini aniqlaysiz.
Biz hozirgacha yozgan dasturlar bitta faylda bitta modulda bo'lgan. Loyiha o'sib borishi bilan siz kodni bir nechta modullarga va keyin bir nechta fayllarga bo'lish orqali tartibga solishingiz kerak. Paketda bir nechta binary cratelar va ixtiyoriy ravishda bitta kutubxona cratesi bo'lishi mumkin. Paket o'sishi bilan siz qismlarni alohida cratelarga ajratib olishingiz mumkin, ular tashqi bog'liqlikka aylanadi. Ushbu bo'lim ushbu texnikaning barchasini o'z ichiga oladi. Birgalikda rivojlanadigan o'zaro bog'liq paketlar to'plamini o'z ichiga olgan juda katta loyihalar uchun Cargo workspacelarni taqdim etadi, biz ularni 14-bobdagi "Cargo Workspacelari" bo'limida ko'rib chiqamiz.
Shuningdek, biz kodni yuqoriroq darajada qayta ishlatish imkonini beruvchi implement tafsilotlarini muhokama qilamiz: operatsiyani amalga oshirganingizdan so'ng, boshqa kod dastur qanday ishlashini bilmasdan kodingizni umumiy interfeysi orqali chaqirishi mumkin. Kodni yozish usuli qaysi qismlar boshqa koddan foydalanishi uchun public ekanligini va qaysi qismlar o'zgartirish huquqiga ega bo'lgan private implement tafsilotlarini belgilaydi. Bu yodda tutish kerak bo'lgan tafsilotlar miqdorini cheklashning yana bir usuli.
Tegishli tushuncha - bu scope: kod yoziladigan ichki kontekstda "scope" deb belgilangan nomlar to'plami mavjud. Kodni o'qish, yozish va kompilyatsiya qilishda dasturchilar va kompilyatorlar ma'lum bir nuqtadagi ma'lum bir nom o'zgaruvchiga, funksiyaga, structga, enumga, modulga, constantaga yoki boshqa elementga tegishli ekanligini va bu element nimani anglatishini bilishlari kerak. Siz scopelarni yaratishingiz va qaysi nomlar doirasida yoki tashqarida ekanligini o'zgartirishingiz mumkin. Bir xil nomdagi ikkita elementni bir xil scopeda bo'lishi mumkin emas; nom ziddiyatlarini hal qilish uchun tollar mavjud.
Rust sizga kodingizning tuzilishini boshqarish imkonini beruvchi bir qator xususiyatlarga ega, jumladan, qaysi tafsilotlar oshkor bo'lishi, qaysi tafsilotlar shaxsiy va dasturlaringizdagi har bir sohada qanday nomlar mavjudligi. Ba'zan birgalikda modul tizimi deb ataladigan bu xususiyatlar quyidagilarni o'z ichiga oladi:
- Paketlar: Cratelarni qurish, sinab ko'rish va almashish imkonini beruvchi cargo xususiyati
- Cratelar: Kutubxona yoki bajariladigan faylni yaratuvchi modullar daraxti
- Modullar va use: Fayl yoʻllarning tashkil etilishi, koʻlami va maxfiyligini nazorat qilish imkonini beradi
- Pathlar: Struct, funksiya yoki modul kabi elementni nomlash usuli
Ushbu bobda biz ushbu xususiyatlarning barchasini ko'rib chiqamiz, ularning qanday o'zaro ta'sirini muhokama qilamiz va qamrovni boshqarish uchun ulardan qanday foydalanishni tushuntiramiz. Oxir-oqibat, siz modul tizimi haqida yaxshi tushunchaga ega bo'lishingiz va professional kabi sohalar bilan ishlashingiz kerak!
Paketlar va Cratelar
Biz qamrab oladigan modul tizimining birinchi qismlari paketlar va cratelardir.
Crate - bu Rust kompilyatori bir vaqtning o'zida ko'rib chiqadigan eng kichik kod miqdori. Agar siz cargo
o'rniga rustc
ni ishga tushirsangiz va bitta manba faylga o'tsangiz ham (biz 1-bob, "Rust dasturini yozish va ishga tushirish"da qilganimiz kabi), kompilyator bu faylni crate sifatida ko'radi. Cratelar modullarni o'z ichiga olishi mumkin va modullar crate bilan tuzilgan boshqa fayllarda aniqlanishi mumkin, buni keyingi bo'limlarda ko'rib chiqamiz.
Crate ikkita shakldan birida bo'lishi mumkin: binary crate yoki kutubxona cratesi.
Binary cratelar - bu buyruq qatori dasturi yoki server kabi ishga tushirishingiz mumkin bo'lgan bajariladigan faylga kompilyatsiya qilishingiz mumkin bo'lgan dasturlar. Har birida bajariladigan fayl ishga tushganda nima sodir bo'lishini belgilaydigan main
funksiyasi bo'lishi kerak. Biz hozirgacha yaratgan barcha cratelar binary cratelar edi.
Kutubxona cratelari main
funksiyaga ega emas va ular bajariladigan faylga kompilyatsiya qilinmaydi. Buning o'rniga, ular bir nechta loyihalar bilan bo'lishish uchun mo'ljallangan funksionallikni belgilaydi. Misol uchun, biz 2-bobda ishlatgan rand
cratesi tasodifiy sonlarni yaratuvchi funksionallikni taʼminlaydi.
Ko'pincha Rustaceanlar “crate” deganda, ular kutubxona crateni anglatadi va ular "kutubxona" ning umumiy dasturlash tushunchasi bilan "crate" dan foydalanadilar.
Crate root bu Rust kompilyatori cratengizning ildiz modulini yaratishni boshlaydigan manba fayldir (biz modullarni “Qo'llanish doirasi va maxfiylikni nazorat qilish uchun modullarni aniqlash” bo‘limida chuqur tushuntiramiz).
Paket - bu bir yoki bir nechta cratelar to'plami bo'lib, u funksiyalar to'plamini ta'minlaydi. Paketda ushbu cratelarni qanday build qilishni tavsiflovchi Cargo.toml fayli mavjud. Cargo - bu sizning kodingizni yaratishda foydalanayotgan buyruq qatori vositasi uchun binary crateni o'z ichiga olgan paket. Cargo paketida binary crate bog'liq bo'lgan kutubxona cratesi ham mavjud. Boshqa loyihalar Cargo buyruq qatori vositasi ishlatadigan mantiqdan foydalanish uchun cargo kutubxonasi cratesiga bog'liq bo'lishi mumkin.
Paket siz xohlagancha ko'p ikkilik binary cratelarni o'z ichiga olishi mumkin, lekin bitta kutubxona cratesidan ko'p bo'lmasligi kerak. Paketda kamida bitta crate bo'lishi kerak, u kutubxona yoki binary crate bo'lishi kerak.
Keling, paketni yaratganimizda nima sodir bo'lishini ko'rib chiqaylik. Birinchidan, biz cargo new
buyrug'ini kiritamiz:
$ cargo new mening-paketim
Created binary (application) `mening-paketim` package
$ ls mening-paketim
Cargo.toml
src
$ ls mening-paketim/src
main.rs
Biz cargo new
ni ishga tushirgandan so'ng, biz cargo nima yaratganini ko'rish uchun ls
dan foydalanamiz. mening-paketim
jildida bizga paketni taqdim qiluvchi Cargo.toml fayli mavjud. Shuningdek, main.rs ni o'z ichiga olgan src jildi ham mavjud. Matn muharririda Cargo.toml ni oching va src/main.rs haqida hech qanday eslatma yo‘qligiga e'tibor bering. Cargo, src/main.rs paket bilan bir xil nomga ega binary cratening crate ildizi ekanligi haqidagi konventsiyaga amal qiladi. Xuddi shunday, Cargo ham biladiki, agar paket jildida src/lib.rs bo'lsa, paketda paket bilan bir xil nomdagi kutubxona cratesi mavjud va src/lib.rs uning crate ildizi hisoblanadi. Cargo kutubxona yoki binaryni yaratish uchun cratening ildiz fayllarini rustc
ga o'tkazadi.
Bu yerda bizda faqat src/main.rs ni o'z ichiga olgan paket bor, ya'ni u faqat mening-paketim
nomli binary crateni o'z ichiga oladi. Agar paketda src/main.rs
va src/lib.rs
bo'lsa, unda ikkita crate, binary crate va kutubxona cratesi mavjud bo'lib, ikkalasi ham paket bilan bir xil nomga ega. Paketda fayllarni src/bin jildiga joylashtirish orqali bir nechta binary cratelar bo'lishi mumkin: har bir fayl alohida binary crate bo'ladi.
Qo'llanish doirasi va maxfiylikni nazorat qilish uchun modullarni aniqlash
Ushbu bo'limda biz modullar va modul tizimining boshqa qismlari haqida gapiramiz, ya'ni elementlarni nomlash imkonini beruvchi pathlar(yo'llar); pathni qamrab oluvchi use
kalit so'zi; va obyektlarni hammaga ochiq qilish(public) uchun pub
kalit so'zi. Shuningdek, biz as
kalit so'zini, tashqi paketlarni va glob operatorini muhokama qilamiz.
Birinchidan, kelajakda kodingizni tartibga solishda qulay foydalanish uchun qoidalar ro'yxatidan boshlaymiz. Keyin har bir qoidalarni batafsil tushuntiramiz.
Modullar qo'llanmasi
Bu yerda biz modullar, pathlar, use
kalit soʻzi va pub
kalit soʻzining kompilyatorda qanday ishlashi va koʻpchilik ishlab chiquvchilar oʻz kodlarini qanday tashkil qilishlari haqida qisqacha maʼlumot beramiz. Ushbu bobda biz ushbu qoidalarning har biriga misollarni ko'rib chiqamiz va modullar qanday ishlashini takrorlash uchun yaxshi vaqt.
work.
- Cratening ildizidan boshlang: Crateni kompilyatsiya qilishda kompilyator kodni kompilyatsiya qilish uchun avval cratening ildiz fayliga (odatda kutubxona cratesi uchun src/lib.rs yoki binary crate uchun src/main.rs) qaraydi.
- Modullarni e'lon qilish: Cratening ildiz faylida siz yangi modullarni e'lon qilishingiz mumkin; aytaylik, siz
mod poliz
bilanpoliz
modulini e'lon qilasiz; Kompilyator modul kodini quyidagi joylarda qidiradi:- Inline, jingalak qavs ichida
mod poliz
dan keyingi nuqta-vergul o'rnini egallaydi - src/poliz.rs faylida
- src/poliz/mod.rs faylida
- Inline, jingalak qavs ichida
- Submodullarni e'lon qilish: Crate ildizidan boshqa har qanday faylda siz submodullarni e'lon qilishingiz mumkin. Masalan, src/poliz.rs da
mod sabzavotlar;
deb e`lon qilishingiz mumkin. Kompilyator quyi modul kodini quyidagi joylarda ota-modul uchun nomlangan jilddan qidiradi:- Inline, to'g'ridan-to'g'ri
mod sabzavotlar
dan keyin, nuqta-vergul o'rniga jingalak qavslar ichida - src/poliz/sabzavotlar.rs faylida
- src/poliz/sabzavotlar/mod.rs faylida
- Inline, to'g'ridan-to'g'ri
- Modullarda kodlash yo'llari: Modul sizning cratengizning bir qismi bo'lgandan so'ng, maxfiylik qoidalari ruxsat bergan bo'lsa, kod yo'lidan foydalanib, xuddi shu cratening istalgan joyidan ushbu moduldagi kodga murojaat qilishingiz mumkin. Misol uchun, poliz sabzavotlari modulidagi
Pomidor
turiCrate::poliz::sabzavotlar::Pomidor
da topiladi. - Private va Public: Modul ichidagi kod standart bo'yicha uning ota-modullaridan maxfiydir. Modulni public qilish uchun uni
mod
o'rnigapub mod
bilan e’lon qiling. Public moduldagi elementlarni ham hammaga ochiq qilish uchun ularni e'lon qilishdan oldinpub
dan foydalaning. use
kalit so'zi: Bir doiradause
kalit so'zidan foydalanish uzoq yo'llarning takrorlanishini kamaytirish uchun elementlar uchun taxalluslarni yaratadi.Crate::poliz::sabzavotlar::Pomidor
ga murojaat qilishi mumkin bo'lgan har qanday sohada sizuse crate::poliz::sabzavotlar::Pomidor;
bilan taxallus yaratishingiz mumkin va shundan so'ng siz ushbu turdagi ushbu doirada foydalanish uchunPomidor
deb yozishingiz kerak.
Bu erda biz ushbu qoidalarni aks ettiruvchi orqa_hovli
nomli binary crate yaratamiz. Crate jildi, shuningdek, orqa_hovli
deb nomlangan, quyidagi fayllar va jildlarni o'z ichiga oladi:
orqa_hovli
├── Cargo.lock
├── Cargo.toml
└── src
├── poliz
│ └── sabzavotlar.rs
├── poliz.rs
└── main.rs
Bu holda cratening ildiz fayli src/main.rs bo'lib, u quyidagilarni o'z ichiga oladi:
Fayl nomi: src/main.rs
use crate::poliz::sabzavotlar::Pomidor;
pub mod poliz;
fn main() {
let ekin = Pomidor {};
println!("Men {:?} o'stiryapman!", ekin);
}
pub mod poliz;
qatori kompilyatorga src/poliz.rs da topilgan kodni kiritishni aytadi, ya'ni:
Fayl nomi: src/poliz.rs
pub mod sabzavotlar;
Bu yerda pub mod sabzavotlar;
src/poliz/sabzavotlar.rs dagi kod ham kiritilganligini bildiradi. That code is:
#[derive(Debug)]
pub struct Pomidor {}
Keling, ushbu qoidalarning tafsilotlari bilan tanishamiz va ularni amalda ko'rsatamiz!
Modullarda tegishli kodlarni guruhlash
Modullar kodni o'qish va qayta foydalanishni osonlashtirish uchun crate ichida tartibga solish imkonini beradi. Modullar bizga elementlarning maxfiyligini boshqarishga ham imkon beradi, chunki modul ichidagi kod standart boʻyicha shaxsiy(private) hisoblanadi. Private elementlar tashqi foydalanish uchun mavjud bo'lmagan ichki dastur tafsilotlari. Biz modullar va ulardagi elementlarni hammaga ochiq qilishni tanlashimiz mumkin, bu esa ularni tashqi koddan foydalanish va ularga bog'liq bo'lishiga imkon beradi.
Misol tariqasida, restoranning funksionalligini ta'minlaydigan kutubxona cratesini yozamiz. Biz funksiyalarning signaturelarini aniqlaymiz, lekin restoranni implement qilishga emas, balki kodni tashkil etishga e'tibor qaratish uchun ularning tanasini bo'sh qoldiramiz.
Restoran sanoatida restoranning ba'zi qismlari uyning old tomoni va boshqalari uyning orqa tomoni deb ataladi. Uyning old tomoni mijozlar joylashgan joy; Bu mezbonlar mijozlarni joylashtiradigan, serverlar buyurtma va to'lovlarni qabul qiladigan va barmenlar ichimliklar tayyorlaydigan joyni o'z ichiga oladi. Uyning orqa tomonida oshpazlar va oshpazlar oshxonada ishlaydi, idishlarni yuvish mashinalari tozalaydi va menejerlar ma'muriy ishlarni bajaradilar.
Cratemizni shu tarzda tuzish uchun biz uning funksiyalarini ichki modullarga ajratishimiz mumkin. cargo new restoran --lib
ishga tushirish orqali restoran
nomli yangi kutubxona yarating; keyin ba'zi modullar va funksiya signaturelarini aniqlash uchun 7-1 ro'yxatidagi kodni src/lib.rs ichiga kiriting. Mana, uyning old qismi:
Fayl nomi: src/lib.rs
mod uyning_oldi {
mod xizmat {
fn navbat_listiga_qoshish() {}
fn stolga_otirish() {}
}
mod serving {
fn buyurtma_olish() {}
fn buyurtma_berish() {}
fn tolovni_qabul_qilish() {}
}
}
Biz modulni mod
kalit so'zidan keyin modul nomi bilan belgilaymiz (bu holda uyning_oldi
). Keyin modul tanasi jingalak qavslar ichiga kiradi. Modullar ichida biz boshqa modullarni joylashtirishimiz mumkin, masalan, xizmat
va xizmat_korsatish
modullari. Modullar, shuningdek, structlar, enumlar, konstantalar, belgilar va 7-1 ro'yxatdagi kabi boshqa elementlar uchun ta'riflarga ega bo'lishi mumkin.
Modullardan foydalanib, biz bir-biriga bog'liq definitionlarni guruhlashimiz va ular nima uchun bog'liqligini nomlashimiz mumkin. Ushbu koddan foydalanadigan dasturchilar barcha definitionlarni o'qib chiqmasdan, guruhlarga asoslangan kodni boshqarishi mumkin, bu ularga tegishli definitionlarni topishni osonlashtiradi. Ushbu kodga yangi funksiya qo'shadigan dasturchilar dasturni tartibli saqlash uchun kodni qayerga joylashtirishni bilishadi.
Yuqorida aytib o'tganimizdek, src/main.rs va src/lib.rs fayllari crate ildiz modullari deb ataladi. Ularning nomlanishining sababi shundaki, bu ikki faylning birortasining mazmuni modul daraxti deb nomlanuvchi crate modul strukturasining ildizida joylashgan crate
nomli modulni tashkil qiladi.
7-2 ro'yxatda 7-1 ro'yxatdagi strukturaning modul daraxti ko'rsatilgan.
crate
└── uyning_oldi
├── xizmat
│ ├── navbat_listiga_qoshish
│ └── stolga_otirish
└── xizmat_korsatish
├── buyurtma_olish
├── buyurtma_berish
└── tolov_qilish
Bu daraxt ba'zi modullar bir-birining ichida qanday joylashishini ko'rsatadi; masalan, xizmat
uyasi uyning_oldi
ichida. Daraxt shuningdek, ba'zi modullar bir-birining aka-uka ekanligini, ya'ni ular bir modulda aniqlanganligini ko'rsatadi; xizmat
va xizmat_korsatish
uyning_oldi
ichida belgilangan aka-ukalardir. Agar A moduli B modulida joylashgan bo'lsa, biz A moduli B modulining bolasi va B moduli A modulining otasi deb aytamiz. E'tibor bering, butun modul daraxti Crate
nomli yashirin modul ostida joylashgan.
Modul daraxti sizga kompyuteringizdagi fayl tizimining jildlar daraxtini eslatishi mumkin; bu juda to'g'ri taqqoslash! Fayl tizimidagi jildlar singari, siz kodingizni tartibga solish uchun modullardan foydalanasiz. Va xuddi jilddagi fayllar singari, bizga modullarimizni topish usuli kerak.
Modul daraxtidagi elementga murojaat qilish yo'llari
Rust-ga modul daraxtidagi elementni qayerdan topish mumkinligini ko'rsatish uchun biz fayl tizimida harakat qilishda qanday yo'l(path) ishlatgan bo'lsak, xuddi shunday yo'ldan foydalanamiz. Funksiyani chaqirish uchun biz uning yo'lini bilishimiz kerak.
Yo'l ikki shaklda bo'lishi mumkin:
- Absolyut yo'l - bu crate ildizidan boshlanadigan to'liq yo'l; tashqi cretedagi kod uchun mutlaq yo'l crate nomidan boshlanadi va joriy cratedagi kod uchun esa
crate
bilan boshlanadi.. - N isbiy yo‘l joriy moduldan boshlanadi va joriy modulda
self
,super
yoki identifikatordan foydalanadi.
Mutlaq va nisbiy yo‘llardan keyin ikki nuqta (::
) bilan ajratilgan bir yoki bir nechta identifikatorlar keladi.
7-1 ro'yxatiga qaytsak, biz navbat_listiga_qoshish
funksiyasini chaqirmoqchimiz deylik.
Bu so'rash bilan bir xil: navbat_listiga_qoshish
funksiyasining yo'li nima?
7-3 ro'yxatda 7-1 ro'yxati mavjud bo'lib, ba'zi modullar va funksiyalar olib tashlangan.
Biz crate ildizida belgilangan yangi restoranda_ovqatlanish
funksiyasidan navbat_listiga_qoshish
funksiyasini chaqirishning ikkita usulini ko‘rsatamiz. Bu yoʻllar toʻgʻri, ammo bu misolni avvalgidek tuzishga toʻsqinlik qiladigan yana bir muammo bor. Sababini birozdan keyin tushuntiramiz.
restoranda_ovqatlanish
funksiyasi kutubxonamizning public API-ning bir qismidir, shuning uchun biz uni pub
kalit so'zi bilan belgilaymiz. “pub
kalit so'zi bilan yo'llarni ochish” bo‘limida biz pub
haqida batafsilroq to‘xtalib o'tamiz.
Fayl nomi: src/lib.rs
mod uyning_oldi {
mod xizmat {
fn navbat_listiga_qoshish() {}
}
}
pub fn restoranda_ovqatlanish() {
// Mutlaq yo'l (Absolute path)
crate::uyning_oldi::xizmat::navbat_listiga_qoshish();
// Nisbiy yo'l (Relative path)
uyning_oldi::xizmat::navbat_listiga_qoshish();
}
Biz birinchi marta restoranda_ovqatlanish
ichida navbat_listiga_qoshish
funksiyasini chaqirganimizda mutlaq yo'ldan foydalanamiz. navbat_listiga_qoshish
funksiyasi restoranda_ovqatlanish
bilan bir xil crateda belgilangan, ya'ni mutlaq yoʻlni boshlash uchun crate
kalit soʻzidan foydalanishimiz mumkin. Keyin biz navbat_listiga_qoshish
ga o'tgunimizcha ketma-ket modullarning har birini o'z ichiga olamiz. Siz bir xil strukturaga ega fayl tizimini tasavvur qilishingiz mumkin: biz navbat_listiga_qoshish
dasturini ishga tushirish uchun /uyning_oldi/xizmat/navbat_listiga_qoshish
yo'lini belgilaymiz; crate ildizidan boshlash uchun crate
nomidan foydalanish shelldagi fayl tizimi ildizidan boshlash uchun /
dan foydalanishga o'xshaydi.
Biz restoranda_ovqatlanish
ichida navbat_listiga_qoshish
ni ikkinchi marta chaqirganimizda nisbiy yo'ldan foydalanamiz. Yo'l uyning_oldi
bilan boshlanadi, modul nomi restoranda_ovqatlanish
bilan bir xil modul daraxti darajasida belgilangan. Bu yerda fayl tizimi ekvivalenti uyning_oldi/xizmat/navbat_listiga_qoshish
yo'lidan foydalaniladi. Modul nomi bilan boshlash yo'l nisbiy ekanligini bildiradi.
Nisbiy yoki mutlaq yo‘ldan foydalanishni tanlash loyihangiz asosida qabul qilinadigan qaror bo‘lib, element definitioni kodini elementdan foydalanadigan koddan alohida yoki birga ko‘chirish ehtimoli ko‘proq ekanligiga bog‘liq.
Masalan, uyning_oldi
moduli va restoranda_ovqatlanish
funksiyasini mijoz_tajribasi
nomli modulga o‘tkazsak, mutlaq yo‘lni navbat_listiga_qoshish
ga yangilashimiz kerak bo‘ladi, lekin nisbiy yo‘l baribir amal qiladi.
Biroq, agar biz restoranda_ovqatlanish
funksiyasini ovqatlanish
nomli modulga alohida ko'chirsak, restoranda_ovqatlanish
chaqiruvining mutlaq yo'li bir xil bo'lib qoladi, lekin nisbiy yo'l yangilanishi kerak bo'ladi. Umuman olganda, bizning afzal ko'rganimiz mutlaq yo'llarni belgilashdir, chunki biz kod definitionlari va element chaqiruvlarini bir-biridan mustaqil ravishda ko'chirishni xohlaymiz.
Keling, 7-3 ro'yxatini kompilatsiya qilishga harakat qilaylik va nima uchun u hali kompilatsiya bo'lmaganligini bilib olaylik! Biz olgan xato 7-4 ro'yxatda ko'rsatilgan.
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `xizmat` is private
--> src/lib.rs:9:28
|
9 | crate::uyning_oldi::xizmat::navbat_listiga_qoshish();
| ^^^^^^^ private module
|
note: the module `xizmat` is defined here
--> src/lib.rs:2:5
|
2 | mod xizmat {
| ^^^^^^^^^^^
error[E0603]: module `xizmat` is private
--> src/lib.rs:12:21
|
12 | uyning_oldi::xizmat::navbat_listiga_qoshish();
| ^^^^^^^ private module
|
note: the module `xizmat` is defined here
--> src/lib.rs:2:5
|
2 | mod xizmat {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors
Xato xabarlari xizmat
moduli private ekanligini aytadi. Boshqacha qilib aytadigan bo'lsak, bizda xizmat
moduli va navbat_listiga_qoshish
funksiyasi uchun to'g'ri yo'llar mavjud, ammo Rust ulardan foydalanishimizga ruxsat bermaydi, chunki u private bo'limlarga kirish imkoniga ega emas. Rust-da barcha elementlar (funktsiyalar, metodlar, structlar, enumlar, modullar va konstantalar) standart bo'yicha ota-modullar uchun privatedir. Agar siz funksiya yoki struktura kabi elementni yaratmoqchi bo'lsangiz, uni modulga joylashtirasiz.
Ota-moduldagi elementlar ichki modullar ichidagi private elementlardan foydalana olmaydi, lekin bolalar modullaridagi elementlar o'zlarining ota-modullaridagi elementlardan foydalanishi mumkin. Buning sababi shundaki, bolalar modullari o'zlarining amalga oshirish tafsilotlarini o'rab oladi va yashiradi, lekin bolalar modullari ular aniqlangan kontekstni ko'rishlari mumkin. Bizning metaforamizni davom ettirish uchun, maxfiylik qoidalarini restoranning orqa ofisi kabi tasavvur qiling: u erda nima sodir bo'layotgani restoran mijozlari uchun private, ammo ofis menejerlari o'zlari ishlayotgan restoranda hamma narsani ko'rishlari va qilishlari mumkin.
Rust modul tizimining shu tarzda ishlashini tanladi, shuning uchun ichki dastur tafsilotlarini yashirish standart bo'yichadir. Shunday qilib, siz ichki kodning qaysi qismlarini tashqi kodni buzmasdan o'zgartirishingiz mumkinligini bilasiz. Biroq, Rust sizga obyektni hammaga ochiq qilish uchun pub
kalit so'zidan foydalanib, tashqi ajdod modullariga ichki modullar kodining ichki qismlarini ochish imkoniyatini beradi.
pub
kalit so'zi bilan yo'llarni ochish
Keling, 7-4 ro'yxatdagi xatoga qaytaylik, bu bizga xizmat
moduli private ekanligini aytdi. Biz ota-moduldagi restoranda_ovqatlanish
funksiyasi bolalar modulidagi navbat_listiga_qoshish
funksiyasiga kirishini xohlaymiz, shuning uchun biz xizmat
modulini pub
kalit so'zi bilan belgilaymiz, ro'yxat 7-5da ko`rsatilganidek.
Fayl nomi: src/lib.rs
mod uyning_oldi {
pub mod xizmat {
fn navbat_listiga_qoshish() {}
}
}
pub fn restoranda_ovqatlanish() {
// Mutlaq yo'l (Absolute path)
crate::uyning_oldi::xizmat::navbat_listiga_qoshish();
// Nisbiy yo'l (Relative path)
uyning_oldi::xizmat::navbat_listiga_qoshish();
}
Afsuski, 7-5 ro'yxatdagi kod hali ham 7-6 ro'yxatda ko'rsatilganidek xatolikka olib keladi.
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `navbat_listiga_qoshish` is private
--> src/lib.rs:9:37
|
9 | crate::uyning_oldi::xizmat::navbat_listiga_qoshish();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `navbat_listiga_qoshish` is defined here
--> src/lib.rs:3:9
|
3 | fn navbat_listiga_qoshish() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: function `navbat_listiga_qoshish` is private
--> src/lib.rs:12:30
|
12 | uyning_oldi::xizmat::navbat_listiga_qoshish();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `navbat_listiga_qoshish` is defined here
--> src/lib.rs:3:9
|
3 | fn navbat_listiga_qoshish() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors
Nima bo'ldi? mod xizmat
oldiga pub
kalit so‘zini qo‘shish modulni hammaga ochiq qiladi. Ushbu o'zgarish bilan, agar biz uyning_oldi
ga kira olsak, biz xizmat
ga kira olamiz. Lekin xizmat
ning tarkibi hamon private; modulni public qilish uning mazmunini ochiq qilmaydi. Moduldagi pub
kalit so‘zi faqat uning ota-modullaridagi kodni unga murojaat qilish imkonini beradi, uning ichki kodiga kirishga ruxsat bermaydi.
Modullar konteyner bo'lgani uchun modulni faqat public qilish orqali biz ko'p narsa qila olmaymiz; biz oldinga borishimiz va modul ichidagi bir yoki bir nechta narsalarni ham hammaga ochiq qilishni tanlashimiz kerak.
7-6 roʻyxatdagi xatolar navbat_listiga_qoshish
funksiyasi private ekanligini bildiradi.
Maxfiylik qoidalari structlar, enumlar, funksiyalar va metodlar hamda modullarga nisbatan qo'llaniladi.
7-7 ro'yxatda ko'rsatilganidek, definitiondan oldin pub
kalit so'zini qo'shish orqali navbat_listiga_qoshish
funksiyasini ham hammaga ochiq qilaylik.
Fayl nomi: src/lib.rs
mod uyning_oldi {
pub mod xizmat {
pub fn navbat_listiga_qoshish() {}
}
}
pub fn restoranda_ovqatlanish() {
// Mutlaq yo'l (Absolute path)
crate::uyning_oldi::xizmat::navbat_listiga_qoshish();
// Nisbiy yo'l (Relative path)
uyning_oldi::xizmat::navbat_listiga_qoshish();
}
Endi kod kompilyatsiya qilinadi! Nima uchunpub
kalit soʻzini qoʻshish ushbu yoʻllardan navbat_listiga_qoshish
da maxfiylik qoidalariga nisbatan foydalanish imkonini berishini bilish uchun mutlaq va nisbiy yoʻllarni koʻrib chiqamiz.
Mutlaq yo'lda biz crate modul daraxtining ildizi bo'lgan crate
dan boshlaymiz. uyning_oldi
moduli crate ildizida belgilangan. uyning_oldi
ochiq boʻlmasa-da, restoranda_ovqatlanish
funksiyasi uyning_oldi
bilan bir xil modulda aniqlanganligi sababli (yaʼni, restoranda_ovqatlanish
va uyning_oldi
siblingdir ya'ni aka-uka), biz restoranda_ovqatlanish
dan uyning_oldi
ga murojaat qilishimiz mumkin. Keyingi o'rinda pub
bilan belgilangan xizmat
moduli. Biz xizmat
ning ota-moduliga kira olamiz, shuning uchun biz xizmat
ga kira olamiz. Nihoyat, navbat_listiga_qoshish
funksiyasi pub
bilan belgilangan va biz uning asosiy moduliga kira olamiz, shuning uchun bu funksiya chaqiruvi ishlaydi!
Nisbiy yo'lda mantiq birinchi qadamdan tashqari mutlaq yo'l bilan bir xil bo'ladi: yo'l crate ildizidan emas, uyning_oldi
dan boshlanadi. uyning_oldi
moduli restoranda_ovqatlanish
bilan bir xil modul ichida aniqlanadi, shuning uchun restoranda_ovqatlanish
belgilangan moduldan boshlanadigan nisbiy yo‘l ishlaydi. Keyin, xizmat
va navbat_listiga_qoshish
pub
bilan belgilanganligi sababli, qolgan yo‘l ishlaydi va bu funksiya chaqiruvi amal qiladi!
Agar siz kutubxona crateyingizni boshqa loyihalar sizning kodingizdan foydalanishi uchun baham ko'rishni rejalashtirmoqchi bo'lsangiz, public API sizning crateyingiz foydalanuvchilari bilan tuzilgan shartnoma bo'lib, ular sizning kodingiz bilan qanday aloqada bo'lishini belgilaydi. Odamlar sizning crateyingizga bog'liq bo'lishini osonlashtirish uchun public API-ga o'zgartirishlarni boshqarish bo'yicha ko'plab fikrlar mavjud. Bu mulohazalar ushbu kitob doirasidan tashqarida; agar sizni ushbu mavzu qiziqtirsa, Rust API ko'rsatmalariga qarang.
Binary va kutubxonaga ega paketlar uchun eng yaxshi amaliyotlar
Paketda src/main.rs binary crate ildizi ham, src/lib.rs kutubxona cratesi ildizi ham bo‘lishi mumkinligini aytib o'tdik va ikkala crate ham standart bo‘yicha paket nomiga ega bo‘ladi. Odatda, kutubxona va binary crateni o'z ichiga olgan ushbu patternli paketlar kutubxona cratesi bilan kod chaqiradigan bajariladigan faylni ishga tushirish uchun binary crateda yetarli kodga ega bo'ladi. Bu boshqa loyihalarga paket taqdim etadigan eng ko'p funksiyalardan foydalanish imkonini beradi, chunki kutubxona cratesi kodi public bo'lishi mumkin.
Modul daraxti src/lib.rs da aniqlanishi kerak. Keyin har qanday public obyektlar binary crateda paket nomi bilan yo'llarni boshlash orqali ishlatilishi mumkin. Binary crate kutubxona cratesidan foydalanuvchiga aylanadi, xuddi butunlay tashqi crate kutubxona cratesidan foydalanadi: u faqat pulic APIdan foydalanishi mumkin. Bu sizga yaxshi API yaratishga yordam beradi; Siz nafaqat muallif, balki mijoz hamsiz!
12-bobda biz ushbu tashkiliy amaliyotni binary crate va kutubxona cratesini o'z ichiga olgan buyruq qatori dasturi bilan ko'rsatamiz.
Nisbiy yo'llarni super
bilan boshlash
Yo'l boshida super
dan foydalanib, joriy modul yoki crate ildizi emas, balki ota-modulda boshlanadigan nisbiy yo'llarni qurishimiz mumkin. Bu fayl tizimi yoʻlini ..
sintaksisi bilan boshlashga oʻxshaydi. super
dan foydalanish bizga ota-modulda ekanligini biladigan elementga murojaat qilish imkonini beradi, bu modul ota-ona bilan chambarchas bog'liq bo'lsa, modul daraxtini qayta tartibga solishni osonlashtiradi, lekin ota-ona bir kun kelib modul daraxtining boshqa joyiga ko'chirilishi mumkin.
7-8 ro'yxatdagi kodni ko'rib chiqing, unda oshpaz noto'g'ri buyurtmani tuzatgan va uni mijozga shaxsan yetkazgan vaziyatni modellashtiradi. uyning_orqasi
modulida aniqlangan buyurtmani_tuzatish
funksiyasi super
bilan boshlanadigan yetkazib_berish
yo‘lini belgilash orqali asosiy modulda belgilangan yetkazib_berish
funksiyasini chaqiradi:
Fayl nomi: src/lib.rs
fn yetkazib_berish() {}
mod uyning_orqasi {
fn buyurtmani_tuzatish() {
oshpaz();
super::yetkazib_berish();
}
fn oshpaz() {}
}
buyurtmani_tuzatish
funksiyasi uyning_orqasi
modulida joylashgan, shuning uchun biz super
dan uyning_orqasi
ota-moduliga o'tishimiz mumkin. U yerdan yetkazib_berish
ni qidiramiz va uni topamiz.
Muvaffaqiyat! Bizning fikrimizcha, uyning_orqasi
moduli va yetkazib_berish
funksiyasi bir-biri bilan bir xil munosabatda bo'lib qoladi va agar biz cratening modul daraxtini qayta tashkil etishga qaror qilsak, birgalikda harakatlanadi. Shu sababli, biz super
dan foydalandik, shuning uchun kelajakda bu kod boshqa modulga ko‘chirilsa, kodni yangilash uchun kamroq joylarga ega bo‘lamiz.
Structlar va Enumlarni public qilish
Shuningdek, structlar va enumlarni public sifatida belgilash uchun pub
dan foydalanishimiz mumkin, ammo pub
dan structlar va enumlar bilan foydalanish uchun qo'shimcha tafsilotlar mavjud. Agar struct definitiondan oldin pub
dan foydalansak, biz structni hammaga public qilamiz, lekin structning maydonlari hali ham private bo'lib qoladi. Biz har bir sohani alohida-alohida public qilishimiz yoki qilmasligimiz mumkin. 7-9 roʻyxatda biz public qizdirilgan_non
maydoni, lekin private mavsumiy_meva
maydoni bilan public uyning_orqasi:: nonushta
structini belgilab oldik. Bu restoranda mijoz ovqat bilan birga keladigan non turini tanlashi mumkin bo'lgan holatni modellashtiradi, ammo oshpaz qaysi meva mavsumda va omborda borligiga qarab ovqatga hamroh bo'lishini hal qiladi. Mavjud mevalar tezda o'zgaradi, shuning uchun mijozlar mevani tanlay olmaydi yoki hatto qaysi mevani olishini ko'ra olmaydi.
Fayl nomi: src/lib.rs
mod uyning_orqasi {
pub struct Nonushta {
pub yopilgan_non: String,
mavsumiy_meva: String,
}
impl Nonushta {
pub fn yoz(yopilgan_non: &str) -> Nonushta {
Nonushta {
yopilgan_non: String::from(yopilgan_non),
mavsumiy_meva: String::from("shaftoli"),
}
}
}
}
pub fn restoranda_ovqatlanish() {
// Yozda javdar yopilgan noni bilan nonushta buyurtma qiling
let mut ovqat = uyning_orqasi::Nonushta::yoz("Javdar");
// Qaysi nonni xohlashimiz haqidagi fikrimizni o'zgartiring
ovqat.yopilgan_non = String::from("Bug'doy");
println!("Iltimos, {}li yopilgan nonni istayman", ovqat.yopilgan_non);
// Agar izohni olib tashlasak, keyingi qator kompilyatsiya qilinmaydi;
// ovqat bilan birga keladigan mavsumiy mevalarni ko'rish yoki
// o'zgartirishga ruxsat berilmagan
// ovqat.mavsumiy_meva = String::from("ko'katlar");
}
uyning_orqasi::Nonushta
structdagi yopilgan_non
maydoni public bo'lgani uchun restoranda_ovqatlanish
da biz yopilgan_non
maydoniga nuqta belgisi yordamida yozishimiz va o'qishimiz mumkin. Esda tutingki, biz mavsumiy_meva
maydonidan restoranda_ovqatlanish
da foydalana olmaymiz, chunki mavsumiy_meva
privatedir. Qaysi xatoga yo'l qo'yganingizni bilish uchun mavsumiy_meva
maydoni qiymatini o'zgartiruvchi qatorni izohdan chiqarib ko'ring!
Shuni ham yodda tutingki, uyning_orqasi::Nonushta
private maydonga ega bo'lgani uchun struct Nonushta
misolini yaratuvchi public bog'langan funksiyani ta'minlashi kerak (biz uni bu yerda yoz
deb nomladik).Agar Nonushta
bunday funksiyaga ega boʻlmagan boʻlsa, biz restoranda_ovqatlanish
da Nonushta
misolini yarata olmadik, chunki biz restoranda_ovqatlanish
da private mavsumiy_meva
maydonining qiymatini oʻrnata olmadik.
Aksincha, agar biz enumni public qilsak, uning barcha variantlari public bo'ladi. 7 10 roʻyxatda koʻrsatilganidek, bizga faqat enum
kalit soʻzidan oldin pub
kerak boʻladi.
Fayl nomi: src/lib.rs
mod uyning_orqasi {
pub enum Taom {
Palov,
Salat,
}
}
pub fn restoranda_ovqatlanish() {
let buyurtma1 = uyning_orqasi::Taom::Palov;
let buyurtma2 = uyning_orqasi::Taom::Salat;
}
Biz Taom
ro‘yxatini hammaga public qilganimiz uchun restoranda_ovqatlanish
da Palov
va Salat
variantlaridan foydalanishimiz mumkin.
Enumlar, agar ularning variantlari public bo'lmasa, unchalik foydali emas; Har bir holatda pub
bilan barcha enum variantlariga izoh qo'yish zerikarli bo'lar edi, shuning uchun enum variantlari uchun standart umumiy bo'lishi kerak. Structlar ko'pincha maydonlari public bo'lmasdan foydali bo'ladi, shuning uchun struct maydonlari, agar pub
bilan izohlanmagan bo'lsa, standart bo'yicha hamma narsa private bo'lishining umumiy qoidasiga amal qiladi.
pub
bilan bog'liq yana bir holat bor, biz uni ko'rib chiqmaganmiz va bu bizning modul tizimining oxirgi xususiyati: use
kalit so'zi. Biz avval use
ni o'z ichiga olamiz, so'ngra pub
va use
ni qanday birlashtirishni ko'rsatamiz.
use
kalit so'zi bilan yo'llarni doiraga kiritish
Funksiyalarni chaqirish yo'llarini yozishga to'g'ri kelishi noqulay va takroriy tuyulishi mumkin. 7-7-Ro'yxatda navbat_listiga_qoshish
funksiyasiga mutlaq yoki nisbiy yoʻlni tanladikmi, har safar navbat_listiga_qoshish
funksiyasiga murojat qilmoqchi boʻlganimizda, uyning_oldi
va xizmat
ni ham belgilashimiz kerak edi. Yaxshiyamki, bu jarayonni soddalashtirishning bir usuli bor: biz bir marta use
kalit so‘zi bilan yo‘lga nom yaratishimiz mumkin, so‘ngra boshqa hamma joyda qisqaroq nomdan foydalanishimiz mumkin.
7-11 ro'yxatda biz crate::uyning_oldi::xizmat
modulini restoranda_ovqatlanish
funksiyasi doirasiga kiritamiz, shuning uchun restoranda_ovqatlanish
dagi navbat_listiga_qoshish
funksiyasini chaqirish uchun faqat xizmat::navbat_listiga_qoshish
ni belgilashimiz kerak.
Fayl nomi: src/lib.rs
mod uyning_oldi {
pub mod xizmat {
pub fn navbat_listiga_qoshish() {}
}
}
use crate::uyning_oldi::xizmat;
pub fn restoranda_ovqatlanish() {
xizmat::navbat_listiga_qoshish();
}
use
va sohaga yo'lni qo'shish fayl tizimida ramziy havola yaratishga o'xshaydi. Crate ildiziga use crate::uyning_oldi::xizmat
ni qo‘shish orqali xizmat
endi bu doirada haqiqiy nom bo‘lib qoladi, xuddi xizmat
moduli crate ildizida aniqlangandek. use
doirasiga kiritilgan yo'llar boshqa yo'llar kabi maxfiylikni ham tekshiradi.
E'tibor bering, use
faqat use
ishlaydigan aniq doira uchun yorliqni yaratadi. 7-12 roʻyxat restoranda_ovqatlanish
funksiyasini mijoz
nomli yangi bolalar moduliga oʻtkazadi, bu keyinchalik use
statementidan farq qiladi, shuning uchun funksiyaning tanasi kompilyatsiya qilinmaydi:
Fayl nomi: src/lib.rs
mod uyning_oldi {
pub mod xizmat {
pub fn navbat_listiga_qoshish() {}
}
}
use crate::uyning_oldi::xizmat;
mod mijoz {
pub fn restoranda_ovqatlanish() {
xizmat::navbat_listiga_qoshish();
}
}
Kompilyator xatosi yorliq endi mijoz
modulida qo'llanilmasligini ko'rsatadi:
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
warning: unused import: `crate::uyning_oldi::xizmat`
--> src/lib.rs:7:5
|
7 | use crate::uyning_oldi::xizmat;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
error[E0433]: failed to resolve: use of undeclared crate or module `xizmat`
--> src/lib.rs:11:9
|
11 | xizmat::navbat_listiga_qoshish();
| ^^^^^^ use of undeclared crate or module `xizmat`
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
E'tibor bering, use
endi uning doirasida qo'llanilmasligi haqida ogohlantirish ham bor! Bu muammoni hal qilish uchun use
ni mijoz
moduliga ham o‘tkazing yoki mijoz
modulidagi super::xizmat
bilan ota-moduldagi yorliqlarga murojaat qiling.
use
bilan idiomatik yo'llarni yaratish
7-11 ro'yxatda siz shunday deb hayron bo'lishingiz mumkin,Nima uchun biz bir xil natijaga erishish uchun navbat_listiga_qoshish
funksiyasigacha toʻliq yoʻlni ishlatish oʻrniga, crate::uyning_oldi::xizmat
ni ishlatishni belgilab qoʻydik va keyin restoranda_ovqatlanish
ichidagi xizmat::navbat_listiga_qoshish
ga murojat qildik, 7-13 ro'yxatdagi kabi.
Fayl nomi: src/lib.rs
mod uyning_oldi {
pub mod xizmat {
pub fn navbat_listiga_qoshish() {}
}
}
use crate::uyning_oldi::xizmat::navbat_listiga_qoshish;
pub fn restoranda_ovqatlanish() {
navbat_listiga_qoshish();
}
Garchi 7-11 va 7-13 ro'yxatlari bir xil vazifani bajarsa-da, 7-11 ro'yxat funksiyani use
bilan qamrab olishning idiomatik usulidir. Funksiyaning ota-modulini use
bilan qamrab olish funksiyani chaqirishda ota-modulni belgilashimiz kerakligini anglatadi. Funksiyani chaqirishda ota-modulni ko'rsatish, to'liq yo'lning takrorlanishini minimallashtirish bilan birga, funksiya mahalliy sifatida aniqlanmaganligini aniq ko'rsatadi. 7-13 ro'yxatda navbat_listiga_qoshish
qayerda aniqlangani aniq emas.
Boshqa tomondan, use
bilan structlar, enumlar va boshqa elementlarni keltirishda to'liq yo'lni ko'rsatish idiomatikdir. 7-14 ro'yxat standart kutubxonaning HashMap
structini binary crate doirasiga olib kirishning idiomatik usulini ko'rsatadi.
Fayl nomi: src/main.rs
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
Bu idioma ortida hech qanday yaxshi sabab yo'q: Bu shunchaki konventsiya paydo bo'ldi va odamlar Rust kodini shu tarzda o'qish va yozishga o'rganib qolgan.
Bu idiomadan istisno shundaki, biz bir xil nomdagi ikkita elementni use
statementi yordamida doiraga kiritganimizda - Rust bunga yo'l qo'ymaydi. 7-15 ro'yxatda bir xil nomga ega, ammo har xil ota-modullarga ega bo'lgan ikkita Result
turini qanday ko'rinishga kiritish va ularga qanday murojaat qilish kerakligi ko'rsatilgan.
Fayl nomi: src/lib.rs
use std::fmt;
use std::io;
fn funksiya1() -> fmt::Result {
// --snip--
Ok(())
}
fn funksiya2() -> io::Result<()> {
// --snip--
Ok(())
}
Ko'rib turganingizdek, ota-modullardan foydalanish ikkita Result
turini ajratib turadi.
Buning o'rniga use std::fmt::Result
va us std::io::Result
ni belgilagan bo'lsak, bizda bir xil miqyosda ikkita Result
turi bo'lar edi va Rust Result
dan foydalanganda qaysi birini nazarda tutganimizni bilmas edi.
as
kalit so'zi bilan yangi nomlarni taqdim etish
Bir xil nomdagi ikkita turni use
bilan bir xil doiraga olib kirish muammosining yana bir yechimi bor: yoʻldan soʻng biz as
va yangi mahalliy nom yoki tur uchun taxallus belgilashimiz mumkin. 7-16 ro'yxatda ikkita Result
turidan birini as
yordamida qayta nomlash orqali 7-15 ro'yxatdagi kodni yozishning yana bir usuli ko'rsatilgan.
Fayl nomi: src/lib.rs
use std::fmt::Result;
use std::io::Result as IoResult;
fn funksiya1() -> Result {
// --snip--
Ok(())
}
fn funksiya2() -> IoResult<()> {
// --snip--
Ok(())
}
Ikkinchi use
statementida biz std::io::Result
turi uchun yangi IoResult
nomini tanladik, bu endi std::fmt
dan Result
turiga zid kelmaydi, u ham doiraga kiradi. 7-15 va 7-16 ro'yxatlar idiomatik hisoblanadi, shuning uchun tanlov sizga bog'liq!
pub use
bilan nomlarni qayta eksport(re-eksport) qilish
use
kalit so'zidan foydalanib, nomni qamrovga kiritganimizda, yangi doirada mavjud bo'lgan nom private bo'ladi. Bizning kodimizni chaqiradigan kodni xuddi shu kod doirasida aniqlangandek ushbu nomga murojaat qilishini yoqish uchun biz pub
va use
ni birlashtira olamiz. Bu usul re-eksport deb nomlanadi, chunki biz obyektni qamrovga kiritmoqdamiz, lekin elementni boshqa qamrovlarga kiritish uchun ham mavjud qilamiz.
7-17 ro'yxatda 7-11 ro'yxatdagi kod ko'rsatilgan, ildiz modulidagi use
pub use
ga o'zgartirilgan.
Fayl nomi: src/lib.rs
mod uyning_oldi {
pub mod xizmat {
pub fn navbat_listiga_qoshish() {}
}
}
pub use crate::uyning_oldi::xizmat;
pub fn restoranda_ovqatlanish() {
xizmat::navbat_listiga_qoshish();
}
Ushbu o'zgarishdan oldin tashqi kod restoran::uyning_oldi::xizmat::navbat_listiga_qoshish()
yo'lidan foydalanib, navbat_listiga_qoshish
funksiyasini chaqirishi kerak bo'ladi. Endi bu pub use
xizmat
modulini ildiz modulidan qayta eksport qilgan bo‘lsa, tashqi kod endi restoran::xizmat::navbat_listiga_qoshish()
yo‘lidan foydalanishi mumkin..
Qayta eksport qilish sizning kodingizning ichki tuzilishi sizning kodingizni chaqirayotgan dasturchilarning domen haqida o'ylashlaridan farq qilganda foydali bo'ladi. Misol uchun, ushbu restoran metaforasida restoranni boshqaradigan odamlar "uyning old tomoni" va "uyning orqasi" haqida o'ylashadi. Ammo restoranga tashrif buyurgan mijozlar, ehtimol, restoranning qismlari haqida o'ylamaydilar. pub use
bilan biz kodimizni bitta struct bilan yozishimiz mumkin, lekin boshqa structni ko'rsatamiz. Bu bizning kutubxonamizni kutubxonada ishlaydigan dasturchilar va kutubxonaga murojat qilayotgan dasturchilar uchun uchun yaxshi tashkil etilgan holda saqlaydi. Biz 14-bobning “pub use
bilan qulay umumiy APIni eksport qilish” bo‘limida pub use
ning yana bir misolini va uning cratengiz hujjatlariga qanday ta’sir qilishini ko‘rib chiqamiz.
Tashqi paketlardan foydalanish
2-bobda biz tasodifiy raqamlarni olish uchun rand
deb nomlangan tashqi paketdan foydalangan holda taxminiy o'yin loyihasini dasturlashtirdik. Loyihamizda rand
dan foydalanish uchun biz ushbu qatorni Cargo.toml ga qo'shdik:
Fayl nomi: Cargo.toml
rand = "0.8.5"
Cargo.toml-ga rand
ni dependency sifatida qo'shish Cargo-ga crates.io-dan rand
paketini va har qanday bog'liqliklarni yuklab olishni va rand
ni loyihamiz uchun ishlatishni aytadi.
Keyin, rand
ta'riflarini paketimiz doirasiga kiritish uchun biz crate nomidan boshlanadigan use
qatorini qo'shdik, rand
va biz qamrab olmoqchi bo'lgan elementlarni sanab o'tdik. Eslatib o‘tamiz, 2-bobdagi “Tasodifiy raqamni yaratish” bo‘limida biz Rng
traitini qamrab oldik va rand::thread_rng
funksiyasini chaqirdik:
use std::io;
use rand::Rng;
fn main() {
println!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
println!("Yashirin raqam: {yashirin_raqam}");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
println!("Sizning taxminingiz: {taxmin}");
}
Rust hamjamiyatining a'zolari crates.io saytida ko'plab paketlarni taqdim etishdi va ulardan birini o'z paketingizga olish xuddi shu bosqichlarni o'z ichiga oladi: ularni paketingizning Cargo.toml faylida roʻyxatga kiriting va use
dan foydalanib, ularni cratelaridagi elementlarni qamrab oling.
E'tibor bering, standart std
kutubxonasi bizning paketimizdan tashqarida joylashgan cratedir. Standart kutubxona Rust tili bilan birga kelganligi sababli, biz Cargo.toml ni std
qo'shish uchun o'zgartirishimiz shart emas. Ammo biz u yerdan elementlarni paketimiz doirasiga olib kirish uchun use
bilan murojaat qilishimiz kerak. Masalan, HashMap
bilan biz ushbu qatordan foydalanamiz:
#![allow(unused)] fn main() { use std::collections::HashMap; }
Bu standart kutubxona cratesining nomi bo'lgan std
bilan boshlanadigan mutlaq yo'ldir.
Uzun use
ro'yxatini qisqartirish uchun ichki yo'llardan foydalanish
Agar biz bir xil crate yoki bir xil modulda belgilangan bir nechta elementlardan foydalansak, har bir elementni o'z qatoriga qo'yish bizning fayllarimizda juda ko'p vertikal joy egallashi mumkin. Masalan, 2-4 roʻyxatdagi raqamlarni taxmin qilish dasturida mavjud boʻlgan ushbu ikkita use
statementi std
dagi elementlarni qamrab oladi:
Filename: src/main.rs
use rand::Rng;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--
fn main() {
println!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
println!("Yashirin raqam: {yashirin_raqam}");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o'qib bo'lmadi");
println!("Sizning taxminingiz: {taxmin}");
match taxmin.cmp(&yashirin_raqam) {
Ordering::Less => println!("Raqam Kichik!"),
Ordering::Greater => println!("Raqam katta!"),
Ordering::Equal => println!("Siz yutdingiz!"),
}
}
Buning o'rniga, biz bir xil elementlarni bir qatorga kiritish uchun ichki yo'llardan foydalanishimiz mumkin. Buni 7-18 roʻyxatda koʻrsatilganidek, yoʻlning umumiy qismini, keyin ikkita nuqta qoʻyib, soʻngra yoʻllarning bir-biridan farq qiladigan qismlari roʻyxati atrofida jingalak qavslarni belgilash orqali qilamiz.
Fayl nomi: src/main.rs
use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--
fn main() {
println!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
println!("Yashirin raqam: {yashirin_raqam}");
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o'qib bo'lmadi");
let taxmin: u32 = taxmin.trim().parse().expect("Iltimos, raqam yozing!");
println!("Sizning taxminingiz: {taxmin}");
match taxmin.cmp(&yashirin_raqam) {
Ordering::Less => println!("Raqam Kichik!"),
Ordering::Greater => println!("Raqam katta!"),
Ordering::Equal => println!("Siz yutdingiz!"),
}
}
Kattaroq dasturlarda bir xil crate yoki moduldan ko'plab elementlarni o'rnatilgan yo'llar yordamida qamrab olish juda ko'p talab qilinadigan alohida use
statementlari sonini kamaytirishi mumkin!
Siz har qanday darajadagi ichki yo'ldan foydalanishingiz mumkin, bu yo'l qismini ulashuvchi ikkita use
statementini birlashtirishda foydalidir. Masalan, 7-19 ro'yxat ikkita use
statementini ko'rsatadi: biri std::io
ni qamrab oladi va ikkinchisi std::io::Write
ni qamrab oladi.
Fayl nomi: src/lib.rs
use std::io;
use std::io::Write;
Ushbu ikkita yo'lning umumiy qismi std::io
va to'liq birinchi yo'ldir. Ushbu ikkita yo'lni bitta use
statementiga birlashtirish uchun biz 7-20 ro'yxatda ko'rsatilganidek, ichki yo'lda self
dan foydalanishimiz mumkin.
Fayl nomi: src/lib.rs
use std::io::{self, Write};
Bu satr std::io
va std::io::Write
ni qamrab oladi.
Glob operatori
Agar biz yo'lda belgilangan barcha umumiy elementlarni qamrovga kiritmoqchi bo'lsak, biz *
glob operatori tomonidan keyingi yo'lni belgilashimiz mumkin:
#![allow(unused)] fn main() { use std::collections::*; }
Ushbu use
statementi std::collections
da aniqlangan barcha public elementlarni joriy doiraga olib keladi. Glob operatoridan foydalanganda ehtiyot bo'ling! Glob qaysi nomlar qamrovda ekanligini va dasturingizda ishlatiladigan nom qayerda aniqlanganligini aniqlashni qiyinlashtirishi mumkin.
Glob operatori ko'pincha sinovdan o'tgan hamma narsani tests
moduliga kiritish uchun test paytida ishlatiladi; biz bu haqda 11-bobdagi "Testlarni qanday yozish kerak" bo'limida gaplashamiz. Glob operatori ba'zan prelude patterning bir qismi sifatida ham qo'llaniladi: ushbu pattern haqida qo'shimcha ma'lumot olish uchun standart kutubxona texnik hujjatlariga qarang.
Modullarni turli fayllarga ajratish
Hozirgacha ushbu bobdagi barcha misollar bitta faylda bir nechta modullarni aniqladi. Modullar kattalashganda, kodni boshqarishni osonlashtirish uchun ularning definitionlarini alohida faylga ko'chirishingiz mumkin.
Masalan, 7-17 ro'yxatdagi bir nechta restoran moduliga ega bo'lgan koddan boshlaylik. Biz cratening ildiz modulidagi barcha modullarni aniqlash o'rniga modullarni fayllarga ajratamiz. Bunday holda, cratening ildiz fayli src/lib.rs bo'ladi, lekin bu protsedura crate ildiz fayli src/main.rs bo'lgan binary cratelar bilan ham ishlaydi.
Birinchidan, biz uyning_oldi
modulini o'z fayliga chiqaramiz. uyning_oldi
moduli uchun jingalak qavslar ichidagi kodni olib tashlang va faqat mod uyning_oldi
deklaratsiyasini qoldiring, shunda src/lib.rs ro'yxat 7-21da ko'rsatilgan kodni o`z ichiga oladi. E'tibor bering, biz 7-22 ro'yxatda src/uyning_oldi.rs faylini yaratmagunimizcha, bu kompilyatsiya qilinmaydi.
Fayl nomi: src/lib.rs
mod uyning_oldi;
pub use crate::uyning_oldi::xizmat;
pub fn restoranda_ovqatlanish() {
xizmat::navbat_listiga_qoshish();
}
Keyin, jingalak qavslardagi kodni yangi faylga joylashtiring
7-22 ro'yxatda ko'rsatilganidek src/uyning_oldi.rs deb nomlangan. Kompilyator bu faylda nimani izlash kerakligini biladi, chunki u uyning_oldi
deb nomlangan cratening ildiz modulida modul deklaratsiyasiga duch keldi.
Fayl nomi: src/uyning_oldi.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-21-and-22/src/front_of_house.rs}}
Esda tutingki, modul daraxtida bir marta mod
deklaratsiyasidan foydalanib faylni yuklashingiz kerak. Kompilyator fayl loyihaning bir qismi ekanligini bilgandan so'ng (va mod
statementi qo'ygan joyingiz tufayli kod modul daraxtining qayerida joylashganligini biladi), loyihangizdagi boshqa fayllar yuklangan fayl kodiga u e'lon qilingan joyga yo'l orqali murojaat qilishi kerak, bu "Modul daraxtidagi elementga murojaat qilish yo'llari" bo'limida yoritilgan. Boshqacha qilib aytganda, mod
boshqa dasturlash tillarida ko'rishingiz mumkin bo'lgan “include” operatsiyasi emas.
Keyinchalik, biz xizmat
modulini o'z fayliga chiqaramiz. Jarayon biroz boshqacha, chunki xizmat
ildiz modulining emas, balki uyning_oldi
ichki modulidir.Biz xizmat
faylini modul daraxtidagi ajdodlari nomi bilan ataladigan yangi jildga joylashtiramiz, bu holda src/uyning_oldi/.
xizmat
ni ko‘chirishni boshlash uchun biz src/uyning_oldi.rs ni faqat xizmat
moduli deklaratsiyasini o‘z ichiga olgan holda o‘zgartiramiz:
Fayl nomi: src/uyning_oldi.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/no-listing-02-extracting-hosting/src/front_of_house.rs}}
Keyin biz src/uyning_oldi jildini va xizmat
modulida berilgan definitionlarni o'z ichiga olgan xizmat.rs faylini yaratamiz:
Fayl nomi: src/uyning_oldi/xizmat.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/no-listing-02-extracting-hosting/src/front_of_house/hosting.rs}}
Agar biz src jildiga xizmat.rs ni qo'ysak, kompilyator xizmat.rs kodi crate ildizida e'lon qilingan va uyning_oldi
modulining yordamchisi sifatida e'lon qilinmagan xizmat
modulida bo'lishini kutadi. Kompilyator qoidalari qaysi modullarning kodini o'z ichiga olgan fayllarni tekshirish uchun jildlar va fayllar modul daraxtiga to'liq mos kelishini taxmin qiladi.
Muqobil fayl yo'llari
Hozirgacha biz Rust kompilyatori foydalanadigan eng idiomatik fayl yo'llarini ko'rib chiqdik, lekin Rust fayl yo'lining eski uslubini ham qo'llab-quvvatlaydi. Crate ildizida e'lon qilingan
uyning_oldi
nomli modul uchun kompilyator modul kodini quyidagilardan qidiradi: module’s code in:
- src/uyning_oldi.rs (biz nimani qamrab oldik)
- src/uyning_oldi/mod.rs (eski uslub, hali ham qo'llab-quvvatlanadigan yo'l)
uyning_oldi
submodul bo'lganxizmat
nomli modul uchun kompilyator modul kodini qidiradi:
- src/uyning_oldi/xizmat.rs (biz nimani qamrab oldik)
- src/uyning_oldi/xizmat/mod.rs (eski uslub, hali ham qo'llab-quvvatlanadigan yo'l)
Agar bir xil modul uchun ikkala uslubdan foydalansangiz, kompilyator xatosi paydo bo'ladi. Bitta loyihada turli modullar uchun ikkala uslubning aralashmasidan foydalanishga ruxsat beriladi, lekin loyihangizni boshqarayotgan odamlar uchun chalkash bo'lishi mumkin.
mod.rs nomli fayllardan foydalanadigan uslubning asosiy kamchiligi shundaki, sizning loyihangiz mod.rs nomli ko‘plab fayllar bilan tugashi mumkin, ular bir vaqtning o‘zida muharriringizda ochilganda chalkash bo‘lishi mumkin.
Biz har bir modul kodini alohida faylga ko'chirdik va modul daraxti o'zgarishsiz qoldi. restoranda_ovqatlanish
funksiyasi chaqiruvlari, definitionlar turli fayllarda bo'lsa ham, hech qanday o'zgartirishlarsiz ishlaydi. Ushbu texnika modullarni hajmi oshgani sayin yangi fayllarga ko'chirish imkonini beradi.
Esda tutingki, src/lib.rs dagi pub use crate::uyning_oldi::xizmat
statementi ham o'zgarmagan va use
qaysi fayllar cratening bir qismi sifatida tuzilganiga ta'sir qilmaydi. mod
kalit so'zi modullarni e'lon qiladi va Rust ushbu modulga kiradigan kod moduli bilan bir xil nomdagi faylga qaraydi.
Xulosa
Rust sizga paketni bir nechta cratelarga va crateni modullarga bo'lish imkonini beradi, shunda siz bir modulda belgilangan elementlarga boshqa moduldan murojaat qilishingiz mumkin. Buni mutlaq yoki nisbiy yo'llarni belgilash orqali amalga oshirishingiz mumkin. Ushbu yo'llar use
statementi bilan qamrab olinishi mumkin, shuning uchun siz ushbu doiradagi elementdan bir nechta foydalanish uchun qisqaroq yo'ldan foydalanishingiz mumkin. Modul kodi standart boʻyicha maxfiydir, lekin pub
kalit soʻzini qoʻshish orqali definitionlarni hammaga public qilishingiz mumkin.
Keyingi bobda biz standart kutubxonadagi ma'lumotlar tuzilmalarining ba'zi to'plamlarini ko'rib chiqamiz, ulardan siz o'zingizning aniq tartiblangan kodingizda foydalanishingiz mumkin.
Umumiy to'plamlar
Rustning standart kutubxonasi kolleksiyalar deb nomlangan juda foydali ma'lumotlar tuzilmalarini o'z ichiga oladi. Ko'pgina boshqa ma'lumotlar turlari bitta ma'lum qiymatni ifodalaydi, lekin to'plamlar bir nechta qiymatlarni o'z ichiga olishi mumkin. O'rnatilgan array va tuple turlaridan farqli o'laroq, ushbu to'plamlar ko'rsatadigan ma'lumotlar heapda saqlanadi, ya'ni ma'lumotlar miqdori kompilyatsiya vaqtida ma'lum bo'lishi shart emas va dastur ishga tushganda o'sishi yoki qisqarishi mumkin. To'plamning har bir turi o'z imkoniyatlariga ega va ishlash jihatidan farq qiladi, shuning uchun ma'lum bir to'plamni tanlash vaziyatga bog'liq va vaqt o'tishi bilan ishlab chiquvchining mahoratidir. Ushbu bobda biz Rust dasturlarida tez-tez ishlatiladigan uchta to'plamni muhokama qilamiz:
- vector o'zgaruvchan sonli qiymatlarni bir-birining yonida saqlashga imkon beradi.
- string - bu belgilar to'plami. Biz
String
turini avval aytib o'tgan edik, ammo bu bobda biz bu haqda chuqurroq gaplashamiz. - hash map ma'lum bir kalit bilan qiymatni bog'lash imkonini beradi.Bu map deb nomlangan umumiy ma'lumotlar strukturasining o'ziga xos tatbiqidir.
Standart kutubxona tomonidan taqdim etilgan boshqa turdagi to'plamlar haqida bilish uchun texnik hujjatlarga qarang.
Biz vectorlarni, stringlarni va hash-maplarni qanday yaratish va yangilashni, shuningdek, har birining o'ziga xosligini muhokama qilamiz.
Vectorlar bilan qiymatlar ro'yxatini saqlash
Biz ko'rib chiqadigan birinchi to'plam turi Vec<T>
bo'lib, u vector sifatida ham tanilgan.
Vectorlar xotirada barcha qiymatlarni yonma-yon joylashtirgan yagona ma'lumotlar strukturasida bir nechta qiymatlarni saqlash imkonini beradi. Vectorlar faqat bir xil turdagi qiymatlarni saqlashi mumkin. Ular sizda fayldagi matn satrlari yoki xarid qilish savatidagi narsalarning narxlari kabi elementlar ro'yxatiga ega bo'lsangiz foydali bo'ladi.
Yangi vector yaratish
Yangi bo'sh vector yaratish uchun biz 8-1 ro'yxatda ko'rsatilganidek, Vec::new
funksiyasini chaqiramiz.
fn main() { let v: Vec<i32> = Vec::new(); }
E'tibor bering, biz bu erda annation tur qo'shdik. Biz ushbu vectorga hech qanday qiymat kiritmayotganimiz sababli, Rust biz qanday elementlarni saqlashni xohlayotganimizni bilmaydi. Bu muhim nuqta. Vectorlar generiklar yordamida amalga oshiriladi; Biz 10-bobda o'zingizning turlaringiz bilan generiklardan qanday foydalanishni ko'rib chiqamiz. Hozircha shuni bilingki, standart kutubxona tomonidan taqdim etilgan Vec<T>
turi har qanday turni sig'dira oladi.
Muayyan turni ushlab turish uchun vector yaratganimizda, burchakli qavslar([]) ichida turni belgilashimiz mumkin. 8-1 roʻyxatida biz Rustga v
dagi Vec<T>
i32
turidagi elementlarni saqlashini aytdik.
Ko'pincha siz boshlang'ich qiymatlari bilan Vec<T>
ni yaratasiz va Rust siz saqlamoqchi bo'lgan qiymat turini aniqlaydi, shuning uchun kamdan-kam hollarda bu turdagi annotionni bajarishingiz kerak bo'ladi. Rust qulay tarzda vec!
makrosini taqdim etadi, bu esa siz bergan qiymatlarni saqlaydigan yangi vectorni yaratadi. 8-2 roʻyxati 1
, 2
va 3
qiymatlariga ega boʻlgan yangi Vec<i32>
ni yaratadi. Butun son turi i32
dir, chunki bu standart butun son turi, biz 3-bobning "Ma'lumotlar turlari" bo'limida muhokama qilganimizdek.
fn main() { let v = vec![1, 2, 3]; }
Biz boshlang‘ich i32
qiymatlarini berganimiz sababli, Rust v
turi Vec<i32>
ekanligini va tur izohi shart emas degan xulosaga kelishi mumkin. Keyinchalik vectorni qanday o'zgartirishni ko'rib chiqamiz.
Vectorni yangilash
Vector yaratish va unga elementlar qo'shish uchun biz 8-3 ro'yxatda ko'rsatilganidek, push
metodidan foydalanishimiz mumkin.
fn main() { let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8); }
Har qanday o'zgaruvchida bo'lgani kabi, agar biz uning qiymatini o'zgartirish imkoniyatiga ega bo'lishni istasak, 3-bobda muhokama qilinganimizdek, mut
kalit so'zidan foydalanib, uni o'zgaruvchan qilishimiz kerak. Biz joylashtirgan raqamlarning barchasi i32
turiga kiradi va Rust buni maʼlumotlardan chiqaradi, shuning uchun bizga Vec<i32>
annotationi kerak emas.
Vector elementlarini o'qish
Vectorda saqlangan qiymatga murojaat qilishning ikki yo'li mavjud: indekslash yoki get
metodi yordamida. Quyidagi misollarda biz qo'shimcha aniqlik uchun ushbu funksiyalardan qaytariladigan qiymatlar turlarini izohladik.
8-4 ro'yxatda indekslash sintaksisi va get
metodi bilan vectordagi qiymatga kirishning ikkala usuli ko'rsatilgan.
fn main() { let v = vec![1, 2, 3, 4, 5]; let uchinchi: &i32 = &v[2]; println!("Uchinchi element {uchinchi}"); let uchinchi: Option<&i32> = v.get(2); match uchinchi { Some(uchinchi) => println!("Uchinchi element {uchinchi}"), None => println!("Uchinchi element yo'q."), } }
Bu erda bir nechta detallarga e'tibor bering. Uchinchi elementni olish uchun 2
indeks qiymatidan foydalanamiz, chunki vectorlar noldan boshlab raqamlar boʻyicha indekslanadi. &
va []
dan foydalanish bizga indeks qiymatidagi elementga reference beradi. Argument sifatida berilgan indeks bilan get
metodidan foydalansak, biz match
bilan foydalanishimiz mumkin bo'lgan Option<&T>
ni olamiz.
Rust elementga reference qilishning ushbu ikki usulini taqdim etishining sababi shundaki, siz mavjud elementlar doirasidan tashqarida indeks qiymatidan foydalanmoqchi bo'lganingizda dastur qanday harakat qilishini tanlashingiz mumkin. Misol sifatida, keling, besh elementli vectorga ega bo'lganimizda nima sodir bo'lishini ko'rib chiqamiz va keyin 8-5 ro'yxatda ko'rsatilganidek, har bir texnikada 100 indeksidagi elementga kirishga harakat qilamiz.
fn main() { let v = vec![1, 2, 3, 4, 5]; let mavjud_emas = &v[100]; let mavjud_emas = v.get(100); }
Ushbu kodni ishga tushirganimizda, birinchi []
metodi dasturda panic chiqaradi, chunki u mavjud bo'lmagan elementga murojaat qiladi. Ushbu usul vector oxiridan o'tgan elementga kirishga urinish bo'lsa, dasturingiz ishdan chiqishini xohlasangiz yaxshi qo'llaniladi.
get
metodi vectordan tashqaridagi indeksdan o'tganda, panic qo'ymasdan None
ni qaytaradi. Vector doirasidan tashqaridagi elementga kirish vaqti-vaqti bilan oddiy sharoitlarda sodir bo'lishi mumkin bo'lsa, siz ushbu usuldan foydalanasiz. Keyin sizning kodingiz 6-bobda muhokama qilinganidek, Some(&element)
yoki None
ga ega bo'lish mantiqiga ega bo'ladi.Misol uchun, indeks raqamni kiritgan odamdan kelib chiqishi mumkin. Agar ular tasodifan juda katta raqamni kiritsa va dastur None
qiymatiga ega bo'lsa, siz foydalanuvchiga joriy vectorda nechta element borligini aytishingiz va ularga to'g'ri qiymat kiritish uchun yana bir imkoniyat berishingiz mumkin.Bu imlo xatosi tufayli dasturni buzishdan ko'ra foydalanuvchilar uchun qulayroq bo'lar edi!
Dasturda tegishli reference mavjud bo'lsa, borrow tekshiruvi ushbu reference va vector mazmuniga boshqa har qanday referencelar haqiqiyligini ta'minlash uchun ownership va borrowing qoidalarini (4-bobda ko'rsatilgan) amalga oshiradi. Bir xil doirada o'zgaruvchan va o'zgarmas referencelarga ega bo'lolmaysiz degan qoidani eslang. Ushbu qoida 8-6 ro'yxatda qo'llaniladi, bu yerda biz vectordagi birinchi elementga o'zgarmas referenceni ushlab turamiz va elementni oxiriga qo'shishga harakat qilamiz. Agar biz ushbu elementga keyinroq funksiyada murojaat qilsak, bu dastur ishlamaydi:
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let birinchi = &v[0];
v.push(6);
println!("Birinchi element: {birinchi}");
}
Ushbu kodni kompilyatsiya qilish ushbu xatoga olib keladi:
$ 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 birinchi = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("Birinchi element: {birinchi}");
| ----------- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` due to previous error
8-6 ro'yxatdagi kod ishlashi kerakdek ko'rinishi mumkin: nima uchun birinchi elementga reference vector oxiridagi o'zgarishlar haqida qayg'urishi kerak? Bu xato vectorlarning ishlash usuli bilan bog'liq: vectorlar qiymatlarni xotirada bir-birining yoniga qo'yganligi sababli vector oxiriga yangi element qo'shish yangi xotira ajratishni va eski elementlarni yangi bo'sh joyga ko'chirishni talab qilishi mumkin. Hozirda vector saqlanadigan barcha elementlarni bir-birining yoniga qo'yish uchun joy etarli emas. Bunday holda, birinchi elementga reference ajratilgan xotiraga ishora qiladi. Borrowing qoidalari dasturlarning bunday vaziyatga tushishiga yo'l qo'ymaydi.
Eslatma:
Vec<T>
turini implement qilish haqida ko'proq ma'lumot olish uchun "Rustonomikon" ga qarang.
Vectordagi qiymatlarni takrorlash
Vectordagi har bir elementga navbatma-navbat kirish uchun biz indekslarni birma-bir kirish uchun ishlatmasdan, barcha elementlarni takrorlaymiz. 8-7 ro'yxatda i32
qiymatlari vectoridagi har bir elementga o'zgarmas referencelarni olish va ularni chop etish uchun for
siklidan qanday foydalanish ko'rsatilgan.
fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{i}"); } }
Shuningdek, biz barcha elementlarga o'zgartirish kiritish uchun o'zgaruvchan vectordagi har bir elementga o'zgaruvchan referencelarni takrorlashimiz mumkin. 8-8 ro'yxatdagi for
sikli har bir elementga 50
qo'shadi.
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
O'zgaruvchan reference nazarda tutilgan qiymatni o'zgartirish uchun biz +=
operatoridan foydalanishimizdan oldin i
qiymatiga o'tish uchun *
dereference operatoridan foydalanishimiz kerak. Biz 15-bobning "Dereference operatori bilan ko'rsatgichni qiymatga kuzatib borish" bo'limida dereference operatori haqida ko'proq gaplashamiz.
O'zgarmas yoki o'zgaruvchan bo'lsin, vector bo'yicha takrorlash, borrow tekshiruvi qoidalari tufayli xavfsizdir. Agar biz 8-7 va 8-8 ro'yxatlardagi for
siklining tanasiga elementlarni qo'shishga yoki olib tashlashga harakat qilsak, biz 8-6 ro'yxatdagi kodga o'xshash kompilyator xatosiga duch kelamiz. for
siklidagi vectorga murojaat qilish butun vectorni bir vaqtning o'zida o'zgartirishni oldini oladi.
Bir nechta turlarni saqlash uchun enumdan foydalanish
Vectorlar faqat bir xil turdagi qiymatlarni saqlashi mumkin. Bu noqulay bo'lishi mumkin; Har xil turdagi elementlar ro'yxatini saqlash zarurati uchun, albatta, foydalanish holatlari mavjud. Yaxshiyamki, enumlashning variantlari bir xil enum turi ostida aniqlanadi, shuning uchun bizga har xil turdagi elementlarni ko'rsatish uchun bitta tur kerak bo'lganda, enumni aniqlashimiz va ishlatishimiz mumkin!
Misol uchun, biz elektron jadvalning bir qator ustunlarida integer sonlar, ba'zi float raqamlar va ba'zi stringlar mavjud bo'lgan satrdan qiymatlarni olishni xohlaymiz. Variantlari turli qiymat turlariga ega bo'lgan enumni aniqlashimiz mumkin va barcha enum variantlari bir xil turdagi hisoblanadi: enum. Keyin biz ushbu enumni ushlab turish uchun vectorni yaratishimiz mumkin va natijada har xil turlarni ushlab turadi. Biz buni 8-9 ro'yxatda ko'rsatdik.
fn main() { enum ElektronJadval { Int(i32), Float(f64), Text(String), } let qator = vec![ ElektronJadval::Int(3), ElektronJadval::Text(String::from("ko'k")), ElektronJadval::Float(10.12), ]; }
Rust kompilyatsiya vaqtida vectorda qanday turlar bo'lishini bilishi kerak, shuning uchun u har bir elementni saqlash uchun heapda qancha xotira kerakligini aniq biladi. Shuningdek, ushbu vectorda qanday turlarga ruxsat berilganligini aniq bilishimiz kerak. Agar Rust vectorga har qanday turni ushlab turishga ruxsat bergan bo'lsa, bir yoki bir nechta tur vector elementlari ustida bajarilgan operatsiyalarda xatoliklarni keltirib chiqarishi mumkin edi. Enum va match
ifodasidan foydalanish Rust kompilyatsiya vaqtida 6-bobda muhokama qilinganidek, barcha mumkin bo'lgan holatlar ko'rib chiqilishini ta'minlaydi.
Agar siz vectorda saqlash uchun dastur runtimeda oladigan turlarning to'liq to'plamini bilmasangiz, enum texnikasi ishlamaydi. Buning o'rniga, biz 17-bobda ko'rib chiqiladigan trait obyektidan foydalanishingiz mumkin.
Endi biz vectorlardan foydalanishning eng keng tarqalgan usullarini ko'rib chiqdik, standart kutubxona tomonidan Vec<T>
da belgilangan barcha foydali usullar uchun API texnik hujjatlarini ko'rib chiqishni unutmang. Masalan, push
dan tashqari, pop
usuli oxirgi elementni olib tashlaydi va qaytaradi.
Vectordan elementlarni olib tashlash
struct
lar singari, vector ham 8-10 ro'yxatda ko'rsatilganidek, amal qilish doirasidan tashqariga chiqqanda xotirasini bo'shatadi.
fn main() { { let v = vec![1, 2, 3, 4]; // v bilan ish } // <- v doiradan chiqib ketadi va bu erda bo'shatiladi }
Vector o'chirilganda, uning barcha tarkibi ham o'chiriladi: vectorni o'chirish uning tarkibidagi qiymatlarni o'chirishni anglatadi. Borrow tekshiruvi vector mazmuniga har qanday referencelar faqat vectorning o'zi haqiqiy bo'lganda ishlatilishini ta'minlaydi.
Keling, keyingi to'plam turiga o'tamiz: String
!
UTF-8 kodlangan matnni String bilan saqlash
Biz 4-bobda stringlar haqida gapirgan edik, ammo hozir ularni batafsil ko'rib chiqamiz. Yangi Rustaceanlar odatda uchta sababning kombinatsiyasiga ko'ra stringlarga yopishib qolishadi: Rustning mumkin bo'lgan xatolarni ochishga moyilligi, stringlar ko'plab dasturchilar tushunganidan ko'ra murakkabroq ma'lumotlar tuzilishi va UTF-8. Bu omillar shunday birlashadiki, agar siz boshqa dasturlash tillaridan kelgan bo'lsangiz, mavzu murakkab ko'rinishi mumkin.
To'plamlar kontekstida stringlarni muhokama qilish foydalidir, chunki stringlar baytlar to'plami sifatida amalga oshiriladi, shuningdek, bu baytlar matn sifatida talqin qilinganda foydali funksiyalarni ta'minlashning ba'zi usullari. Ushbu bo'limda biz String
bo'yicha har bir to'plam turiga ega bo'lgan yaratish, yangilash va o'qish kabi operatsiyalar haqida gapiramiz. Shuningdek, biz String
ning boshqa to'plamlardan qanday farq qilishini, ya'ni String
ga indekslash odamlar va kompyuterlarning String
ma'lumotlarini qanday talqin qilishlari o'rtasidagi farqlar tufayli qanday murakkablashishini muhokama qilamiz.
String nima?
Biz birinchi navbatda string atamasi bilan nimani nazarda tutayotganimizni aniqlaymiz. Rust asosiy tilda faqat bitta string turiga ega, bu str
qator slice boʻlib, odatda uning &str
shaklida koʻrinadi. 4-bobda biz boshqa joyda saqlangan ba'zi UTF-8 kodlangan string ma'lumotlariga referencelar bo'lgan string slicelar haqida gaplashdik. Masalan, satr literallari dasturning binary tizimida saqlanadi va shuning uchun satr slicedir.
Rust standart kutubxonasi tomonidan taqdim etilgan String
turi asosiy tilga o'rnatilmagan va kengaytiriladigan, o'zgaruvchan, ega bo'lgan, UTF-8 kodlangan string turidir. Rustaceanlar Rust tilidagi "stringlar" ga murojaat qilganda, ular bu turlardan birini emas, balki String
yoki string slice &str
turlarini nazarda tutishi mumkin. Garchi bu bo'lim asosan String haqida bo'lsa-da, ikkala tur ham Rust standart kutubxonasida ko'p qo'llaniladi, String
va string slicelari UTF-8 da kodlangan.
Yangi String yaratish
Vec<T>
bilan mavjud bo'lgan bir xil amallarning ko'pchiligi String
bilan ham mavjud, chunki String
aslida qo'shimcha kafolatlar, cheklovlar va imkoniyatlarga ega baytlar vectori atrofida o'rash sifatida amalga oshiriladi. Vec<T>
va String
bilan bir xil ishlaydigan funksiyaga misol qilib, 8-11 ro'yxatda ko'rsatilgan yangi turdagi misolni yaratuvchi new
funksiyadir.
fn main() { let mut s = String::new(); }
Ushbu satr s
deb nomlangan yangi bo'sh qatorni yaratadi, biz keyin unga ma'lumotlarni yuklashimiz mumkin. Ko'pincha, biz stringni boshlamoqchi bo'lgan dastlabki ma'lumotlarga ega bo'lamiz. Buning uchun biz string literallari kabi Display
traittini amalga oshiradigan har qanday turda mavjud bo'lgan to_string
metotidan foydalanamiz. Ro'yxat 8-12 ikkita misolni ko'rsatadi.
fn main() { let malumot = "dastlabki tarkib"; let s = malumot.to_string(); // the method also works on a literal directly: let s = "dastlabki tarkib".to_string(); }
Bu kod dastlabki tarkib
ni o‘z ichiga olgan stringni yaratadi.
Satr literalidan String
yaratish uchun String::from
funksiyasidan ham foydalanishimiz mumkin. 8-13 ro'yxatdagi kod to_string
funksiyasidan foydalanadigan 8-12 ro'yxatdagi kodga teng:
fn main() { let s = String::from("dastlabki tarkib"); }
Stringlar juda ko'p narsalar uchun ishlatilganligi sababli, biz stringlar uchun juda ko'p turli xil umumiy API'lardan foydalanishimiz mumkin, bu bizga juda ko'p imkoniyatlarni taqdim etadi. Ulardan ba'zilari ortiqcha bo'lib tuyulishi mumkin, ammo ularning barchasi o'z joylariga ega! Bunday holda, String::from
va to_string
bir xil ishni bajaradi, shuning uchun tanlov sizga eng yoqqan uslubga bog'liq.
Yodda tutingki, stringlar UTF-8 bilan kodlangan, shuning uchun 8-14 ro'yxatda ko'rsatilganidek, biz ularga har qanday to'g'ri kodlangan ma'lumotlarni kiritishimiz mumkin.
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("Salom"); let hello = String::from("Здравствуйте"); let hello = String::from("Hola"); }
Bularning barchasi yaroqli String
qiymatlari.
Stringni yangilash
Agar siz unga ko'proq ma'lumot kiritsangiz, String
hajmi kattalashishi mumkin va uning tarkibi Vec<T>
tarkibidagi kabi o'zgarishi mumkin. Bundan tashqari, String
qiymatlarini birlashtirish uchun +
operatori yoki format!
makrosidan qulay foydalanish mumkin.
push_str
va push
yordamida stringga biriktirish
Biz 8-15 roʻyxatda koʻrsatilganidek, string boʻlagini qoʻshish uchun push_str
metodidan foydalanib String
ni kengaytirishimiz mumkin.
fn main() { let mut s = String::from("dastur"); s.push_str("chi"); }
Ushbu ikki qatordan keyin s
tarkibida dasturchi
bo'ladi. push_str
metodi string bo'lagini oladi, chunki biz parametrga egalik qilishni xohlamaymiz. Masalan, 8-16 roʻyxatdagi kodda biz uning mazmunini s1
ga qoʻshgandan soʻng s2
dan foydalanish imkoniyatiga ega boʻlishni xohlaymiz.
fn main() { let mut s1 = String::from("dastur"); let s2 = "chi"; s1.push_str(s2); println!("s2 - {s2}"); }
Agar push_str
metodi s2
ga egalik qilgan bo‘lsa, biz uning qiymatini oxirgi satrda chop eta olmaymiz. Biroq, bu kod biz kutgandek ishlaydi!
push
metodi parametr sifatida bitta belgini oladi va uni String
ga qo'shadi. 8-17 ro'yxatda push
metodi yordamida String
ga v
harfi qo'shiladi.
fn main() { let mut s = String::from("su"); s.push('v'); }
Natijada, s
tarkibida suv
bo'ladi.
+
operatori yoki format!
makrosidan foydalanib satrlarni birlashtirish
Ko'pincha siz ikkita mavjud satrni birlashtirishni xohlaysiz. Buning usullaridan biri 8-18 ro'yxatda ko'rsatilganidek, +
operatoridan foydalanishdir.
fn main() { let s1 = String::from("Salom, "); let s2 = String::from("Rust!"); let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used }
s3
qatorida Salom, Rust!
bo'ladi. Qo‘shishdan keyin s1
ning endi haqiqiy emasligi va s2
ga referenceni qo‘llaganimiz sababi +
operatoridan foydalanganda chaqirilayotgan metodning imzosi bilan bog‘liq.
+
operatori add
metodidan foydalanadi, uning imzosi quyidagicha ko'rinadi:
fn add(self, s: &str) -> String {
Standart kutubxonada siz umumiy va tegishli turlar yordamida aniqlangan add
ni ko'rasiz. Bu erda biz aniq turlarni almashtirdik, bu metodni String
qiymatlari bilan chaqirganimizda sodir bo'ladi. Biz 10-bobda generiklarni muhokama qilamiz.
Ushbu imzo bizga +
operatorining murakkab bitlarini tushunishimiz kerak bo'lgan maslahatlarni beradi.
Birinchidan, s2
&
belgisiga ega, ya'ni biz birinchi satrga ikkinchi satrning referenceni qo'shmoqdamiz. Buning sababi add
funksiyasidagi s
parametri: biz faqat String
ga &str
qo'shishimiz mumkin; biz ikkita String
qiymatini qo'sha olmaymiz. Lekin kuting – &s2
turi add
uchun ikkinchi parametrda ko‘rsatilganidek, &str
emas, &String
dir. Xo'sh, nima uchun 8-18 ro'yxatdagi kod kompilyatsiya bo'ladi?
add
chaqiruvida &s2
dan foydalanishimiz sababi shundaki, kompilyator &String
argumentini &str
ga majburlashi(coerce) mumkin. Biz add
metodini chaqirganimizda Rust deref coercion dan foydalanadi, bu erda &s2
ni &s2[..]
ga aylantiradi.
Biz 15-bobda coercion haqida batafsilroq gaplashamiz. add
s
parametriga egalik qilmaganligi sababli, s2
bu amaldan keyin ham haqiqiy String
bo'lib qoladi.
Ikkinchidan, imzoda add
self
egalik qilishini ko'rishimiz mumkin, chunki self
da &
yo'q. Bu shuni anglatadiki, 8-18-sonli ro'yxatdagi s1
add
chaqiruviga o'tkaziladi va bundan keyin endi yaroqsiz bo'ladi. Shunday qilib, let s3 = s1 + &s2;
har ikkala satrdan nusxa ko'chiradigan va yangisini yaratadiganga o'xshasa-da, bu statement aslida s1
ga egalik qiladi va s2
mazmunining nusxasini qo'shadi, va keyin natijaga egalik huquqini qaytaradi. Boshqacha qilib aytganda, u juda ko'p nusxa ko'chirayotganga o'xshaydi, lekin unday emas; implement qilish nusxalashdan ko'ra samaraliroq.
Agar biz bir nechta satrlarni birlashtirishimiz kerak bo'lsa, +
operatorining xatti-harakati noqulay bo'ladi:
fn main() { let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = s1 + "-" + &s2 + "-" + &s3; }
Bu vaqtda s
tic-tac-toe
bo'ladi. Barcha +
va "
belgilar bilan nima sodir bo'layotganini ko'rish qiyin. Murakkab qatorlarni birlashtirish uchun biz format!
makrosidan foydalanishimiz mumkin:
fn main() { let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{s1}-{s2}-{s3}"); }
Ushbu kod s
ni tic-tac-toe
ga ham o'rnatadi. format!
makrosi println!
kabi ishlaydi, lekin natijani ekranga chop etish o'rniga mazmuni bilan String
ni qaytaradi. Kodning format!
dan foydalanilgan versiyasini o‘qish ancha oson va format!
makrosi tomonidan yaratilgan kod bu chaqiruv uning parametrlaridan birortasiga egalik qilmasligi uchun havolalardan foydalanadi.
Stringlarni indekslash
Ko'pgina boshqa dasturlash tillarida stringdagi alohida belgilarga indeks bo'yicha murojaat qilish orqali kirish to'g'ri va keng tarqalgan operatsiya hisoblanadi. Biroq, agar siz Rust-da indekslash sintaksisidan foydalanib, String
qismlariga kirishga harakat qilsangiz, xatoga duch kelasiz. 8-19 ro'yxatdagi noto'g'ri kodni ko'rib chiqing.
fn main() {
let s1 = String::from("salom");
let h = s1[0];
}
Ushbu kod quyidagi xatoga olib keladi:
$ 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`
= help: the following other types implement trait `Index<Idx>`:
<String as Index<RangeFrom<usize>>>
<String as Index<RangeFull>>
<String as Index<RangeInclusive<usize>>>
<String as Index<RangeTo<usize>>>
<String as Index<RangeToInclusive<usize>>>
<String as Index<std::ops::Range<usize>>>
For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` due to previous error
Xato va eslatma Rust indekslashni qo'llab-quvvatlamasligini aytadi. Lekin nega yo'q? Bu savolga javob berish uchun Rust stringlarni xotirada qanday saqlashini muhokama qilishimiz kerak.
Ichki vakillik
String
turi - bu Vec<u8>
turidagi wrapper. Keling, 8-14 ro'yxatdagi to'g'ri kodlangan UTF-8 misol stringlarini ko'rib chiqaylik. Birinchidan, bu:
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("Salom"); let hello = String::from("Здравствуйте"); let hello = String::from("Hola"); }
Bunday holda, len
4 bo'ladi, ya'ni "Hola" qatorini saqlaydigan vektor 4 bayt uzunlikda. Bu harflarning har biri UTF-8 da kodlanganda 1 baytni oladi. Biroq, keyingi qator sizni hayratda qoldirishi mumkin. (E'tibor bering, bu qator arabcha 3 raqami emas, kirill alifbosining bosh harfi Ze bilan boshlanadi.)
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("Salom"); let hello = String::from("Здравствуйте"); let hello = String::from("Hola"); }
String uzunligi so'ralganda, siz 12 deb aytishingiz mumkin. Aslida, Rustning javobi 24: bu UTF 8 da “Здравствуйте” ni kodlash uchun zarur bo'lgan baytlar soni, chunki bu satrdagi har bir Unicode skalyar qiymati 2 bayt xotirani oladi. Shuning uchun, satr baytlaridagi indeks har doim ham haqiqiy Unicode skalyar qiymatiga mos kelmaydi. Namoyish qilish uchun ushbu yaroqsiz Rust kodini ko'rib chiqing:
let salom = "Здравствуйте";
let javob = &salom[0];
Siz allaqachon bilasizki, javob
birinchi harf bo'lgan З
bo'lmaydi. UTF-8 da kodlanganda, З
birinchi bayti 208
, ikkinchisi esa 151
, shuning uchun javob
aslida 208
bo'lishi kerakdek tuyuladi, lekin 208
o'z-o'zidan haqiqiy belgi emas. Agar foydalanuvchi ushbu qatorning birinchi harfini so'ragan bo'lsa, 208
ni qaytarish, ehtimol bu emas; ammo, bu Rust bayt indeksi 0 bo'lgan yagona ma'lumotdir. Foydalanuvchilar odatda bayt qiymatini qaytarishni xohlamaydilar, hatto satrda faqat lotin harflari bo‘lsa ham: agar &“hello”[0]
bayt qiymatini qaytaruvchi yaroqli kod bo‘lsa, u h
emas, 104
ni qaytaradi. .
Javob shundaki, kutilmagan qiymatni qaytarmaslik va darhol topilmasligi mumkin bo'lgan xatolarni keltirib chiqarmaslik uchun Rust ushbu kodni umuman kompilyatsiya qilmaydi va ishlab chiqish jarayonida tushunmovchiliklarning oldini oladi.
Baytlar va skalyar qiymatlar va grafema klasterlari!
UTF-8 bilan bog'liq yana bir nuqta shundaki, Rust nuqtai nazaridan satrlarga qarashning uchta mos usuli mavjud: baytlar, skalyar qiymatlar va grafema klasterlari (biz harflar deb ataydigan narsaga eng yaqin narsa).
Devanagari skriptida yozilgan hindcha “नमस्ते” so'ziga qarasak, u u8
qiymatlari vektori sifatida saqlanadi, bu quyidagicha ko'rinadi:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
Bu 18 bayt va kompyuterlar oxir-oqibat bu ma'lumotlarni qanday saqlaydi. Agar biz ularni Rustning char
turiga ega bo'lgan Unicode skalyar qiymatlari sifatida qarasak, bu baytlar quyidagicha ko'rinadi:
['न', 'म', 'स', '्', 'त', 'े']
Bu yerda oltita char
qiymati bor, lekin to'rtinchi va oltinchi harflar emas: ular o'z-o'zidan ma'noga ega bo'lmagan diakritikdir. Nihoyat, agar baytlarni grafema klasterlari sifatida ko'rib chiqsak, inson to'rt harfli hindcha so'z deb ataydigan narsani olamiz:
["न", "म", "स्", "ते"]
Rust kompyuterlar saqlaydigan ma'lumotlar qatorini talqin qilishning turli usullarini taqdim etadi, shunda ma'lumotlar qaysi inson tilida bo'lishidan qat'i nazar, har bir dastur kerakli talqinni tanlashi mumkin.
Rust bizga belgini olish uchun String
ga indekslashga ruxsat bermasligining yakuniy sababi shundaki, indekslash operatsiyalari doimo konstanta(doimiy) vaqtni oladi (O(1)). Ammo Strin
g bilan ishlashni kafolatlab bo‘lmaydi, chunki Rust qancha to‘g‘ri belgilar mavjudligini aniqlash uchun tarkibni boshidan indeksgacha bosib o‘tishi kerak edi.
String bo'laklari
Satrni indekslash ko'pincha noto'g'ri fikrdir, chunki satrni indekslash operatsiyasining qaytish turi qanday bo'lishi kerakligi aniq emas: bayt qiymati, belgi, grafema klasteri yoki satr bo'lagi. Agar chindan ham string bo'laklarini yaratish uchun indekslardan foydalanish kerak bo'lsa, Rust sizdan aniqroq bo'lishingizni so'raydi.
Bitta raqam bilan []
yordamida indeksatsiya qilish o'rniga, muayyan baytlarni o'z ichiga olgan string bo'laklarini yaratish uchun diapazon bilan []
dan foydalanishingiz mumkin:
#![allow(unused)] fn main() { let salom = "Здравствуйте"; let s = &salom[0..4]; }
Bu erda s
qatorning dastlabki 4 baytini o'z ichiga olgan &str
bo'ladi.
Avvalroq, biz ushbu belgilarning har biri 2 baytdan iborat bo'lganligini aytib o'tgan edik, ya'ni s
Зд
bo'ladi.
Agar biz &salom[0..1]
kabi belgi baytlarining faqat bir qismini kesishga harakat qilsak, Rust ish runtimeda xuddi vektordagi yaroqsiz indeksga kirish kabi panic qo'yadi:
$ 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
Ehtiyotkorlik bilan string bo'laklarni yaratish uchun diapazonlardan foydalanishingiz kerak, chunki bu dasturni buzishi mumkin.
Stringlarni takrorlash usullari
String bo'laklari bilan ishlashning eng yaxshi usuli - belgilar yoki baytlarni xohlaysizmi, aniq bo'lishdir. Unicode skalyar qiymatlari uchun chars
metodidan foydalaning. “Зд” da chars
ni chaqirish char
turidagi ikkita qiymatni ajratib turadi va qaytaradi va har bir elementga kirish uchun natijani takrorlashingiz mumkin:
#![allow(unused)] fn main() { for c in "Зд".chars() { println!("{c}"); } }
Ushbu kod quyidagilarni chop etadi:
З
д
Shu bilan bir qatorda, bytes
metodi boshqa domenga mos kelishi mumkin bo'lgan har bir baytni qaytaradi:
#![allow(unused)] fn main() { for b in "Зд".bytes() { println!("{b}"); } }
Ushbu kod ushbu satrni tashkil etuvchi to'rt baytni chop etadi:
208
151
208
180
Lekin esda tutingki, joriy Unicode skalyar qiymatlari 1 baytdan ortiq bo'lishi mumkin.
Devanagari skriptidagi kabi satrlardan grafema klasterlarini olish juda murakkab, shuning uchun bu funksiya standart kutubxona tomonidan ta'minlanmagan. Agar sizga ushbu funksiya kerak bo'lsa, crates.io saytida cratelar mavjud.
Stringlar unchalik oddiy emas
Xulosa qilib aytganda, satrlar murakkab. Turli xil dasturlash tillari ushbu murakkablikni dasturchiga qanday taqdim etish bo'yicha turli xil tanlovlar qiladi. Rust String
ma'lumotlarini to'g'ri ishlashni barcha Rust dasturlari uchun standart xatti-harakatga aylantirishni tanladi, bu esa dasturchilar UTF-8 ma'lumotlari bilan ishlash haqida oldindan ko'proq o'ylashlari kerakligini anglatadi. Ushbu o'zaro kelishuv boshqa dasturlash tillarida ko'rinadiganidan ko'ra ko'proq stringlarning murakkabligini ochib beradi, lekin keyinchalik ishlab chiqish jarayonida paydo bo'lishi mumkin bo'lgan ASCII bo'lmagan belgilar xatolarini qayta ishlash zaruratini oldini oladi.
Yaxshi xabar shundaki, standart kutubxona ushbu murakkab vaziyatlarni to'g'ri hal qilishga yordam beradigan String
va &str
turlaridan iborat ko'plab funksiyalarni taklif etadi. Satrda qidirish uchun contains
va qator qismlarini boshqa satr bilan almashtirish uchun replace
kabi foydali metodlar uchun texnik hujjatlarni ko'rib chiqing.
Keling, biroz murakkabroq narsaga o'taylik: HashMap!
Hash Maplarda bog'langan qiymatlarga ega kalitlarni saqlash
Umumiy to'plamlarimizning oxirgisi hash map. HashMap<K, V>
turi K
turidagi kalitlarning V
turidagi qiymatlarga xaritasini xeshlash funksiyasi yordamida saqlaydi, bu kalit va qiymatlarni xotiraga qanday joylashtirishini belgilaydi. Ko'pgina dasturlash tillari bunday ma'lumotlar strukturasini qo'llab-quvvatlaydi, lekin ular ko'pincha bir nechtasini nomlash uchun hash, map, object, hash table, dictionary, yoki associative array kabi boshqa nomlardan foydalanadilar.
Hash maplar ma'lumotlarni vectorlar bilan bo'lgani kabi indeks yordamida emas, balki har qanday turdagi kalit yordamida qidirmoqchi bo'lsangiz foydali bo'ladi. Misol uchun, o'yinda siz har bir jamoaning balini hesh-mapda kuzatib borishingiz mumkin, unda har bir kalit jamoaning nomi va qiymatlar har bir jamoaning ballidir. Jamoa nomi berilgan bo'lsa, siz uning ballini olishingiz mumkin.
Ushbu bo'limda biz hesh-mapllarining asosiy API-ni ko'rib chiqamiz, ammo yana ko'plab foydali funksiyalar standart kutubxona tomonidan HashMap<K, V>
da belgilangan funksiyalarda yashiringan.
Har doimgidek, qo'shimcha ma'lumot olish uchun standart kutubxona texnik hujjatlarini tekshiring.
Yangi Hash Map yaratish
Bo'sh hesh mapni yaratishning bir usuli - new
dan foydalanish va insert
bilan elementlarni qo'shish. 8-20 ro'yxatda biz nomlari Yashil va Sariq bo'lgan ikkita jamoaning ballarini kuzatib boramiz. Yashil jamoa 10 ball bilan, Sariq jamoa esa 50 ball bilan boshlanadi.
fn main() { use std::collections::HashMap; let mut ballar = HashMap::new(); ballar.insert(String::from("Yashil"), 10); ballar.insert(String::from("Sariq"), 50); }
E'tibor bering, biz birinchi navbatda standart kutubxonaning to'plamlar qismidagi HashMap
dan foydalanishimiz kerak. Bu quyidagicha bo'ladi use std::collections::HashMap;
. Bizning uchta keng tarqalgan to'plamlarimiz orasida bu eng kam qo'llaniladi, shuning uchun u muqaddimada avtomatik ravishda kiritilgan funtsiyalarga kiritilmagan. Hash Maplar standart kutubxonadan ham kamroq qo'llab-quvvatlanadi; masalan, ularni yaratish uchun o'rnatilgan makros mavjud emas.
Vectorlar singari, hash maplar ham o'z ma'lumotlarini heapda saqlaydi. Ushbu HashMap
da String
turidagi kalitlar va i32
turidagi qiymatlar mavjud. Vectorlar singari, hash maplar ham bir xildir: barcha kalitlar bir-biri bilan bir xil turdagi va barcha qiymatlar bir xil turga ega bo'lishi kerak.
HashMap-dagi ma'lumotlarga kirish
Biz 8-21 ro'yxatda ko'rsatilganidek, get
metodiga kalitni taqdim etish orqali hash mapdan qiymat olishimiz mumkin.
fn main() { use std::collections::HashMap; let mut ballar = HashMap::new(); ballar.insert(String::from("Yashil"), 10); ballar.insert(String::from("Sariq"), 50); let jamoa_nomi = String::from("Yashil"); let ball = ballar.get(&jamoa_nomi).copied().unwrap_or(0); }
Bu yerda ball
Yashil jamoa bilan bog'liq qiymatga ega bo'ladi va natija 10
bo'ladi. get
metodi Option<&V>
ni qaytaradi; agar hesh-mapda ushbu kalit uchun qiymat bo'lmasa, get
None
ni qaytaradi. Bu dastur Option<&i32>
emas, Option<i32>
olish uchun copied
ga murojaat qilib Option
ni boshqaradi, so'ngra unwrap_or
ballar
da ushbu kalit uchun ma'lumotlar bo'lmasa, ballni nolga o'rnatish uchun.
Biz hesh-mapdagi har bir kalit/qiymat juftligini vectorlar bilan bo'lgani kabi, for
siklidan foydalangan holda takrorlashimiz mumkin:
fn main() { use std::collections::HashMap; let mut ballar = HashMap::new(); ballar.insert(String::from("Yashil"), 10); ballar.insert(String::from("Sariq"), 50); for (key, value) in &scores { println!("{key}: {value}"); } }
Ushbu kod har bir juftlikni tasodifiy tartibda chop etadi:
Sariq: 50
Yashil: 10
Hash Maplar va Ownership(Egalik)
Copy
traitini amalga oshiradigan turlar uchun, masalan, i32
, qiymatlar hesh-mapiga ko'chiriladi. String
kabi tegishli qiymatlar uchun qiymatlar boshqa joyga koʻchiriladi va 8-22 roʻyxatda koʻrsatilganidek, hesh-map ushbu qiymatlarning egasi boʻladi.
fn main() { use std::collections::HashMap; let maydon_nomi = String::from("Sevimli rang"); let maydon_qiymati = String::from("Yashil"); let mut map = HashMap::new(); map.insert(maydon_nomi, maydon_qiymati); // maydon_nomi va maydon_qiymati hozirda yaroqsiz, ulardan foydalanib ko'ring va qanday // kompilyator xatosiga yo'l qo'yganingizni ko'ring! }
maydon_nomi
va maydon_qiymati
o'zgaruvchilari qiymatlari insert
metodini chaqirish orqali HashMap-ga ko'chirilgandan keyin foydalana olmaymiz.
Agar biz HashMap-ga qiymatlarga referencelar kiritsak, ular HashMap-ga ko'chirilmaydi. Murojaatlar ko'rsatadigan qiymatlar hech bo'lmaganda hesh-mapda amal qiladigan vaqt davomida amal qilishi kerak. Biz ushbu muammolar haqida 10-bobning "Lifetime bilan referencelarni tekshirish" bo'limida ko'proq gaplashamiz.
Hash Mapni yangilash
Kalit va qiymat juftlarining soni o'sishi mumkin bo'lsa-da, har bir noyob kalit bir vaqtning o'zida u bilan bog'langan faqat bitta qiymatga ega bo'lishi mumkin (lekin aksincha emas: masalan, Yashil jamoa ham, Sariq jamoa ham ballar
hash-mapida saqlangan 10 qiymatiga ega bo'lishi mumkin).
Hash-mapdagi ma'lumotlarni o'zgartirmoqchi bo'lganingizda, kalit allaqachon tayinlangan qiymatga ega bo'lgan holatda qanday ishlashni hal qilishingiz kerak. Eski qiymatni butunlay e'tiborsiz qoldirib, eski qiymatni yangi qiymat bilan almashtirishingiz mumkin. Eski qiymatni saqlab qolishingiz va yangi qiymatni e'tiborsiz qoldirishingiz mumkin, faqat kalitda qiymat bo'lmasa, yangi qiymat qo'shishingiz mumkin. Yoki eski qiymat va yangi qiymatni birlashtira olasiz. Keling, bularning har birini qanday qilishni ko'rib chiqaylik!
Qiymatni qayta yozish
Agar biz kalit va qiymatni hash-mapga kiritsak va keyin boshqa qiymat bilan bir xil kalitni kiritsak, bu kalit bilan bog'langan qiymat almashtiriladi. Ro'yxat 8-23dagi kod ikki marta insert
ni chaqirsa ham, hash-mapda faqat bitta kalit/qiymat juftligi bo'ladi, chunki biz har ikki marta Yashil jamoa kaliti qiymatini kiritamiz.
fn main() { use std::collections::HashMap; let mut ballar = HashMap::new(); ballar.insert(String::from("Yashil"), 10); ballar.insert(String::from("Yashil"), 25); println!("{:?}", ballar); }
Bu kod {"Yashil": 25}
ni chop etadi. 10
ning asl qiymati ustiga yozildi.
Kalit va qiymatni faqat kalit mavjud bo'lmaganda qo'shish
Hash Mapda ma'lum bir kalit allaqachon qiymatga ega yoki yo'qligini tekshirish odatiy holdir, keyin quyidagi amallarni bajaring: agar kalit hash-mapda mavjud bo'lsa, mavjud qiymat avvalgidek qolishi kerak. Agar kalit mavjud bo'lmasa, insert va uning qiymatini kiriting.
Hash Mapda buning uchun entry
deb nomlangan maxsus API mavjud bo'lib, siz tekshirmoqchi bo'lgan kalitni parametr sifatida qabul qiladi. entry
metodining qaytish qiymati Entry
nomli enum bo‘lib, u mavjud yoki bo‘lmasligi mumkin bo‘lgan qiymatni ifodalaydi. Aytaylik, biz Sariq jamoa uchun kalitning u bilan bog'liq qiymati bor-yo'qligini tekshirmoqchimiz. Agar shunday bo'lmasa, biz 50 qiymatini qo'shishni xohlaymiz va Yashil jamoa uchun ham xuddi shunday. Entry
API-dan foydalanib, kod Ro'yxat 8-24 kabi ko'rinadi.
fn main() { use std::collections::HashMap; let mut ballar = HashMap::new(); ballar.insert(String::from("Yashil"), 10); ballar.entry(String::from("Sariq")).or_insert(50); ballar.entry(String::from("Yashil")).or_insert(50); println!("{:?}", ballar); }
Entry
da or_insert
metodi mos keladigan Entry
kaliti qiymatiga o'zgaruvchan referenceni qaytarish uchun belgilangan, agar bu kalit mavjud bo'lsa, parametrni ushbu kalit uchun yangi qiymat sifatida kiritadi va yangi qiymatga o'zgaruvchan referenceni qaytaradi. Ushbu metod mantiqni o'zingiz yozishdan ko'ra ancha toza va u xavfsizroq va borrowing qoidalariga mos keladi.
8-24-raqamdagi kodni ishga tushirish {"Sariq": 50, "Yashil": 10}
chop etiladi. entry
ga birinchi chaqiruv 50 qiymatiga ega Sariq jamoa uchun kalitni kiritadi, chunki sariq jamoada allaqachon qiymat yo'q. entry
ga ikkinchi chaqiruv hash-mapni o'zgartirmaydi, chunki Yashil jamoa allaqachon 10 qiymatiga ega.
Eski qiymat asosida yangi qiymatni yangilash
Hash-Maplar uchun yana bir keng tarqalgan foydalanish holati kalit qiymatini izlash va keyin uni eski qiymat asosida yangilashdir. Misol uchun, 8-25 ro'yxatda har bir so'z ba'zi matnda necha marta takrorlanganini hisoblaydigan kodni ko'rsatadi. Biz kalit sifatida so'zlar bilan hash-mapdan foydalanamiz va bu so'zni necha marta ko'rganimizni kuzatib borish uchun qiymatni oshiramiz. Agar so'zni birinchi marta ko'rayotgan bo'lsak, avval 0 qiymatini kiritamiz.
fn main() { use std::collections::HashMap; let matn = "salom rust qiziqarli rust"; let mut map = HashMap::new(); for soz in matn.split_whitespace() { let hisoblash = map.entry(soz).or_insert(0); *hisoblash += 1; } println!("{:?}", map); }
Bu kod {"qiziqarli": 1, "salom": 1, "rust": 2}
ni chop etadi. Siz boshqa tartibda chop etilgan bir xil kalit/qiymat juftliklarini ko'rishingiz mumkin: “Hash-Mapdagi qiymatlarga kirish” bo'limidan hash-mapni takrorlash ixtiyoriy tartibda sodir bo'lishini eslang.
split_whitespace
metodi matn
qiymatining bo'sh joy bilan ajratilgan pastki bo'limlari ustidan iteratorni qaytaradi.or_insert
metodi belgilangan kalit qiymatiga o'zgaruvchan havolani (&mut V
) qaytaradi. Bu yerda biz o'zgaruvchan referenceni hisoblash
o'zgaruvchisida saqlaymiz, shuning uchun bu qiymatni belgilash uchun avval yulduzcha (*
) yordamida hisoblash
ga murojaat qilishimiz kerak. O'zgaruvchan reference for
siklining oxirida ko'lamdan chiqib ketadi, shuning uchun bu o'zgarishlarning barchasi xavfsiz va borrowing qoidalari bilan ruxsat etiladi.
Hashing funksiyalari
Odatda, HashMap
SipHash nomli hashlash funksiyasidan foydalanadi, u 1 hash-jadvallari ishtirokidagi Xizmatni rad etish (DoS) hujumlariga qarshilik ko'rsatishi mumkin. Bu mavjud bo'lgan eng tezkor hashlash algoritmi emas, lekin ishlashning pasayishi bilan birga keladigan yaxshi xavfsizlik uchun kelishuv bunga arziydi. Agar siz kodingizni profilga kiritsangiz va standart hash funksiyasi sizning maqsadlaringiz uchun juda sekin ekanligini aniqlasangiz, boshqa hasherni belgilash orqali boshqa funksiyaga o'tishingiz mumkin. hasher bu BuildHasher
traitini amalga oshiradigan tur. Traitlar va ularni qanday implement qilish haqida 10-bobda gaplashamiz. Siz o'zingizning hasheringizni noldan implement qilishingiz shart emas; crates.io-da boshqa Rust foydalanuvchilari tomonidan baham ko'rilgan kutubxonalar mavjud bo'lib, ular ko'plab umumiy hashlash algoritmlarini implement qiladigan hasherlarni ta'minlaydi.
Xulosa
Vectorlar, stringlar va hash-maplar ma'lumotlarni saqlash, kirish va o'zgartirish kerak bo'lganda dasturlarda zarur bo'lgan katta hajmdagi funksionallikni ta'minlaydi. Mana endi hal qilish uchun tayyorlanishingiz kerak bo'lgan ba'zi mashqlar:
-
Butun sonlar roʻyxati berilgan boʻlsa, vectordan foydalaning va roʻyxatning medianasini (tartiblanganda, oʻrtadagi qiymat) va rejimni (koʻpincha sodir boʻladigan qiymat; bu yerda hash-map foydali boʻladi) qaytaring.
-
Satrlarni pig lotin tiliga aylantiring. Har bir so'zning birinchi undoshi so'z oxiriga ko'chiriladi va "ay" qo'shiladi, shuning uchun "birinchi" "birinchi-ay" bo'ladi. Unli tovush bilan boshlangan so‘zlarning oxiriga “hay” qo‘shiladi (“olma” “olma-hay”ga aylanadi). UTF-8 kodlash haqidagi tafsilotlarni yodda tuting!
-
Hash Map va vectorlardan foydalanib, foydalanuvchiga kompaniyadagi bo'limga xodimlarning ismlarini qo'shishga ruxsat berish uchun matn interfeysini yarating. Masalan, "Asilbekni muhandislikka qo'shish" yoki "Sardorni savdoga qo'shish". Keyin foydalanuvchi bo'limdagi barcha odamlar yoki kompaniyadagi barcha odamlar ro'yxatini bo'limlar bo'yicha, alifbo tartibida tartiblangan holda olishiga ruxsat bering.
Standart kutubxona API texnik hujjatlari vectorlar, stringlar va hash-maplarda ushbu mashqlar uchun foydali bo'lgan usullarni tavsiflaydi!
Biz operatsiyalar muvaffaqiyatsiz bo'lishi mumkin bo'lgan yanada murakkab dasturlarga kirishmoqdamiz, shuning uchun xatolarni hal qilishni muhokama qilish uchun ajoyib vaqt. Qani kettik.
Xatolar bilan ishlash
Xatolar dasturiy ta'minotdagi hayot haqiqatidir, shuning uchun Rust nimadir noto'g'ri bo'lgan vaziyatlarni hal qilish uchun bir qator xususiyatlarga ega. Ko'p hollarda Rust sizdan xatolik ehtimolini tan olishingizni va kodingizni kompilyatsiya qilishdan oldin ba'zi choralarni ko'rishingizni talab qiladi. Ushbu talab sizning kodingizni ishlab chiqarishga joylashtirishdan oldin xatolarni aniqlashingiz va ularni to'g'ri hal qilishingizni ta'minlash orqali dasturingizni yanada mustahkam qiladi!
Rust xatolarni ikkita asosiy toifaga ajratadi: tiklash mumkin va tiklab bo‘lmaydigan xatolar. file not found (fayl topilmadi) xatosi kabi tiklanadigan xatolik uchun biz muammo haqida foydalanuvchiga xabar berishni va operatsiyani qaytadan urinib ko'rishni xohlaymiz. Qayta tiklanmaydigan xatolar har doim xato belgilaridir, masalan, array oxiridan tashqaridagi joyga kirishga urinish va shuning uchun biz dasturni darhol to'xtatmoqchimiz.
Aksariyat tillar bu ikki turdagi xatolarni farqlamaydi va istisnolar(exceptions) kabi mexanizmlardan foydalangan holda ikkalasini ham xuddi shunday hal qiladi. Rustda istisnolar yo'q. Buning o'rniga, u tiklanadigan xatolar uchun Result<T, E>
turiga va dastur tuzatib bo'lmaydigan xatolikka duch kelganda jarayonni to'xtatuvchi panic!
makrosiga ega. Bu bob avval panic!
chaqirish haqida so'z boradi, soʻngra Result<T, E>
qiymatlarini qaytarish haqida so'z boradi. Bundan tashqari, biz xatolikdan xalos bo'lish yoki bajarishni to'xtatish haqida qaror qabul qilishda fikrlarni o'rganamiz.
panic!
bilan tuzatib bo'lmaydigan xatolar
Ba'zida sizning kodingizda yomon narsalar sodir bo'ladi va siz bu haqda hech narsa qila olmaysiz. Bunday hollarda Rustda panic!
makrosi mavjud. Amalda panic chaqirishning ikki yo'li mavjud: kodimizni panic chiqaradigan harakatni amalga oshirish (masalan, arrayning oxiridan o'tib kirish) yoki aniq panic!
makrosini chaqirish.
Ikkala holatda ham biz dasturimizda panicni keltirib chiqaramiz. Odatiy bo'lib, bu panic muvaffaqiyatsizlik xabarini chop etadi, dasturni bajarish paytida ajratilgan resurslarni tozalaydi, stackni tozalaydi va ishdan chiqadi. Atrof-muhit o'zgaruvchisi orqali siz panic paydo bo'lganda panic manbasini kuzatishni osonlashtirish uchun Rust chaqiruvlar srackini ko'rsatishi ham mumkin.
panicga javoban stackni bo'shatish yoki bekor qilish
Odatiy bo'lib, panic paydo bo'lganda, dastur o'chiriladi, ya'ni Rust stekni zaxiraga olib chiqadi va duch kelgan har bir funksiyadan ma'lumotlarni tozalaydi. Biroq, bu orqaga qaytish va tozalash juda ko'p ishdir. Rust, shuning uchun dasturni tozalamasdan tugatadigan darhol bekor qilishning muqobilini tanlashga imkon beradi.
Dastur ishlatgan xotira keyinchalik operatsion tizim tomonidan tozalanishi kerak bo'ladi. Agar loyihangizda natijada olingan binary faylni iloji boricha kichikroq qilishingiz kerak bo'lsa, Cargo.toml faylingizdagi tegishli
[profile]
bo'limlarigapanic = 'abort'
qo'shish orqali panic bo'yicha bo'shatishdan bekor qilishga o'tishingiz mumkin. Misol uchun, agar siz bo'shatish rejimida panic holatini to'xtatmoqchi bo'lsangiz, quyidagilarni qo'shing:[profile.release] panic = 'abort'
Keling, oddiy dasturda panic!
deb chaqirib ko'raylik:
Fayl nomi: src/main.rs
fn main() { panic!("halokatli xato"); }
Dasturni ishga tushirganingizda, siz shunga o'xshash narsani ko'rasiz:
$ 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 'halokatli xato', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
panic!
chaqiruvi oxirgi ikki qatordagi xato xabarini keltirib chiqaradi.
Birinchi qatorda panic haqidagi xabarimiz va manba kodimizdagi panic sodir bo'lgan joy ko'rsatilgan: src/main.rs:2:5 bu bizning src/main.rs faylimizning ikkinchi qatori, beshinchi belgisi ekanligini bildiradi.
Bunday holda, ko'rsatilgan qator bizning kodimizning bir qismidir va agar biz ushbu qatorga o'tsak, biz panic!
makro chaqiruvini ko'ramiz. Boshqa hollarda, panic!
chaqiruvi bizning kodimiz chaqiradigan kodda bo'lishi mumkin, va xato xabari tomonidan bildirilgan fayl nomi va satr raqami boshqa birovning kodi bo'ladi, bu yerda panic!
makro chaqiriladi, kodimizning oxir-oqibat panic!
chaqiruviga olib kelgan qatori emas. Kodimizning muammoga sabab bo'lgan qismini aniqlash uchun panic!
chaqiruvidan kelgan funksiyalarning backtracedan foydalanishimiz mumkin. Keyinchalik orqaga qaytish(backtraces) haqida batafsilroq gaplashamiz.
panic!
Backtracedan foydalanish
Yana bir misolni ko‘rib chiqaylik, panic!
chaqiruvi to‘g‘ridan-to‘g‘ri makroni chaqiruvchi kodimizdan emas, balki kodimizdagi xato tufayli kutubxonadan kelganida qanday bo‘lishini ko‘raylik. 9-1 ro'yxatida joriy indekslar doirasidan tashqarida vectordagi indeksga kirishga harakat qiladigan ba'zi kodlar mavjud.
Fayl nomi: src/main.rs
fn main() { let v = vec![1, 2, 3]; v[99]; }
Bu erda biz vectorimizning 100-elementiga kirishga harakat qilyapmiz (bu indeks 99-da, chunki indekslash noldan boshlanadi), lekin vectorda faqat 3 ta element mavjud.
Bunday vaziyatda Rust panic qo'yadi. []
dan foydalanish elementni qaytarishi kerak, lekin siz noto'g'ri indeksni o'tkazyapsiz: Rust qaytara oladigan element yo'q.
C tilida ma'lumotlar strukturasining oxiridan tashqarida o'qishga urinish aniqlanmagan xatti-harakatlardir. Siz xotirada ma'lumotlar strukturasidagi ushbu elementga mos keladigan har qanday joyni olishingiz mumkin, garchi xotira ushbu tuzilishga tegishli bo'lmasa ham. Bu buffer overread deb ataladi va agar tajovuzkor indeksni ma'lumotlar tuzilmasidan keyin saqlanadigan ma'lumotlarni o'qishga ruxsat etilmasligi kerak bo'lgan tarzda o'zgartira olsa, xavfsizlik zaifliklariga olib kelishi mumkin.
Dasturingizni bunday zaiflikdan himoya qilish uchun, agar siz mavjud bo'lmagan indeksdagi elementni o'qishga harakat qilsangiz, Rust ishlashni to'xtatadi va davom etishni rad etadi. Keling, sinab ko'raylik va ko'ramiz:
$ 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 xatolik main.rs
ning 4-qatoriga ishora qiladi, bu yerda biz 99 indeksiga kirishga harakat qilamiz. Keyingi eslatma satrida biz xatoga nima sabab bo'lganligining backtraceni olish uchun RUST_BACKTRACE
muhit o'zgaruvchisini o'rnatishimiz mumkinligini aytadi. backtrace - bu dastur bajarilishining ma'lum bir nuqtasiga qadar chaqirilgan barcha funktsiyalar ro'yxatini. Backtrace boshqa tillarda bo'lgani kabi Rust tilida ham xuddi shunday ishlaydi. Shuning uchun, biz boshqa joylarda bo'lgani kabi, backtrace ma'lumotlarini o'qishni tavsiya qilamiz - siz yozgan fayllar haqidagi ma'lumotlarni ko'rmaguningizcha yuqoridan pastga o'qing. Bu muammo paydo bo'lgan joy. Yuqoridagi satrlar sizning kodingiz chaqirgan koddir; Quyidagi satrlar sizning kodingiz deb ataladigan koddir. Ushbu oldingi va keyingi qatorlar asosiy Rust kodini, standart kutubxona kodi yoki siz foydalanayotgan cratelarni o'z ichiga olishi mumkin. RUST_BACKTRACE
muhit oʻzgaruvchisini 0 dan tashqari istalgan qiymatga oʻrnatish orqali backtraceni olishga harakat qilaylik. 9-2 ro'yxati siz ko'rgan narsaga o'xshash natijani ko'rsatadi.
$ 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/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
2: core::panicking::panic_bounds_check
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9
6: panic::main
at ./src/main.rs:4:5
7: core::ops::function::FnOnce::call_once
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Bu juda ko'p natija! Siz ko'rgan aniq chiqish operatsion tizimingiz va Rust versiyasiga qarab farq qilishi mumkin. Ushbu ma'lumotlar bilan backtrace uchun debug symbollari yoqilgan bo'lishi kerak. Nosozliklarni tuzatish belgilari standart boʻyicha --release
opsiyasiz cargo build
yoki cargo run
funksiyalaridan foydalanilganda yoqilgan.
9-2 ro'yxatdagi chiqishda backtracening 6-qatori muammoni keltirib chiqarayotgan loyihamizdagi chiziqqa ishora qiladi: src/main.rs ning 4-qatori. Agar dasturimiz panic bo'lishini istamasak, biz yozgan faylni eslatib o'tgan birinchi qatorda ko'rsatilgan joydan tekshirishni boshlashimiz kerak. 9-1 ro'yxatida biz panic qo'yadigan kodni ataylab yozganmiz. panicni tuzatish usuli vector indekslari doirasidan tashqarida elementni so'ramaslikdir. Kelajakda sizning kodingiz panic qo'zg'atganda, siz panic qo'zg'ash uchun kod qanday harakatlarni amalga oshirayotganini va buning o'rniga kod nima qilishi kerakligini aniqlashingiz kerak bo'ladi.
Biz panic!
makrosi va qachon panic!
qo'llashimiz kerak va qachon foydalanmaslik kerakligi haqidagi muhokamaga qaytamiz! Ushbu bobning keyingi qismida “panic!
qo'yish yoki panic!
qo'ymaslik” haqida gapalashamiz.
Keyinchalik, Result
yordamida xatoni qanday tiklashni ko'rib chiqamiz.
Result
bilan tiklanadigan xatolar
Ko'pgina xatolar dasturni butunlay to'xtatishni talab qiladigan darajada jiddiy emas. Ba'zan, funksiya bajarilmasa, bu siz osongina talqin qilishingiz va javob berishingiz mumkin bo'lgan sababdir. Misol uchun, agar siz faylni ochishga urinib ko'rsangiz va fayl mavjud bo'lmagani uchun bu operatsiya bajarilmasa, jarayonni tugatish o'rniga faylni yaratishni xohlashingiz mumkin.
2-bobdagi “Potentsial muvaffaqiyatsizlikni Result
bilan hal qilish” bo'limini eslang: biz u yerda muvaffaqiyatsizliklarni hal qilish uchun ikkita variantga ega bo'lgan Ok
va Err
varianti bo'lgan Result
enumidan foydalanganmiz. Enumning o'zi quyidagicha aniqlanadi:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
T
va E
umumiy turdagi parametrlardir: biz generiklarni 10-bobda batafsilroq muhokama qilamiz. Siz hozir bilishingiz kerak bo'lgan narsa shundaki, T
Ok
variantida muvaffaqiyatli holatda qaytariladigan qiymat turini bildiradi, va E
Err
(Xato) variantida xatolik holatida qaytariladigan xato turini ifodalaydi. Result
da ushbu umumiy turdagi parametrlar mavjud bo'lganligi sababli, biz qaytarmoqchi bo'lgan muvaffaqiyatli qiymat va xato qiymati har xil bo'lishi mumkin bo'lgan turli vaziyatlarda Result
turidan va unda belgilangan funksiyalardan foydalanishimiz mumkin.
Keling, Result
qiymatini qaytaruvchi funksiyani chaqiraylik, chunki funksiya bajarilmasligi mumkin. 9-3 ro'yxatda biz faylni ochishga harakat qilamiz.
Fayl nomi: src/main.rs
use std::fs::File; fn main() { let fayl_ochish = File::open("olma.txt"); }
File::open
return(qaytish) turi Result<T, E>
dir. File::open
ni amalga oshirishdagi umumiy T
turi muvaffaqiyatli qabul qilingan qiymat turiga, std::fs::File
ga, ya'ni fayl deskriptoriga mos keladi. Xato qiymatida ishlatiladigan E
turi std::io::Error
. Qaytish(return) turi File::open
ga chaqiruv muvaffaqiyatli bo'lishi va biz o'qishimiz yoki yozishimiz mumkin bo'lgan fayl handleni qaytarishi mumkinligini anglatadi. Funksiya chaqiruvi ham muvaffaqiyatsiz bo'lishi mumkin: masalan, fayl mavjud bo'lmasligi yoki faylga kirish uchun ruxsatimiz bo'lmasligi mumkin. File::open
funksiyasi muvaffaqiyatli yoki muvaffaqiyatsiz bo'lganligini va bir vaqtning o'zida bizga fayl identifikatori yoki xato haqida ma'lumot beradigan metodga ega bo'lishi kerak. Ushbu ma'lumot aynan Result
enumini bildiradi.
Agar File::open
muvaffaqiyatli bo'lsa, fayl_ochish
qiymati fayl identifikatorini o'z ichiga olgan Ok
misoli bo'ladi.
Muvaffaqiyatsiz bo'lgan taqdirda, fayl_ochish
dagi qiymat Err
misoli bo'lib, sodir bo'lgan xato turi haqida qo'shimcha ma'lumotni o'z ichiga oladi.
File::open
qiymatiga qarab turli amallarni bajarish uchun 9-3-Ro'yxatdagi kodga o'zgartirishimiz kerak. 9-4 ro'yxatda biz 6-bobda muhokama qilgan asosiy tool - match
ifodasi yordamida Result
ni boshqarishning bir usuli ko'rsatilgan.
Fayl nomi: src/main.rs
use std::fs::File; fn main() { let fayl_ochish = File::open("olma.txt"); let fayl = match fayl_ochish { Ok(file) => file, Err(error) => panic!("Faylni ochishda muammo: {:?}", error), }; }
E'tibor bering, Option
enumi kabi, Result
enumi va uning variantlari avtomatik import (prelude) orqali kiritilgan, shuning uchun biz match
qatoridagi Ok
va Err
variantlaridan oldin Result::
ni belgilashimiz shart emas.
Natija Ok
bo'lsa, bu kod Ok
variantidan ichki file
qiymatini qaytaradi va biz ushbu faylni ishlov berish qiymatini fayl_ochish
o'zgaruvchisiga tayinlaymiz. match
dan so'ng biz o'qish yoki yozish uchun fayl boshqaruvidan foydalanishimiz mumkin.
match
ning boshqa qismi File::open
dan Err
qiymatini oladigan holatni boshqaradi. Ushbu misolda biz panic!
makrosini tanladik. Agar joriy jildimizda olma.txt nomli fayl bo‘lmasa va biz ushbu kodni ishga tushirsak, biz panic!
makrosidan quyidagi natijani ko‘ramiz:
$ 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
Odatdagidek, bu chiqish bizga nima noto'g'ri ketganligini aniq aytadi.
Turli xil xatolarga moslashish
9-4 roʻyxatdagi kod nima uchun File::open
muvaffaqiyatsiz boʻlishidan qatʼiy nazar panic!
qoʻyadi.
Biroq, biz turli sabablarga ko'ra turli xil harakatlarni amalga oshirishni xohlaymiz: agar fayl mavjud bo'lmagani uchun File::open
muvaffaqiyatsiz bo'lsa, biz faylni yaratmoqchimiz va fayl boshqaruvini yangi faylga qaytaramiz. Agar File::open
boshqa sabablarga ko'ra, masalan, faylni ochishga ruxsatimiz yo'qligi sababli muvaffaqiyatsiz bo'lsa, biz hali ham kodga 9-4 ro'yxatdagi kabi panic!
qo'yishini xohlaymiz. Buning uchun biz 9-5 ro'yxatda ko'rsatilgan ichki match
ifodasini qo'shamiz.
Fayl nomi: src/main.rs
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let fayl_ochish = File::open("olma.txt");
let fayl = match fayl_ochish {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("olma.txt") {
Ok(fc) => fc,
Err(e) => panic!("Fayl yaratishda muammo: {:?}", e),
},
boshqa_xato => {
panic!("Faylni ochishda muammo: {:?}", boshqa_xato);
}
},
};
}
File::open
Err
variantida qaytaradigan qiymat turi io::Error
bo'lib, bu standart kutubxona tomonidan taqdim etilgan strukturadir. Ushbu strukturada io::ErrorKind
qiymatini olish uchun chaqirishimiz mumkin bo'lgan kind
metodi mavjud. io::ErrorKind
enumi standart kutubxona tomonidan taqdim etilgan va io
operatsiyasi natijasida yuzaga kelishi mumkin bo'lgan turli xil xatolarni ifodalovchi variantlarga ega. Biz foydalanmoqchi boʻlgan variant ErrorKind::NotFound
boʻlib, biz ochmoqchi boʻlgan fayl hali mavjud emasligini bildiradi. Shunday qilib, biz fayl_ochish
bo'yicha mos kelamiz, lekin bizda error.kind()
da ichki match ham bor.
Biz ichki matchni tekshirmoqchi bo'lgan shart - error.kind()
tomonidan qaytarilgan qiymat ErrorKind
enumining NotFound
variantidir. Agar shunday bo'lsa, biz faylni File::create
yordamida yaratishga harakat qilamiz. Biroq, File::create
ham muvaffaqiyatsiz bo'lishi mumkinligi sababli, bizga ichki match
ifodasida ikkinchi arm kerak. Faylni yaratib bo'lmaganda, boshqa xato xabari chop etiladi. Tashqi match
ning ikkinchi armi bir xil bo'lib qoladi, shuning uchun dastur yetishmayotgan fayl xatosidan tashqari har qanday xato haqida panic qo'yadi.
Result<T, E>
bilanmatch
dan foydalanishning muqobillariBu juda ko'p
match
!match
ifodasi juda foydali, lekin ayni paytda juda primitivdir. 13-bobda sizResult<T, E>
da belgilangan koʻplab metodlarda qoʻllaniladigan yopilishlar(closures) haqida bilib olasiz. Ushbu metodlar kodingizdagiResult<T, E>
qiymatlari bilan ishlashdamatch
dan foydalanishdan ko'ra qisqaroq bo'lishi mumkin.Misol uchun, 9-5 ro'yxatda ko'rsatilgan mantiqni yozishning yana bir usuli, bu safar closures va
unwrap_or_else
metodi yordamida:use std::fs::File; use std::io::ErrorKind; fn main() { let fayl_ochish = File::open("olma.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("olma.txt").unwrap_or_else(|error| { panic!("Fayl yaratishda muammo: {:?}", error); }) } else { panic!("Faylni ochishda muammo: {:?}", error); } }); }
Garchi bu kod 9-5 roʻyxatdagi kabi harakatga ega boʻlsa-da, unda
match
iboralari mavjud emas va oʻqish uchun qulayroq. 13-bobni o‘qib bo‘lgach, ushbu misolga qayting va standart kutubxona hujjatlaridaunwrap_or_else
metodini qidiring. Ushbu metodlarning ko'pchiligi xatolar bilan shug'ullanayotganda katta o'rinlimatch
iboralarni tozalashi mumkin.
Xatoda panic uchun yorliqlar: unwrap
va expect
match
dan foydalanish yetarlicha yaxshi ishlaydi, lekin u biroz batafsil bo'lishi mumkin va har doim ham maqsadni yaxshi bildirmaydi. Result<T, E>
turida turli, aniqroq vazifalarni bajarish uchun belgilangan koʻplab yordamchi metodlar mavjud. unwrap
metodi biz 9-4 ro'yxatda yozgan match
iborasi kabi implemen qilinadigan yorliq metodidir. Agar Result
qiymati Ok
varianti bo'lsa, unwrap
qiymati Ok
ichidagi qiymatni qaytaradi. Agar Result
Err
varianti bo‘lsa, unwrap
biz uchun panic!
makrosini chaqiradi. Mana amaldagi unwrap
misoli:
Fayl nomi: src/main.rs
use std::fs::File; fn main() { let fayl_ochish = File::open("olma.txt").unwrap(); }
Agar biz ushbu kodni olma.txt faylisiz ishga tushiradigan bo‘lsak, biz panic!
chaqiruvidan xato xabarini ko‘ramiz.
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:4:49
Xuddi shunday, expect
metodi bizga panic!
xato xabarini tanlash imkonini beradi.
unwrap
o'rniga expect
dan foydalanish va yaxshi xato xabarlarini taqdim etish niyatingizni bildirishi va panic manbasini kuzatishni osonlashtirishi mumkin. expect
sintaksisi quyidagicha ko'rinadi:
Fayl nomi: src/main.rs
use std::fs::File; fn main() { let fayl_ochish = File::open("olma.txt") .expect("olma.txt ushbu loyihaga kiritilishi kerak"); }
Biz expect
dan xuddi unwrap
kabi foydalanamiz: fayl boshqaruvini qaytarish yoki panic!
makrosini chaqirish uchun.panic!
chaqiruvida expect
tomonidan foydalanilgan xato xabari unwrap
ishlatadigan standart panic!
xabari emas, balki expect
parametriga o‘tadigan parametr bo‘ladi. Bu qanday ko'rinishga ega:
thread 'main' panicked at 'olma.txt should be included in this project: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:5:10
Ishlab chiqarish sifati kodida ko'pchilik Rustaceanlar unwrap
o'rniga expect
ni tanlaydilar va nima uchun operatsiya har doim muvaffaqiyatli bo'lishi kutilayotgani haqida ko'proq kontekst beradi. Shunday qilib, agar sizning taxminlaringiz noto'g'ri ekanligi isbotlangan bo'lsa, debuggingda foydalanish uchun ko'proq ma'lumotga ega bo'lasiz.
Xatoni yo'naltirish - Propagating
Funksiyani amalga oshirish muvaffaqiyatsiz bo'lishi mumkin bo'lgan narsani chaqirganda, xatoni funksiyaning o'zida hal qilish o'rniga, nima qilish kerakligini hal qilish uchun xatoni chaqiruvchi kodga qaytarishingiz mumkin. Bu xatoni propagating deb nomlanadi va chaqiruv kodini ko'proq nazorat qiladi, bu yerda kodingiz kontekstida mavjud bo'lgan narsadan ko'ra xatoni qanday hal qilish kerakligini ko'rsatadigan ko'proq ma'lumot yoki mantiq bo'lishi mumkin.
Misol uchun, 9-6 ro'yxati fayldan foydalanuvchi nomini o'qiydigan funksiyani ko'rsatadi. Agar fayl mavjud bo'lmasa yoki o'qib bo'lmasa, bu funksiya ushbu xatolarni funksiya chaqirgan kodga qaytaradi.
Fayl nomi: src/main.rs
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn fayldan_foydalanuvchi_nomini_olish() -> Result<String, io::Error> { let foydalanuvchi_fayli_natijasi = File::open("olma.txt"); let mut foydalanuvchi_fayli = match foydalanuvchi_fayli_natijasi { Ok(file) => file, Err(e) => return Err(e), }; let mut foydalanuvchi = String::new(); match foydalanuvchi_fayli.read_to_string(&mut foydalanuvchi) { Ok(_) => Ok(foydalanuvchi), Err(e) => Err(e), } } }
Bu funksiyani ancha qisqaroq tarzda yozish mumkin, lekin biz xatolarni qayta ishlashni o'rganish uchun uning ko'p qismini qo'lda qilishdan boshlaymiz; oxirida biz qisqaroq yo'lni ko'rsatamiz. Avval funksiyaning qaytish turini ko'rib chiqamiz: Result<String, io::Error>
. Bu funksiya Result<T, E>
turidagi qiymatni qaytarayotganini bildiradi, bunda parametr T
aniq turdagi String
bilan to'ldirilgan, va E
umumiy turi aniq turdagi io::Error
bilan to`ldirilgan.
Agar bu funksiya hech qanday muammosiz muvaffaqiyatli bajarilsa, ushbu funksiyani chaqiruvchi kod String
ga ega boʻlgan Ok
qiymatini oladi – bu funksiya fayldan o'qigan foydalanuvchi nomi. Agar bu funksiya biron bir muammoga duch kelsa, murojaat qiluvchi kod io::Error
misolini o'z ichiga olgan Err
qiymatini oladi, unda muammolar nima bo'lganligi haqida qo'shimcha ma'lumot mavjud. Biz ushbu funktsiyaning qaytish turi sifatida io::Error
ni tanladik, chunki bu funksiyaning tanasida bajarilmay qolishi mumkin bo‘lgan ikkala operatsiyadan qaytarilgan xato qiymatining turi: File::open
funksiyasi va read_to_string
metodi.
Funksiyaning asosiy qismi File::open
funksiyasini chaqirish orqali boshlanadi. Keyin biz Result
qiymatini 9-4 ro'yxatdagi match
ga o'xshash match
bilan ishlaymiz.
Agar File::open
muvaffaqiyatli bajarilsa, file
pattern o'zgaruvchisidagi fayl ishlovi foydalanuvchi_fayli
o'zgaruvchan o'zgaruvchisidagi qiymatga aylanadi va funksiya davom etadi. Err
holatida, panic!
deb chaqirish o‘rniga, biz return
kalit so‘zidan funksiyadan to‘liq chiqib ketish uchun foydalanamiz va xato qiymatini File::open
dan, endi e
pattern o‘zgaruvchisiga o‘tkazamiz, bu funksiya xato qiymati sifatida chaqiruvchi kodga qaytaradi.
Shunday qilib, agar bizda foydalanuvchi_fayli
da fayl boshqaruvi mavjud bo'lsa, keyin funksiya foydalanuvchi
o'zgaruvchisida yangi String
yaratadi va fayl mazmunini foydalanuvchi
ni o'qish uchun foydalanuvchi_fayli
da fayl boshqaruvidagi read_to_string
metodini chaqiradi. read_to_string
metodi ham Result
ni qaytaradi, chunki u File::open
muvaffaqiyatli bo'lishi ham mumkin, muvaffaqiyatsiz bo'lishi ham mumkin. Demak, ushbu Result
bilan ishlash uchun bizga yana bir match
kerak bo'ladi: agar read_to_string
muvaffaqiyatli bo'lsa, demak, bizning funksiyamiz muvaffaqiyatli bo'ldi va biz foydalanuvchi nomini hozirda Ok
bilan o'ralgan foydalanuvchi
faylidan qaytaramiz. Agar read_to_string
bajarilmasa, biz xato qiymatini xuddi File::open
qiymatini qayta ishlagan match
da xato qiymatini qaytarganimizdek qaytaramiz. Biroq, biz return
ni aniq aytishimiz shart emas, chunki bu funksiyadagi oxirgi ifoda.
Ushbu kodni chaqiruvchi kod foydalanuvchi nomini o'z ichiga olgan Ok
qiymatini yoki io::Error
ni o'z ichiga olgan Err
qiymatini olishni boshqaradi. Ushbu qiymatlar bilan nima qilishni hal qilish chaqiruv kodiga bog'liq. Agar chaqiruv kodi Err
qiymatini olsa, u panic!
deb chaqirishi va dasturni buzishi mumkin, standart foydalanuvchi nomidan foydalaning yoki foydalanuvchi nomini fayldan boshqa joydan qidiring, masalan. Bizda chaqiruv kodi aslida nima qilmoqchi ekanligi haqida yetarli ma'lumot yo'q, shuning uchun biz barcha muvaffaqiyat yoki xato ma'lumotlarini to'g'ri ishlashi uchun xatolarni propagate qilamiz.
Xatolarni propagating qilish namunasi Rustda shu qadar keng tarqalganki, Rust buni osonlashtirish uchun savol belgisi operatori ?
beradi.
Propagating xatolar uchun qisqa kod: ?
operatori
9-7 ro'yxatda 9-6 ro'yxatdagi kabi funksiyaga ega bo'lgan foydalanuvchi_fayli
ilovasi ko'rsatilgan, ammo bu dastur ?
operatoridan foydalanadi.
Fayl nomi: src/main.rs
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn fayldan_foydalanuvchi_nomini_olish() -> Result<String, io::Error> { let mut foydalanuvchi_fayli = File::open("olma.txt")?; let mut foydalanuvchi = String::new(); foydalanuvchi_fayli.read_to_string(&mut foydalanuvchi)?; Ok(foydalanuvchi) } }
Result
qiymatidan keyin qoʻyilgan ?
9-6 roʻyxatdagi Result
qiymatlarini boshqarish uchun biz belgilagan match
iboralari bilan deyarli bir xil ishlaydi. Agar Result
qiymati Ok
bo'lsa, Ok
ichidagi qiymat ushbu ifodadan qaytariladi va dastur davom etadi. Agar qiymat Err
bo'lsa, Err
butun funktsiyadan qaytariladi, xuddi biz return
kalit so'zidan foydalanganimizdek, xato qiymati chaqiruvchi kodga propagate qiladi.
9-6 roʻyxatdagi match
ifodasi va ?
operatori nima qilishi oʻrtasida farq bor: ?
operatori chaqirilgan xato qiymatlari from
funksiyasidan oʻtadi, qiymatlarni bir turdan ikkinchi turga aylantirish uchun foydalaniladigan standart kutubxonadagi From
traitida aniqlanadi.
?
operatori from
funksiyasini chaqirganda, qabul qilingan xato turi joriy funksiyaning qaytish turida aniqlangan xato turiga aylanadi. Bu funksiya muvaffaqiyatsiz bo'lishi mumkin bo'lgan barcha usullarni ifodalash uchun bitta xato turini qaytarganda foydalidir, agar qismlar turli sabablarga ko'ra ishlamay qolsa ham.
Misol uchun, biz 9-7 ro'yxatdagi fayldan_foydalanuvchi_nomini_olish
funksiyasini o'zgartirishimiz mumkin, bu biz aniqlagan OurError
nomli maxsus xato turini qaytarishimiz mumkin. Agar io::Error
dan OurError
misolini yaratish uchun OurError
uchun impl From<io::Error> for OurError
ni ham aniqlasak, keyin fayldan_foydalanuvchi_nomini_olish
asosiy qismidagi ?
operatori chaqiruvlari from
ga murojaat qiladi va funksiyaga boshqa kod qo'shmasdan xato turlarini o'zgartiradi.
foydalanuvchi_fayli
o'zgaruvchisiga qaytaradi.Agar xatolik yuzaga kelsa, ?
operatori butun funksiyadan erta qaytadi va chaqiruvchi kodga istalgan Err
qiymatini beradi. Xuddi shu narsa read_to_string
chaqiruvi oxiridagi ?
uchun ham amal qiladi.
?
operatori ko'plab nosozliklarni bartaraf qiladi va bu funksiyani amalga oshirishni soddalashtiradi. 9-8 ro'yxatda ko'rsatilganidek, biz ushbu kodni ?
dan keyin metod chaqiruvlar zanjiridan foydalansak, bu kodni yanada qisqartirishimiz mumkin.
Fayl nomi: src/main.rs
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn fayldan_foydalanuvchi_nomini_olish() -> Result<String, io::Error> { let mut foydalanuvchi = String::new(); File::open("olma.txt")?.read_to_string(&mut foydalanuvchi)?; Ok(foydalanuvchi) } }
Biz foydalanuvchi
da yangi String
yaratishni funksiya boshiga o‘tkazdik; bu qism o'zgarmagan. foydalanuvchi_fayli
oʻzgaruvchisini yaratish oʻrniga, File::open("olma.txt")?
natijasiga toʻgʻridan-toʻgʻri read_to_string
chaqiruvlarini bogʻladik. Bizda read_to_string
chaqiruvi oxirida hali ham ?
bor va biz xatoliklarni qaytarish oʻrniga File::open
va read_to_string
muvaffaqiyatli boʻlganda ham foydalanuvchi
ni oʻz ichiga olgan OK
qiymatini qaytaramiz. Funksionallik yana 9-6 va 9-7 ro'yxatdagi kabi; Bu uni yozishning boshqacha, ergonomik usuli.
9-9 ro'yxati fs::read_to_string
yordamida buni yanada qisqartirish yo'lini ko'rsatadi.
Fayl nomi: 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") } }
Faylni stringda o'qish juda keng tarqalgan operatsiya, shuning uchun standart kutubxona faylni ochadigan, yangi String
yaratadigan qulay fs::read_to_string
funksiyasini ta'minlaydi fayl mazmunini o'qiydi, mazmunini o'sha String
ga qo'yadi va uni qaytaradi. Albatta, fs::read_to_string
dan foydalanish bizga xatolarni qanday hal qilishni tushuntirishga imkon bermaydi, shuning uchun biz birinchi navbatda uzoq yo'lni o'rgandik.
?
Operatoridan qayerda foydalanish mumkin
?
operatori faqat qaytarish turi ?
ishlatiladigan qiymatga mos keladigan funksiyalarda ishlatilishi mumkin. Buning sababi, ?
operatori biz 9-6 ro'yxatda belgilagan match
ifodasi kabi funksiyadan tashqari qiymatni erta qaytarish uchun belgilangan. 9-6 roʻyxatda match
Result
qiymatidan foydalanilgan va erta qaytish armi Err(e)
qiymatini qaytargan. Funksiyaning qaytish turi Result
bo'lishi kerak, shunda u ushbu return
bilan mos keladi.
9-10 ro'yxatda, agar biz ?
dan foydalanadigan qiymat turiga mos kelmaydigan qaytish turi bilan main
funksiyada ?
operatoridan foydalansak, qanday xatoga duch kelamiz:
Fayl nomi: src/main.rs
use std::fs::File;
fn main() {
let fayl_ochish = File::open("olma.txt")?;
}
Ushbu kod faylni ochadi, bu muvaffaqiyatsiz bo'lishi mumkin. ?
operatori File::open
tomonidan qaytarilgan Result
qiymatiga amal qiladi, lekin bu main
funksiya Result
emas, ()
qaytish turiga ega. Ushbu kodni kompilyatsiya qilganimizda, biz quyidagi xato xabarini olamiz:
$ 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:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let fayl_ochish = File::open("olma.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= 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 xato bizga ?
operatoridan faqat Result
, Option
yoki FromResidual
ni qo'llaydigan boshqa turdagi qaytaruvchi funksiyada foydalanishga ruxsat berilganligini ko`rsatadi.
Xatoni tuzatish uchun sizda ikkita variant bor. Tanlovlardan biri, funksiyangizning qaytish turini ?
operatoridan foydalanayotgan qiymatga mos keladigan qilib o'zgartirish, agar bunga hech qanday cheklovlar bo'lmasa. Boshqa usul esa, Result<T, E>
ni mos keladigan usulda boshqarish uchun match
yoki Result<T, E>
metodlaridan birini qo`llashdir.
Xato xabarida, shuningdek, ?
ni Option<T>
qiymatlari bilan ham foydalanish mumkinligi aytilgan. Result
da ?
dan foydalanish kabi, siz ?
dan faqat Option
ni qaytaradigan funksiyada foydalanishingiz mumkin. ?
operatorining Option<T>
bo'yicha chaqirilgandagi xatti-harakati Result<T, E>
da chaqirilgandagi xatti-harakatiga o'xshaydi: agar qiymat None
bo'lsa None
bo'ladi o'sha paytda funksiyadan erta qaytariladi. Agar qiymat Some
bo'lsa, Some
ichidagi qiymat ifodaning natijaviy qiymati bo`lib, funksiya davom etadi. 9-11 ro'yxatda berilgan matndagi birinchi qatorning oxirgi belgisini topadigan funksiya misoli mavjud:
fn birinchi_satrning_oxirgi_belgisi(matn: &str) -> Option<char> { matn.lines().next()?.chars().last() } fn main() { assert_eq!( birinchi_satrning_oxirgi_belgisi("Salom Do'stim\n Ahvollaring qanday?"), Some('m') ); assert_eq!(birinchi_satrning_oxirgi_belgisi(""), None); assert_eq!(birinchi_satrning_oxirgi_belgisi("\nhi"), None); }
Bu funksiya Option<char>
ni qaytaradi, chunki u yerda belgi(character) boʻlishi mumkin, lekin yoʻq boʻlishi ham mumkin. Bu kod matn
string argumentini oladi va undagi lines
metodini chaqiradi, bu esa satrdagi satrlar ustidan iteratorni qaytaradi. Ushbu funksiya birinchi qatorni tekshirmoqchi bo'lganligi sababli, iteratordan birinchi qiymatni olish uchun iteratorda next
ni chaqiradi. Agar matn
boʻsh qator boʻlsa, next
ga murojat qilish None
ni qaytaradi, bu holda biz birinchi_satrning_oxirgi_belgisi
dan None
ni toʻxtatish va qaytarish uchun ?
operatoridan foydalanamiz. Agar matn
bo'sh qator bo'lmasa, next
matn
dagi birinchi qatorning string sliceni o'z ichiga olgan Some
qiymatini qaytaradi.
?
operatori satr bo'lagini chiqaradi va biz uning belgilarining iteratorini olish uchun ushbu qator bo'limidagi chars
larni chaqirishimiz mumkin. Bizni ushbu birinchi qatordagi oxirgi belgi qiziqtiradi, shuning uchun biz iteratordagi oxirgi elementni qaytarish uchun last
deb chaqiramiz.
Bu Option
dir, chunki birinchi qator boʻsh satr boʻlishi mumkin, masalan, matn
boʻsh satr bilan boshlansa, lekin "\nhi"
dagi kabi boshqa qatorlarda belgilar boʻlsa. Biroq, agar birinchi qatorda oxirgi belgi bo'lsa, u Some
variantida qaytariladi. O'rtadagi ?
operatori bu mantiqni ifodalashning ixcham usulini beradi, bu funksiyani bir qatorda amalga oshirish imkonini beradi. Agar biz Option
da?
operatoridan foydalana olmasak, biz bu mantiqni ko'proq metod chaqiruvlari yoki match
ifodasi yordamida amalga oshirishimiz kerak edi.
Esda tutingki, ?
operatoridan Result
qaytaruvchi funksiyada Result
da foydalanishingiz mumkin, va ?
operatoridan Option
qaytaradigan funksiyada Option
da foydalanishingiz mumkin, lekin siz aralashtirib, moslashtira olmaysiz. ?
operatori Result
ni avtomatik ravishda Option
ga yoki aksincha o'zgartirmaydi; Bunday hollarda konvertatsiyani aniq amalga oshirish uchun Result
dagi ok
metodi yoki Option
dagi ok_or
kabi metodlardan foydalanishingiz mumkin.
Hozirgacha biz ishlatgan barcha main
funksiyalar ()
ni qaytaradi. main
funksiya maxsus, chunki u bajariladigan dasturlarning kirish va chiqish nuqtasi bo'lib, dasturlar kutilgandek harakat qilishi uchun uning qaytish(return) turi qanday bo'lishi mumkinligiga cheklovlar mavjud.
Yaxshiyamki, main
Result<(), E>
ni ham qaytarishi mumkin. 9-12 ro'yxatda 9-10 ro'yxatdagi kod mavjud, biroq biz main
ning qaytish turini Result<(), Box<dyn Error>>
qilib o'zgartirdik va oxiriga Ok(())
qaytish qiymatini qo'shdik. Ushbu kod endi kompilyatsiya qilinadi:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let fayl_ochish = File::open("olma.txt")?;
Ok(())
}
Box<dyn Error>
turi bu trait ob'ekti bo'lib, biz 17-bobning "Turli turdagi qiymatlarga ruxsat beruvchi trait ob'ektlaridan foydalanish" bo'limida gaplashamiz. Hozircha siz Box<dyn Error>
ni “har qanday xato” degan ma'noni anglatadi deb o'ylashingiz mumkin. Box<dyn Error>
xato turi bilan main
funksiyadagi Result
qiymatida ?
dan foydalanishga ruxsat beriladi, chunki bu har qanday Err
qiymatini erta qaytarish imkonini beradi. Garchi bu main
funksiyaning tanasi faqat std::io::Error
turidagi xatolarni qaytarsa ham, Box<dyn Error>
ni belgilab, main
funksiyaga boshqa xatolarni qaytaruvchi ko'proq kod qo'shilsa ham, bu kod to'g'ri bo'lib qoladi.
main
funksiya Result<(), E>
ni qaytarsa, bajariladigan fayl(executable file) 0
qiymati bilan chiqadi, agar main
Ok(())
qaytarsa va main
Err
qiymatini qaytarsa nolga teng bo'lmagan qiymat bilan chiqadi. C tilida yozilgan bajariladigan fayllar(executable file) chiqqanda butun sonlarni qaytaradi: muvaffaqiyatli chiqqan dasturlar 0
butun sonini qaytaradi, xatoga yo'l qo'ygan dasturlar esa 0
dan boshqa butun sonni qaytaradi. Rust shuningdek, ushbu konventsiyaga mos kelishi uchun bajariladigan fayllardan butun(integer) sonlarni qaytaradi.
main
funksiya std::process::Termination
traitini amalga oshiradigan har qanday turlarni qaytarishi mumkin, bunda ExitCode
qaytaruvchi report
funksiyasi mavjud. O'zingizning turlaringiz uchun Termination
traitini qo'llash bo'yicha qo'shimcha ma'lumot olish uchun standart kutubxona texnik hujjatlariga murojaat qiling.
Endi biz panic!
chaqirish yoki Result
ni qaytarish tafsilotlarini muhokama qilganimizdan so‘ng, keling, qaysi hollarda qaysi biri to‘g‘ri kelishini hal qilish mavzusiga qaytaylik.
panic!
yoki panic!
qo'ymaslik
Xo'sh, qachon panic!
deb murojat qilish va qachon Result
ni qaytarish kerakligini qanday hal qilasiz? Kodda panic paydo bo'lganda, uni tiklashning iloji yo'q. Qayta tiklashning mumkin bo'lgan yo'li bormi yoki yo'qmi, har qanday xatolik uchun panic!
deb chaiqruv qilishingiz mumkin, lekin siz chaqiruv kodi nomidan vaziyatni tuzatib bo'lmaydi degan qarorga kelasiz. Result
qiymatini qaytarishni tanlaganingizda, siz chaqiruv kodini tanlash imkoniyatini berasiz. Chaqiruv kodi vaziyatga mos keladigan tarzda tiklashga urinishi mumkin yoki Err
dagi xatoni qayta tiklab bo'lmaydi, deb qaror qilishi va panic!
qo'yishi mumkin, bu sizning tiklanadigan xatongizni tuzatib bo'lmaydiganga aylantiradi. Shuning uchun, muvaffaqiyatsiz bo'lishi mumkin bo'lgan funksiyani belgilashda Result
ni qaytarish yaxshi standart tanlovdir.
Misollar, prototip kodi va testlar kabi holatlarda Result
ni qaytarish o'rniga panic qo'yadigan kodni yozish maqsadga muvofiqdir. Keling, nima uchun ekanligini ko'rib chiqaylik, keyin kompilyator muvaffaqiyatsizlik mumkin emasligini ayta olmaydigan vaziyatlarni muhokama qilaylik, lekin siz inson sifatida buni qila olasiz. Bob kutubxona kodida panic qo'yish yoki yo'qligini hal qilish bo'yicha ba'zi umumiy ko'rsatmalar bilan yakunlanadi.
Misollar, Prototip Kodi va Testlar
Ba'zi bir kontseptsiyani tasvirlash uchun misol yozayotganingizda, shuningdek, xatolarni qayta ishlash kodini o'z ichiga olgan holda, misolni kamroq tushunarli qilish mumkin. Misollarda, panic qo'zg'atishi mumkin bo'lgan unwrap
kabi metodga murojaat qilish sizning ilovangiz xatoliklarni qanday hal qilishini xohlayotganingiz uchun to'ldiruvchi sifatida tushuniladi, bu sizning kodingizning qolgan qismi nima qilayotganiga qarab farq qilishi mumkin.
Xuddi shunday, prototiplashda xatolarni qanday hal qilishni hal qilishdan oldin unwrap
va expect
metodllari juda qulaydir.
Dasturingizni yanada mustahkamroq qilishga tayyor bo'lganingizda ular kodingizda aniq belgilar qoldiradilar.
Agar testda metod chaqiruvi muvaffaqiyatsiz bo'lsa, bu metod sinovdan o'tkazilayotgan funksiya bo'lmasa ham, butun test muvaffaqiyatsiz bo'lishini xohlaysiz. Chunki panic!
– bu sinovning muvaffaqiyatsiz deb belgilanishi, unwrap
yoki expect
deb atalgan narsa aynan shunday bo'lishi kerak.
Siz kompilyatordan ko'ra ko'proq ma'lumotga ega bo'lgan holatlar
Agar sizda Result
Ok
qiymatiga ega bo'lishini ta'minlaydigan boshqa mantiqqa ega bo'lsangiz, unwrap
yoki expect
ni chaqirish ham o'rinli bo'lardi, ammo mantiq kompilyator tushunadigan narsa emas. Siz hali ham Result
qiymatiga ega bo'lasiz, uni hal qilishingiz kerak: siz murojaat qilayotgan har qanday operatsiya sizning vaziyatingizda mantiqan imkonsiz bo'lsa ham, umuman muvaffaqiyatsiz bo'lish ehtimoli bor. Agar siz kodni qo‘lda tekshirish orqali sizda hech qachon Err
varianti bo‘lmasligiga ishonch hosil qilsangiz, unwrap
deb nomlash juda maqbuldir, va expect
matnida hech qachon Err
varianti bo'lmaydi deb o'ylagan sababni hujjatlash yaxshiroqdir. Mana bir misol:
fn main() { use std::net::IpAddr; let asosiy: IpAddr = "127.0.0.1" .parse() .expect("Qattiq kodlangan IP manzil haqiqiy bo'lishi kerak"); }
Qattiq kodlangan stringni tahlil qilish orqali IpAddr
misolini yaratmoqdamiz. Biz 127.0.0.1
to‘g‘ri IP manzil ekanligini ko‘ramiz, shuning uchun bu yerda expect
dan foydalanish mumkin. Biroq, qattiq kodlangan, yaroqli satrga ega bo'lish parse
metodining qaytish turini o'zgartirmaydi: biz hali ham Result
qiymatini olamiz va kompilyator bizni Result
bilan ishlashga majbur qiladi, go‘yo Err
varianti mumkin, chunki kompilyator bu satr har doim haqiqiy IP manzil ekanligini ko‘rish uchun yetarlicha aqlli emas. Agar IP-manzillar qatori dasturga qattiq kodlanganidan ko'ra foydalanuvchidan kelgan bo'lsa va shuning uchun muvaffaqiyatsizlikka uchragan bo'lsa, biz, albatta, Result
ni yanada ishonchli tarzda boshqarishni xohlaymiz.
Ushbu IP-manzil qattiq kodlangan degan taxminni eslatib o'tsak, agar kelajakda IP-manzilni boshqa manbadan olishimiz kerak bo'lsa, bizni expect
ni xatolarni boshqarish kodini yaxshiroq o'zgartirishga undaydi.
Xatolarni bartaraf etish bo'yicha ko'rsatmalar
Agar kodingiz yomon holatda bo'lishi mumkin bo'lsa, kodingiz panic qo'yishi tavsiya etiladi. Shu nuqtai nazardan, yomon holat deganda baʼzi taxminlar(assumption), kafolatlar(guarantee), shartnomalar(contract,) yoki oʻzgarmasliklar buzilganda, masalan, notoʻgʻri qiymatlar, qarama-qarshi qiymatlar yoki yetishmayotgan qiymatlar kodingizga oʻtkazilganda, shuningdek quyidagilardan biri yoki bir nechtasi:
- Yomon holat - foydalanuvchi noto'g'ri formatda ma'lumotlarni kiritishi kabi vaqti-vaqti bilan sodir bo'lishi mumkin bo'lgan narsadan farqli o'laroq, kutilmagan narsa.
- Ushbu nuqtadan keyin sizning kodingiz har qadamda muammoni tekshirishdan ko'ra, bu yomon holatda bo'lmaslikka tayanishi kerak.
- Siz foydalanadigan turlarda ushbu ma'lumotni kodlashning yaxshi usuli yo'q. Biz 17-bobning "Turlar sifatida kodlash holatlari va behaviorlari" bo'limida nimani nazarda tutayotganimizni misol qilib ko'rib chiqamiz.
Agar kimdir sizning kodingizga chaqiruv qilsa va mantiqiy bo'lmagan qiymatlarni o'tkazsa, kutubxona foydalanuvchisi bu holatda nima qilishni xohlashini hal qilishi uchun xatolikni qaytarish yaxshidir. Biroq, davom etish xavfli yoki zararli bo'lishi mumkin bo'lgan hollarda, eng yaxshi tanlov panic!
deb chaqiruv qilish va kutubxonangizdan foydalanuvchini kodidagi xatolik haqida ogohlantirish bo'lishi mumkin, shunda ular ishlab chiqish jarayonida uni tuzatishi mumkin. Xuddi shunday, panic!
ko'pincha sizning nazoratingizdan tashqarida bo'lgan tashqi kodga chaqiruv qilsangiz va uni tuzatishning imkoni bo'lmagan yaroqsiz holatni qaytarsangiz mos keladi.
Biroq, muvaffaqiyatsizlik kutilganda, panic!
chaqiruv qilishdan ko'ra, Result
ni qaytarish maqsadga muvofiqdir. Misollar, tahlilchiga noto'g'ri tuzilgan ma'lumotlar yoki tarif chegarasiga yetganingizni bildiruvchi holatni qaytaruvchi HTTP so'rovini o'z ichiga oladi. Bunday hollarda, Result
ni qaytarish, chaqiruv kodi qanday ishlov berishni hal qilishi kerak bo'lgan muvaffaqiyatsizlik kutilgan imkoniyat ekanligini ko'rsatadi.
Agar kodingiz noto'g'ri qiymatlar yordamida chaqirilgan bo'lsa, foydalanuvchini xavf ostiga qo'yishi mumkin bo'lgan operatsiyani bajarganda, kodingiz avval qiymatlarning haqiqiyligini tekshirishi va qiymatlar noto'g'ri bo'lsa panic qo'yishi kerak.Bu asosan xavfsizlik nuqtai nazaridan: noto'g'ri ma'lumotlar bilan ishlashga urinish kodingizni zaifliklarga olib kelishi mumkin.
Agar siz chegaradan tashqari xotiraga kirishga harakat qilsangiz, standart kutubxona panic!
deb chaqirishining asosiy sababi shu: joriy ma'lumotlar tuzilishiga tegishli bo'lmagan xotiraga kirishga urinish umumiy xavfsizlik muammosidir. Funksiyalarda ko'pincha shartnomalar(contracts) mavjud: agar kirish ma'lum talablarga javob bersa, ularning xatti-harakati kafolatlanadi. Shartnoma buzilganda panic qo'yish mantiqan to'g'ri keladi, chunki shartnoma buzilishi har doim chaqiruv qiluvchi tomonidagi xatolikni ko'rsatadi va bu siz chaqiruv kodini aniq ko'rib chiqishni xohlagan xatolik emas. Aslida, chaqiruv kodini tiklashning oqilona usuli yo'q; kodni chaqiruvchi dasturchilar kodni tuzatishi kerak. Funksiya uchun shartnomalar, ayniqsa buzilish panic keltirib chiqaradigan bo'lsa, funksiya uchun API texnik hujjatlarida tushuntirilishi kerak.
Biroq, barcha funksiyalaringizda ko'plab xatolarni tekshirish batafsil va zerikarli bo'ladi. Yaxshiyamki, siz Rustning turdagi tizimidan (va shunday qilib, kompilyator tomonidan amalga oshiriladigan turdagi tekshirish) siz uchun ko'plab tekshiruvlarni amalga oshiradi. Agar funksiyangiz parametr sifatida ma'lum bir turga ega bo'lsa, kompilyator sizda haqiqiy qiymatga ega ekanligiga ishonch hosil qilgan holda kodingiz mantig'ini davom ettirishingiz mumkin. Misol uchun, agar sizda Option
emas, balki turingiz bo'lsa, dasturingiz nothing(hech narsa) emas, balki something(nimadir) bo'lishini kutadi. Sizning kodingiz Some
va None
variantlari uchun ikkita holatni ko'rib chiqishi shart emas: aniq qiymatga ega bo'lish uchun faqat bitta holat bo'ladi. Funksiyangizga hech narsa o'tkazmaslikka harakat qiladigan kodni kompilyatsiya qilinmaydi, shuning uchun funksiyangiz runtimeda bu holatni tekshirishi shart emas.
Yana bir misol, parametr hech qachon manfiy bo'lmasligini ta'minlaydigan u32
kabi belgisiz butun son turidan foydalanishdir.
Tasdiqlash uchun maxsus turlarni yaratish
Keling, bir qadam oldin haqiqiy qiymatga ega ekanligimizga ishonch hosil qilish uchun Rust turi tizimidan foydalanish g'oyasini olaylik va tekshirish uchun maxsus turni yaratishni ko'rib chiqaylik. 2-bobdagi taxmin qilish o'yinini eslang, unda bizning kodimiz foydalanuvchidan 1 dan 100 gacha bo'lgan raqamni taxmin qilishni so'radi. Biz hech qachon foydalanuvchining taxmini o'sha raqamlar o'rtasida ekanligini tasdiqlaganimiz yo'q, uni bizning maxfiy raqamimizga nisbatan tekshirishdan oldin; biz faqat taxmin ijobiy ekanligini tasdiqladik. Bunday holda, natijalar unchalik dahshatli emas edi: bizning "Raqam katta!" yoki "Raqam Kichik!" chiqishimiz hali ham to'g'ri bo'lar edi. Lekin foydalanuvchini to'g'ri taxmin qilishga va foydalanuvchi diapazondan tashqaridagi raqamni taklif qilganda va foydalanuvchi, masalan, raqamlar o'rniga harflarni kiritganda, boshqacha xatti-harakatlarga ega bo'lishga undash yaxshi bo'lardi.
Buning usullaridan biri potentsial manfiy raqamlarga ruxsat berish uchun taxminni faqat u32
o‘rniga i32
sifatida tahlil qilish va keyin diapazondagi raqamni tekshirishni qo‘shishdir, masalan:
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
loop {
// --snip--
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
let taxmin: i32 = match taxmin.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if taxmin < 1 || taxmin > 100 {
println!("Yashirin raqam 1 dan 100 gacha bo'ladi.");
continue;
}
match taxmin.cmp(&yashirin_raqam) {
// --snip--
Ordering::Less => println!("Raqam Kichik!"),
Ordering::Greater => println!("Raqam katta!"),
Ordering::Equal => {
println!("Siz yutdingiz!");
break;
}
}
}
}
If
ifodasi bizning qiymatimiz diapazondan tashqarida yoki yo‘qligini tekshiradi, foydalanuvchiga muammo haqida xabar beradi va siklning keyingi iteratsiyasini boshlash uchun continue
ni chaqiradi va yana bir taxminni so‘raydi. if
ifodasidan keyin taxmin
1 dan 100 gacha ekanligini bilgan holda taxmin
va maxfiy raqam o‘rtasidagi taqqoslashni davom ettirishimiz mumkin.
Biroq, bu ideal echim emas: agar dastur faqat 1 dan 100 gacha bo'lgan qiymatlarda ishlaganligi juda muhim bo'lsa va bu talab bilan ko'plab funksiyalarga ega bo'lsa, har bir funksiyada bunday tekshiruvga ega bo'lish zerikarli bo'ladi (va ishlashga ta'sir qilishi mumkin).
Buning o'rniga, biz yangi turni yaratishimiz va tekshirishlarni hamma joyda takrorlashdan ko'ra, turdagi namunani yaratish uchun funksiyaga qo'yishimiz mumkin. Shunday qilib, funksiyalar o'zlarining imzolarida yangi turdan foydalanishlari va ular olgan qiymatlardan ishonchli foydalanishlari xavfsiz bo'ladi. 9-13 roʻyxatda Taxmin
turini aniqlashning bir usuli koʻrsatilgan, bu new
funksiya 1 dan 100 gacha boʻlgan qiymatni qabul qilsagina Taxmin
misolini yaratadi.
#![allow(unused)] fn main() { pub struct Taxmin { qiymat: i32, } impl Taxmin { pub fn new(qiymat: i32) -> Taxmin { if qiymat < 1 || qiymat > 100 { panic!("Taxmin qilingan qiymat 1 dan 100 gacha bo'lishi kerak, {} qabul qilnmaydi.", qiymat); } Taxmin { qiymat } } pub fn qiymat(&self) -> i32 { self.qiymat } } }
Birinchidan, biz i32
ga ega qiymat
nomli maydonga ega Taxmin
nomli structni aniqlaymiz. Bu yerda raqam saqlanadi.
Keyin biz Taxmin
da new
nomli bog'langan funktsiyani amalga oshiramiz, u Taxmin
qiymatlari misollarini yaratadi. new
funksiya i32
turidagi qiymat
nomli bitta parametrga ega bo‘lishi va Taxmin
ni qaytarishi uchun belgilangan. new
funksiyaning asosiy qismidagi kod qiymat
ni 1 dan 100 gacha ekanligiga ishonch hosil qilish uchun tekshiradi.
Agar qiymat
bu sinovdan o‘tmasa, biz panic!
chaqiruvini qilamiz, bu chaqiruv kodini yozayotgan dasturchini tuzatishi kerak bo‘lgan xatolik haqida ogohlantiradi, chunki bu diapazondan tashqarida qiymat
bilan Taxmin
yaratish Taxmin::new
tayanadigan qoidani buzadi. Taxmin::new
panic qo'zg'atishi mumkin bo'lgan shartlar uning API texnik hujjatlarida muhokama qilinishi kerak; biz 14-bobda yaratgan API texnik hujjatlarida panic!
ehtimolini ko‘rsatuvchi hujjatlar konventsiyalarini qamrab olamiz. Agar qiymat
testdan o'tgan bo'lsa, biz uning qiymat
maydoni qiymat
parametriga o'rnatilgan yangi Taxmin
yaratamiz va Taxmin
ni qaytaramiz.
Keyinchalik, biz self
ni oladigan, boshqa parametrlarga ega bo'lmagan va i32
ni qaytaradigan qiymat
nomli metodni qo'llaymiz. Bunday usul ba'zan getter(oluvchi) deb ataladi, chunki uning maqsadi o'z maydonlaridan ba'zi ma'lumotlarni olish va uni qaytarishdir. Ushbu umumiy metod zarur, chunki Taxmin
strukturasining qiymat
maydoni shaxsiydir(private). qiymat
maydoni shaxsiy(private) bo'lishi juda muhim, shuning uchun Taxmin
strukturasi yordamida kod to'g'ridan-to'g'ri qiymat
ni o'rnatishga ruxsat berilmaydi: moduldan tashqaridagi kod Taxmin::new
funksiyasidan Taxmin
misolini yaratish uchun foydalanishi kerak, shunday qilib, Taxmin
ning Taxmin::new
funksiyasidagi shartlar bo‘yicha tekshirilmagan qiymat
ga ega bo‘lishining imkoni yo‘qligini ta’minlaydi.
Parametrga ega bo'lgan yoki faqat 1 dan 100 gacha bo'lgan raqamlarni qaytaradigan funksiya o'z imzosida i32
emas, Taxmin
ni olishi yoki qaytarishi va uning tanasida qo'shimcha tekshiruvlar o'tkazishga hojat qolmasligini e'lon qilishi mumkin.
Xulosa
Rust-ning xatolarni boshqarish xususiyatlari sizga yanada mustahkam kod yozishga yordam berish uchun mo'ljallangan.
panic!
makrosi dasturingiz u bardosh bera olmaydigan holatda ekanligini bildiradi va noto‘g‘ri yoki noto‘g‘ri qiymatlar bilan davom etish o‘rniga jarayonni to‘xtatishni aytish imkonini beradi. Result
enumi operatsiyalar muvaffaqiyatsiz bo'lishi va kodingiz tiklanishi mumkinligini bildirish uchun Rust turdagi tizimdan foydalanadi. Kodingizga chaqiruv qiladigan kod potentsial muvaffaqiyat yoki muvaffaqiyatsizlikni hal qilishi kerakligini aytish uchun Result
dan foydalanishingiz mumkin. Tegishli vaziyatlarda panic!
va Result
dan foydalanish muqarrar muammolar oldida kodingizni yanada ishonchli qiladi.
Endi siz standart kutubxonada Option
va Result
enumlari bilan generiklardan foydalanishning foydali usullarini ko'rganingizdan so'ng, biz generiklar qanday ishlashi va ularni kodingizda qanday ishlatishingiz haqida gaplashamiz.
Generik turlar, Traitlar va Lifetimelar
Har bir dasturlash tilida kontseptsiyalarning takrorlanishini samarali boshqarish vositalari mavjud. Rustda bunday vositalardan biri generiklar: concrete turlari yoki boshqa xususiyatlar uchun mavhum stendlar. Kodni kompilyatsiya qilish va ishga tushirishda ularning o'rnida nima bo'lishini bilmasdan, biz generiklarning xatti-harakatlarini yoki ularning boshqa generiklar bilan qanday bog'liqligini ifodalashimiz mumkin.
Funktsiyalar i32
yoki String
kabi aniq turdagi o'rniga ba'zi umumiy turdagi parametrlarni olishi mumkin, xuddi shu tarzda funksiya bir xil kodni bir nechta aniq qiymatlarda ishlatish uchun noma'lum qiymatlarga ega parametrlarni oladi. Aslida, biz 6-bobda Option<T>
, 8-bobda Vec<T>
va HashMap<K, V>
va 9-bobda Result<T, E>
bilan generiklardan allaqachon foydalanganmiz. Ushbu bobda siz o'zingizning turlaringizni, funksiyalaringizni va metodlaringizni generiklar bilan qanday aniqlashni o'rganasiz!
Birinchidan, kodning takrorlanishini kamaytirish uchun funksiyani qanday chiqarishni ko'rib chiqamiz. Keyin biz bir xil texnikadan faqat parametrlari turida farq qiladigan ikkita funksiyadan umumiy funksiyani yaratamiz. Shuningdek, biz struct va enum ta'riflarida generik turlardan qanday foydalanishni tushuntiramiz.
Keyin xulq-atvorni umumiy tarzda aniqlash uchun traitlar dan qanday foydalanishni o'rganasiz. Har qanday turdan farqli o'laroq, faqat ma'lum bir xatti-harakatga ega bo'lgan turlarni qabul qilish uchun umumiy turni cheklash uchun traitlarni umumiy turlar bilan birlashtira olasiz.
Va nihoyat, biz lifetimelar haqida gaplashamiz: kompilyatorga referencelar bir-biriga qanday bog'liqligi haqida ma'lumot beradigan turli xil generiklar. Lifetimelar kompilyatorga olingan qiymatlar haqida yetarli ma'lumot berishga imkon beradi, shunda u murojaatlar bizning yordamimizsiz ko'proq holatlarda haqiqiy bo'lishini ta'minlaydi.
Funksiyani ajratib olish orqali takrorlanishni olib tashlash
Generiklar bizga kodning takrorlanishini olib tashlash uchun bir nechta turlarni ifodalovchi maxsus turlarni to'ldiruvchi bilan almashtirishga imkon beradi. Generik sintaksisga kirishdan oldin, keling, birinchi navbatda, ma'lum qiymatlarni bir nechta qiymatlarni ifodalovchi to'ldiruvchi bilan almashtiradigan funksiyani chiqarib, generik turlarni o'z ichiga olmaydigan tarzda takrorlashni qanday olib tashlashni ko'rib chiqaylik. Keyin generik funksiyani chiqarish uchun xuddi shu texnikani qo'llaymiz! Funksiyaga chiqarishingiz mumkin bo'lgan takrorlangan kodni qanday tanib olishni ko'rib chiqsangiz, generiklardan foydalanishi mumkin bo'lgan takrorlangan kodni taniy boshlaysiz.
Biz ro'yxatdagi eng katta raqamni topadigan 10-1 ro'yxatidagi qisqa dasturdan boshlaymiz.
Fayl nomi: src/main.rs
fn main() { let raqamlar_listi = vec![34, 50, 25, 100, 65]; let mut eng_katta = &raqamlar_listi[0]; for raqam in &raqamlar_listi { if raqam > eng_katta { eng_katta = raqam; } } println!("Eng katta raqam {}", eng_katta); assert_eq!(*eng_katta, 100); }
Biz butun sonlar roʻyxatini raqamlar_listi
oʻzgaruvchisida saqlaymiz va roʻyxatdagi birinchi raqamga referenceni eng_katta
nomli oʻzgaruvchiga joylashtiramiz. Keyin biz roʻyxatdagi barcha raqamlarni takrorlaymiz va agar joriy raqam eng_katta
da saqlangan raqamdan katta boʻlsa, ushbu oʻzgaruvchidagi referenceni almashtiramiz.
Biroq, agar joriy raqam hozirgacha ko'rilgan eng katta raqamdan kichik yoki unga teng bo'lsa, o'zgaruvchi o'zgarmaydi va kod ro'yxatdagi keyingi raqamga o'tadi. Ro'yxatdagi barcha raqamlarni ko'rib chiqqandan so'ng, eng_katta
eng katta raqamga ishora qilishi kerak, bu holda bu 100 ga teng.
Bizga endi ikki xil raqamlar ro‘yxatidagi eng katta raqamni topish vazifasi qo‘yildi. Buning uchun biz 10-1 roʻyxatdagi kodni takrorlashni tanlashimiz va 10-2 roʻyxatda koʻrsatilganidek, dasturning ikki xil joyida bir xil mantiqdan foydalanishimiz mumkin.
Fayl nomi: src/main.rs
fn main() { let raqamlar_listi = vec![34, 50, 25, 100, 65]; let mut eng_katta = &raqamlar_listi[0]; for raqam in &raqamlar_listi { if raqam > eng_katta { eng_katta = raqam; } } println!("Eng katta raqam {}", eng_katta); let raqamlar_listi = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let mut eng_katta = &raqamlar_listi[0]; for raqam in &raqamlar_listi { if raqam > eng_katta { eng_katta = raqam; } } println!("Eng katta raqam {}", eng_katta); }
Ushbu kod ishlayotgan bo'lsa-da, kodni takrorlash zerikarli va xatolarga moyil. Shuningdek, biz kodni o'zgartirmoqchi bo'lganimizda uni bir nechta joyda yangilashni unutmasligimiz kerak.
Ushbu takrorlanishni bartaraf qilish uchun biz parametrda berilgan butun sonlar ro'yxatida ishlaydigan funktsiyani aniqlash orqali abstraksiya yaratamiz. Ushbu yechim bizning kodimizni aniqroq qiladi va bizga ro'yxatdagi eng katta raqamni topish tushunchasini mavhum tarzda ifodalash imkonini beradi.
10-3 ro'yxatda biz eng katta raqamni topadigan kodni eng_katta
deb nomlangan funksiyaga chiqaramiz. Keyin biz 10-2 ro'yxatdagi ikkita ro'yxatdagi eng katta raqamni topish uchun funksiyani chaqiramiz. Bundan tashqari, biz kelajakda ega bo'lishi mumkin bo'lgan i32
qiymatlarining boshqa ro'yxatida ham funksiyadan foydalanishimiz mumkin.
Fayl nomi: src/main.rs
fn eng_katta(list: &[i32]) -> &i32 { let mut eng_katta = &list[0]; for element in list { if element > eng_katta { eng_katta = element; } } eng_katta } fn main() { let raqamlar_listi = vec![34, 50, 25, 100, 65]; let natija = eng_katta(&raqamlar_listi); println!("Eng katta raqam {}", natija); assert_eq!(*natija, 100); let raqamlar_listi = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let natija = eng_katta(&raqamlar_listi); println!("Eng katta raqam {}", natija); assert_eq!(*natija, 6000); }
eng_katta
funksiya list
deb nomlangan parametrga ega bo'lib, biz funktsiyaga o'tkazishimiz mumkin bo'lgan i32
qiymatlarining har qanday aniq qismini ifodalaydi. Natijada, biz funksiyani chaqirganimizda, kod biz kiritadigan maxsus qiymatlarda ishlaydi.
Xulosa qilib aytganda, biz kodni kodni 10-2-ro'yxadan 10-3-ro'yxaga oʻzgartirish uchun qilgan qadamlarimiz:
- Ikki nusxadagi kodni aniqlang.
- Ikki nusxadagi kodni funktsiya tanasiga chiqarib oling va ushbu kodning kirish va qaytish qiymatlarini funktsiya imzosida belgilang.
- Buning o'rniga funktsiyani chaqirish uchun ikki nusxadagi kodning ikkita nusxasini yangilang.
Keyinchalik, kodning takrorlanishini kamaytirish uchun generiklar bilan bir xil qadamlardan foydalanamiz. Xuddi shu tarzda, funksiya tanasi ma'lum qiymatlar o'rniga mavhum list
bo'yicha ishlay oladi, generiklar kodni mavhum turlarda ishlashga imkon beradi.
Misol uchun, bizda ikkita funksiya bor edi deylik: biri i32
qiymatlari bo‘limidagi eng katta elementni topadigan va ikkinchisi char
qiymatlari bo‘limidagi eng katta elementni topadigan. Bu takroriylikni qanday yo'q qilamiz? Keling, bilib olaylik!
Generik ma'lumotlar turlari
Funksiya imzolari yoki structlar kabi elementlar uchun definitionlarni yaratish uchun biz generik(umumiy) ma'lumotlardan foydalanamiz, keyin ularni turli xil aniq ma'lumotlar turlari bilan ishlatishimiz mumkin. Keling, avval generiklar yordamida funksiyalar, structlar, enumlar va metodlarni qanday aniqlashni ko'rib chiqaylik. Keyin biz generiklar kod ishlashiga qanday ta'sir qilishini muhokama qilamiz.
Funksiya ta'riflarida
Generiklardan foydalanadigan funksiyani belgilashda biz generiklarni funksiya imzosiga joylashtiramiz, u yerda biz odatda parametrlarning ma'lumotlar turlarini va qiymatni qaytaramiz. Bu bizning kodimizni yanada moslashuvchan qiladi va kodning takrorlanishining oldini olish bilan birga funksiyamizni chaqiruvchilarga ko'proq funksionallik beradi.
eng_katta
funksiyamizni davom ettirsak, 10-4 roʻyxatda ikkalasi ham boʻlakdagi eng katta qiymatni topadigan ikkita funksiya koʻrsatilgan. Keyin biz ularni generiklardan foydalanadigan yagona funksiyaga birlashtiramiz.
Fayl nomi: src/main.rs
fn eng_katta_i32(list: &[i32]) -> &i32 { let mut eng_katta = &list[0]; for element in list { if element > eng_katta { eng_katta = element; } } eng_katta } fn eng_katta_char(list: &[char]) -> &char { let mut eng_katta = &list[0]; for element in list { if element > eng_katta { eng_katta = element; } } eng_katta } fn main() { let raqamlar_listi = vec![34, 50, 25, 100, 65]; let natija = eng_katta_i32(&raqamlar_listi); println!("Eng katta raqam {}", natija); assert_eq!(*natija, 100); let char_list = vec!['y', 'm', 'a', 'q']; let natija = eng_katta_char(&char_list); println!("Eng katta belgi {}", natija); assert_eq!(*natija, 'y'); }
eng_katta_i32
funksiyasi biz 10-3 roʻyxatda ajratib olingan funksiya boʻlib, u boʻlakdagi eng katta i32
ni topadi. eng_katta_char
funksiyasi bo‘lakdagi eng katta char
ni topadi. Funksiya organlari bir xil kodga ega, shuning uchun bitta funksiyaga generik turdagi parametrni kiritish orqali takrorlanishni bartaraf qilaylik.
Yangi bitta funksiyada turlarni parametrlash uchun, biz funksiyaning qiymat parametrlari uchun qilganimiz kabi, tur parametrini nomlashimiz kerak. Tur parametri nomi sifatida istalgan identifikatordan foydalanishingiz mumkin. Lekin biz T
dan foydalanamiz, chunki Rust-dagi parametr nomlari odatda qisqa, koʻpincha harfdan iborat boʻladi va Rustning tur nomlash konventsiyasi UpperCamelCase hisoblanadi. “type(tur)” so'zining qisqartmasi T
, Rust dasturchilarining ko'pchiligining standart tanlovidir.
Funksiya tanasida parametrdan foydalanganda, biz imzoda parametr nomini e'lon qilishimiz kerak, shunda kompilyator bu nom nimani anglatishini biladi.
Xuddi shunday, biz funktsiya imzosida tup parametri nomini ishlatganimizda, uni ishlatishdan oldin parametr nomini e'lon qilishimiz kerak. Generik eng_katta
funksiyani aniqlash uchun burchakli qavslar ichida <>
nomi deklaratsiyasini funksiya nomi va parametrlar ro'yxati orasiga qo'ying, masalan:
fn eng_katta<T>(list: &[T]) -> &T {
Biz bu taʼrifni shunday oʻqiymiz: eng_katta
funksiyasi T
turiga nisbatan umumiydir. Bu funksiya list
nomli bitta parametrga ega, bu T
turidagi qiymatlar bo'lagidir. eng_katta
funksiya bir xil turdagi T
qiymatiga referenceni qaytaradi.
10-5 ro'yxatda imzodagi umumiy ma'lumotlar turidan foydalangan holda birlashtirilgan eng_katta
funksiya ta'rifi ko'rsatilgan. list shuningdek, funktsiyani i32
yoki char
qiymatlari bilan qanday chaqirishimiz mumkinligini ko'rsatadi. E'tibor bering, bu kod hali kompilyatsiya qilinmaydi, ammo biz uni ushbu bobda keyinroq tuzatamiz.
Fayl nomi: src/main.rs
fn eng_katta<T>(list: &[T]) -> &T {
let mut eng_katta = &list[0];
for element in list {
if element > eng_katta {
eng_katta = element;
}
}
eng_katta
}
fn main() {
let raqamlar_listi = vec![34, 50, 25, 100, 65];
let natija = eng_katta(&raqamlar_listi);
println!("Eng katta raqam {}", natija);
let char_list = vec!['y', 'm', 'a', 'q'];
let natija = eng_katta(&char_list);
println!("Eng katta belgi {}", natija);
}
Agar dasturni hozir kompilyatsiya qilsak, biz quyidagi xatolikni olamiz:
$ 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 element > eng_katta {
| ------- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T`
|
1 | fn eng_katta<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
Yordam matnida std::cmp::PartialOrd
qayd etilgan, bu trait va biz keyingi bo'limda traitlar haqida gaplashamiz. Hozircha shuni bilingki, bu xato eng_katta
tanasi T
bo'lishi mumkin bo'lgan barcha mumkin bo'lgan turlar uchun ishlamasligini bildiradi. Kod tanasidagi T
turidagi qiymatlarni solishtirmoqchi bo'lganimiz uchun biz faqat qiymatlari ordere qilinadigan turlardan foydalanishimiz mumkin. Taqqoslashni yoqish uchun standart kutubxona std::cmp::PartialOrd
traitiga ega bo'lib, uni turlarga tatbiq etishingiz mumkin (bu trait haqida batafsil ma'lumot uchun C ilovasiga qarang). Yordam matnining taklifiga amal qilib, biz T
uchun amal qiladigan turlarni faqat PartialOrd
-ni qo'llaydiganlar bilan cheklaymiz va bu misol kompilyatsiya qilinadi, chunki standart kutubxona PartialOrd
ni ham i32
va char
da qo'llaydi.
Struktura Definitionlarida
Shuningdek, biz <>
sintaksisi yordamida bir yoki bir nechta maydonlarda generik turdagi parametrlardan foydalanish uchun structlarni belgilashimiz mumkin. Ro'yxat 10-6 har qanday turdagi x
va y
koordinata qiymatlarini saqlash uchun Point<T>
structni belgilaydi.
Fayl nomi: 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 }; }
Struktura ta'riflarida generiklardan foydalanish sintaksisi funksiya ta'riflarida qo'llaniladigan sintaksisiga juda oʻxshaydi. Birinchidan, burchakli qavslar ichida strukturaning nomidan keyin tur parametrining nomini e'lon qilamiz. Keyin biz aniq ma'lumotlar turlarini ko'rsatadigan struct ta'rifida generik turdan foydalanamiz.
Esda tutingki, biz Point<T>
ni aniqlash uchun faqat bitta generik turdan foydalanganmiz, bu taʼrifda aytilishicha, Point<T>
structi ba'zi bir T
turiga nisbatan umumiy boʻlib, x
va y
maydonlari qaysi turdagi boʻlishidan qatʼi nazar bir xil turdagi dir. Agar biz 10-7 ro'yxatdagi kabi har xil turdagi qiymatlarga ega bo'lgan Point<T>
nusxasini yaratsak, bizning kodimiz kompilyatsiya qilinmaydi.
Fayl nomi: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let ishlamaydi = Point { x: 5, y: 4.0 };
}
Ushbu misolda, biz x
ga 5 butun qiymatini belgilaganimizda, kompilyatorga T
generik turi Point<T>
misoli uchun butun son bo'lishini bildiramiz. Keyin biz x
bilan bir xil turga ega ekanligini aniqlagan y
uchun 4.0 ni belgilaganimizda, biz quyidagi turdagi nomuvofiqlik xatosini olamiz:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let ishlamaydi = 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
va y
ikkalasi ham generik bo'lgan, lekin har xil turlarga ega bo'lishi mumkin bo'lgan Point
strukturasini aniqlash uchun biz bir nechta generik turdagi parametrlardan foydalanishimiz mumkin. Masalan, 10-8 roʻyxatda biz Point
taʼrifini T
va U
turlari boʻyicha umumiy qilib oʻzgartiramiz, bunda x
T
turiga, y
esa U
turiga tegishli.
Fayl nomi: src/main.rs
struct Point<T, U> { x: T, y: U, } fn main() { let ikkita_integer = Point { x: 5, y: 10 }; let ikkita_float = Point { x: 1.0, y: 4.0 }; let integer_va_float = Point { x: 5, y: 4.0 }; }
Endi ko'rsatilgan Point
ning barcha misollariga ruxsat berilgan! Ta'rifda siz xohlagancha turdagi parametrlardan generik foydalanishingiz mumkin, lekin bir nechtadan ko'proq foydalanish kodingizni o'qishni qiyinlashtiradi. Agar siz kodingizda ko'plab generik turlar kerakligini aniqlasangiz, bu sizning kodingizni kichikroq qismlarga qayta qurish kerakligini ko'rsatishi mumkin.
Enum Definitionlarida
Structlar bilan qilganimizdek, ularning variantlarida generik ma'lumotlar turlarini saqlash uchun enumlarni belgilashimiz mumkin. Biz 6-bobda foydalanilgan standart kutubxona taqdim etadigan Option<T>
enumini yana bir ko'rib chiqamiz:
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
Bu ta'rif endi siz uchun yanada ma'noli bo'lishi kerak. Ko'rib turganingizdek, Option<T>
enum T
turiga nisbatan generik va ikkita variantga ega: T
turidagi bitta qiymatga ega Some
va hech qanday qiymatga ega bo'lmagan None
varianti.
Option<T>
enum yordamida biz ixtiyoriy qiymatning mavhum kontseptsiyasini ifodalashimiz mumkin va Option<T>
umumiy bo'lgani uchun biz ixtiyoriy qiymatning turi qanday bo'lishidan qat`i nazar, bu abstraktsiyadan foydalanishimiz mumkin.
Enumlar bir nechta generik turlardan ham foydalanishi mumkin. Biz 9-bobda aytib o'tgan Result
enumining ta'rifi ushbu foydalanishga misoldir:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
Result
enumlari ikki xil, T
va E
uchun generikdir va ikkita variantga ega: T
turidagi qiymatga ega OK
va E
turidagi qiymatga ega bo'lgan Err
. Bu taʼrif Result
enumidan bizda muvaffaqiyatli boʻlishi mumkin boʻlgan (T
turidagi qiymatni qaytarish) yoki muvaffaqiyatsiz boʻlishi mumkin boʻlgan (E
turidagi xatolikni qaytarish) istalgan joyda foydalanishni qulay qiladi. Aslida, biz 9-3 ro'yxatdagi faylni shunday ochar edik, bu yerda fayl muvaffaqiyatli ochilganda T
std::fs::File
turi bilan to'ldirilgan va faylni ochishda muammolar yuzaga kelganda E
std::io::Error
turi bilan to`ldirilgan.
Kodingizdagi vaziyatlarni faqat ular ega bo'lgan qiymatlar turlarida farq qiluvchi bir nechta struct yoki enum ta'riflari bilan tanib olganingizda, uning o'rniga generik turlardan foydalanish orqali takrorlanishdan qochishingiz mumkin.
Metod Definitionlarida
Biz structlar va enumlar bo'yicha metodlarni qo'llashimiz mumkin (5-bobda qilganimiz kabi) va ularning ta'riflarida generik turlardan ham foydalanishimiz mumkin. 10-9 ro'yxatda biz 10-6 ro'yxatda belgilagan Point<T>
structi ko'rsatilgan va unda x
nomli metod qo'llaniladi.
Fayl nomi: 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()); }
Bu yerda biz Point<T>
da x
nomli metodni belgilab oldik, u x
maydonidagi ma`lumotlarga referenceni qaytaradi.
Esda tutingki, biz impl
dan keyin T
ni e'lon qilishimiz kerak, shuning uchun biz Point<T>
turidagi metodlarni amalga oshirayotganimizni aniqlash uchun T
dan foydalanishimiz mumkin. T
ni impl
dan keyin generik tur sifatida e'lon qilish orqali Rust Point
dagi burchak qavslaridagi tur aniq tur emas, balki generik tur ekanligini aniqlay oladi. Biz ushbu umumiy parametr uchun struct taʼrifida eʼlon qilingan generik parametrdan boshqa nom tanlashimiz mumkin edi, lekin bir xil nomdan foydalanish odatiy hisoblanadi. Generik turni e'lon qiladigan impl
ichida yozilgan metodlar, generik turdagi o'rnini bosadigan aniq turdagi qanday bo'lishidan qat'i nazar, har qanday turdagi namunada aniqlanadi.
Tur bo'yicha metodlarni belgilashda generik turlarga cheklovlarni ham belgilashimiz mumkin. Biz, masalan, har qanday generik turdagi Point<T>
misollarida emas, balki faqat Point<f32>
misollarida metodlarni amalga oshirishimiz mumkin. 10-10 ro'yxatda biz f32
aniq turidan foydalanamiz, ya'ni impl
dan keyin hech qanday turni e'lon qilmaymiz.
Fayl nomi: src/main.rs
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } impl Point<f32> { fn kelib_chiqishidan_masofa(&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()); }
Bu kod Point<f32>
turi kelib_chiqishidan_masofa
metodiga ega bo'lishini bildiradi; T
f32
turiga tegishli bo'lmagan Point<T>
ning boshqa misollarida bu metod aniqlanmaydi. Metod bizning pointimizning koordinatadagi nuqtadan qanchalik uzoqligini o'lchaydi (0,0, 0,0) va faqat floating point turlari uchun mavjud bo'lgan matematik operatsiyalardan foydalanadi.
Struct taʼrifidagi generik turdagi parametrlar har doim ham oʻsha structning metod imzolarida foydalanadigan parametrlar bilan bir xil boʻlavermaydi. 10-11 roʻyxatda misolni aniqroq qilish uchun Point
structsi uchun X1
va Y1
va aralashtirish
metodi imzosi uchun X2
Y2
generik turlari qoʻllaniladi. Metod yangi Point
misolini yaratadi
self
Point
(X1
turidagi) x
qiymati va o'tkazilgan Point
(Y2
turidagi) y
qiymati.
Fayl nomi: src/main.rs
struct Point<X1, Y1> { x: X1, y: Y1, } impl<X1, Y1> Point<X1, Y1> { fn aralashtirish<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: "Salom", y: 'c' }; let p3 = p1.aralashtirish(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); }
main
da biz x
uchun i32
(5
qiymati bilan) va y
uchun f64
(10,4
qiymati bilan) bo'lgan Point
ni aniqladik. p2
o'zgaruvchisi bu Point
structi bo'lib, x
(Salom
qiymati bilan) va y
(c
qiymati bilan) uchun char
bo'lagiga ega. p1
da aralashtirish
ni p2
argumenti bilan chaqirish bizga p3
ni beradi, bunda x
uchun i32
bo‘ladi, chunki x
p1
dan kelgan. p3
o‘zgaruvchisi y
uchun char
ga ega bo‘ladi, chunki y
p2
dan kelgan. println!
makro chaqiruvi p3.x = 5, p3.y = c
ni chop etadi.
Ushbu misolning maqsadi ba'zi generik parametrlar impl
bilan e'lon qilingan va ba'zilari metod ta'rifi bilan e'lon qilingan vaziyatni ko'rsatishdir. Bu erda X1
va Y1
generik parametrlari impl
dan keyin e'lon qilinadi, chunki ular struct ta'rifiga mos keladi. X2
va Y2
generik parametrlari fn aralashtirish
dan keyin e'lon qilinadi, chunki ular faqat metodga tegishli.
Generiklar yordamida kodning ishlashi
Generik turdagi parametrlardan foydalanganda ish vaqti narxi bor yoki yo'qligini sizni qiziqtirgan bo'lishi mumkin. Yaxshi xabar shundaki, generik turlardan foydalanish dasturingizning aniq turlariga qaraganda sekinroq ishlashiga olib kelmaydi.
Rust buni kompilyatsiya vaqtida generiklar yordamida kodni monomorfizatsiya qilish orqali amalga oshiradi. Monomorfizatsiya - bu kompilyatsiya paytida ishlatiladigan aniq turlarni to'ldirish orqali generik kodni maxsus kodga aylantirish jarayoni. Ushbu jarayonda kompilyator biz 10-5 ro'yxatdagi generik funksiyani yaratishda qo'llagan qadamlarning teskarisini bajaradi: kompilyator generik kod chaqiriladigan barcha joylarni ko'rib chiqadi va generik kod chaqirilgan aniq turlar uchun kod ishlab chiqaradi.
Keling, bu standart kutubxonaning umumiy Option<T>
enum yordamida qanday ishlashini ko'rib chiqaylik:
#![allow(unused)] fn main() { let integer = Some(5); let float = Some(5.0); }
Rust ushbu kodni kompilyatsiya qilganda, u monomorfizatsiyani amalga oshiradi. Ushbu jarayon davomida kompilyator Option<T>
misollarida ishlatilgan qiymatlarni o'qiydi va ikki xil Option<T>
ni aniqlaydi: biri i32
, ikkinchisi esa f64
. Shunday qilib, u Option<T>
ning umumiy ta'rifini i32
va f64
uchun ixtisoslashgan ikkita ta'rifga kengaytiradi va shu bilan umumiy ta'rifni o'ziga xos ta'riflar bilan almashtiradi.
Kodning monomorflashtirilgan versiyasi quyidagiga o'xshaydi (kompilyator biz tasvirlash uchun ishlatayotganimizdan boshqa nomlardan foydalanadi):
Fayl nomi: 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); }
Generik Option<T>
kompilyator tomonidan yaratilgan maxsus ta`riflar bilan almashtiriladi. Rust generik kodni har bir misolda turni belgilaydigan kodga kompilyatsiya qilganligi sababli, biz generiklardan foydalanish uchun hech qanday ish vaqti to'lamaymiz. Kod ishga tushganda, agar biz har bir ta'rifni qo'lda takrorlagan bo'lsak, xuddi shunday ishlaydi. Monomorfizatsiya jarayoni Rust generiklarini runtimeda juda samarali qiladi.
Traitlar: umumiy xatti-harakatni aniqlash
trait ma'lum bir turga ega bo'lgan va boshqa turlar bilan bo'lishishi mumkin bo'lgan funksionallikni belgilaydi. Biz umumiy xatti-harakatni mavhum tarzda aniqlash uchun traitlardan foydalanishimiz mumkin. Generik tur ma'lum xatti-harakatlarga ega bo'lgan har qanday tur bo'lishi mumkinligini aniqlash uchun trait (bound)chegaralari dan foydalanishimiz mumkin.
Eslatma: Traitlar ba'zi farqlarga ega bo'lsa-da, ko'pincha boshqa tillarda interfeyslar deb ataladigan xususiyatga o'xshaydi.
Traitni aniqlash
Turning xatti-harakati biz ushbu turga murojaat qilishimiz mumkin bo'lgan metodlardan iborat. Agar biz ushbu turlarning barchasida bir xil metodlarni chaqira olsak, har xil turlar bir xil xatti-harakatlarga ega. Trait ta'riflari - bu qandaydir maqsadga erishish uchun zarur bo'lgan xatti-harakatlar to'plamini aniqlash uchun metod imzolarini birgalikda guruhlash usuli.
Misol uchun, bizda turli xil va hajmdagi matnlarni o'z ichiga olgan bir nechta structlar mavjud deylik: ma'lum bir joyda joylashtirilgan yangiliklarni o'z ichiga olgan YangiMaqola
structi va eng ko'pi 280 belgidan iborat bo'lishi mumkin bo'lgan Maqola
yangi post, retpost yoki boshqa postga javob ekanligini ko'rsatadigan metama'lumotlar.
Biz YangiMaqola
yoki Maqola
misolida saqlanishi mumkin bo‘lgan ma’lumotlarning qisqacha mazmunini ko‘rsata oladigan aggregator
nomli media agregator kutubxonasini yaratmoqchimiz. Buni amalga oshirish uchun bizga har bir tur bo'yicha xulosa kerak bo'ladi va biz ushbu xulosani misolda umumiy_xulosa
metodini chaqirish orqali so'raymiz. 10-12 ro'yxatda ushbu xatti-harakatni ifodalovchi umumiy Xulosa
traitining ta'rifi ko'rsatilgan.
Fayl nomi: src/lib.rs
pub trait Xulosa {
fn umumiy_xulosa(&self) -> String;
}
Bu yerda biz trait
kalit so'zidan foydalanib traitni e'lon qilamiz, so'ngra belgi nomi, bu holda Xulosa
. Shuningdek, biz ushbu traitni pub
deb e’lon qildik, shunda bu cratega bog‘liq bo‘lgan cratelar ham bu traitdan foydalanishi mumkin, buni bir necha misollarda ko‘ramiz. Jingalak qavslar ichida biz ushbu traitni amalga oshiradigan turlarning xatti-harakatlarini tavsiflovchi metod imzolarini e'lon qilamiz, bu holda fn umumiy_xulosa(&self) -> String
.
Metod imzosidan so'ng, jingalak qavslar ichida amalga oshirish o'rniga, biz nuqta-verguldan foydalanamiz. Ushbu traitni amalga oshiradigan har bir tur metod tanasi uchun o'ziga xos xatti-harakatni ta'minlashi kerak. Kompilyator Xulosa
traitiga ega boʻlgan har qanday turda aynan shu imzo bilan aniqlangan umumiy_xulosa
metodi boʻlishini talab qiladi.
Traitining tanasida bir nechta metodlar bo'lishi mumkin: metod imzolari har bir satrda bittadan ko'rsatilgan va har bir satr nuqtali vergul bilan tugaydi.
Turga xos traitni amalga oshirish
Endi biz Xulosa
traiti metodlarining kerakli imzolarini aniqlaganimizdan so‘ng, uni media agregatorimizdagi turlarga qo‘llashimiz mumkin. 10-13 roʻyxat sarlavhadan foydalanadigan YangiMaqola
structidagi Xulosa
traitining amalga oshirilishini koʻrsatadi, muallif va umumiy_xulosa
qaytish qiymatini yaratish uchun joy. Maqola
structi uchun biz umumiy_xulosa
ni foydalanuvchi nomi va undan keyin maqolaning butun matni sifatida belgilaymiz, maqola mazmuni allaqachon 280 belgi bilan cheklangan deb hisoblaymiz.
Fayl nomi: src/lib.rs
pub trait Xulosa {
fn umumiy_xulosa(&self) -> String;
}
pub struct YangiMaqola {
pub sarlavha: String,
pub manzil: String,
pub muallif: String,
pub mazmuni: String,
}
impl Xulosa for YangiMaqola {
fn umumiy_xulosa(&self) -> String {
format!("{}, by {} ({})", self.sarlavha, self.muallif, self.manzil)
}
}
pub struct Maqola {
pub foydalanuvchi: String,
pub mazmuni: String,
pub javob_berish: bool,
pub repost: bool,
}
impl Xulosa for Maqola {
fn umumiy_xulosa(&self) -> String {
format!("{}: {}", self.foydalanuvchi, self.mazmuni)
}
}
Traitni turga tatbiq etish odatiy usullarni amalga oshirishga o'xshaydi. Farqi shundaki, impl
dan so'ng biz amalga oshirmoqchi bo'lgan trait nomini qo'yamiz, so'ng for
kalit so'zidan foydalanamiz va keyin traitni amalga oshirmoqchi bo'lgan tur nomini belgilaymiz. impl
blokida biz trait ta'rifi belgilagan metod imzolarini qo'yamiz. Har bir imzodan keyin nuqta-vergul qo'yish o'rniga, biz jingalak qavslardan foydalanamiz va metod tanasini o'ziga xos xatti-harakat bilan to'ldiramiz, biz traitning metodlari ma'lum bir turga ega bo'lishini xohlaymiz.
Kutubxona YangiMaqola
va Maqola
da Xulosa
traitini joriy qilganligi sababli, crate foydalanuvchilari YangiMaqola
va Maqola
misollaridagi xususiyat metodlarini biz odatdagi metodlar deb ataganimizdek chaqirishlari mumkin. Yagona farq shundaki, foydalanuvchi o'ziga xos traitni turlari bilan bir qatorda qamrab olishi kerak. Binary crate bizning aggregator
kutubxonamiz cratesidan qanday foydalanishi mumkinligiga misol:
use aggregator::{Xulosa, Maqola};
fn main() {
let maqola = Maqola {
foydalanuvchi: String::from("ismoilovdev"),
mazmuni: String::from(
"Rust kitobi juda foydali ekan, men juda ko'p bilimlarni o'zlashtirdim",
),
javob_berish: false,
repost: false,
};
println!("1 ta yangi xabar: {}", maqola.umumiy_xulosa());
}
Bu kod 1 ta yangi xabar: ismoilovdev: Rust kitobi juda foydali ekan, men juda ko'p bilimlarni o'zlashtirdim
chop etadi.
aggregator
cratesiga bog'liq bo'lgan boshqa cratelar ham Xulosa
traitini o'z turlari bo'yicha Xulosa
ni amalga oshirish uchun qamrab olishi mumkin. E'tiborga olish kerak bo'lgan cheklashlardan biri shundaki, biz trait yoki turning hech bo'lmaganda bittasi bizning cratemiz uchun mahalliy(local) bo'lsa, biz traitni turga qo'llashimiz mumkin. Misol uchun, biz Maqola
kabi maxsus turdagi Display
kabi standart kutubxona traitlarini aggregator
crate funksiyamizning bir qismi sifatida amalga oshirishimiz mumkin, chunki Maqola
turi aggregator
cratemiz uchun mahalliydir. Shuningdek, biz Vec<T>
da Xulosa
ni aggregator
cratemizda ham qo‘llashimiz mumkin, chunki Xulosa
traiti aggregator
cratemiz uchun mahalliydir.
Ammo biz tashqi turlarga tashqi traitlarni amalga oshira olmaymiz. Masalan, biz aggregator
cratemiz ichida Vec<T>
da Display
traitini amalga oshira olmaymiz, chunki Display
va Vec<T>
ikkalasi ham standart kutubxonada belgilangan va bizning aggregator
cratemiz uchun mahalliy emas. Bu cheklash kogerentlik(coherence) deb nomlangan xususiyatning bir qismi va aniqrog'i yetim qoidasi(orphan rule), chunki ota-ona turi mavjud emasligi sababli shunday nomlangan. Bu qoida boshqa odamlarning kodi sizning kodingizni buzmasligini ta'minlaydi va aksincha. Qoidalarsiz ikkita crate bir xil turdagi bir xil traitni amalga oshirishi mumkin edi va Rust qaysi dasturdan foydalanishni bilmaydi.
Standart ilovalar
Ba'zan har bir turdagi barcha metodlarni amalga oshirishni talab qilish o'rniga, traitdagi ba'zi yoki barcha metodlar uchun standart xatti-harakatlarga ega bo'lish foydali bo'ladi. Keyin, biz traitni ma'lum bir turga qo'llaganimizda, har bir metodning standart xatti-harakatlarini saqlab qolishimiz yoki bekor qilishimiz mumkin.
Roʻyxat 10-14da biz 10-12 ro'yxatda bo'lgani kabi faqat metod imzosini belgilash o'rniga Xulosa
traitining umumiy_xulosa
metodi uchun standart qatorni belgilaymiz.
Fayl nomi: src/lib.rs
pub trait Xulosa {
fn umumiy_xulosa(&self) -> String {
String::from("(Batafsil...)")
}
}
pub struct YangiMaqola {
pub sarlavha: String,
pub manzil: String,
pub muallif: String,
pub mazmuni: String,
}
impl Xulosa for YangiMaqola {}
pub struct Maqola {
pub foydalanuvchi: String,
pub mazmuni: String,
pub javob_berish: bool,
pub repost: bool,
}
impl Xulosa for Maqola {
fn umumiy_xulosa(&self) -> String {
format!("{}: {}", self.foydalanuvchi, self.mazmuni)
}
}
YangiMaqola
misollarini umumlashtirish uchun standart ilovadan foydalanish uchun biz bo'sh impl
blokini impl Xulosa for YangiMaqola {}
bilan belgilaymiz.
Biz YangiMaqola
da to‘g‘ridan-to‘g‘ri umumiy_xulosa
metodini endi aniqlamasak ham, biz standart bo‘yicha dasturni taqdim etdik va YangiMaqola
Xulosa
traitini amalga oshirishini belgilab oldik. Natijada, biz hali ham YangiMaqola
misolida umumiy_xulosa
metodini quyidagicha chaqirishimiz mumkin:
use aggregator::{self, YangiMaqola, Xulosa};
fn main() {
let maqola = YangiMaqola {
sarlavha: String::from("Tesla yangi elektromobil ustida ishlayapti"),
manzil: String::from("USA"),
muallif: String::from("Elon Musk"),
mazmuni: String::from(
"Hozirgi kunda Tesla yangi innovatsion elektromobil\
ustida ishlamoqda.",
),
};
println!("Yangi maqola mavjud! {}", maqola.umumiy_xulosa());
}
Bu kod Yangi maqola mavjud! (Batafsil...)
ni chop etadi.
Standart dasturni yaratish bizdan 10-13 roʻyxatdagi Maqola
dagi Xulosa
ni amalga oshirish haqida biror narsani oʻzgartirishimizni talab qilmaydi. Buning sababi, standart dasturni bekor qilish sintaksisi standart dasturga ega bo'lmagan trait metodini amalga oshirish sintaksisi bilan bir xil.
Standart ilovalar bir xil traitga ega bo'lgan boshqa metodlarni chaqirishi mumkin, hatto bu boshqa metodlarda standart dastur bo'lmasa ham. Shunday qilib, trait juda ko'p foydali funksiyalarni taqdim etishi mumkin va amalga oshiruvchilardan faqat uning kichik qismini ko'rsatishni talab qiladi. Misol uchun, biz Xulosa
traitini amalga oshirish zarur bo'lgan muallif_haqida
metodiga ega bo'lish uchun belgilashimiz va keyin muallif_haqida
metodini chaqiradigan standart amalga oshirishga ega bo'lgan umumiy_xulosa
metodini belgilashimiz mumkin:
pub trait Xulosa {
fn muallif_haqida(&self) -> String;
fn umumiy_xulosa(&self) -> String {
format!("(Batafsil: {}...)", self.muallif_haqida())
}
}
pub struct Maqola {
pub foydalanuvchi: String,
pub mazmuni: String,
pub javob_berish: bool,
pub repost: bool,
}
impl Xulosa for Maqola {
fn muallif_haqida(&self) -> String {
format!("@{}", self.foydalanuvchi)
}
}
Xulosa
ning ushbu versiyasidan foydalanish uchun biz faqat bir turdagi traitni amalga oshirganimizda muallif_haqida
ni aniqlashimiz kerak:
pub trait Xulosa {
fn muallif_haqida(&self) -> String;
fn umumiy_xulosa(&self) -> String {
format!("(Batafsil: {}...)", self.muallif_haqida())
}
}
pub struct Maqola {
pub foydalanuvchi: String,
pub mazmuni: String,
pub javob_berish: bool,
pub repost: bool,
}
impl Xulosa for Maqola {
fn muallif_haqida(&self) -> String {
format!("@{}", self.foydalanuvchi)
}
}
muallif_haqida
ni aniqlaganimizdan so'ng, biz Maqola
structi misollarida umumiy_xulosa
deb atashimiz mumkin va umumiy_xulosa
standart bajarilishi biz taqdim etgan muallif_haqida
ta'rifini chaqiradi. Biz muallif_haqida
ni qo'llaganimiz sababli, Xulosa
traiti bizga boshqa kod yozishni talab qilmasdan umumiy_xulosa
metodining harakatini berdi.
use aggregator::{self, Xulosa, Maqola};
fn main() {
let maqola = Maqola {
foydalanuvchi: String::from("ismoilovdev"),
mazmuni: String::from(
"Rust kitobi juda foydali ekan, men juda ko'p bilimlarni o'zlashtirdim",
),
javob_berish: false,
repost: false,
};
println!("1 ta yangi xabar: {}", maqola.umumiy_xulosa());
}
Bu kod 1 ta yangi xabar: (Batafsil: @ismoilovdev...)
ni chop etadi.
Shuni esda tutingki, xuddi shu metodni bekor qilish orqali standart dasturni chaqirish mumkin emas.
Traitlar parametr sifatida
Endi siz traitlarni qanday aniqlash va amalga oshirishni bilganingizdan so'ng, biz ko'plab turlarni qabul qiladigan funksiyalarni aniqlash uchun traitlardan qanday foydalanishni o'rganishimiz mumkin. Biz 10-13 roʻyxatdagi YangiMaqola
va Maqola
turlari uchun joriy qilingan Xulosa
traitidan foydalanamiz, uning element
parametri boʻyicha umumlashtirish metodlini chaqiradigan xabar_berish
funksiyasini belgilaymiz, u Xulosa
traitini amalga oshiradi. Buning uchun biz impl Trait
sintaksisidan foydalanamiz, masalan:
pub trait Xulosa {
fn umumiy_xulosa(&self) -> String;
}
pub struct YangiMaqola {
pub sarlavha: String,
pub manzil: String,
pub muallif: String,
pub mazmuni: String,
}
impl Xulosa for YangiMaqola {
fn umumiy_xulosa(&self) -> String {
format!("{}, by {} ({})", self.sarlavha, self.muallif, self.manzil)
}
}
pub struct Maqola {
pub foydalanuvchi: String,
pub mazmuni: String,
pub javob_berish: bool,
pub repost: bool,
}
impl Xulosa for Maqola {
fn umumiy_xulosa(&self) -> String {
format!("{}: {}", self.foydalanuvchi, self.mazmuni)
}
}
pub fn xabar_berish(element: &impl Xulosa) {
println!("Tezkor xabarlar! {}", element.umumiy_xulosa());
}
element
parametri uchun aniq tur o'rniga biz impl
kalit so'zini va trait nomini belgilaymiz. Ushbu parametr belgilangan traitni amalga oshiradigan har qanday turni qabul qiladi. xabar_berish
qismida biz Xulosa
traitidan kelib chiqadigan element
bo‘yicha har qanday metodlarni chaqirishimiz mumkin, masalan, umumiy_xulosa
. Biz xabar_berish
ga chaiqruv qilishimiz va YangiMaqola
yoki Maqola
ning istalgan misolida o'tishimiz mumkin. Funksiyani String
yoki i32
kabi boshqa har qanday turdagi chaqiruvchi kod kompilyatsiya qilinmaydi, chunki bu turlar Xulosa
ni amalga oshirmaydi.
Traitlarni cheklash sintaksisi
impl Trait
sintaksisi oddiy holatlar uchun ishlaydi, lekin aslida trait bound deb nomlanuvchi uzunroq shakl uchun sintaksis shakaridir; bu shunday ko'rinadi:
pub fn xabar_berish<T: Xulosa>(element: &T) {
println!("Tezkor xabarlar! {}", element.umumiy_xulosa());
}
Ushbu uzunroq shakl oldingi bo'limdagi misolga teng, ammo batafsilroq. Trait chegaralarini ikki nuqta va ichki burchakli qavslardan keyin umumiy tur parametri e'lon qilingan holda joylashtiramiz.
impl Trait
sintaksisi qulay va oddiy holatlarda ixchamroq kodni yaratadi, to'liqroq traitlar bilan bog'langan sintaksisi esa boshqa holatlarda ko'proq murakkablikni ifodalashi mumkin. Misol uchun, bizda Xulosa
ni amalga oshiradigan ikkita parametr bo'lishi mumkin. Buni impl Trait
sintaksisi bilan bajarish quyidagicha ko'rinadi:
pub fn xabar_berish(element1: &impl Xulosa, element2: &impl Xulosa) {
Agar biz ushbu funksiya element1
va element2
turli xil turlarga ega bo'lishini istasak, impl Trait
dan foydalanish maqsadga muvofiqdir (agar ikkala tur ham Xulosa
ni qo'llasa). Agar biz ikkala parametrni bir xil turga ega bo'lishga majburlamoqchi bo'lsak, quyidagi kabi trait bounddan foydalanishimiz kerak:
pub fn xabar_berish<T: Xulosa>(element1: &T, element2: &T) {
element1
va element2
parametrlarining turi sifatida belgilangan umumiy T
turi funksiyani shunday cheklaydiki, element1
va element2
uchun argument sifatida berilgan qiymatning aniq turi bir xil bo`lishi kerak.
+
sintaksisi bilan bir nechta trait chegaralarini belgilash
Bundan tashqari, biz bir nechta traitlarni belgilashimiz mumkin. Aytaylik, biz xabar_berish
funksiyasidan display formatlash hamda element
bo‘yicha umumiy_xulosa
dan foydalanishni xohladik: biz xabar_berish
ta'rifida element
Display
va Xulosa
ni ham amalga oshirishi kerakligini belgilaymiz. Buni +
sintaksisi yordamida amalga oshirishimiz mumkin:
pub fn xabar_berish(element: &(impl Xulosa + Display)) {
+
sintaksisi generik turdagi belgilar chegaralari bilan ham amal qiladi:
pub fn xabar_berish<T: Xulosa+ Display>(element: &T) {
Belgilangan ikkita trait chegarasi bilan xabar_berish
asosiy qismi umumiy_xulosa
deb chaqirishi va element
ni formatlash uchun {}
dan foydalanishi mumkin.
where
bandlari bilan aniqroq trait bounds(chegaralari)
Haddan tashqari ko'p belgilar boundlaridan foydalanish o'zining salbiy tomonlariga ega. Har bir generikning o'ziga xos trait boundlari bor, shuning uchun bir nechta umumiy turdagi parametrlarga ega funksiyalar funksiya nomi va uning parametrlar ro'yxati o'rtasida ko'plab belgilar bilan bog'liq ma'lumotlarni o'z ichiga olishi mumkin, bu funksiya imzosini o'qishni qiyinlashtiradi. Shu sababli, Rust funksiya imzosidan keyin where
bandida trait boundlarini belgilash uchun muqobil sintaksisga ega.
fn boshqa_funksiya<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
biz where
bandidan foydalanishimiz mumkin, masalan:
fn boshqa_funksiya<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
unimplemented!()
}
Bu funksiya imzosi kamroq chalkash: funksiya nomi, parametrlar ro'yxati va qaytish turi bir-biriga yaqin bo'lib, ko'p trait boundlari bo'lmagan funksiyaga o'xshaydi.
Traitlarni amalga oshiradigan Return(qaytaruvchi) turlar
Bu yerda ko'rsatilganidek, traitni amalga oshiradigan ba'zi turdagi qiymatni qaytarish(return) uchun impl Trait
sintaksisini return holatida ham ishlatishimiz mumkin:
pub trait Xulosa {
fn umumiy_xulosa(&self) -> String;
}
pub struct YangiMaqola {
pub sarlavha: String,
pub manzil: String,
pub muallif: String,
pub mazmuni: String,
}
impl Xulosa for YangiMaqola {
fn umumiy_xulosa(&self) -> String {
format!("{}, by {} ({})", self.sarlavha, self.muallif, self.manzil)
}
}
pub struct Maqola {
pub foydalanuvchi: String,
pub mazmuni: String,
pub javob_berish: bool,
pub repost: bool,
}
impl Xulosa for Maqola {
fn umumiy_xulosa(&self) -> String {
format!("{}: {}", self.foydalanuvchi, self.mazmuni)
}
}
fn return_xulosa() -> impl Xulosa {
Maqola {
foydalanuvchi: String::from("ismoilovdev"),
mazmuni: String::from(
"Rust kitobi juda foydali ekan, men juda ko'p bilimlarni o'zlashtirdim",
),
javob_berish: false,
repost: false,
}
}
Qaytish(return) turi uchun impl Xulosa
dan foydalanib, biz return_xulosa
funksiyasi aniq turga nom bermasdan Xulosa
traitini amalga oshiradigan ba'zi turlarni qaytarishini aniqlaymiz. Bunday holda, return_xulosa
Maqola
ni qaytaradi, lekin bu funksiyani chaqiruvchi kod buni bilishi shart emas.
Qaytish turini faqat u amalga oshiradigan traitga ko'ra belgilash qobiliyati, ayniqsa, biz 13-bobda ko'rib chiqiladigan closurelar va iteratorlar kontekstida foydalidir. Closures va iteratorlar faqat kompilyator biladigan turlarni yoki belgilash uchun juda uzoq turlarni yaratadi. impl Trait
sintaksisi sizga funksiya juda uzun turni yozishga hojat qoldirmasdan Iterator
traitini amalga oshiradigan ba'zi turlarni qaytarishini qisqacha belgilash imkonini beradi.
Biroq, faqat bitta turni qaytarayotgan bo'lsangiz, impl Trait
dan foydalanishingiz mumkin. Masalan, YangiMaqola
yoki Maqola
ni qaytaruvchi impl Xulosa
sifatida ko‘rsatilgan qaytarish turiga ega bo‘lgan bu kod ishlamaydi:
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,
}
}
}
YangiMaqola
yoki Maqola
ni qaytarishga impl Trait
sintaksisi kompilyatorda qanday amalga oshirilishi bilan bog‘liq cheklovlar tufayli ruxsat berilmaydi. Ushbu xatti-harakat bilan funksiyani qanday yozishni biz 17-bobning "Turli turdagi qiymatlarga ruxsat beruvchi trait ob'ektlaridan foydalanish" bo'limida ko'rib chiqamiz.
Metodlarni shartli ravishda amalga oshirish uchun Trait Boundlardan foydalanish
Umumiy turdagi parametrlardan foydalanadigan impl
bloki bilan trait bounddan foydalanib, biz belgilangan traitlarni amalga oshiradigan turlar uchun metodlarni shartli ravishda amalga oshirishimiz mumkin. Masalan, 10-15-ro'yxatdagi Pair<T>
turi har doim yangi Pair<T>
nusxasini qaytarish uchun new
funksiyasini amalga oshiradi (5-bobning “Metodlarni aniqlash” boʻlimidan eslaylikki, Self
bu impl
bloki turiga tegishli turdagi taxallus(alias) boʻlib, bu holda Pair<T>
boʻladi). Ammo keyingi impl
blokida Pair<T>
faqat cmp_display
metodini qo'llaydi, uning ichki turi(inner type) T
taqqoslash imkonini beruvchi PartialOrd
traitini va chop etish imkonini beruvchi Display
traittini amalga oshiradi.
Fayl nomi: 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!("Eng katta a'zo x = {}", self.x);
} else {
println!("Eng katta a'zo y = {}", self.y);
}
}
}
Biz shartli ravishda boshqa traitni amalga oshiradigan har qanday tur uchun traitni amalga oshirishimiz mumkin. Trait boundlarni qondiradigan har qanday turdagi tarittni amalga oshirish blanket implementations deb nomlanadi va Rust standart kutubxonasida keng qo'llaniladi. Masalan, standart kutubxona Display
traitini amalga oshiradigan har qanday turdagi ToString
traitini amalga oshiradi. Standart kutubxonadagi impl
bloki ushbu kodga o'xshaydi:
impl<T: Display> ToString for T {
// --snip--
}
Standart kutubxonada bu keng qamrovli dastur mavjud bo'lganligi sababli, biz Display
traitini amalga oshiradigan har qanday turdagi ToString
traiti bilan aniqlangan to_string
metodini chaqirishimiz mumkin. Masalan, biz butun sonlarni mos keladigan String
qiymatlariga shunday aylantirishimiz mumkin, chunki butun sonlar Display
ni amalga oshiradi:
#![allow(unused)] fn main() { let s = 3.to_string(); }
Blanket implementationlari "Implementors" bo'limidagi trait uchun texnik hujjatlarda ko'rinadi.
Traitlar va trait boundlar takrorlanishni kamaytirish uchun generik turdagi parametrlardan foydalanadigan kod yozishga imkon beradi, shuningdek, generik turning o'ziga xos xatti-harakatlariga ega bo'lishini kompilyatorga ko'rsatishga imkon beradi. Keyin kompilyator trait bilan bog'langan ma'lumotlardan bizning kodimiz bilan qo'llaniladigan barcha aniq turlar to'g'ri xatti-harakatni ta'minlaydiganligini tekshirish uchun foydalanishi mumkin. Dinamik ravishda tuzilgan tillarda, agar biz metodni aniqlamagan turdagi metodni chaqirsak, runtimeda xatoga yo'l qo'yamiz. Ammo Rust bu xatolarni vaqtni kompilyatsiya qilish uchun ko'chiradi, shuning uchun biz kodimiz ishga tushgunga qadar muammolarni hal qilishga majbur bo'lamiz. Bundan tashqari, biz runtimeda xatti-harakatni tekshiradigan kod yozishimiz shart emas, chunki biz kompilyatsiya vaqtida allaqachon tekshirganmiz. Bu generiklarning moslashuvchanligidan voz kechmasdan ishlashni yaxshilaydi.
Referencelarni lifetime bilan tekshirish
Lifetimelar - biz allaqachon uchratgan generiklarning yana bir turi. Turning biz xohlagan xatti-harakatga ega bo'lishini ta'minlash o'rniga, lifetime referencelar biz uchun kerak bo'lganda haqiqiyligini ta'minlaydi.
4-bobdagi [“Referencelar va Borrowing”](references-and-borrowing) bo‘limida biz muhokama qilmagan bir tafsilot shundan iboratki, Rust-dagi har bir referenceda o‘sha referencening amal qilish doirasi lifetime bo‘ladi. Ko'pincha, lifetimelar yashirin va inferred bo'ladi, ko'p hollarda bo'lgani kabi, turlar ham inferred qilinadi.Biz faqat bir nechta tur mumkin bo'lganda turlarga izoh berishimiz kerak. Shunga o'xshab, biz referencelarning lifetime bir necha xil yo'llar bilan bog'lanishi mumkin bo'lgan lifetimelarini izohlashimiz kerak. Rust bizdan runtimeda ishlatiladigan haqiqiy referencelar haqiqiy bo'lishini ta'minlash uchun generik lifetime parametrlaridan foydalangan holda munosabatlarga izoh berishimizni talab qiladi.
Lifetimeni izohlash boshqa dasturlash tillarining ko'pchiligida mavjud bo'lgan tushuncha ham emas, shuning uchun bu notanish tuyuladi. Garchi biz ushbu bobda lifetimeni to'liq qamrab olmasak ham, kontseptsiyadan qulay bo'lishingiz uchun lifetime sintaksisga duch kelishingiz mumkin bo'lgan umumiy usullarni muhokama qilamiz.
Lifetimeda dangling referencelarni oldini olish
Lifetimening asosiy maqsadi dasturga reference qilish uchun mo'ljallangan ma'lumotlardan boshqa ma'lumotlarga reference qilishiga olib keladigan dangling referencelar ning oldini olishdir. 10-16 ro'yxatdagi dasturni ko'rib chiqing, uning tashqi va ichki ko'lami(tashqi va ichki ishlash doirasi) bor.
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
Eslatma: 10-16, 10-17 va 10-23 ro'yxatlardagi misollar o'zgaruvchilarni ularga boshlang'ich qiymat bermasdan e'lon qiladi, shuning uchun o'zgaruvchi nomi tashqi doirada mavjud. Bir qarashda, bu Rustning null qiymatlari yo'qligiga zid bo'lib tuyulishi mumkin. Biroq, agar biz o'zgaruvchiga qiymat berishdan oldin foydalanmoqchi bo'lsak, biz kompilyatsiya vaqtida xatoga duch kelamiz, bu Rust haqiqatan ham null qiymatlarga ruxsat bermasligini ko'rsatadi.
Tashqi qamrov boshlang‘ich qiymati bo‘lmagan r
nomli o‘zgaruvchini, ichki qamrov esa boshlang‘ich qiymati 5 bo‘lgan x
nomli o‘zgaruvchini e’lon qiladi. Ichki doirada(qamrov) biz x
ga reference sifatida r
qiymatini belgilashga harakat qilamiz. Keyin ichki qamrov tugaydi va biz qiymatni r
da chop etishga harakat qilamiz. Ushbu kod kompilyatsiya qilinmaydi, chunki biz undan foydalanishga urinishdan oldin r
qiymati ko'rib chiqilmaydi. Mana xato xabari:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | 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
x
o'zgaruvchisi "yetarlicha uzoq umr ko'rmaydi". Sababi, 7-qatorda ichki qamrov tugashi bilan x
amaldan tashqarida bo'ladi. Lekin r
tashqi doira uchun hamon amal qiladi; uning qamrovi kengroq bo'lgani uchun biz uni "uzoq yashaydi" deymiz. Agar Rust ushbu kodning ishlashiga ruxsat bergan bo'lsa, r
x
doiradan chiqib ketganda ajratilgan xotiraga reference bo'ladi va biz r
bilan qilishga uringan har qanday narsa to'g'ri ishlamaydi. Xo'sh, Rust bu kodning yaroqsizligini qanday aniqlaydi?
Bu borrow(qarz) tekshiruvidan foydalanadi.
Borrow tekshiruvchisi
Rust kompilyatorida barcha borrowlar to'g'ri yoki yo'qligini aniqlash uchun ko'lamlarni taqqoslaydigan borrow tekshiruvi(borrow checker) mavjud. 10-17 ro'yxat 10-16 ro'yxati bilan bir xil kodni ko'rsatadi, ammo o'zgaruvchilarning lifetime(ishlash muddatini) ko'rsatadigan izohlar bilan.
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
Bu yerda biz r
ning lifetimeni a
bilan va x
ning lifetimeni b
bilan izohladik. Ko'rib turganingizdek, ichki b
bloki tashqi 'a
lifetime blokdan ancha kichik. Kompilyatsiya vaqtida Rust ikki lifetimening o'lchamini solishtiradi va r
ning lifetime 'a
ekanligini, lekin u 'b
lifetime(umr bo'yi) xotiraga ishora qilishini ko'radi. Dastur rad etildi, chunki 'b
'a
dan qisqaroq: reference mavzusi reference kabi uzoq vaqt yashamaydi.
Ro'yxat 10-18 kodni tuzatadi, shuning uchun u dangling referencega ega emas va hech qanday xatosiz kompilyatsiya qilinadi.
fn main() { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {}", r); // | | // --+ | } // ----------+
Bu erda x
'b
muddatiga ega, bu holda 'a
dan kattaroqdir. Bu r
x
ga murojaat qilishi mumkin degan ma'noni anglatadi, chunki Rust r
dagi reference har doim x
amalda bo`lishini biladi.
Endi siz referencelarning amal qilish muddati qayerda ekanligini va referencelar har doim haqiqiy boʻlishini taʼminlash uchun Rust lifetimeni qanday tahlil qilishini bilganingizdan soʻng, keling, funksiyalar kontekstida parametrlarning generik lifetime va qiymatlarni qaytarishni koʻrib chiqaylik.
Funksiyalarning generik lifetime
Biz ikkita satr bo'lagining uzunligini qaytaradigan funksiyani yozamiz. Bu funksiya ikkita satr bo'lagini oladi va bitta satr bo'lagini qaytaradi. eng_uzun
funksiyasini amalga oshirganimizdan so'ng, 10-19 ro'yxatdagi kod Eng uzun satr - abcd
ni chop etishi kerak.
Fayl nomi: src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("Eng uzun satr {}", result);
}
E'tibor bering, biz funksiya satrlarni emas, referencelar bo'lgan satr bo'laklarini olishni xohlaymiz, chunki biz eng_uzun
funksiya uning parametrlariga egalik qilishni xohlamaymiz. 10 19 roʻyxatda biz foydalanadigan parametrlar nima uchun biz xohlagan parametrlar ekanligi haqida koʻproq muhokama qilish uchun 4-bobdagi “String slicelari parametr sifatida” boʻlimiga qarang.
Agar biz 10-20 ro'yxatda ko'rsatilganidek, eng_uzun
funksiyasini amalga oshirishga harakat qilsak, u kompilyatsiya qilinmaydi.
Fayl nomi: src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let natija = eng_uzun(string1.as_str(), string2);
println!("Eng uzun satr {}", natija);
}
fn eng_uzun(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
Buning o'rniga biz lifetime haqida gapiradigan quyidagi xatoni olamiz:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn eng_uzun(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 eng_uzun<'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
Yordam matni shuni ko'rsatadiki, return(qaytarish) turiga umumiy lifetime parametri kerak, chunki Rust qaytarilayotgan reference x
yoki y
ga tegishli ekanligini aniqlay olmaydi. Aslida, biz ham bilmaymiz, chunki bu funksiyaning asosiy qismidagi if
bloki x
ga referenceni, else
bloki esa y
ga referenceni qaytaradi!
Ushbu funksiyani aniqlaganimizda, biz ushbu funksiyaga o'tadigan aniq qiymatlarni bilmaymiz, shuning uchun if
yoki else
ishi bajarilishini bilmaymiz. Shuningdek, biz uzatiladigan referencelarning aniq amal qilish muddatini bilmaymiz, shuning uchun biz qaytaradigan(return) lifetime har doim haqiqiy bo'lishini aniqlash uchun 10-17 va 10-18 ro'yxatlarda bo'lgani kabi qamrovni ko'rib chiqa olmaymiz. Borrow tekshiruvchisi buni ham aniqlay olmaydi, chunki u x
va y
ning ishlash lifetime qaytarilgan qiymatning lifetime(ishlash muddati) bilan qanday bog'liqligini bilmaydi. Ushbu xatoni tuzatish uchun biz referencelar o'rtasidagi munosabatni aniqlaydigan umumiy lifetime parametrlarini qo'shamiz, shunda borrow tekshiruvi tahlilini amalga oshirishi mumkin.
Lifetime annotation sintaksisi
Lifetime annotationlar referencelarning qancha yashashini ko'rishini o'zgartirmaydi. Aksincha, ular lifetimega ta'sir qilmasdan, bir-biriga ko'plab murojaatlarning umrbod lifetimelar munosabatlarini tasvirlaydi. Signature generik turdagi parametrni ko'rsatsa, funksiyalar har qanday turni qabul qilishi mumkin bo'lgani kabi, funksiyalar ham umumiy lifetime parametrini belgilash orqali har qanday xizmat muddati bilan murojaatlarni qabul qilishi mumkin.
Lifetime annotationlar biroz noodatiy sintaksisga ega: lifetime parametrlarining nomlari apostrof ('
) bilan boshlanishi kerak va odatda generik turlar kabi kichik va juda qisqa bo'ladi. Ko'pchilik lifetime annotation birinchi izoh uchun 'a
nomidan foydalanadi. Annotationi reference turidan ajratish uchun boʻsh joydan foydalanib, biz lifetime parametr annotationlarini referencening &
belgisidan keyin joylashtiramiz.
Mana bir nechta misollar: lifetime parametri bo'lmagan i32
ga reference, 'a
nomli lifetime parametriga ega i32
ga reference va lifetime 'a
bo'lgan i32
ga o'zgaruvchan reference.
&i32 // reference
&'a i32 // aniq lifetimega ega bo'lgan reference
&'a mut i32 // aniq lifetimega ega o'zgaruvchan reference
Bir umrlik lifetime annotatsiyaning o'zi katta ma'noga ega emas, chunki annotatsiyalar Rustga bir nechta referencelalarning lifetime generik parametrlari bir-biriga qanday bog'liqligini aytib berish uchun mo'ljallangan. Keling, eng_uzun
funksiya kontekstida lifetime annotatsiyalarning bir-biriga qanday bog'liqligini ko'rib chiqaylik.
Funksiya signaturelaridagi lifetime annotatsiyalar
Funksiya signaturelarida lifetime annotatsiyalardan foydalanish uchun biz generik tur parametrlari bilan qilganimiz kabi, funksiya nomi va parametrlar ro'yxati o'rtasida burchak qavslar ichida generik lifetime parametrlarini e'lon qilishimiz kerak.
Biz signature quyidagi cheklovni ifodalashini istaymiz: qaytarilgan(return) reference ikkala parametr ham to'g'ri bo'lsa, haqiqiy bo'ladi. Bu parametrlarning lifetime(ishlash muddati) va qaytariladigan(return) qiymat o'rtasidagi bog'liqlikdir. 10-21 ro'yxatda ko'rsatilganidek, biz lifetimega 'a
deb nom beramiz va keyin uni har bir referencega qo'shamiz.
Fayl nomi: src/main.rs
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let natija = eng_uzun(string1.as_str(), string2); println!("Eng uzun satr {}", natija); } fn eng_uzun<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Ushbu kod 10-19-sonli ro'yxatdagi main
funksiyadan foydalanganda biz xohlagan natijani kompilyatsiya qilishi va ishlab chiqarishi kerak.
Funktsiya signaturesi endi Rustga ma'lum bir lifetimeda 'a
funksiyasi ikkita parametrni qabul qilishini aytadi, ularning har ikkalasi ham kamida lifetime 'a
bo'lgan string bo'laklaridir. Funktsiya signaturesi, shuningdek, Rustga funksiyadan qaytarilgan string bo'lagi hech bo'lmaganda 'a
lifetimegacha yashashini aytadi.
Amalda, bu eng_uzun
funksiya tomonidan qaytarilgan referencening lifetime, funksiya argumentlari bilan bog'liq bo'lgan qiymatlarning eng kichik lifetimesi bilan bir xil ekanligini anglatadi. Bu munosabatlar Rust ushbu kodni tahlil qilishda foydalanishini xohlaydigan narsadir.
Esda tutingki, biz ushbu funksiya signaturesida lifetime parametrlarini belgilaganimizda, biz kiritilgan yoki qaytarilgan qiymatlarning lifetimeni o'zgartirmaymiz. Aksincha, biz borrowni tekshiruvchi(borrow checker) ushbu cheklovlarga rioya qilmaydigan har qanday qiymatlarni rad etishi kerakligini ta'kidlaymiz. Shuni esda tutingki, eng_uzun
funksiya x
va y
qancha vaqt ishlashini aniq bilishi shart emas, faqat ushbu signatureni qondiradigan 'a
ga baʼzi bir qamrovni almashtirish mumkin.
Funksiyalarda lifetimeni izohlashda annotationlar funksiya tanasida emas, balki funksiya signaturesida bo'ladi. Signaturedagi turlar singari, lifetime annotationlar funksiya shartnomasining bir qismiga aylanadi. Funktsiya signaturelari lifetime shartnomani o'z ichiga oladi, degan ma'noni anglatadi Rust kompilyatori tahlil qilish osonroq bo'lishi mumkin. Agar funksiyaga izoh berish yoki uni chaqirish bilan bog'liq muammo bo'lsa, kompilyator xatolari kodimizning bir qismiga va cheklovlarga aniqroq ishora qilishi mumkin. Buning o'rniga, Rust kompilyatori biz lifetime munosabatlari haqida ko'proq taxminlar qilgan bo'lsa, kompilyator faqat muammoning sababidan bir necha qadam uzoqda bizning kodimizdan foydalanishni ko'rsatishi mumkin.
Biz eng_uzun
ga aniq referencelar berganimizda, 'a
o‘rniga qo‘yilgan aniq lifetime x
doirasining y
doirasiga to‘g‘ri keladigan qismidir. Boshqacha qilib aytadigan bo'lsak, 'a
generik lifetimesi x
va y
ning eng kichik lifetimaga teng bo'lgan aniq lifetimeni oladi. Biz qaytarilgan(return) referencega bir xil lifetime parametri 'a
bilan izoh berganimiz sababli, qaytarilgan reference x
va y
lifetimening kichikroq uzunligi uchun ham amal qiladi.
Keling, turli xil aniq lifetimelarga ega bo'lgan referencelarni o'tkazish orqali eng_uzun
funksiyani qanday cheklashini ko'rib chiqaylik. Ro'yxat 10-22 - bu oddiy misol.
Fayl nomi: src/main.rs
fn main() { let string1 = String::from("uzundan uzun string"); { let string2 = String::from("xyz"); let natija = eng_uzun(string1.as_str(), string2.as_str()); println!("Eng uzun satr {}", natija); } } fn eng_uzun<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Bu misolda string1
tashqi qamrov oxirigacha amal qiladi, string2
ichki qamrov oxirigacha amal qiladi va natija
ichki doiraning oxirigacha amal qiladigan narsaga ishora qiladi. Ushbu kodni ishga tushiring va siz borrowni tekshiruvchi tasdiqlaganini ko'rasiz; u kompilyatsiya qiladi va Eng uzun satr - uzundan uzun string
ni yaratadi.
Keyinchalik, natija
dagi referencening lifetime ikkita argumentning kichikroq lifetime bo'lishi kerakligini ko'rsatadigan misolni ko'rib chiqaylik. Biz natija
o'zgaruvchisi deklaratsiyasini ichki doiradan tashqariga o'tkazamiz, lekin qiymatni belgilashni string2
bilan doiradagi natija
o'zgaruvchisiga qoldiramiz. Keyin, natija
ni ishlatadigan println!
ni ichki doira tugagandan so‘ng, ichki doiradan tashqariga o‘tkazamiz. 10-23 ro'yxatdagi kod kompilyatsiya qilinmaydi.
Fayl nomi: src/main.rs
fn main() {
let string1 = String::from("uzundan uzun string");
let natija;
{
let string2 = String::from("xyz");
natija = eng_uzun(string1.as_str(), string2.as_str());
}
println!("Eng uzun satr {}", natija);
}
fn eng_uzun<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Ushbu kodni kompilyatsiya qilmoqchi bo'lganimizda, biz quyidagi xatoni olamiz:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
6 | natija = eng_uzun(string1.as_str(), string2.as_str());
| ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("Eng uzun satr {}", natija);
| ------ borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error
Xato shuni ko'rsatadiki, natija
println!
bayonoti uchun haqiqiy bo'lishi uchun string2
tashqi doiraning oxirigacha amal qilishi kerak. Rust buni biladi, chunki biz funksiya parametrlarining lifetimeni(ishlash muddati) va qiymatlarni bir xil 'a
parametridan foydalangan holda izohladik.
Inson sifatida biz ushbu kodni ko'rib chiqamiz va string1
string2
dan uzunroq ekanligini ko'rishimiz mumkin va shuning uchun natija
string1
ga referenceni o'z ichiga oladi.
string1
hali amaldan tashqariga chiqmaganligi sababli, string1
ga reference println!
bayonoti uchun amal qiladi. Biroq, kompilyator bu holatda reference haqiqiy ekanligini ko'ra olmaydi. Biz Rustga aytdikki, eng_uzun
funksiya tomonidan qaytarilgan referencening lifetime uzatilgan referencelarning lifetimesidan kichikroq vaqt bilan bir xil. Shuning uchun, borrowni tekshirish vositasi 10-23 ro'yxatdagi kodga ruxsat bermaydi, chunki noto'g'ri reference mavjud.
eng_uzun
funksiyaga oʻtkazilgan referencelarning qiymatlari va amal lifetime va qaytarilgan(return) referencedan qanday foydalanishni oʻzgartiruvchi koʻproq tajribalar ishlab chiqishga harakat qiling. Kompilyatsiya qilishdan oldin tajribalaringiz borrow tekshiruvidan o'tadimi yoki yo'qmi haqida faraz qiling; keyin siz haq ekanligingizni tekshiring!
Lifetime nuqtai nazaridan fikrlash
Lifetime parametrlarini belgilashingiz kerak bo'lgan metod sizning funksiyangiz nima qilayotganiga bog'liq. Misol uchun, agar biz eng_uzun
funksiyasini amalga oshirishni har doim eng uzun satr bo'lagini emas, balki birinchi parametrni qaytarish uchun o'zgartirgan bo'lsak, y
parametrida lifetimeni belgilashimiz shart emas. Quyidagi kod kompilyatsiya qilinadi:
Fayl nomi: src/main.rs
fn main() { let string1 = String::from("abcd"); let string2 = "efghijklmnopqrstuvwxyz"; let natija = eng_uzun(string1.as_str(), string2); println!("Eng uzun satr {}", natija); } fn eng_uzun<'a>(x: &'a str, y: &str) -> &'a str { x }
Biz x
parametri va qaytarish(return) turi uchun lifetime 'a
parametrini belgiladik, lekin y
parametri uchun emas, chunki y
ning lifetimesi x
yoki qaytarish qiymati bilan hech qanday aloqasi yo'q.
Funksiyadan mos yozuvlar qaytarilganda, qaytarish turi uchun lifetime parametri parametrlardan birining lifetime parametriga mos kelishi kerak. Agar qaytarilgan reference parametrlardan biriga tegishli bo'lmasa, u ushbu funksiya doirasida yaratilgan qiymatga murojaat qilishi kerak. Biroq, bu dangling reference bo'ladi, chunki funksiya oxirida qiymat doiradan chiqib ketadi.
Kompilyatsiya qilmaydigan eng_uzun
funksiyani amalga oshirishga urinishlarni ko'rib chiqing:
Fayl nomi: src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let natija = eng_uzun(string1.as_str(), string2);
println!("Eng uzun satr {}", natija);
}
fn eng_uzun<'a>(x: &str, y: &str) -> &'a str {
let natija = String::from("haqiqatan ham uzun satr");
natija.as_str()
}
Bu erda, biz qaytish turi uchun lifetime parametr 'a
ni belgilagan bo'lsak ham, bu dastur kompilyatsiya qilinmaydi, chunki qaytish qiymatining lifetime parametrlarning lifetime bilan umuman bog'liq emas. Mana biz olgan xato xabari:
$ 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
Muammo shundaki, natija
ishchi ko'lamdan tashqariga chiqadi va eng_uzun
funksiya oxirida tozalanadi. Shuningdek, biz funksiyadan natija
ga referenceni qaytarishga harakat qilyapmiz. Dangling referenceni o'zgartiradigan lifetime parametrlarini belgilashning iloji yo'q va Rust bizga dangling reference yaratishga ruxsat bermaydi. Bunday holda, eng yaxshi tuzatish mos yozuvlar emas, balki tegishli referencelar turini qaytarish bo'ladi, shuning uchun chaqiruv funksiyasi qiymatni tozalash uchun javobgar bo'ladi.
Oxir oqibat, lifetime sintaksisi turli parametrlarning ishlash muddatini va funktsiyalarning qaytish qiymatlarini bog'lashdir. Ular ulangandan so'ng, Rust xotira xavfsizligini ta'minlaydigan operatsiyalarga ruxsat berish va dangling pointerlarni yaratish yoki xotira xavfsizligini boshqa tarzda buzadigan operatsiyalarga ruxsat berish uchun yetarli ma'lumotga ega.
Struktura ta'riflarida lifetime annotationlar
Hozirgacha biz belgilagan structlar barcha egalik turlariga ega. Biz referencelarni saqlash uchun structlarni belgilashimiz mumkin, ammo bu holda structning ta'rifidagi har bir referencega lifetime annotation qo'shishimiz kerak bo'ladi. 10-24 roʻyxatda ImportantExcerpt
nomli struktura mavjud boʻlib, u string sliceni saqlaydi.
Fayl nomi: src/main.rs
struct ImportantExcerpt<'a> { qism: &'a str, } fn main() { let roman = String::from("Meni yaxshi dasturchi edim. Bir necha yil oldin..."); let birinchi_jumla = roman.split('.').next().expect("'.' belgisini topib bo'lmadi."); let i = ImportantExcerpt { qism: birinchi_jumla, }; }
Bu structda bir satr boʻlagini oʻz ichiga oluvchi qism
maydoni mavjud boʻlib, bu referencelardir. Generik(umumiy) ma'lumotlar turlarida bo'lgani kabi, biz structning nomidan keyin burchakli qavslar ichida generik lifetime parametrining nomini e'lon qilamiz, shuning uchun biz structning ta'rifi tanasida lifetime parametridan foydalanishimiz mumkin. Bu izoh ImportantExcerpt
namunasi oʻzining qism
maydonidagi referencedan uzoqlasha olmasligini bildiradi.
Bu yerda main
funksiyasi roman
oʻzgaruvchisiga tegishli String
ning birinchi jumlasiga referenceni oʻz ichiga olgan ImportantExcerpt
strukturasining namunasini yaratadi. roman
dagi ma'lumotlar ImportantExcerpt
misoli yaratilishidan oldin mavjud. Bundan tashqari, roman
ImportantExcerpt
ishchi doirasi tashqariga chiqmagunicha, ishchi doiradan chiqib ketmaydi, shuning uchun ImportantExcerpt
misolidagi reference haqiqiy hisoblanadi.
Lifetime Elision
Siz har bir referencening lifetime(ishlash muddati) borligini va referencelardan foydalanadigan funksiyalar yoki structlar uchun lifetime parametrlarini belgilashingiz kerakligini bilib oldingiz. Biroq, 4-bobda biz 4-9-ro'yxatdagda funksiyaga ega bo'ldik, u keyin yana 10-25 ro'yxatda ko'rsatiladi, unda kod lifetime annotationsiz tuzilgan.
Fayl nomi: 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); }
Ushbu funktsiyaning lifetime annotationlarsiz kompilyatsiya qilinishining sababi tarixiydir: Rust-ning dastlabki versiyalarida (1.0-dan oldingi) bu kod kompilyatsiya bo'lmagan bo'lardi, chunki har bir reference aniq ishlash muddatini talab qiladi. O'sha paytda funktsiya signaturesi quyidagicha yozilgan bo'lar edi:
fn birinchi_belgi<'a>(s: &'a str) -> &'a str {
Rust-da juda ko'p kod yozgandan so'ng, Rust jamoasi Rust dasturchilari muayyan vaziyatlarda bir xil lifetime annotatiolarni qayta-qayta kiritayotganini aniqladilar. Bu vaziyatlarni oldindan aytish mumkin edi va bir nechta deterministik patternlarga amal qildi. Ishlab chiquvchilar ushbu patternlarni kompilyator kodiga dasturlashtirdilar, shuning uchun borrow tekshiruvi ushbu vaziyatlarda lifetimeni(ishlash muddatini) aniqlay oladi va aniq izohlarga muhtoj bo'lmaydi.
Rust tarixining ushbu qismi dolzarbdir, chunki ko'proq deterministik patternlar paydo bo'lishi va kompilyatorga qo'shilishi mumkin. Kelajakda undan ham kamroqlifetime annotationlar talab qilinishi mumkin.
Rustning referencelarni tahlil qilishda dasturlashtirilgan patternlar lifetime elision qoidalari(lifetime elision rules) deb ataladi. Bu dasturchilar rioya qilishi kerak bo'lgan qoidalar emas; ular kompilyator ko'rib chiqadigan muayyan holatlar to'plamidir va agar sizning kodingiz ushbu holatlarga mos keladigan bo'lsa, lifetime vaqtlarini aniq yozishingiz shart emas.
Elision qoidalari to'liq xulosa chiqarmaydi. Agar Rust qoidalarni qat'iy qo'llasa, lekin referencelarning qancha vaqt ishlashi(lifetime) haqida hali ham noaniqlik mavjud bo'lsa, kompilyator qolgan referencelarning lifetime(ishlash muddati) qancha bo'lishi kerakligini taxmin qila olmaydi. Taxmin qilish o'rniga, kompilyator sizga lifetime annotationlarni qo'shish orqali hal qilishingiz mumkin bo'lgan xatoni beradi.
Funksiya yoki metod parametrlari bo‘yicha lifetime kirish lifetime (input lifetimes), qaytariladigan(return) qiymatlar bo‘yicha lifetime chiqish lifetimei (output lifetimes) deb ataladi.
Aniq izohlar(annotation) bo'lmasa, kompilyator referencelarning lifetimeni aniqlash uchun uchta qoidadan foydalanadi. Birinchi qoida kirish lifetimega(input lifetimes), ikkinchi va uchinchi qoidalar esa chiqish lifetimega(output lifetimes) tegishli. Agar kompilyator uchta qoidaning oxiriga yetib borsa va hali ham lifetimeni(foydalanish muddati) aniqlay olmaydigan referencelar mavjud bo'lsa, kompilyator xato bilan to'xtaydi.
Bu qoidalar fn
ta'riflari hamda impl
bloklari uchun amal qiladi.
Birinchi qoida shundaki, kompilyator mos yozuvlar bo'lgan har bir parametrga lifetime parametrni belgilaydi.
Ikkinchi qoida shuki, agar aynan bitta kirish lifetime(input) parametri mavjud boʻlsa, u lifetime barcha chiqish(output) lifetime parametrlariga tayinlanadi: fn foo<'a>(x: &'a i32) -> &'a i32
.
Uchinchi qoida shundaki, agar kirish lifetime bir nechta parametrlar mavjud bo'lsa, lekin ulardan biri &self
yoki &mut self
bo'lsa, chunki bu metod bo'lsa, self
lifetime barcha chiqish lifetime parametrlariga tayinlanadi. Ushbu uchinchi qoida metodlarni o'qish va yozishni ancha yaxshi qiladi, chunki kamroq belgilar kerak.
Tasavvur qilaylik, biz kompilyatormiz. 10-25 roʻyxatdagi birinchi_belgi
funksiyasi signaturesidagi referencelarning lifetimeni(amal qilish muddati) aniqlash uchun biz ushbu qoidalarni qoʻllaymiz. Signature referencelalar bilan bog'liq bo'lmagan lifetimesiz(muddatsiz) boshlanadi:
fn birinchi_belgi(s: &str) -> &str {
Keyin kompilyator birinchi qoidani qo'llaydi, bu har bir parametr o'z lifetimesini oladi. Biz uni odatdagidek 'a
deb ataymiz, shuning uchun endi signature quyidagicha:
fn birinchi_belgi<'a>(s: &'a str) -> &str {
Ikkinchi qoida amal qiladi, chunki aynan bitta kirish lifetime mavjud. Ikkinchi qoida bitta kirish(input) parametrining lifetime chiqish lifetimesiga tayinlanishini bildiradi, shuning uchun signature endi quyidagicha:
fn birinchi_belgi<'a>(s: &'a str) -> &'a str {
Endi ushbu funksiya signaturesidagi barcha referencelar lifetimesiga ega va kompilyator dasturchiga ushbu funksiya signaturesidagi lifetimeni izohlashiga hojat qoldirmasdan tahlilini davom ettirishi mumkin.
Keling, yana bir misolni ko'rib chiqaylik, bu safar biz 10 20 ro'yxatda ishlashni boshlaganimizda lifetime parametrlarga ega bo'lmagan eng_uzun
funksiyadan foydalangan holda:
fn eng_uzun(x: &str, y: &str) -> &str {
Keling, birinchi qoidani qo'llaymiz: har bir parametr o'z lifetimeni oladi. Bu safar bizda bitta emas, ikkita parametr bor, shuning uchun bizda ikkita lifetime bor:
fn eng_uzun<'a, 'b>(x: &'a str, y: &'b str) -> &str {
Siz ikkinchi qoida qo'llanilmasligini ko'rishingiz mumkin, chunki bir nechta kirish lifetime mavjud. Uchinchi qoida ham qo'llanilmaydi, chunki eng_uzun
- bu metod emas, balki funksiya, shuning uchun parametrlarning hech biri self
emas. Barcha uchta qoidani ko'rib chiqqandan so'ng, biz qaytish(return) turining lifetime nima ekanligini hali aniqlay olmadik. Shuning uchun biz 10-20 ro'yxatdagi kodni kompilyatsiya qilishda xatoga yo'l qo'ydik: kompilyator lifetime elision qoidalari bo'yicha ishladi, lekin signaturedagi referencelarning butun lifetimeni aniqlay olmadi.
Uchinchi qoida haqiqatan ham faqat metod signaturelarida amal qilganligi sababli, biz ushbu kontekstda lifetimeni ko'rib chiqamiz, nima uchun uchinchi qoida biz metod signaturelariga tez-tez izoh qo'yishimiz shart emasligini tushunish uchun.
Metod ta'riflarida(defination) Lifetime Annotationlar
Biz lifetime bo'lgan strukturada metodlarni qo'llaganimizda, biz 10-11 ro'yxatda ko'rsatilgan generik turdagi parametrlar bilan bir xil sintaksisdan foydalanamiz. Lifetime parametrlarini qayerda e'lon qilishimiz va ishlatishimiz ularning struktura maydonlari yoki metod parametrlari va qaytish(return) qiymatlari bilan bog'liqligiga bog'liq.
Struct maydonlarining lifetime nomlari har doim impl
kalit so'zidan keyin e'lon qilinishi va keyin structning nomidan keyin ishlatilishi kerak, chunki bu lifetimelar strukturaning bir qismidir.
impl
blokidagi metod signaturelarida referencelar struct maydonlaridagi referencelar lifetimega bog'langan bo'lishi mumkin yoki ular mustaqil bo'lishi mumkin. Bundan tashqari, lifetime elision qoidalari ko'pincha metod signaturelarida lifetime annotationlar kerak bo'lmasligi uchun shunday qiladi. 10-24 ro'yxatda biz aniqlagan ImportantExcerpt
nomli strukturadan foydalanib, ba'zi misollarni ko'rib chiqaylik.
Birinchidan, biz daraja
deb nomlangan metoddan foydalanamiz, uning yagona parametri self
ga reference va qaytariladigan qiymati i32
bo‘lib, hech narsaga reference emas:
struct ImportantExcerpt<'a> { qism: &'a str, } impl<'a> ImportantExcerpt<'a> { fn daraja(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn qismni_elon_qilish_qaytarih(&self, elon_qilish: &str) -> &str { println!("Diqqat iltimos: {}", elon_qilish); self.qism } } fn main() { let roman = String::from("Meni yaxshi dasturchi edim. Bir necha yil oldin..."); let birinchi_jumla = roman.split('.').next().expect("'.' belgisini topib bo'lmadi."); let i = ImportantExcerpt { qism: birinchi_jumla, }; }
impl
dan keyin lifetime parametr deklaratsiyasi va tur nomidan keyin foydalanish talab qilinadi, lekin biz birinchi elision qoida tufayli self
ga referencening lifetimeni izohlashimiz shart emas.
Mana uchinchi umr bo'yi elision qoida qo'llaniladigan misol:
struct ImportantExcerpt<'a> { qism: &'a str, } impl<'a> ImportantExcerpt<'a> { fn daraja(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn qismni_elon_qilish_qaytarih(&self, elon_qilish: &str) -> &str { println!("Diqqat iltimos: {}", elon_qilish); self.qism } } fn main() { let roman = String::from("Meni yaxshi dasturchi edim. Bir necha yil oldin..."); let birinchi_jumla = roman.split('.').next().expect("'.' belgisini topib bo'lmadi."); let i = ImportantExcerpt { qism: birinchi_jumla, }; }
Ikkita kirish(input) lifetime bor, shuning uchun Rust birinchi lifetime elision qoidasini qo'llaydi va &self
va elon_qilish
ga ham o'z lifetimeni beradi. Keyin, parametrlardan biri &self
bo'lgani uchun qaytarish(return) turi &self
lifetimeni oladi va barcha lifetimelar hisobga olingan.
Statik Lifetime
Muhokama qilishimiz kerak bo'lgan maxsus lifetime bu 'static
bo'lib, bu ta'sirlangan reference dasturning butun muddati davomida yashashi mumkinligini bildiradi. Barcha satr literallari 'static
lifetimega ega, biz ularga quyidagicha izoh berishimiz mumkin:
#![allow(unused)] fn main() { let s: &'static str = "Mening statik lifetimem bor."; }
Ushbu satrning matni to'g'ridan-to'g'ri dasturning binary faylida saqlanadi, u har doim mavjud. Shunday qilib, barcha satr literallarining lifetime 'static
dir.
Xato xabarlarida 'static
lifetimedan foydalanish bo'yicha takliflarni ko'rishingiz mumkin. Biroq, 'static
ni referencening lifetime sifatida belgilashdan oldin, sizda mavjud bo'lgan reference haqiqatan ham dasturingizning butun ish vaqti davomida yashaydimi yoki yo'qmi va buni xohlaysizmi, deb o'ylab ko'ring. Ko'pincha, 'static
lifetimeni ko'rsatadigan xato xabari dangling reference yaratishga urinish yoki mavjud bo'lgan lifetimelarning mos kelmasligi natijasida paydo bo'ladi. Bunday hollarda, yechim 'static
lifetimeni ko'rsatmasdan, bu muammolarni tuzatishdir.
Generik tur parametrlari, Trait boundlar va birgalikdagi lifetime
Keling, generik turdagi parametrlarni, trait boundlarini va lifetimeni bitta funksiyada belgilash sintaksisini qisqacha ko'rib chiqaylik!
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let natija = elon_bilan_eng_uzun( string1.as_str(), string2, "Bugun kimningdir tug'ilgan kuni!", ); println!("Eng uzun satr {}", natija); } use std::fmt::Display; fn elon_bilan_eng_uzun<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: Display, { println!("E'lon! {}", ann); if x.len() > y.len() { x } else { y } }
Bu 10-21 roʻyxatdagi eng_uzun
funksiya boʻlib, u ikki qatorning uzunroq qismini qaytaradi. Ammo endi u where
bandida ko'rsatilgandek Display
traitini amalga oshiradigan har qanday tur tomonidan to'ldirilishi mumkin bo'lgan T
generik turidagi ann
nomli qo'shimcha parametrga ega. Ushbu qo'shimcha parametr {}
yordamida chop etiladi, shuning uchun Display
trait boundi(trait chegarasi) zarur. Lifetimelar generik tur bo'lganligi sababli, lifetime parametri 'a
va generik turdagi parametr T
funksiya nomidan keyin burchakli qavslar ichida bir xil ro'yxatda joylashgan.
Xulosa
Biz ushbu bobda juda ko'p narsalarni ko'rib chiqdik! Endi siz generik(umumiy) turdagi parametrlar, traitlar va trait boundlari(trait chegaralari) va generik lifetime parametrlari haqida bilganingizdan so'ng, siz ko'p turli vaziyatlarda ishlaydigan kodni takrorlashsiz yozishga tayyorsiz. Generik turdagi parametrlar kodni turli turlarga qo'llash imkonini beradi. Traitlar va traitlar boundlari(chegara) turlar generik(umumiy) bo'lsa ham, ular kodga kerak bo'lgan xatti-harakatlarga ega bo'lishini ta'minlaydi. Ushbu moslashuvchan kodda hech qanday dangling referencelar bo'lmasligini ta'minlash uchun lifetime annotationlardan qanday foydalanishni o'rgandingiz. Va bu tahlillarning barchasi kompilyatsiya vaqtida sodir bo'ladi, bu runtimening ishlashiga ta'sir qilmaydi!
Ishoning yoki ishonmang, biz ushbu bobda muhokama qilgan mavzular bo'yicha ko'p narsalarni o'rganishimiz mumkin: 17-bobda traitlardan foydalanishning yana bir usuli bo'lgan trait ob'ektlari muhokama qilinadi. Bundan tashqari,lifetime annotationlarni o'z ichiga olgan murakkab stsenariylar ham mavjud, ular sizga faqat juda ilg'or stsenariylarda kerak bo'ladi; ular uchun siz Rust Reference ni o'qishingiz kerak. Ammo keyin siz Rust-da testlarni qanday yozishni o'rganasiz, shunda kodingiz kerakli tarzda ishlayotganiga ishonch hosil qilishingiz mumkin.
Avtomatlashtirilgan testlarni yozish
Edsger W. Dijkstra o'zining 1972 yildagi "Kamtar dasturchi(The Humble Programmer,)" inshosida "Dasturni sinovdan o'tkazish xatolar(buglar) mavjudligini ko'rsatishning juda samarali usuli bo'lishi mumkin, ammo bu ularning yo'qligini ko'rsatish uchun umidsiz darajada yetarli emas" dedi. Bu biz imkon qadar ko'proq sinab ko'rmasligimiz kerak degani emas!
Bizning dasturlarimizdagi to'g'rilik bizning kodimiz biz rejalashtirgan narsani qanchalik darajada bajarashi. Rust dasturlarning to'g'riligi haqida yuqori darajadagi e'tibor bilan yaratilgan, ammo to'g'riligi murakkab va isbotlash oson emas. Rust turidagi tizim bu yukning katta qismini o'z zimmasiga oladi, ammo turdagi tizim hamma narsani ushlay olmaydi. Shunday qilib, Rust avtomatlashtirilgan dasturiy ta'minot testlarini yozishni qo'llab-quvvatlaydi.
Aytaylik, ikkita_qoshish
funksiyasini yozamiz, unga qaysi raqam uzatilsa, unga 2 qo'shiladi. Ushbu funksiya signaturesi integer(butun) sonni parametr sifatida qabul qiladi va natijada butun sonni qaytaradi. Biz ushbu funksiyani amalga oshirganimizda va kompilyatsiya qilganimizda, Rust, masalan, String
qiymatini yoki ushbu funksiyaga noto'g'ri referenceni o'tkazmasligimizga ishonch hosil qilish uchun siz hozirgacha o'rgangan barcha turdagi tekshiruvlarni va borrowlarni tekshirishni amalga oshiradi. Ammo Rust bu funksiya biz ko'zlagan narsani aniq bajarishini tekshira olmaydi, ya'ni, masalan, parametr plus 10 yoki minus 50 emas, balki plus 2 parametrini qaytaradi! Bu yerda testlar kiradi.
Biz, masalan, ikkita_qoshish
funksiyasiga 3
o'tganimizda, qaytarilgan qiymat 5
bo`lishini tasdiqlovchi testlarni yozishimiz mumkin. Mavjud har qanday to'g'ri xatti-harakat o'zgarmaganligiga ishonch hosil qilish uchun kodimizga o'zgartirish kiritganimizda biz ushbu testlarni bajarishimiz mumkin.
Sinov - bu murakkab mahorat: biz bu bobda yaxshi testlarni qanday yozish haqida har bir tafsilotni yorita olmasak-da, Rustning sinov qurilmalari mexanikasini muhokama qilamiz. Testlarni yozishda sizga mavjud bo'lgan izohlar(annotation) va makroslar, standart xatti-harakatlar va testlarni bajarish uchun taqdim etilgan variantlar, shuningdek, testlarni unit testlari va integratsiya testlariga qanday tashkil qilish haqida gaplashamiz.
Testlarni qanday yozish kerak
Testlar - bu sinovdan tashqari kod kutilgan tarzda ishlayotganligini tasdiqlovchi Rust funksiyalari. Test funksiyalari organlari odatda ushbu uchta harakatni bajaradi:
- Har qanday kerakli ma'lumotlarni yoki holatni o'rnating.
- Test qilmoqchi bo'lgan kodni ishga tushiring.
- Natijalar siz kutgan narsa ekanligini tasdiqlang.
Keling, Rust ushbu amallarni bajaradigan testlarni yozish uchun taqdim etgan xususiyatlarni ko'rib chiqaylik, ular orasida test
atributi, bir nechta makroslar va should_panic
atributi mavjud.
Test funksiyasining anatomiyasi
Eng sodda qilib aytganda, Rust-dagi test test
atributi bilan izohlangan funksiyadir. Atributlar Rust kodining bo'laklari haqidagi metama'lumotlardir; bir misol, biz 5-bobda structlar bilan ishlatgan derive
atributidir. Funksiyani test funksiyasiga oʻzgartirish uchun fn
oldidan qatorga #[test]
qoʻshing. cargo test
buyrug'i bilan testlarni o'tkazganingizda, Rust izohli funksiyalarni ishga tushiradigan test dasturining binaryrini yaratadi va har bir test funksiyasidan o'tgan yoki muvaffaqiyatsizligi haqida hisobot beradi.
Har safar biz Cargo bilan yangi kutubxona loyihasini yaratganimizda, biz uchun test funksiyasi bo'lgan test moduli avtomatik ravishda yaratiladi. Ushbu modul sizga testlarni yozish uchun shablonni taqdim etadi, shuning uchun har safar yangi loyihani boshlaganingizda aniq struktura va sintaksisni izlashga hojat qolmaydi. Siz xohlagancha qo'shimcha test funksiyalari va test modullarini qo'shishingiz mumkin!
Har qanday kodni sinab ko'rishdan oldin shablon testi bilan tajriba o'tkazish orqali testlar qanday ishlashining ba'zi jihatlarini o'rganamiz. Keyin biz yozgan ba'zi kodlarni chaqiradigan va uning xatti-harakati to'g'riligini tasdiqlaydigan haqiqiy dunyo testlarini yozamiz.
Keling, ikkita raqamni qo'shadigan qoshuvchi
nomli yangi kutubxona loyihasini yarataylik:
$ cargo new qoshuvchi --lib
Created library `qoshuvchi` project
$ cd qoshuvchi
qoshuvchi
kutubxonangizdagi src/lib.rs faylining mazmuni 11-1 roʻyxatdagi kabi koʻrinishi kerak.
Fayl nomi: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn ishlaydi() {
let natija = 2 + 2;
assert_eq!(natija, 4);
}
}
Hozircha, keling, yuqoridagi ikkita qatorga e'tibor bermaylik va funksiyaga e'tibor qarataylik. #[test]
izohiga e'tibor bering: bu atribut bu test funksiyasi ekanligini bildiradi, shuning uchun test ishtirokchisi bu funksiyani test sifatida ko'rishni biladi. Umumiy stsenariylarni oʻrnatish yoki umumiy operatsiyalarni bajarishda yordam beradigan tests
modulida testdan tashqari funksiyalar ham boʻlishi mumkin, shuning uchun biz har doim qaysi funksiyalar test ekanligini koʻrsatishimiz kerak.
Misol funksiya tanasi 2 va 2 qo‘shilishi natijasini o‘z ichiga olgan natija
4 ga teng ekanligini tasdiqlash uchun assert_eq!
makrosidan foydalanadi. Ushbu tasdiq odatiy test formatiga misol bo'lib xizmat qiladi. Ushbu sinovdan o'tishini ko'rish uchun uni ishga tushiramiz.
cargo test
buyrug'i 11-2 ro'yxatda ko'rsatilganidek, loyihamizdagi barcha testlarni amalga oshiradi.
$ cargo test
Compiling qoshuvchi v0.1.0 (file:///projects/qoshuvchi)
Finished test [unoptimized + debuginfo] target(s) in 0.57s
Running unittests src/lib.rs (target/debug/deps/qoshuvchi-92948b65e88960b4)
running 1 test
test tests::ishlaydi ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests qoshuvchi
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Cargo kompilyatsiya qilindi va sinovdan o'tdi. Biz running 1 test
qatorini ko'ramiz. Keyingi qatorda ishlaydi
deb nomlangan yaratilgan test funksiyasining nomi va bu testni bajarish natijasi ok
ekanligini ko'rsatadi. Umumiy xulosa test natijasi test result: ok.
barcha testlardan muvaffaqiyatli oʻtganligini va 1 passed;
deb yozilgan qismi muvaffaqiyatli oʻtganligini bildiradi; 0 failed
muvaffaqiyatsiz boʻlgan testlar sonini ifodalaydi.
Muayyan misolda ishlamasligi uchun testni e'tiborsiz(ignor) deb belgilash mumkin; Biz buni ushbu bobning keyingi qismida "Agar aniq talab qilinmasa, ba'zi testlarni e'tiborsiz qoldirish" bo'limida ko'rib chiqamiz. Bu yerda biz buni qilmaganimiz sababli, xulosada 0 ignored
0-ta eʼtibor berilmagan koʻrsatiladi. Shuningdek, biz argumentni faqat nomi satrga mos keladigan testlarni o'tkazish uchun cargo test
buyrug'iga o'tkazishimiz mumkin; bu filtrlash deb ataladi va biz buni "Testlar to'plamini nomi bo'yicha ishga tushirish" bo'limida ko'rib chiqamiz. Shuningdek, biz bajarilayotgan testlarni filtrlamadik, shuning uchun xulosa oxirida 0 filtered out
0-ta filtrlangan deb ko‘rsatiladi.
0 measured
statistikasi samaradorlikni o'lchaydigan benchmark testlari uchundir.
Benchmark testlari, ushbu yozuvdan boshlab, faqat nightly Rust-da mavjud. Batafsil ma'lumot olish uchun benchmark testlari haqidagi hujjatlarga qarang.
Doc-tests adder
(Hujjat testlari qoʻshuvchisi) dan boshlanadigan test natijasining keyingi qismi har qanday hujjat sinovlari natijalariga moʻljallangan. Bizda hali hech qanday hujjat sinovlari yo'q, lekin Rust API hujjatlarida ko'rinadigan har qanday kod misollarini to'plashi mumkin.
Bu xususiyat hujjatlaringiz va kodingizni sinxronlashtirishga yordam beradi! Hujjat testlarini qanday yozishni 14-bobning “Hujjatlarga sharhlar test sifatida” bo‘limida muhokama qilamiz. Hozircha biz Doc-tests
chiqishini e'tiborsiz qoldiramiz.
Keling, testni o'z ehtiyojlarimizga moslashtirishni boshlaylik. Avval ishlaydi
funksiyasining nomini tadqiqot
kabi boshqa nomga o'zgartiring, masalan:
Fayl nomi: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn tadqiqot() {
assert_eq!(2 + 2, 4);
}
}
Keyin yana cargo test
bajaring. Chiqish(output) endi ishlaydi
o‘rniga tadqiqot
ni ko‘rsatadi:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.59s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::tadqiqot ... 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
Endi biz yana bir test qo'shamiz, lekin bu safar muvaffaqiyatsiz bo'lgan testni qilamiz! Test funktsiyasidagi biror narsa panic qo'zg'atganda, testlar muvaffaqiyatsiz tugaydi. Har bir test yangi threadda o'tkaziladi va asosiy(main) thread sinov chizig'i o'lganini ko'rsa, test muvaffaqiyatsiz deb belgilanadi. 9-bobda biz panic qo'zg'ashning eng oddiy yo'li panic!
makrosini chaqirish haqida gapirdik. Yangi testni boshqa
funksiya sifatida kiriting, shunda src/lib.rs faylingiz 11-3 roʻyxatiga oʻxshaydi.
Fayl nomi: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn tadqiqot() {
assert_eq!(2 + 2, 4);
}
#[test]
fn boshqa() {
panic!("Ushbu test muvaffaqiyatsizlikka uchradil");
}
}
cargo test
yordamida testlarni qaytadan test qiling. Chiqish 11-4 ro'yxatga o'xshash bo'lishi kerak, bu bizning tadqiqot
sinovimizdan o'tganligini va boshqa
muvaffaqiyatsiz ekanligini ko'rsatadi.
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.72s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::boshqa ... FAILED
test tests::tadqiqot ... ok
failures:
---- tests::boshqa stdout ----
thread 'tests::boshqa' 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::boshqa
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`
OK
o'rniga test tests::boshqa
qatori FAILED
ni koʻrsatadi. Shaxsiy natijalar va xulosa o'rtasida ikkita yangi bo'lim paydo bo'ladi: birinchisida har bir sinov muvaffaqiyatsizligining batafsil sababi ko'rsatiladi. Bunday holda, biz src/lib.rs faylidagi 10-qatordagi panicked at 'Make this test fail'
da panic qo'ygani uchun boshqa
muvaffaqiyatsizlikka uchraganligi haqidagi tafsilotlarni olamiz. Keyingi bo'limda barcha muvaffaqiyatsiz testlarning nomlari keltirilgan, bu juda ko'p sinovlar va ko'plab batafsil muvaffaqiyatsiz sinov natijalari mavjud bo'lganda foydalidir. Muvaffaqiyatsiz test nomidan uni osonroq debug qilish uchun ishlatishimiz mumkin; testlarni o'tkazish usullari haqida ko'proq "Testlar qanday o'tkazilishini nazorat qilish" section bo'limida gaplashamiz.
Xulosa qatori oxirida ko'rsatiladi: umuman olganda, bizning test natijasimiz FAILED
muvaffaqiyatsiz. Bizda bitta test sinovi bor edi va bitta sinov muvaffaqiyatsiz tugadi.
Sinov natijalari turli stsenariylarda qanday ko‘rinishini ko‘rganingizdan so‘ng, keling, testlarda foydali bo‘lgan panic!
dan tashqari ba’zi makrolarni ko‘rib chiqaylik.
Natijalarni assert!
makrosi bilan tekshirish!
Standart kutubxona tomonidan taqdim etilgan assert!
makrosi testdagi baʼzi shartlar true
(toʻgʻri) boʻlishini taʼminlashni istasangiz foydali boʻladi. Biz assert!
makrosiga mantiqiy(boolean) qiymatga baholovchi argument beramiz. Qiymat true
bo'lsa, hech narsa sodir bo'lmaydi va sinovdan o'tadi. Agar qiymat false
bo‘lsa, assert!
makros testning muvaffaqiyatsiz bo‘lishiga olib kelishi uchun panic!
chaqiradi. assert!
makrosidan foydalanish bizning kodimiz biz rejalashtirgan tarzda ishlayotganligini tekshirishga yordam beradi.
5-bob, 5-15-ro'yxarda biz 11-5-ro'yxardada takrorlangan Kvadrat
strukturasi va ushlab_tur
metodidan foydalandik. Keling, ushbu kodni src/lib.rs fayliga joylashtiramiz, so'ngra assert!
makrosidan foydalanib, u uchun testlarni yozamiz.
Fayl nomi: src/lib.rs
#[derive(Debug)]
struct Kvadrat {
kenglik: u32,
balandlik: u32,
}
impl Kvadrat {
fn ushlab_tur(&self, boshqa: &Kvadrat) -> bool {
self.kenglik > other.kenglik && self.balandlik > boshqa.balandlik
}
}
ushlab_tur
metodi mantiqiy(boolean) qiymatini qaytaradi, ya'ni bu assert!
makrosi uchun mukammal foydalanish holati. 11-6 ro'yxatda biz kengligi 8 va balandligi 7 bo'lgan Kvadrat
misolini yaratish va uning kengligi 5 va balandligi 1 bo'lgan boshqa Kvadrat
misolini ushlab turishi mumkinligini tekshirish orqali ushlab_tur
metodini bajaradigan testni yozamiz.
Fayl nomi: src/lib.rs
#[derive(Debug)]
struct Kvadrat {
kenglik: u32,
balandlik: u32,
}
impl Kvadrat {
fn ushlab_tur(&self, boshqa: &Kvadrat) -> bool {
self.kenglik > boshqa.kenglik && self.balandlik > boshqa.balandlik
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn katta_kichikni_ushlab_turadi() {
let kattaroq = Kvadrat {
kenglik: 8,
balandlik: 7,
};
let kichikroq = Kvadrat {
kenglik: 5,
balandlik: 1,
};
assert!(kattaroq.ushlab_tur(&kichikroq));
}
}
E'tibor bering, biz tests
moduliga yangi qator qo'shdik: use super::*;
. tests
moduli odatiy modul bo'lib, biz 7-bobda "Modul daraxtidagi elementga murojaat qilish yo'llari" bo'limida ko'rib chiqqan odatiy ko'rinish qoidalariga amal qiladi. tests
moduli ichki modul bo'lgani uchun biz tashqi moduldagi sinovdan o'tayotgan kodni ichki modul doirasiga kiritishimiz kerak. Biz bu yerda globdan foydalanamiz, shuning uchun tashqi modulda biz aniqlagan har qanday narsa ushbu tests
modulida mavjud bo'ladi.
Biz sinovimizga katta_kichikni_ushlab_turadi
deb nom berdik va o‘zimizga kerak bo‘lgan ikkita Kvadrat
misolini yaratdik.
Keyin biz assert!
makrosini chaqirdik va uni kattaroq.ushlab_tur(&kichikroq)
deb chaqirish natijasini berdik. Bu ifoda true
ni qaytarishi kerak, shuning uchun testimiz muvaffaqiyatli o'tishi kerak. Keling, bilib olaylik!
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (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
Test muvaffaqiyatli o'tadi! Keling, yana bir sinovni qo'shamiz, bu safar kichikroq kvadrat kattaroq kvadratni ushlab turolmaydi:
Fayl nomi: src/lib.rs
#[derive(Debug)]
struct Kvadrat {
kenglik: u32,
balandlik: u32,
}
impl Kvadrat {
fn ushlab_tur(&self, boshqa: &Kvadrat) -> bool {
self.kenglik > boshqa.kenglik && self.balandlik > boshqa.balandlik
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn katta_kichikni_ushlab_turadi() {
// --snip--
let kattaroq = Kvadrat {
kenglik: 8,
balandlik: 7,
};
let kichikroq = Kvadrat {
kenglik: 5,
balandlik: 1,
};
assert!(kattaroq.ushlab_tur(&kichikroq));
}
#[test]
fn kichik_kattani_ushlolmaydi() {
let kattaroq = Kvadrat {
kenglik: 8,
balandlik: 7,
};
let kichikroq = Kvadrat {
kenglik: 5,
balandlik: 1,
};
assert!(!kichikroq.ushlab_tur(&kattaroq));
}
}
Chunki bu holda ushlab_tur
funksiyasining to'g'ri natijasi false
bo'lsa, biz uni assert!
makrosiga o'tkazishdan oldin bu natijani inkor etishimiz kerak. Natijada, agar ushlab_tur
false
qiymatini qaytarsa, testimiz o'tadi:
$ cargo test
Compiling kvadrat v0.1.0 (file:///projects/kvadrat)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/kvadrat-6584c4561e48942e)
running 2 tests
test tests::katta_kichikni_ushlab_turadi ... ok
test tests::kichik_kattani_ushlolmaydi ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests kvadrat
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Ikkita sinovdan o'tadi! Keling, kodimizga xatolik kiritganimizda test natijalarimiz bilan nima sodir bo'lishini ko'rib chiqaylik. Kengliklarni solishtirganda katta belgisini kichikroq belgisi bilan almashtirish orqali ushlab_tur
metodini amalga oshirishni o‘zgartiramiz:
#[derive(Debug)]
struct Kvadrat {
kenglik: u32,
balandlik: u32,
}
// --snip--
impl Kvadrat {
fn ushlab_tur(&self, boshqa: &Kvadrat) -> bool {
self.kenglik < boshqa.kenglik && self.balandlik > boshqa.balandlik
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn katta_kichikni_ushlab_turadi() {
let kattaroq = Kvadrat {
kenglik: 8,
balandlik: 7,
};
let kichikroq = Kvadrat {
kenglik: 5,
balandlik: 1,
};
assert!(kattaroq.ushlab_tur(&kichikroq));
}
#[test]
fn kichik_kattani_ushlolmaydi() {
let kattaroq = Kvadrat {
kenglik: 8,
balandlik: 7,
};
let kichikroq = Kvadrat {
kenglik: 5,
balandlik: 1,
};
assert!(!kichikroq.ushlab_tur(&kattaroq));
}
}
Sinovlarni o'tkazish endi quyidagilarga olib keladi:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (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 'tests::larger_can_hold_smaller' 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`
Sinovlarimiz xatoni aniqladi! kattaroq.kenglik
8 va kichikroq.kenglik
5 bo'lganligi sababli, ushlab_tur
da kengliklarni taqqoslash endi false
ni qaytaradi: 8 5-dan kichik emas.
Tenglikni assert_eq!
va assert_ne!
makroslari bilan tekshirish
Funksionallikni tekshirishning keng tarqalgan usuli - bu testdan o'tayotgan kod natijasi va kod qaytarilishini kutayotgan qiymat o'rtasidagi tenglikni tekshirish. Buni assert!
makrosidan foydalanib, unga ==
operatori yordamida ifoda o'tkazishingiz mumkin. Biroq, bu shunday keng tarqalgan testki, standart kutubxona ushbu testni yanada qulayroq bajarish uchun bir juft makros-assert_eq!
va assert_ne!
-ni taqdim etadi. Ushbu makrolar mos ravishda tenglik yoki tengsizlik uchun ikkita argumentni solishtiradi. Agar tasdiqlash muvaffaqiyatsiz bo'lsa, ular ikkita qiymatni chop etadilar, bu esa nima uchun sinov muvaffaqiyatsiz tugaganini ko'rishni osonlashtiradi; aksincha, assert!
makros false
qiymatiga olib kelgan qiymatlarni chop etmasdan, ==
ifodasi uchun false
qiymatini olganligini bildiradi.
11-7 ro'yxatda biz o'z parametriga 2
qo'shadigan ikkita_qoshish
nomli funksiyani yozamiz, so'ngra bu funksiyani assert_eq!
makrosidan foydalanib tekshiramiz.
Fayl nomi: src/lib.rs
pub fn ikkita_qoshish(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ikkita_qosh() {
assert_eq!(4, ikkita_qoshish(2));
}
}
Keling test o'tganligini tekshirib ko'raylik!
$ cargo test
Compiling qoshuvchi v0.1.0 (file:///projects/qoshuvchi)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/qoshuvchi-92948b65e88960b4)
running 1 test
test tests::ikkita_qosh ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests qoshuvchi
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Argument sifatida 4
ni assert_eq!
ga o'tkazamiz, bu esa ikkita_qoshish(2)
ni chaqirish natijasiga teng. Ushbu test qatori test tests::it_adds_two ... ok
va ok
matni testimiz muvaffaqiyatli o'tganligini bildiradi!
assert_eq!
muvaffaqiyatsiz bo'lganda qanday ko'rinishini ko'rish uchun kodimizga xato kiritamiz. ikkita_qoshish
funksiyasining bajarilishini o'rniga 3
qo'shish uchun o`zgartiramiz:
pub fn ikkita_qoshish(a: i32) -> i32 {
a + 3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ikkita_qosh() {
assert_eq!(4, ikkita_qoshish(2));
}
}
Testlarni qayta ishga tushiring:
$ cargo test
Compiling qoshuvchi v0.1.0 (file:///projects/qoshuvchi)
Finished test [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/qoshuvchi-92948b65e88960b4)
running 1 test
test tests::ikkita_qosh ... FAILED
failures:
---- tests::ikkita_qosh stdout ----
thread 'tests::ikkita_qosh' 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::ikkita_qosh
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`
Bizning sinovimiz xatoni aniqladi! ikkita_qosh
testi muvaffaqiyatsiz tugadi va xabarda muvaffaqiyatsizlikka uchragan tasdiqlash assertion failed: `(left == right)`
va left
va right
qiymatlari nima. Bu xabar nosozliklarni(debugging) tuzatishni boshlashimizga yordam beradi: left
(chap) argumenti 4
edi, lekin ikkita_qoshish(2)
bo'lgan right
(o'ng) argumenti 5
edi. Tasavvur qilishingiz mumkinki, bu bizda juda ko'p sinovlar o'tkazilayotganda ayniqsa foydali bo'ladi.
E'tibor bering, ba'zi dasturlash tillarda va test tizimlarida(framework) tenglikni tasdiqlash funksiyalari parametrlari expected
va actual
deb nomlanadi va biz argumentlarni ko'rsatish tartibi muhim ahamiyatga ega. Biroq, Rustda ular left
va right
deb nomlanadi va biz kutgan qiymat va kod ishlab chiqaradigan qiymatni belgilash tartibi muhim emas. Biz ushbu testdagi tasdiqni assert_eq!(ikkita_qoshish(2), 4)
deb yozishimiz mumkin, natijada assertion failed: `(left == right)`
ko'rsatiladigan bir xil xato xabari paydo bo'ladi.
assert_ne!
makros biz bergan ikkita qiymat teng bo'lmasa o'tadi va teng bo'lsa muvaffaqiyatsiz bo'ladi. Ushbu makro biz qiymat nima bo'lishini amin bo'lmagan holatlar uchun juda foydali bo'ladi, lekin biz qiymat nima bo'lmasligi kerakligini bilamiz.
Misol uchun, agar biz biron-bir tarzda uning kiritilishini o'zgartirishi kafolatlangan funksiyani sinab ko'rayotgan bo'lsak, lekin kirishni o'zgartirish metodi testlarimizni o'tkazadigan hafta kuniga bog'liq bo'lsa, tasdiqlash uchun eng yaxshi narsa, funksiyaning chiqishi kirishga teng emasligi bo'lishi mumkin.
Sirt ostida assert_eq!
va assert_ne!
makroslari mos ravishda ==
va !=
operatorlaridan foydalanadi. Tasdiqlar bajarilmasa, bu makroslar debug formati yordamida o‘z argumentlarini chop etadi, ya’ni solishtirilayotgan qiymatlar PartialEq
va Debug
traitlarini bajarishi kerak. Barcha primitiv turlar va standart kutubxona turlarining aksariyati bu traittlarni amalga oshiradi. O'zingiz belgilagan structlar va enumlar uchun ushbu turlarning tengligini tasdiqlash uchun PartialEq
ni qo'llashingiz kerak bo'ladi. Tasdiqlash muvaffaqiyatsizlikka uchraganida qiymatlarni chop etish uchun Debug
ni ham qo'llashingiz kerak bo'ladi. 5-bobdagi 5-12 roʻyxatda aytib oʻtilganidek, ikkala trait ham derivable traitli boʻlganligi sababli, bu odatda struct yoki enum taʼrifiga #[derive(PartialEq, Debug)]
izohini qoʻshishdek oddiy. Ushbu va boshqa "Derivable Trait"lari haqida batafsil ma'lumot olish uchun C ilovasiga qarang.
Maxsus nosozlik xabarlarini qo'shish
Shuningdek, assert!
, assert_eq!
va assert_ne!
makroslariga ixtiyoriy argumentlar sifatida xato xabari bilan chop etiladigan maxsus xabarni qo'shishingiz mumkin. Kerakli argumentlardan so‘ng ko‘rsatilgan har qanday argumentlar format!
makrosiga uzatiladi (8-bobda "+
operatori yoki format!
makrosi bilan birlashtirish" bo‘limida muhokama qilingan), shuning uchun siz {}
to'ldirgichlar va qiymatlarni o'z ichiga olgan format qatorini o'tkazishingiz mumkin. Maxsus xabarlar tasdiqlash nimani anglatishini hujjatlashtirish uchun foydalidir; test muvaffaqiyatsiz tugagach, kod bilan bog'liq muammo nimada ekanligini yaxshiroq tushunasiz.
Masalan, bizda odamlarni ism bilan kutib oladigan funksiya bor va biz funksiyaga kiritgan ism chiqishda(output) paydo bo‘lishini sinab ko‘rmoqchimiz:
Fayl nomi: src/lib.rs
pub fn salomlashish(name: &str) -> String {
format!("Salom {}!", name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn salomlash() {
let natija = salomlashish("Azizbek");
assert!(natija.contains("Azizbek"));
}
}
Ushbu dasturga qoʻyiladigan talablar hali kelishib olinmagan va salomlashish boshidagi Salom
matni oʻzgarishiga ishonchimiz komil. Talablar o'zgarganda testni yangilashni xohlamasligimizga qaror qildik, shuning uchun salomlashish
funksiyasidan qaytarilgan qiymatga aniq tenglikni tekshirish o‘rniga, biz faqat chiqishda kirish parametrining matni borligini tasdiqlaymiz.
Endi standart sinov xatosi qanday koʻrinishini koʻrish uchun name
ni chiqarib tashlash uchun salomlashish
ni oʻzgartirish orqali ushbu kodga xatolik kiritamiz:
pub fn salomlashish(name: &str) -> String {
String::from("Salom!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn salomlash() {
let natija = salomlashish("Azizbek");
assert!(natija.contains("Azizbek"));
}
}
Ushbu testni bajarish quyidagi natijalarni beradi:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::salomlash ... FAILED
failures:
---- tests::salomlash stdout ----
thread 'tests::salomlash' panicked at 'assertion failed: result.contains(\"Azizbek\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::salomlash
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`
Bu natija faqat tasdiqlash(assertion) muvaffaqiyatsizligini va tasdiqlash qaysi qatorda ekanligini ko'rsatadi. Foydaliroq xato xabari salomlashish
funksiyasidan qiymatni chop etadi. Keling, salomlashish
funksiyasidan olingan haqiqiy qiymat bilan toʻldirilgan maxsus xabar to'ldiruvchisi(plaseholder) bilan format qatoridan iborat maxsus xato xabarini qoʻshamiz:
pub fn salomlashish(name: &str) -> String {
String::from("Salom!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn salomlash() {
let natija = salomlashish("Azizbek");
assert!(
natija.contains("Azizbek"),
"Salomlashishda ism yo'q, qiymat `{}` edi",
natija
);
}
}
Endi sinovni o'tkazganimizda, biz ko'proq ma'lumot beruvchi xato xabarini olamiz:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.93s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::salomlash ... FAILED
failures:
---- tests::salomlash stdout ----
thread 'tests::salomlash' panicked at 'Greeting did not contain name, value was `Salom!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::salomlash
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`
Sinov natijasida biz haqiqatda olgan qiymatni ko'rishimiz mumkin, bu biz kutgan narsaning o'rniga nima sodir bo'lganligini aniqlashga yordam beradi.
should_panic
yordamida panic tekshirish
Qaytish(return) qiymatlarini tekshirishdan tashqari, bizning kodimiz xato holatlarini biz kutganidek hal qilishini tekshirish muhimdir. Misol uchun, biz 9-bob, 9-13 ro'yxatda yaratgan Taxmin
turini ko'rib chiqaylik. Taxmin
dan foydalanadigan boshqa kod Taxmin
misollarida faqat 1 dan 100 gacha bo'lgan qiymatlarni o'z ichiga olishi kafolatiga bog'liq. Ushbu diapazondan(chegaradan) tashqaridagi qiymatga ega Taxmin
misolini yaratishga urinish panic qo'yishini ta'minlaydigan test yozishimiz mumkin.
Buni test funksiyamizga should_panic
atributini qo‘shish orqali qilamiz. Funktsiya ichidagi kod panic qo'zg'atsa, test o'tadi;funksiya ichidagi kod panic qo'ymasa, test muvaffaqiyatsiz tugaydi.
11-8 ro'yxatda Taxmin::new
xatolik holatlari biz kutgan vaqtda sodir bo'lishini tekshiradigan test ko'rsatilgan.
Fayl nomi: 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);
}
}
Biz #[should_panic]
atributini #[test]
atributidan keyin va u amal qiladigan test funksiyasidan oldin joylashtiramiz. Keling, ushbu testdan o'tgan natijani ko'rib chiqaylik:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (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
Yaxshi ko'rinadi! Endi shartni olib tashlash orqali kodimizga xatolik kiritamiz,
agar qiymat 100 dan katta bo'lsa, new
funksiya panic qo'zg'atadi:
pub struct Taxmin {
qiymat: i32,
}
// --snip--
impl Taxmin {
pub fn new(qiymat: i32) -> Taxmin {
if qiymat < 1 {
panic!("Taxmin qilingan qiymat 1 dan 100 gacha bo'lishi kerak, {} qabul qilinmaydi.", qiymat);
}
Taxmin { qiymat }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn _100_dan_ortiq() {
Taxmin::new(200);
}
}
Sinovni 11-8 ro'yxatda o'tkazganimizda, u muvaffaqiyatsiz bo'ladi:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::_100_dan_ortiq - should panic ... FAILED
failures:
---- tests::_100_dan_ortiq stdout ----
note: test did not panic as expected
failures:
tests::_100_dan_ortiq
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`
Biz bu holatda unchalik foydali xabar olmaymiz, lekin test funksiyasini ko‘rib chiqsak, u #[should_panic]
bilan izohlanganini ko‘ramiz. Biz erishgan muvaffaqiyatsizlik test funksiyasidagi kod panic qo'zg'atmaganligini anglatadi.
should_panic
ishlatadigan testlar noaniq bo'lishi mumkin. Agar test biz kutgandan boshqa sababga ko'ra panic qo'zg'atsa ham, should_panic
testi o'tadi. should_panic
testlarini aniqroq qilish uchun biz should_panic
atributiga ixtiyoriy expected
parametrini qo'shishimiz mumkin. Test dasturi xato xabarida taqdim etilgan matn mavjudligiga ishonch hosil qiladi. Masalan, 11-9 ro'yxatdagi Taxmin
uchun o'zgartirilgan kodni ko'rib chiqing, bu erda new
funksiya qiymat juda kichik yoki juda kattaligiga qarab turli xabarlar bilan panicga tushadi.
Fayl nomi: 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 = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
Bu testdan o‘tadi, chunki biz should_panic
atributining expected
parametriga qo‘ygan qiymat Taxmin::new
funksiyasi panicga tushadigan xabarning substringi hisoblanadi. Biz kutgan vahima haqidagi xabarni toʻliq koʻrsatishimiz mumkin edi, bu holda Taxmin qilingan qiymat 1 dan 100 gacha bo'lishi kerak, 200 qabul qilinmaydi.
. Siz belgilashni tanlagan narsa panic xabarining qanchalik noyob yoki dinamik ekanligiga va testingiz qanchalik aniq bo'lishini xohlayotganingizga bog'liq. Bunday holda, test funksiyasidagi kod else if qiymat > 100
holatini bajarishini ta`minlash uchun panic xabarining substringi kifoya qiladi.
expected
xabari bilan should_panic
testi muvaffaqiyatsiz tugashi bilan nima sodir bo'lishini ko'rish uchun if qiymat < 1
va else if qiymat > 100
bloklarini almashtirish orqali kodimizga yana xato kiritamiz:
pub struct Taxmin {
qiymat: i32,
}
impl Taxmin {
pub fn new(qiymat: i32) -> Taxmin {
if qiymat < 1 {
panic!(
"Taxmin qilingan qiymat 1 dan 100 gacha bo'lishi kerak, {} qabul qilinmaydi.",
qiymat
);
} else if qiymat > 100 {
panic!(
"Taxmin qilingan qiymat 1 dan katta yoki teng bo'lishi kerak, {} qabul qilinmaydi.",
qiymat
);
}
Taxmin { qiymat }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "100 dan kichik yoki teng")]
fn _100_dan_ortiq() {
Taxmin ::new(200);
}
}
Bu safar biz should_panic
testini o'tkazsak, u muvaffaqiyatsiz bo'ladi:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::_100_dan_ortiq - should panic ... FAILED
failures:
---- tests::_100_dan_ortiq stdout ----
thread 'tests::_100_dan_ortiq' panicked at 'Taxmin qilingan qiymat 1 dan katta yoki teng bo'lishi kerak, 200 qabul qilinmaydi.', 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: `"Taxmin qilingan qiymat 1 dan katta yoki teng bo'lishi kerak, 200 qabul qilinmaydi."`,
expected substring: `"100 dan kichik yoki teng"`
failures:
tests::_100_dan_ortiq
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`
Muvaffaqiyatsizlik xabari shuni ko'rsatadiki, bu test biz kutgandek panic qo'zg'atdi, lekin panic xabarida kutilgan Taxmin qilingan qiymat 100 dan kichik yoki unga teng bo'lishi kerak
qatori yo'q edi. Bu holatda biz olgan vahima xabari: Taxmin qilingan qiymat 1 dan katta yoki teng bo'lishi kerak, 200 qabul qilinmaydi.
. Endi biz xatomiz qayerda ekanligini aniqlashni boshlashimiz mumkin!
Testlarda Result<T, E>
dan foydalanish
Bizning testlarimiz muvaffaqiyatsiz bo'lganda panic qo'zg'atadi. Biz Result<T, E>
dan foydalanadigan testlarni ham yozishimiz mumkin! 11-1 roʻyxatidagi test Result<T, E>
dan foydalanish va panic oʻrniga Err
ni qaytarish uchun qayta yozilgan:
#[cfg(test)]
mod tests {
#[test]
fn ishlaydi() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("ikki qo'shish ikki to'rtga teng emas"))
}
}
}
ishlaydi
funksiyasi endi Result<(), String>
qaytish(return) turiga ega. Funksiya tanasida assert_eq!
makrosini chaqirishdan ko'ra, testdan o'tganda Ok(())
va test muvaffaqiyatsiz bo'lganda ichida String
bilan Err
ni qaytaramiz.
Testlarni Result<T, E>
qaytaradigan qilib yozish testlar matnida savol belgisi operatoridan foydalanish imkonini beradi, bu testlarni yozishning qulay usuli bo'lishi mumkin, agar ulardagi har qanday operatsiya Err
variantini qaytarsa, muvaffaqiyatsiz bo'lishi mumkin.
Result<T, E>
ishlatadigan testlarda #[should_panic]
izohidan(annotation) foydalana olmaysiz. Amaliyot Err
variantini qaytarishini tasdiqlash uchun Result<T, E>
qiymatida savol belgisi operatoridan foydalanmang. Buning oʻrniga assert!(value.is_err())
dan foydalaning.
Endi siz testlarni yozishning bir necha usullarini bilganingizdan so'ng, keling, testlarimizni o'tkazganimizda nima sodir bo'layotganini ko'rib chiqamiz va cargo test
bilan foydalanishimiz mumkin bo'lgan turli xil variantlarni ko'rib chiqamiz.
Testlar qanday o'tkazilishini nazorat qilish
Xuddi cargo run
kodingizni kompilyatsiya qilib, natijada olingan binaryni ishga tushirganidek, cargo test
kodingizni test rejimida kompilyatsiya qiladi va natijada olingan binary testni ishga tushiradi. cargo test
tomonidan ishlab chiqarilgan binary faylning standart xatti-harakati barcha testlarni parallel ravishda bajarish va sinov testlari paytida hosil bo'lgan chiqishni(output) olish, natijaning ko'rsatilishiga yo'l qo'ymaslik va sinov natijalari bilan bog'liq chiqishni o'qishni osonlashtirishdir. Biroq, siz ushbu standart xatti-harakatni o'zgartirish uchun buyruq qatori parametrlarini belgilashingiz mumkin.
Ba'zi buyruq qatori opsiyalari cargo test
ga, ba'zilari esa natijada olingan binary testga o'tadi. Ushbu ikki turdagi argumentlarni ajratish uchun siz cargo test
ga, so'ngra ajratuvchi --
ga o'tadigan argumentlarni, so'ngra test binarysiga o'tadigan argumentlarni sanab o'tasiz. cargo test --help
ni ishga tushirish cargo test
bilan foydalanishingiz mumkin bo'lgan variantlarni ko'rsatadi va cargo test -- --help
ni ishga tushirish ajratuvchidan(separator) keyin foydalanishingiz mumkin bo'lgan variantlarni ko'rsatadi.
Testlarni parallel yoki ketma-ket bajarish
Bir nechta testlarni bajarganingizda, standart bo'yicha ular threadlar yordamida parallel ravishda ishlaydi, ya'ni ular tezroq ishlashni tugatadi va siz tezroq fikr-mulohaza olasiz. Testlar bir vaqtning o'zida ishlayotganligi sababli, sizning testlaringiz bir-biriga yoki umumiy holatga, jumladan, joriy ishchi jildi yoki muhit o'zgaruvchilari kabi umumiy muhitga bog'liq emasligiga ishonch hosil qilishingiz kerak.
Misol uchun, sizning har bir testingiz diskda test-output.txt nomli fayl yaratadigan va ushbu faylga ba'zi ma'lumotlarni yozadigan ba'zi kodlarni ishga tushiradi. Keyin har bir test ushbu fayldagi ma'lumotlarni o'qiydi va faylda har bir testda har xil bo'lgan ma'lum bir qiymat borligini tasdiqlaydi. Testlar bir vaqtning o'zida bajarilganligi sababli, bitta test faylni boshqa test yozish va o'qish oralig'ida faylni qayta yozishi mumkin. Ikkinchi test kod noto'g'ri bo'lgani uchun emas, balki parallel ravishda ishlayotganda testlar bir-biriga xalaqit bergani uchun muvaffaqiyatsiz bo'ladi. Bitta yechim har bir test boshqa faylga yozishiga ishonch hosil qilishdir; yana bir yechim testlarni birma-bir bajarishdir.
Agar siz testlarni parallel ravishda o'tkazishni xohlamasangiz yoki ishlatilgan threadlar sonini yanada aniqroq nazorat qilishni istasangiz, siz --test threads
buyru'gini va foydalanmoqchi bo'lgan threadlar sonini test binaryga yuborishingiz mumkin. Quyidagi misolni ko'rib chiqing:
$ cargo test -- --test-threads=1
Biz dasturga parallelizmdan foydalanmaslikni aytib, test threadlari sonini 1
ga o'rnatdik. Testlarni bitta thread yordamida o'tkazish ularni parallel ravishda bajarishdan ko'ra ko'proq vaqt talab etadi, ammo agar ular umumiy holatga ega bo'lsa, testlar bir-biriga xalaqit bermaydi.
Funktsiya natijalarini ko'rsatish
Odatiy bo'lib, agar testdan o'tgan bo'lsa, Rustning test kutubxonasi standart chiqishda chop etilgan barcha narsalarni yozib oladi. Misol uchun, agar testda println!
ni chaqirsak va testdan o'tgan bo'lsa, terminalda println!
chiqishini ko'rmaymiz; biz faqat testdan o'tganligini ko'rsatadigan qatorni ko'ramiz. Agar test muvaffaqiyatsiz tugasa, biz xato xabarining qolgan qismi bilan standart chiqishda chop etilganini ko'ramiz.
Misol tariqasida, 11-10 ro'yxatida o'z parametrining qiymatini chop etadigan va 10 ni qaytaradigan ahmoqona funksiya, shuningdek, o'tgan test va muvaffaqiyatsizlikka uchragan test mavjud.
Fayl nomi: src/lib.rs
fn print_qiladi_va_10_qaytaradi(a: i32) -> i32 {
println!("Men {} qiymatini oldim", a);
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_muvaffaqiyatli() {
let qiymat = print_qiladi_va_10_qaytaradi(4);
assert_eq!(10, qiymat);
}
#[test]
fn test_muvaffaqiyatsiz() {
let qiymat = print_qiladi_va_10_qaytaradi(8);
assert_eq!(5, qiymat);
}
}
Ushbu testlarni cargo test
bilan bajarganimizda, biz quyidagi natijani ko'ramiz:
$ cargo test
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::test_muvaffaqiyatsiz ... FAILED
test tests::test_muvaffaqiyatli ... ok
failures:
---- tests::test_muvaffaqiyatsiz stdout ----
I got the value 8
thread 'tests::test_muvaffaqiyatsiz' 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::test_muvaffaqiyatsiz
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`
E'tibor bering, bu chiqishning hech bir joyida Men 4 qiymatini oldim
ni ko'rmaymiz, ya'ni testdan o'tganda chop etiladi. Bu chiqish qo'lga olindi. Muvaffaqiyatsiz bo'lgan test natijasi, Men 8-qiymatni oldim
, test xulosasi chiqishi bo'limida paydo bo'ladi, bu test muvaffaqiyatsizligi sababini ham ko'rsatadi.
Agar biz testlardan o'tish uchun yozilgan qiymatlarni ham ko'rishni istasak, Rust-ga --show-output
bilan muvaffaqiyatli testlar natijasini ham ko'rsatishni aytishimiz mumkin.
$ cargo test -- --show-output
11-10 ro'yxatdagi testlarni yana --show-output
buyrug'i bilan o'tkazganimizda, biz quyidagi natijani ko'ramiz:
$ 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 src/lib.rs (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 'tests::this_test_will_fail' 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`
Testlar to'plamini nomi bo'yicha bajarish(ishga tushirish)
Ba'zan to'liq test to'plamini ishga tushirish uzoq vaqt talab qilishi mumkin. Agar siz ma'lum bir sohada kod ustida ishlayotgan bo'lsangiz, faqat ushbu kodga tegishli testlarni o'tkazishni xohlashingiz mumkin. Argument sifatida oʻtkazmoqchi boʻlgan test(lar)ning nomi yoki nomlarini cargo test
oʻtish orqali qaysi testlarni oʻtkazishni tanlashingiz mumkin.
Testlar kichik to‘plamini qanday bajarishni ko‘rsatish uchun avval 11-11 ro‘yxatda ko‘rsatilganidek, ikkita_qoshish
funksiyamiz uchun uchta test yaratamiz va qaysi birini bajarishni tanlaymiz.
Fayl nomi: src/lib.rs
pub fn ikkita_qoshish(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ikkita_qoshish_va_ikki() {
assert_eq!(4, ikkita_qoshish(2));
}
#[test]
fn uchta_qoshish_va_ikki() {
assert_eq!(5, ikkita_qoshish(3));
}
#[test]
fn yuz() {
assert_eq!(102, ikkita_qoshish(100));
}
}
Agar biz testlarni hech qanday argumentlarsiz o'tkazsak, avval ko'rganimizdek, barcha testlar parallel ravishda ishlaydi:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 3 tests
test tests::uchta_qoshish_va_ikki ... ok
test tests::ikkita_qoshish_va_ikki ... ok
test tests::yuz ... 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
Yagona testlarni o'tkazish
Biz har qanday test funksiyasining nomini faqat shu testni oʻtkazish uchun cargo test
ga oʻtkazishimiz mumkin:
$ cargo test yuz
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.69s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::yuz ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Faqat yuz
nomli test o'tkazildi; qolgan ikkita test bu nomga mos kelmadi. Sinov natijasi, oxirida 2 filtered out
belgisini ko‘rsatish orqali bizda boshqa testlar o‘tkazilmaganligini bildiradi.
Biz bir nechta testlarning nomlarini shu tarzda aniqlay olmaymiz; faqat cargo test
ga berilgan birinchi qiymatdan foydalaniladi. Ammo bir nechta testlarni o'tkazishning bir usuli bor.
Bir nechta testlarni o'tkazish uchun filtrlash
Biz test nomining bir qismini belgilashimiz mumkin va nomi shu qiymatga mos keladigan har qanday test bajariladi. Masalan, ikkita testimiz nomi qoshish
ni o‘z ichiga olganligi sababli, biz cargo test qoshish
ni ishga tushirish orqali ikkalasini ham ishga tushirishimiz mumkin:
$ cargo test add
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::uchta_qoshish_va_ikki ... ok
test tests::ikkita_qoshish_va_ikki ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
Bu buyruq nomidagi qoshish
bilan barcha testlarni o'tkazdi va yuz
nomli testni filtrladi. Shuni ham yodda tutingki, test paydo bo'ladigan modul test nomining bir qismiga aylanadi, shuning uchun biz modul nomini filtrlash orqali moduldagi barcha testlarni bajarishimiz mumkin.
Maxsus talab qilinmasa, ba'zi testlarni e'tiborsiz qoldirish
Ba'zida bir nechta maxsus testlarni bajarish juda ko'p vaqt talab qilishi mumkin, shuning uchun siz cargo test
ning ko'p bosqichlarida ularni istisno qilishingiz mumkin. Siz oʻtkazmoqchi boʻlgan barcha testlarni argument sifatida roʻyxatga olish oʻrniga, bu yerda koʻrsatilganidek, ularni istisno qilish uchun ignore
(eʼtibor bermaslik) atributidan foydalanib, vaqt talab qiluvchi testlarga izoh qoʻyishingiz mumkin:
Fayl nomi: src/lib.rs
#[test]
fn ishlamoqda() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn qiyin_test() {
// code that takes an hour to run
}
#[test]
dan keyin biz chiqarib tashlamoqchi bo'lgan testga #[ignore]
qatorini qo'shamiz. Endi testlarimizni o'tkazganimizda, ishlamoqda
ishlaydi, lekin qiyin_test
ishlamaydi:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test qiyin_test ... ignored
test ishlamoqda ... 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
qiyin_test
funksiyasi ignore
ro'yxatiga kiritilgan. Agar biz faqat e'tiborga olinmagan(ignor qilingan) testlarni o'tkazmoqchi bo'lsak, biz cargo test -- --ignored
dan foydalanishimiz mumkin:
$ cargo test -- --ignored
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test qiyin_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
Qaysi sinovlar o'tkazilishini nazorat qilish orqali siz cargo test
natijalari tez bo'lishiga ishonch hosil qilishingiz mumkin. ignored
testlar natijalarini tekshirish mantiqiy bo'lgan nuqtada bo'lganingizda va natijalarni kutishga vaqtingiz bo'lsa, uning o'rniga cargo test -- --ignored
ni ishga tushirishingiz mumkin. Agar siz barcha testlarni ular e'tiborsiz(ignor) qoldiriladimi yoki yo'qmi, o'tkazmoqchi bo'lsangiz, cargo test -- --include-ignored
ni ishga tushirishingiz mumkin.
Test tashkil etish
Bobning boshida aytib o'tilganidek, test murakkab intizom bo'lib, turli odamlar turli terminologiya va tashkilotdan foydalanadilar. Rust hamjamiyati testlarni ikkita asosiy toifaga ko'ra o'ylaydi: birlik testlari(unit test) va integratsiya testlari(integration test). Unit testlari kichikroq va ko'proq yo'naltirilgan bo'lib, bir vaqtning o'zida bitta modulni alohida sinovdan o'tkazadi va private interfeyslarni sinab ko'rishi mumkin. Integratsiya testlari kutubxonangizdan(library) butunlay tashqarida bo'lib, kodingizdan faqat public interfeysdan foydalangan holda va har bir test uchun bir nechta modullardan foydalangan holda boshqa har qanday tashqi kod kabi foydalaning.
Kutubxonangizning qismlari siz kutgan narsani alohida va birgalikda bajarishini ta'minlash uchun ikkala turdagi testlarni yozish muhimdir.
Unit Testlar
Unit testlarining maqsadi kodning qayerda ekanligi va kutilganidek ishlamayotganligini tezda aniqlash uchun kodning har bir birligini(unit) qolgan kodlardan alohida tekshirishdan iborat. Unit testlarini har bir fayldagi src jildiga ular tekshirayotgan kod bilan joylashtirasiz. Konventsiya har bir faylda test funktsiyalarini o'z ichiga olgan tests
nomli modul yaratish va modulga cfg(test)
bilan izoh berishdan iborat.
Testlar moduli va #[cfg(test)]
Testlar modulidagi #[cfg(test)]
izohi Rustga test kodini faqat cargo test
ni bajarganingizda kompilyatsiya qilishni va ishga tushirishni aytadi, cargo build
ni ishga tushirganingizda emas.
Bu siz faqat kutubxona qurmoqchi bo'lganingizda kompilyatsiya vaqtini tejaydi va natijada tuzilgan artefaktda joyni tejaydi, chunki testlar kiritilmagan. Integratsiya testlari boshqa jildga o‘tgani uchun ularga #[cfg(test)]
izohi kerak emasligini ko‘rasiz. Biroq, unit testlari kod bilan bir xil fayllarda joylashganligi sababli, ular kompilyatsiya qilingan natijaga kiritilmasligini belgilash uchun #[cfg(test)]
dan foydalanasiz.
Eslatib o'tamiz, biz ushbu bobning birinchi qismida yangi qoshuvchi
loyihasini yaratganimizda, Cargo biz uchun ushbu kodni yaratdi:
Fayl nomi: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn ishlaydi() {
let natija = 2 + 2;
assert_eq!(natija, 4);
}
}
Bu kod avtomatik ravishda yaratilgan test modulidir. cfg
atributi konfiguratsiya(configuration) degan ma'noni anglatadi va Rustga quyidagi element faqat ma'lum bir konfiguratsiya opsiyasi berilganda kiritilishi kerakligini aytadi. Bunday holda, konfiguratsiya opsiyasi test
bo'lib, u Rust tomonidan testlarni kompilyatsiya qilish va ishga tushirish uchun taqdim etiladi. cfg
atributidan foydalanib, Cargo bizning test kodimizni faqat cargo test
bilan faol ravishda o'tkazganimizdagina kompilyatsiya qiladi. Bunga #[test]
bilan izohlangan funksiyalarga qoʻshimcha ravishda ushbu modulda boʻlishi mumkin boʻlgan har qanday yordamchi funksiyalar kiradi.
Private funksiyalarni testdan o'tkazish
Sinov hamjamiyatida private(xususiy) funksiyalarni to'g'ridan-to'g'ri testdan o'tkazish kerakmi yoki yo'qmi degan bahs-munozaralar mavjud va boshqa tillar private funktsiyalarni test qilib ko'rishni qiyinlashtiradi yoki imkonsiz qiladi. Qaysi sinov mafkurasiga rioya qilishingizdan qat'i nazar, Rust maxfiylik qoidalari sizga private funksiyalarni test qilish imkonini beradi.
11-12 roʻyxatdagi kodni ichki_qoshuvchi
private funksiyasi bilan koʻrib chiqing.
Fayl nomi: src/lib.rs
pub fn ikkita_qoshish(a: i32) -> i32 {
ichki_qoshuvchi(a, 2)
}
fn ichki_qoshuvchi(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ichki() {
assert_eq!(4, ichki_qoshuvchi(2, 2));
}
}
Esda tutingki, ichki_qoshuvchi
funksiyasi pub
sifatida belgilanmagan. Testlar shunchaki Rust kodi va tests
moduli shunchaki boshqa moduldir. "Modul daraxtidagi elementga murojaat qilish yo'llari" bo'limida muhokama qilganimizdek, bolalar modullaridagi elementlar o'zlarining asosiy modullaridagi elementlardan foydalanishlari mumkin. Ushbu testda biz test
modulining ota-onasining barcha elementlarini use super::*
yordamida qamrab olamiz va keyin test ichki_qoshuvchi
ni chaqirishi mumkin. Agar private(shaxsiy) funksiyalarni sinab ko'rish kerak deb o'ylamasangiz, Rustda sizni bunga majbur qiladigan hech narsa yo'q.
Integratsiya testlari
Rust-da integratsiya testlari kutubxonangizdan butunlay tashqarida. Ular kutubxonangizdan boshqa kodlar kabi foydalanadilar, ya'ni ular faqat kutubxonangizning umumiy API qismi bo'lgan funksiyalarni chaqira oladi. Ularning maqsadi kutubxonangizning ko'p qismlari to'g'ri ishlashini tekshirishdir. O'z-o'zidan to'g'ri ishlaydigan kod birliklari integratsiyalashganda muammolarga duch kelishi mumkin, shuning uchun integratsiyalangan kodni sinovdan o'tkazish ham muhimdir. Integratsiya testlarini yaratish uchun sizga birinchi navbatda tests jildi kerak bo'ladi.
tests jildi
Biz loyiha jildimizning yuqori darajasida, src yonida tests jildini yaratamiz. Cargo ushbu jildda integratsiya test fayllarini qidirishni biladi. Keyin biz xohlagancha test fayllarini yaratishimiz mumkin va Cargo har bir faylni alohida crate sifatida tuzadi.
Keling, integratsiya testini yarataylik. 11-12 ro'yxatdagi kod hali ham src/lib.rs faylida bo'lsa, tests jildini yarating va tests/integratsiya_test.rs nomli yangi fayl yarating. Sizning fayl tuzilishingiz tuzilishi quyidagicha ko'rinishi kerak:
qoshuvchi
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integratsiya_test.rs
11-13 ro'yxatdagi kodni tests/integratsiya_test.rs fayliga kiriting:
Fayl nomi: tests/integration_test.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-13/tests/integration_test.rs}}
tests
jildidagi har bir fayl alohida cratedir, shuning uchun biz kutubxonamizni har bir test cratesi doirasiga kiritishimiz kerak. Shuning uchun biz kodning yuqori qismiga unit testlarida kerak bo'lmagan use qoshuvchi
ni qo'shamiz.
Bizga tests/integration_test.rs da #[cfg(test)]
bilan hech qanday kodga izoh berish shart emas. Cargo tests
jildini maxsus ko'rib chiqadi va bu jilddagi fayllarni faqat biz cargo test
buyrug'ini ishga tushirganimizda kompilyatsiya qiladi. Keling cargo test
qilib ishlatamiz:
$ cargo test
Compiling qoshuvchi v0.1.0 (file:///projects/qoshuvchi)
Finished test [unoptimized + debuginfo] target(s) in 1.31s
Running unittests src/lib.rs (target/debug/deps/qoshuvchi-1082c4b063a8fbe6)
running 1 test
test tests::ichki ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integratsiya_test.rs (target/debug/deps/integratsiya_test-1082c4b063a8fbe6)
running 1 test
test ikkita_qoshish ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests qoshuvchi
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Chiqishning(output) uchta bo'limiga unit testlari, integratsiya testlari va doc testlari kiradi. E'tibor bering, agar bo'limdagi biron bir test muvaffaqiyatsiz bo'lsa, keyingi bo'limlar bajarilmaydi. Misol uchun, agar unit testi muvaffaqiyatsiz bo'lsa, integratsiya va doc testlari uchun hech qanday natija bo'lmaydi, chunki bu testlar faqat barcha unit testlari o'tgan taqdirdagina amalga oshiriladi.
Unit testlari uchun birinchi bo'lim biz ko'rganimiz bilan bir xil: har bir unit testi uchun bitta satr (biz 11 12 roʻyxatga qoʻshgan ichki
deb nomlangan) va keyin unit testlari uchun xulosa qator.
Integratsiya testlari bo'limi Running tests/integration_test.rs
qatoridan boshlanadi. Keyin, ushbu integratsiya testidagi har bir test funksiyasi uchun qator va Doc-tests adder
boʻlimi boshlanishidan oldin integratsiya testi natijalari uchun xulosa qatori mavjud.
Har bir integratsiya test faylining o'z bo'limi bor, shuning uchun tests jildiga ko'proq fayllar qo'shsak, ko'proq integratsiya test bo'limlari bo'ladi.
cargo test
ga argument sifatida test funksiyasining nomini ko‘rsatib, biz hali ham muayyan integratsiya test funksiyasini ishga tushirishimiz mumkin. Muayyan integratsiya test faylida barcha testlarni bajarish uchun cargo test
ning --test
argumentidan keyin fayl nomidan foydalaning:
$ cargo test --test integration_test
Compiling qoshuvchi v0.1.0 (file:///projects/qoshuvchi)
Finished test [unoptimized + debuginfo] target(s) in 0.64s
Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)
running 1 test
test ikkita_qoshish ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Bu buyruq faqat tests/integration_test.rs faylidagi testlarni bajaradi.
Integratsiya testlarida submodullar
Ko'proq integratsiya testlarini qo'shsangiz, ularni tartibga solishga yordam berish uchun tests jildida ko'proq fayllar yaratishni xohlashingiz mumkin; masalan, siz test funktsiyalarini ular test qilib ko'rayotgan funksiyalari bo'yicha guruhlashingiz mumkin. Yuqorida aytib o'tilganidek, tests jildidagi har bir fayl o'zining alohida cratesi sifatida tuzilgan bo'lib, bu oxirgi foydalanuvchilar sizning cratengizdan qanday foydalanishini yanada yaqinroq taqlid qilish uchun alohida qamrovlarni yaratish uchun foydalidir. Biroq, bu shuni anglatadiki, tests jildidagi fayllar src dagi fayllarga o'xshamaydi, chunki kodni modul va fayllarga qanday ajratish haqida 7-bobda o'rgangansiz.
tests jildidagi fayllarning har xil xatti-harakatlari bir nechta integratsiya test fayllarida foydali bo'ladigan yordamchi funktsiyalar to'plamiga ega bo'lganingizda sezilarli bo'ladi. Aytaylik, siz ularni umumiy modulga chiqarish uchun 7-bob, "Modullarni turli fayllarga ajratish" bosqichlarini bajarishga harakat qilyapsiz. Misol uchun, agar biz tests/common.rs ni yaratsak va unga setup
nomli funksiyani joylashtirsak, biz bir nechta test fayllaridagi bir nechta test funksiyalaridan chaqirmoqchi bo'lgan setup
ga ba'zi kodlarni qo'shishimiz mumkin:
Fayl nomi: tests/common.rs
pub fn setup() {
// kutubxonangiz testlariga xos sozlash(setup) kodi bu yerga tushadi
}
Testlarni qayta ishga tushirganimizda, biz common.rs fayli uchun test chiqishida yangi bo'limni ko'ramiz, garchi bu faylda hech qanday test funksiyalari mavjud bo'lmasa ham, biz hech qanday joydan setup
funksiyasini chaqirmagan bo'lsak ham:
$ cargo test
Compiling qoshuvchi v0.1.0 (file:///projects/qoshuvchi)
Finished test [unoptimized + debuginfo] target(s) in 0.89s
Running unittests src/lib.rs (target/debug/deps/qoshuvchi-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 qoshuvchi
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Test natijalarida setup
ko'rinishida running 0 tests
ko'rsatilishi biz xohlagan narsa emas. Biz shunchaki kodni boshqa integratsiya test fayllari bilan baham ko'rmoqchi edik.
Test natijasida common
ko'rinishini oldini olish uchun tests/common.rs yaratish o'rniga biz tests/common/mod.rs ni yaratamiz. Loyiha jildi(fayl structurasi) endi shunday ko'rinadi:
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
Bu eski nomlash konventsiyasi bo'lib, Rust biz 7-bobning "Muqobil fayl yo'llari(path)" bo'limida aytib o'tganimizni ham tushunadi.
Biz setup
funksiya kodini tests/common/mod.rs ga ko'chirsak va tests/common.rs faylini o'chirsak, test chiqishidagi bo'lim endi ko'rinmaydi. tests jildining pastki jildlaridagi fayllar alohida cratelar sifatida kompilyatsiya qilinmaydi yoki test chiqishida(output) bo'limlarga(section) ega emas.
tests/common/mod.rs ni yaratganimizdan so'ng, biz uni modul sifatida har qanday integratsiya test faylidan foydalanishimiz mumkin. Bu yerda tests/integration_test.rs da ikkita_qoshish
testidan setup
funksiyasini chaqirish misoli keltirilgan:
Fayl nomi: tests/integration_test.rs
use qoshuvchi;
mod common;
#[test]
fn ikkita_qoshish() {
common::setup();
assert_eq!(4, qoshuvchi::ikkita_qosh(2));
}
Esda tutingki, mod common;
deklaratsiyasi biz 7-21 roʻyxatda koʻrsatgan modul deklaratsiyasi bilan bir xil. Keyin test funksiyasida biz common::setup()
funksiyasini chaqirishimiz mumkin.
Binary Cratelar uchun integratsiya testlari
Agar bizning loyihamiz faqat src/main.rs faylini o'z ichiga olgan va src/lib.rs fayliga ega bo'lmagan ikkilik crate(binary crate) bo'lsa, biz tests jildida integratsiya testlarini yarata olmaymiz va src/main.rs faylida belgilangan funksiyalarni use
statementi bilan qamrab ololmaymiz. Faqat kutubxona cratelari(library crate) boshqa cratelar foydalanishi mumkin bo'lgan funksiyalarni ko'rsatadi; binary cratelar o'z-o'zidan ishlashi uchun mo'ljallangan.
Bu binary faylni ta'minlovchi Rust loyihalarida src/lib.rs faylida yashovchi logicni chaqiruvchi(call logic) oddiy src/main.rs fayliga ega bo'lishining sabablaridan biri. Ushbu structedan foydalanib, integratsiya testlari kutubxona cratesini muhim funksiyalarni mavjud qilish uchun use
bilan sinab ko'rishi mumkin.
Agar muhim funksiya ishlayotgan bo'lsa, src/main.rs faylidagi kichik kod miqdori ham ishlaydi va bu kichik kod miqdorini sinab ko'rish kerak emas.
Xulosa
Rust-ning test xususiyatlari(feature) kod qanday ishlashini belgilash usulini taqdim etadi va u siz kutganingizdek ishlashini ta'minlaydi, hatto siz o'zgartirishlar kiritsangiz ham. Unit testlari kutubxonaning turli qismlarini alohida bajaradi va private impelement qilish tafsilotlarini sinab ko'rishi mumkin. Integratsiya testlari kutubxonaning ko'p qismlari to'g'ri ishlashini tekshiradi va ular tashqi kod uni ishlatadigan tarzda kodni sinab ko'rish uchun kutubxonaning umumiy API'sidan foydalanadilar. Rustning type systemi va ownership qoidalari ba'zi xatolarning oldini olishga yordam bergan bo'lsa ham, testlar sizning kodingiz qanday ishlashi bilan bog'liq bo'lgan mantiqiy xatolarni kamaytirish uchun hali ham muhimdir.
Keling, ushbu bobda va oldingi boblarda olgan bilimlaringizni loyiha ustida ishlash uchun birlashtiraylik!
I/O loyihasi: Buyruqlar qatori dasturini yaratish(command line)
Ushbu bob siz hozirgacha o'rgangan ko'plab ko'nikmalarning qisqacha mazmuni va yana bir nechta standart kutubxona xususiyatlarining o'rganilishidir. Biz hozirda mavjud bo'lgan Rust tushunchalarini mashq qilish uchun fayl va buyruq qatori kiritish/chiqarish(input/output) bilan o'zaro ta'sir qiluvchi buyruq qatori vositasini(command line tool) yaratamiz.
Rust-ning tezligi, xavfsizligi, bitta ikkilik chiqishi(single binary output) va platformalararo9cross-platform qo'llab-quvvatlashi uni buyruqlar qatori vositalarini(command line tools) yaratish uchun ideal tilga aylantiradi, shuning uchun loyihamiz uchun biz klassik buyruq qatori qidiruv vositasi grep
ning o'z versiyasini yaratamiz (globally search a regular expression and print) qidirish. Foydalanishning eng oddiy holatida grep
belgilangan faylni belgilangan qator uchun qidiradi. Buning uchun grep
o'z argumenti sifatida fayl yo'li(file path) va satrni oladi. Keyin u faylni o'qiydi, o'sha faylda string argumentini o'z ichiga olgan qatorlarni topadi va bu satrlarni chop(print qiladi) etadi.
Yo'l davomida biz buyruq qatori vositasini boshqa ko'plab buyruq qatori vositalari ishlatadigan terminal xususiyatlaridan qanday foydalanishni ko'rsatamiz. Biz foydalanuvchiga vositamizning harakatini sozlash imkonini berish uchun atrof-muhit o'zgaruvchisining qiymatini(environment variable) o'qiymiz.
Shuningdek, biz xato xabarlarini standart chiqish (stdout
) o'rniga standart xato konsoli oqimiga (stderr
) chop qilamiz, shuning uchun, masalan, foydalanuvchi ekranda xato xabarlarini ko'rayotganda muvaffaqiyatli chiqishni faylga yo'naltirishi mumkin.
Rust hamjamiyatining bir a'zosi Andrew Gallant allaqachon grep
ning ripgrep
deb nomlangan to'liq xususiyatli, juda tez versiyasini yaratgan. Taqqoslash uchun, bizning versiyamiz ancha sodda bo'ladi, ammo bu bob sizga ripgrep
kabi real loyihani tushunish uchun zarur bo'lgan asosiy bilimlarni beradi.
Bizning grep
loyihamiz siz hozirgacha o'rgangan bir qator tushunchalarni birlashtiradi:
- Kodni tashkil qilish (7-bobda modullar haqida bilib olganlaringizdan foydalangan holda)
- Vectorlar va stringlardan foydalanish (to'plamlar(collection), 8-bob)
- Xatolarni qayta ishlash(handling error) (9-bob)
- Kerakli hollarda traitlar va lifetimelardan foydalanish (10-bob)
- Testlar yozish (11-bob)
Shuningdek, biz 13 va 17-boblarda batafsil yoritilgan closurelar, iteratorlar va trait obyektlarini qisqacha tanishtiramiz.
Buyruqlar qatori argumentlarini qabul qilish
Keling, har doimgidek, cargo new
bilan yangi loyiha yarataylik. Loyihamizni tizimingizda mavjud boʻlgan grep
konsol dasturidan farqlash uchun uni minigrep
deb ataymiz.
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrep
Birinchi vazifa minigrep
ni ikkita buyruq qatori argumentlarini qabul qilishdir: fayl yo'li va izlash uchun satr. Ya'ni, biz o'z dasturimizni cargo run
bilan ishga tushirishni xohlaymiz, quyidagi argumentlar cargo
uchun emas, balki dasturimizga tegishli ekanligini ko'rsatadigan ikkita tire(qo'shaloq chiziq), qidirish uchun satr va qidiruv uchun faylga yo'l. ichida, shunga o'xshash:
$ cargo run -- qidiruv-matni namuna-fayl.txt
Hozirda cargo new
tomonidan yaratilgan dastur biz bergan argumentlarni qayta ishlay olmaydi. crates.io-dagi ba'zi mavjud kutubxonalar buyruq qatori argumentlarini qabul qiladigan dastur yozishda yordam berishi mumkin, ammo siz ushbu kontseptsiyani endigina o'rganayotganingiz uchun keling, bu imkoniyatni o'zimiz amalga oshiraylik.
Argument qiymatlarini o'qish
minigrep
ga biz o'tadigan buyruq qatori argumentlarining qiymatlarini o'qishni yoqish uchun bizga Rust standart kutubxonasida taqdim etilgan std::env::args
funksiyasi kerak bo'ladi. Bu funksiya minigrep
ga uzatilgan buyruq qatori argumentlarining iteratorini qaytaradi. Biz iteratorlarni 13-bobda to'liq ko'rib chiqamiz. Hozircha siz iteratorlar haqida faqat ikkita ma'lumotni bilishingiz kerak: iteratorlar bir qator qiymatlarni ishlab chiqaradi va biz uni vector kabi to'plamga(collection) aylantirish uchun iteratorda collect
metodini chaqirishimiz mumkin,iterator ishlab chiqaradigan barcha elementlarni o'z ichiga oladi.
12-1 ro'yxatidagi kod minigrep
dasturingizga unga berilgan har qanday buyruq qatori argumentlarini o'qish va keyin qiymatlarni vectorga yig'ish imkonini beradi.
Fayl nomi: src/main.rs
use std::env; fn main() { let args: Vec<String> = env::args().collect(); dbg!(args); }
Birinchidan, biz std::env
modulini use
statementi bilan qamrab olamiz, shunda uning args
funksiyasidan foydalanamiz. std::env::args
funksiyasi modullarning ikki darajasida joylashganligiga e'tibor bering. Biz 7-bobda muhokama qilganimizdek, istalgan funksiya bir nechta modulda joylashgan bo‘lsa, biz funksiyani emas, balki ota-modulni qamrab olishni tanladik. Shunday qilib, biz std::env
dan boshqa funksiyalardan bemalol foydalanishimiz mumkin. Bu, shuningdek, use std::env::args
ni qo‘shib, so‘ngra funksiyani faqat args
bilan chaqirishdan ko‘ra kamroq noaniqdir, chunki args
joriy modulda aniqlangan funksiya bilan osongina xato qilishi mumkin.
args
funksiyasi va notog'ri UnicodeE'tibor bering, agar biron bir argumentda noto'g'ri Unicode bo'lsa,
std::env::args
panic qo'zg'atadi. Agar dasturingiz noto'g'ri Unicode o'z ichiga olgan argumentlarni qabul qilishi kerak bo'lsa, o'rnigastd::env::args_os
dan foydalaning. Bu funksiyaString
qiymatlari o‘rnigaOsString
qiymatlarini ishlab chiqaruvchi iteratorni qaytaradi. Biz bu yerda soddalik uchunstd::env::args
dan foydalanishni tanladik, chunkiOsString
qiymatlari platformalar uchun farq qiladi va ular bilan ishlashString
qiymatlariga qaraganda murakkabroq.
main
ning birinchi qatorida biz env::args
deb nomlaymiz va iteratorni iterator tomonidan ishlab chiqarilgan barcha qiymatlarni o'z ichiga olgan vectorga aylantirish uchun darhol collect
dan foydalanamiz. Biz ko'p turdagi to'plamlarni(collection) yaratish uchun collect
funksiyasidan foydalanishimiz mumkin, shuning uchun biz stringlar vectorini xohlashimizni ko'rsatish uchun args
turiga aniq izoh beramiz. Rust-da turlarga juda kamdan-kam izoh qo'yishimiz kerak bo'lsa-da, collect
funksiyasi siz tez-tez izohlashingiz kerak bo'lgan funksiyadir, chunki Rust siz xohlagan to'plam turini aniqlay olmaydi.
Nihoyat, debug makrosi yordamida vectorni chop etamiz. Keling, kodni avval argumentsiz, keyin esa ikkita argument bilan ishga tushirishga harakat qilaylik:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/minigrep`
[src/main.rs:5] args = [
"target/debug/minigrep",
]
$ cargo run -- anor mevalar
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 1.57s
Running `target/debug/minigrep anor mevalar`
[src/main.rs:5] args = [
"target/debug/minigrep",
"anor",
"mevalar",
]
E'tibor bering, vectordagi birinchi qiymat "target/debug/minigrep"
bo'lib, bu bizning ikkilik(binary) faylimiz nomidir. Bu C dagi argumentlar ro'yxatining xatti-harakatiga mos keladi, bu dasturlarga ularni bajarishda chaqirilgan nomdan foydalanishga imkon beradi. Agar siz uni xabarlarda chop qilmoqchi bo'lsangiz yoki dasturni chaqirish uchun qanday buyruq qatori taxalluslari(alias) ishlatilganiga qarab dasturning harakatini o'zgartirmoqchi bo'lsangiz, dastur nomiga kirish ko'pincha qulaydir. Ammo ushbu bobning maqsadlari uchun biz buni e'tiborsiz qoldiramiz va faqat bizga kerak bo'lgan ikkita argumentni saqlaymiz.
Argument qiymatlarini o'zgaruvchilarda saqlash
Dastur hozirda buyruq qatori argumentlari sifatida ko'rsatilgan qiymatlarga kirish imkoniyatiga ega. Endi biz ikkita argumentning qiymatlarini o'zgaruvchilarda saqlashimiz kerak, shuning uchun biz dasturning qolgan qismida qiymatlardan foydalanishimiz mumkin. Biz buni 12-2 ro'yxatda qilamiz.
Fayl nomi: src/main.rs
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let sorov = &args[1];
let fayl_yoli = &args[2];
println!("{} qidirilmoqda", sorov);
println!("{} faylida", fayl_yoli);
}
Biz vectorni chop etganimizda ko'rganimizdek, dastur nomi vectordagi birinchi qiymatni args[0]
oladi, shuning uchun biz 1
indeksidan argumentlarni boshlaymiz. minigrep
ning birinchi argumenti biz qidirayotgan satrdir, shuning uchun biz birinchi argumentga referenceni sorov
o‘zgaruvchisiga qo‘yamiz. Ikkinchi argument fayl yo'li bo'ladi, shuning uchun biz fayl_yoli
o'zgaruvchisiga ikkinchi argumentga reference qilamiz.
Kod biz xohlagandek ishlayotganini isbotlash uchun biz ushbu o'zgaruvchilarning qiymatlarini vaqtincha chop qilamiz. test
va namuna.txt
argumentlari bilan ushbu dasturni qayta ishga tushiramiz:
$ cargo run -- test namuna.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`
test qidirilmoqda
namuna.txt faylida
Ajoyib, dastur ishlayapti! Bizga kerak bo'lgan argumentlarning qiymatlari to'g'ri o'zgaruvchilarga saqlanmoqda. Keyinchalik ba'zi potentsial noto'g'ri vaziyatlarni hal qilish uchun xatolarni qayta ishlash usullarini qo'shamiz, masalan, foydalanuvchi hech qanday argument keltirmasa; Hozircha biz bu holatni e'tiborsiz qoldiramiz va uning o'rniga fayllarni o'qish imkoniyatlarini qo'shish ustida ishlaymiz.
Faylni o'qish
Endi biz fayl_yoli
argumentida koʻrsatilgan faylni oʻqish funksiyasini qoʻshamiz. Birinchidan, uni sinab ko'rish uchun bizga namuna fayli kerak: biz bir nechta takroriy so'zlar bilan bir nechta satrlarda kichik hajmdagi matnli fayldan foydalanamiz. 12-3 ro'yxatda Olma haqida she'r bor, u yaxshi ishlaydi! Loyihangizning root darajasida olma.txt nomli fayl yarating va “Olma” she’rini kiriting.
Fayl nomi: olma.txt
Tanishaylik, men - olma,
Nomimga quloq solma.
Olaver, ikkilanmay,
Ishtahang bo'lsin karnay
Reklamaga hojat yo'q
Ta'mim rosa yoqimli.
Ortganini quritsang,
Qishda yeysan qoqimni
Men sizlarni olmangiz,
Xomligimda olmangiz!
Asilbekga o'xshab so'ng,
Voy qornim deb qolmangiz!
Bog'larda chiroymanda,
Vitaminga boymanda!
Pishganimda yemasangiz,
Qolasizda armonda!
Matn joyida bo'lgan holda src/main.rs ni tahrirlang va 12-4 ro'yxatda ko'rsatilganidek, faylni o'qish uchun kod qo'shing.
Fayl nomi: src/main.rs
use std::env;
use std::fs;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let sorov = &args[1];
let fayl_yoli = &args[2];
println!("{} qidirilmoqda", sorov);
println!("{} faylida", fayl_yoli);
let tarkib = fs::read_to_string(fayl_yoli)
.expect("Faylni o'qiy olishi kerak edi");
println!("Fayl tarkibi:\n{tarkib}");
}
Birinchidan, biz standart kutubxonaning tegishli qismini use
statementi bilan keltiramiz: fayllar bilan ishlash uchun bizga std::fs
kerak.
main
da yangi fs::read_to_string
statementi fayl_yoli
ni oladi, bu faylni ochadi va fayl mazmunining std::io::Result<String>
ni qaytaradi.
Shundan so'ng, fayl o'qilgandan keyin tarkib
qiymatini chop etadigan vaqtinchalik println!
statementini yana qo'shamiz, shuning uchun dasturning hozirgacha ishlayotganligini tekshirishimiz mumkin.
Keling, ushbu kodni birinchi buyruq qatori argumenti sifatida istalgan qator bilan ishga tushiramiz (chunki biz hali qidiruv qismini amalga oshirmaganmiz) va ikkinchi argument sifatida olma.txt fayli:
$ cargo run -- men olma.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`
men qidirilmoqda
olma.txt faylidan
Fayl tarkibi:
Tanishaylik, men - olma,
Nomimga quloq solma.
Olaver, ikiklanmay,
Ishtahang bo'lsin karnay
Reklamaga hojat yo'q
Ta'mim rosa yoqimli.
Ortganini quritsang,
Qishda yeysan qoqimni
Men sizlarni olmangiz,
Xomligimda olmangiz!
Asilbekga o'xshab so'ng,
Voy qornim deb qolmangiz!
Bog'larda chiroymanda,
Vitaminga boymanda!
Pishganimda yemasangiz,
Qolasizda armonda!
Ajoyib! Ushbu kod fayl mazmunini o'qiydi va fayl mazmunini chop etdi. Ammo kodda bir nechta kamchiliklar mavjud. Ayni paytda main
funksiya bir nechta mas'uliyatga ega: umuman olganda, har bir funksiya faqat bitta vazifa uchun javobgar bo'lsa, funksiyalar aniqroq va ularni saqlash osonroq bo'ladi. Boshqa muammo shundaki, biz xatolarni imkon qadar yaxshi hal qilmayapmiz. Dastur hali ham kichik, shuning uchun bu kamchiliklar katta muammo emas, lekin dastur o'sib ulg'aygan sayin ularni toza tuzatish qiyinroq bo'ladi. Dasturni ishlab chiqishda refaktorlashni erta boshlash yaxshi amaliyotdir, chunki kichikroq hajmdagi kodlarni qayta ishlash ancha oson. Biz buni keyin qilamiz.
Modullilikni va xatolarni boshqarishni yaxshilash uchun refaktoring
Dasturimizni yaxshilash uchun dastur tuzilishi va uning yuzaga kelishi mumkin bo'lgan xatolarni qanday hal qilishi bilan bog'liq bo'lgan to'rtta muammoni tuzatamiz. Birinchidan, bizning main
funksiyamiz endi ikkita vazifani bajaradi: u argumentlarni tahlil qiladi va fayllarni o'qiydi. Dasturimiz o'sib borishi bilan main
funksiya boshqaradigan alohida vazifalar soni ortadi. Funksiyaga mas'uliyat yuklagan sari, uning qismlaridan birini buzmasdan fikr yuritish, sinab ko'rish va o'zgartirish qiyinroq bo'ladi. Har bir funksiya bitta vazifa uchun javobgar bo'lishi uchun funksionallikni ajratish yaxshiroqdir.
Bu muammo ikkinchi muammo bilan ham bog'liq: sorov
va fayl_yoli
bizning dasturimiz uchun konfiguratsiya o'zgaruvchilari bo'lsa-da, dastur mantig'ini bajarish uchun tarkib
kabi o'zgaruvchilardan foydalaniladi. main
qancha uzun bo'lsa, biz ko'proq o'zgaruvchilarni qamrab olishimiz kerak bo'ladi; bizda qancha ko'p o'zgaruvchilar mavjud bo'lsa, ularning har birining maqsadini kuzatib borish shunchalik qiyin bo'ladi. Maqsadlari aniq bo'lishi uchun konfiguratsiya o'zgaruvchilarini bitta tuzilishga guruhlash yaxshidir.
Uchinchi muammo shundaki, biz faylni o‘qib chiqmaganda xato xabarini chop etish uchun expect
tugmasidan foydalanganmiz, biroq xato xabari “Faylni o‘qishi kerak edi” degan yozuvni chiqaradi. Faylni o'qish bir necha usul bilan muvaffaqiyatsiz bo'lishi mumkin: masalan, fayl yetishmayotgan bo'lishi mumkin yoki bizda uni ochishga ruxsat yo'q.
Hozirda, vaziyatdan qat'i nazar, biz hamma narsa uchun bir xil xato xabarini chop qilamiz, bu esa foydalanuvchiga hech qanday ma'lumot bermaydi!
To‘rtinchidan, biz turli xil xatolarni qayta ishlash uchun expect
dan qayta-qayta foydalanamiz va agar foydalanuvchi dasturimizni yetarlicha argumentlarni ko'rsatmasdan ishga tushirsa, Rustdan index out of bounds
("chegaradan tashqari indeks") xatosini oladi va bu muammoni aniq tushuntirmaydi. Xatolarni qayta ishlash mantig'ini o'zgartirish kerak bo'lsa, kelajakdagi saqlovchilar(maintainerlar) kod bilan maslahatlashish uchun faqat bitta joyga ega bo'lishlari uchun barcha xatolarni qayta ishlash kodi bir joyda bo'lsa yaxshi bo'lar edi. Xatolarni qayta ishlash uchun barcha kodlar bir joyda bo'lsa, biz oxirgi foydalanuvchilarimiz uchun mazmunli bo'lgan xabarlarni chop etishimizni ta'minlaydi.
Keling, loyihamizni qayta tiklash orqali ushbu to'rtta muammoni hal qilaylik.
Binary loyihalar uchun vazifalarni ajratish
Bir nechta vazifalar uchun javobgarlikni main
funksiyaga taqsimlashning tashkiliy muammosi ko'plab ikkilik(binary) loyihalar uchun umumiydir. Natijada, Rust hamjamiyati main
kattalasha boshlaganda ikkilik dasturning alohida muammolarini ajratish bo'yicha ko'rsatmalar ishlab chiqdi. Bu jarayon quyidagi bosqichlardan iborat:
-
Dasturingizni main.rs va lib.rs ga bo'ling va dasturingiz mantig'ini lib.rs ga o'tkazing.
-
Agar buyruq satrini tahlil qilish mantig'i kichik bo'lsa, u main.rs da qolishi mumkin.
-
Buyruqlar qatorini tahlil qilish mantig'i murakkablasha boshlagach, uni main.rs dan chiqarib, lib.rs ga o'tkazing.
Ushbu jarayondan keyin main
funksiyada qoladigan mas'uliyatlar quyidagilar bilan cheklanishi kerak:
- Argument qiymatlari bilan buyruq satrini tahlil qilish mantig'ini chaqirish
- Boshqa har qanday konfiguratsiyani sozlash
- lib.rs da
run
funksiyasini chaqirish run
xatoni qaytarsa, xatoni hal qilish
Ushbu pattern vazifalarni ajratish bilan bog'liq: main.rs dasturni ishga tushirishni boshqaradi va lib.rs topshirilgan vazifaning barcha mantig'ini boshqaradi. main
funksiyani toʻgʻridan-toʻgʻri test qilib koʻra olmasligingiz sababli, ushbu structura dasturingizning barcha mantig'ini lib.rs funksiyalariga koʻchirish orqali test qilib koʻrish imkonini beradi. main.rs da qolgan kod uni o'qish orqali uning to'g'riligini tekshirish uchun yetarlicha kichik bo'ladi. Keling, ushbu jarayonni kuzatib, dasturimizni qayta ishlaymiz.
Argument tahlilchisini(parser) chiqarish
Argumentlarni tahlil qilish(parsing qilish) funksiyasini main
buyruq satrini tahlil qilish mantig'ini src/lib.rs ga ko'chirishga tayyorlash uchun chaqiradigan funksiyaga ajratamiz. Ro'yxat 12-5 main
ning yangi boshlanishini ko'rsatadi, u parse_config
yangi funksiyasini chaqiradi, biz buni hozircha src/main.rs da aniqlaymiz.
Fayl nomi: src/main.rs
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let (sorov, fayl_yoli) = parse_config(&args);
// --snip--
println!("{} qidirilmoqda", sorov);
println!("{} faylida", fayl_yoli);
let tarkib = fs::read_to_string(fayl_yoli)
.expect("Faylni o'qiy olishi kerak edi");
println!("Fayl tarkibi:\n{tarkib}");
}
fn parse_config(args: &[String]) -> (&str, &str) {
let sorov = &args[1];
let fayl_yoli = &args[2];
(sorov, fayl_yoli)
}
Biz hali ham buyruq qatori argumentlarini vectorga yig‘moqdamiz, lekin 1-indeksdagi argument qiymatini sorov
o‘zgaruvchisiga va 2 indeksidagi argument qiymatini main
funksiyasi ichidagi fayl_yoli
o‘zgaruvchisiga belgilash o‘rniga, butun vectorni parse_config
funksiyasiga o‘tkazamiz. Keyin parse_config
funksiyasi qaysi argument qaysi o'zgaruvchiga kirishini aniqlaydigan mantiqni ushlab turadi va qiymatlarni main
ga qaytaradi. Biz hali ham sorov
va fayl_yoli
o'zgaruvchilarini main
da yaratamiz, lekin main
endi buyruq qatori argumentlari va o'zgaruvchilari qanday mos kelishini aniqlash vazifasiga ega emas.
Ushbu qayta ishlash bizning kichik dasturimiz uchun ortiqcha bo'lib tuyulishi mumkin, ammo biz kichik, bosqichma-bosqich refactoring qilmoqdamiz. Ushbu o'zgartirishni amalga oshirgandan so'ng, argumentni tahlil qilish hali ham ishlayotganligini tekshirish uchun dasturni qayta ishga tushiring. Muammolar yuzaga kelganda sabablarini aniqlashga yordam berish uchun taraqqiyotingizni tez-tez tekshirib turish yaxshidir.
Konfiguratsiya qiymatlarini guruhlash
parse_config
funksiyasini yanada yaxshilash uchun yana bir kichik qadam tashlashimiz mumkin.
Ayni paytda biz tupleni qaytarmoqdamiz, lekin keyin darhol bu tupleni yana alohida qismlarga ajratamiz. Bu, ehtimol, bizda hali to'g'ri mavhumlik yo'qligining belgisidir.
Yaxshilash uchun joy borligini ko'rsatadigan yana bir ko'rsatkich parse_config
ning config
qismidir, bu biz qaytaradigan ikkita qiymat bir-biriga bog'liqligini va ikkalasi ham bitta konfiguratsiya qiymatining bir qismi ekanligini anglatadi. Biz hozirda bu mantiqni ma'lumotlar strukturasida yetkazmayapmiz, bundan tashqari ikkita qiymatni tuplega guruhlash; Buning o'rniga biz ikkita qiymatni bitta strukturaga joylashtiramiz va har bir struktura maydoniga mazmunli nom beramiz. Buni qilish ushbu kodning kelajakdagi saqlovchilariga(maintainerlarga) turli qadriyatlar bir-biriga qanday bog'liqligini va ularning maqsadi nima ekanligini tushunishni osonlashtiradi.
12-6 ro'yxatda parse_config
funksiyasining yaxshilanishi ko'rsatilgan.
Fayl nomi: src/main.rs
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("{} qidirilmoqda", config.sorov);
println!("{} faylida", config.fayl_yoli);
let tarkib = fs::read_to_string(config.fayl_yoli)
.expect("Faylni o'qiy olishi kerak edi");
// --snip--
println!("Fayl tarkibi:\n{tarkib}");
}
struct Config {
sorov: String,
fayl_yoli: String,
}
fn parse_config(args: &[String]) -> Config {
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Config { sorov, fayl_yoli }
}
Biz sorov
va fayl_yoli
nomli maydonlarga ega bo'lishi uchun aniqlangan Config
nomli structi qo'shdik. Endi parse_config
signaturesi Config
qiymatini qaytarishini bildiradi. Biz args
dagi String
qiymatlariga reference qilingan satr bo‘laklarini qaytargan parse_config
korpusida endi Config
ga tegishli String
qiymatlarini o‘z ichiga olgan holda belgilaymiz. main
dagi args
oʻzgaruvchisi argument qiymatlarining owneri(ega) boʻlib, faqat parse_config
funksiyasiga ularni borrowga(qarz olish) ruxsat beradi, yaʼni Config
args
qiymatlariga ownership(egalik) qilmoqchi boʻlsa, Rustning borrowing(qarz olish) qoidalarini buzgan boʻlamiz.
String
ma'lumotlarini boshqarishning bir qancha usullari mavjud; Eng oson, garchi unchalik samarasiz bo'lsa ham, route qiymatlar bo'yicha clone
metodini chaqirishdir.
Bu Config
nusxasi uchun ma'lumotlarning to'liq nusxasini oladi, bu esa satr(string) ma'lumotlariga referenceni saqlashdan ko'ra ko'proq vaqt va xotirani oladi. Biroq, ma'lumotlarni klonlash bizning kodimizni juda sodda qiladi, chunki biz referencelarning lifetimeni(ishlash muddati) boshqarishimiz shart emas; bu holatda, soddalikka erishish uchun ozgina ishlashdan voz kechish foydali savdodir.
clone
dan foydalanishning o'zaro kelishuvlariKo'pgina Rustaceanlar orasida
clone
dan foydalanish vaqti xarajati tufayli ownership muammolarini hal qilish uchun foydalanmaslik tendentsiyasi mavjud. 13-bobda siz ushbu turdagi vaziyatda samaraliroq usullardan qanday foydalanishni o'rganasiz. Ammo hozircha rivojlanishni davom ettirish uchun bir nechta satrlarni nusxalash ma'qul, chunki siz bu nusxalarni faqat bir marta qilasiz va fayl yo'li va so'rovlar qatori juda kichik. Birinchi o'tishda kodni giperoptimallashtirishga urinishdan ko'ra, biroz samarasiz ishlaydigan dasturga ega bo'lish yaxshiroqdir. Rust bilan tajribangiz ortgan sayin, eng samarali yechimdan boshlash osonroq bo'ladi, ammo hozirchaclone
deb nomlash juda maqbuldir.
Biz main
ni yangiladik, shuning uchun u parse_config
tomonidan qaytarilgan Config
namunasini config
nomli o‘zgaruvchiga joylashtiradi va biz avval alohida sorov
va fayl_yoli
o‘zgaruvchilaridan foydalangan kodni yangiladik, shuning uchun u endi Config
strukturasidagi maydonlardan foydalanadi.
Endi bizning kodimiz sorov
va fayl_yoli
bir-biriga bog'liqligini va ularning maqsadi dastur qanday ishlashini sozlash ekanligini aniqroq bildiradi. Ushbu qiymatlardan foydalanadigan har qanday kod ularni maqsadlari uchun nomlangan maydonlardagi config
misolida topishni biladi.
Config
uchun konstruktor yaratish
Hozircha biz main
dan buyruq qatori argumentlarini tahlil qilish uchun javob beradigan mantiqni chiqarib oldik va uni parse_config
funksiyasiga joylashtirdik. Bu bizga sorov
va fayl_yoli
qiymatlari o'zaro bog'liqligini va bu munosabatlar bizning kodimizda ko'rsatilishi kerakligini ko'rishga yordam berdi. Keyin biz sorov
va fayl_yoli
ning tegishli maqsadini nomlash va parse_config
funksiyasidan qiymatlar nomlarini stuct maydoni nomi sifatida qaytarish uchun Config
structini qo'shdik.
Endi parse_config
funksiyasining maqsadi Config
misolini yaratish bo‘lganligi sababli, biz parse_config
ni oddiy funksiyadan Config
structi bilan bog'langan new
funksiyaga o‘zgartirishimiz mumkin. Ushbu o'zgarish kodni yanada idiomatik qiladi. Biz standart kutubxonada String
kabi turlarning namunalarini String::new
ni chaqirish orqali yaratishimiz mumkin. Xuddi shunday, parse_config
ni Config
bilan bog‘langan new
funksiyaga o‘zgartirib, Config::new
ni chaqirish orqali Config
misollarini yaratishimiz mumkin bo‘ladi. 12-7 ro'yxat biz qilishimiz kerak bo'lgan o'zgarishlarni ko'rsatadi.
Fayl nomi: src/main.rs
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("{} qidirilmoqda", config.sorov);
println!("{} faylida", config.fayl_yoli);
let tarkib = fs::read_to_string(config.fayl_yoli)
.expect("Faylni o'qiy olishi kerak edi");
println!("Fayl tarkibi:\n{tarkib}");
// --snip--
}
// --snip--
struct Config {
sorov: String,
fayl_yoli: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Config { sorov, fayl_yoli }
}
}
Biz parse_config
deb chaqirgan main
ni yangilab, Config::new
deb chaqirdik. Biz parse_config
nomini new
ga o‘zgartirdik va uni new
funksiyani Config
bilan bog‘laydigan impl
blokiga o‘tkazdik. Ishlayotganiga ishonch hosil qilish uchun ushbu kodni qayta kompilyatsiya qilib ko'ring.
Qayta ishlash xatolarini tuzatish
Endi biz xatolarimizni tuzatish ustida ishlaymiz. Eslatib o'tamiz, args
vectoridagi qiymatlarga 1 yoki indeks 2 da kirishga urinish vector uchtadan kam elementni o'z ichiga olgan bo'lsa, dastur panic paydo bo'ladi. Dasturni hech qanday argumentlarsiz ishga tushirishga harakat qiling; u shunday ko'rinadi:
$ 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
(indeks chegaradan tashqarida: len 1, lekin indeks 1) qatori dasturchilar uchun moʻljallangan xato xabaridir. Bu bizning oxirgi foydalanuvchilarga nima qilish kerakligini tushunishga yordam bermaydi. Keling, buni hozir tuzatamiz.
Xato xabarini yaxshilash
Ro'yxat 12-8da biz new
funksiyasiga chek qo'shamiz, bu 1 va 2 indekslarga kirishdan oldin bo'lakning yetarlicha uzunligini tasdiqlaydi. Agar bo'lak yetarlicha uzun bo'lmasa, dastur panic chiqaradi va yaxshiroq xato xabarini ko'rsatadi.
Fayl nomi: src/main.rs
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("{} qidirilmoqda", config.sorov);
println!("{} faylida", config.fayl_yoli);
let tarkib = fs::read_to_string(config.fayl_yoli)
.expect("Faylni o'qiy olishi kerak edi");
println!("Fayl tarkibi:\n{tarkib}");
}
struct Config {
sorov: String,
fayl_yoli: String,
}
impl Config {
// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("argumentlar yetarli emas");
}
// --snip--
let sorov= args[1].clone();
let fayl_yoli = args[2].clone();
Config { sorov, fayl_yoli }
}
}
Bu kod biz 9-13 roʻyxatda yozgan Taxmin::new
funksiyasiga oʻxshaydi, bu yerda qiymat
argumenti amaldagi qiymatlar oraligʻidan tashqarida boʻlganida panic!
deb chaqirdik. Bu yerda bir qator qiymatlar mavjudligini tekshirish o‘rniga, biz args
uzunligi kamida 3 ekanligini va funksiyaning qolgan qismi ushbu shart bajarilgan deb taxmin qilingan holda ishlashini tekshiramiz. Agar args
uchta elementdan kam boʻlsa, bu shart toʻgʻri boʻladi va dasturni darhol tugatish uchun panic!
makrosini chaqiramiz.
new
da qoʻshimcha bir necha qator kodlar mavjud boʻlsa, keling, xatolik qanday koʻrinishini koʻrish uchun dasturni argumentlarsiz yana ishga tushiramiz:
$ 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 'argumentlar yetarli emas', src/main.rs:26:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Bu chiqish yaxshiroq: endi bizda oqilona xato xabari bor. Biroq, bizda foydalanuvchilarga berishni istamaydigan begona ma'lumotlar ham bor. Ehtimol, biz 9-13 roʻyxatda qoʻllagan texnikamizdan foydalanish bu yerda eng yaxshisi emas: panic!
chaqiruvi 9-bobda muhokama qilinganidek, foydalanish muammosidan koʻra dasturlash muammosiga koʻproq mos keladi. Buning o'rniga biz 9-bobda o'rgangan boshqa texnikadan foydalanamiz - muvaffaqiyat yoki xatoni ko'rsatadigan Result
ni qaytarish.
panic!
o‘rniga Result
ni qaytarish
Buning o'rniga, muvaffaqiyatli holatda Config
misolini o'z ichiga olgan va xatolik holatida muammoni tasvirlaydigan Result
qiymatini qaytarishimiz mumkin. Shuningdek, biz funksiya nomini new
dan build
ga o'zgartiramiz, chunki ko'plab dasturchilar new
funksiyalar hech qachon ishlamay qolmasligini kutishadi. Config::build
main
bilan bog'langanda, muammo borligini bildirish uchun Result
turidan foydalanishimiz mumkin.Keyin biz main
ni Err
variantini panic!
chaqiruvi keltirib chiqaradigan thread 'main'
va RUST_BACKTRACE
haqidagi matnsiz foydalanuvchilarimiz uchun amaliyroq xatoga aylantirishimiz mumkin.
12-9 ro'yxatda biz hozir Config::build
deb nomlanayotgan funksiyaning qaytish(result) qiymatiga va Result
ni qaytarish uchun zarur bo'lgan funksiyaning tanasiga qilishimiz kerak bo'lgan o'zgarishlar ko'rsatilgan. E'tibor bering, biz main
ni ham yangilamagunimizcha, bu kompilyatsiya qilinmaydi, biz buni keyingi ro'yxatda qilamiz.
Fayl nomi: src/main.rs
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("{} qidirilmoqda", config.sorov);
println!("{} faylida", config.fayl_yoli);
let tarkib = fs::read_to_string(config.fayl_yoli)
.expect("Faylni o'qiy olishi kerak edi");
println!("Fayl tarkibi:\n{tarkib}");
}
struct Config {
sorov: String,
fayl_yoli: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
Bizning build
funksiyamiz muvaffaqiyatli holatda Config
misoli va xato holatida &'static str
bilan Result
ni qaytaradi. Bizning xato qiymatlarimiz har doim 'static
lifetimega ega bo'lgan satr harflari(string literal) bo'ladi. Biz funksiyaning asosiy qismiga ikkita o'zgartirish kiritdik: agar foydalanuvchi yetarli argumentlarni o'tkazmasa, panic!
deb chaqirish o'rniga, biz endi Err
qiymatini qaytaramiz va Config
qaytish(return) qiymatini OK
bilan o'rab oldik. Ushbu o'zgarishlar funksiyani yangi turdagi signaturega moslashtiradi.
Config::build
dan Err
qiymatini qaytarish main
funksiyaga build
funksiyasidan qaytarilgan Result
qiymatini boshqarish imkonini beradi va xato holatida jarayondan tozaroq chiqish imkonini beradi.
Config::build
ga murojaat qilish va xatolarni qayta ishlash
Xato holatini hal qilish va foydalanuvchi uchun qulay xabarni chop etish uchun biz 12-10 roʻyxatda koʻrsatilganidek, Config::build
tomonidan qaytariladigan Result
ni qayta ishlash uchun main
ni yangilashimiz kerak. Shuningdek, biz panic!
dan nolga teng bo‘lmagan xato kodi bilan buyruq qatori dasturidan chiqish va uning o‘rniga uni qo‘lda amalga oshirish mas’uliyatini o‘z zimmamizga olamiz. Nolga teng bo'lmagan chiqish holati - bu bizning dasturimizni chaqirgan jarayonga dastur xato holati bilan chiqqanligi haqida signal berish uchun konventsiya.
Fayl nomi: src/main.rs
use std::env;
use std::fs;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Argumentlarni tahlil qilish muammosi: {err}");
process::exit(1);
});
// --snip--
println!("{} qidirilmoqda", config.sorov);
println!("{} faylida", config.fayl_yoli);
let tarkib = fs::read_to_string(config.fayl_yoli)
.expect("Faylni o'qiy olishi kerak edi");
println!("Fayl tarkibi:\n{tarkib}");
}
struct Config {
sorov: String,
fayl_yoli: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
Ushbu ro'yxatda biz hali batafsil ko'rib chiqmagan metoddan foydalandik: standart kutubxona tomonidan Result<T, E>
da aniqlangan unwrap_or_else
.
unwrap_or_else
dan foydalanish bizga panic!
qo'ymaydigan xatoliklarni aniqlash imkonini beradi. Agar Result
Ok
qiymati bo'lsa, bu metodning harakati unwrap
ga o'xshaydi: u Ok
o'ralayotgan(wrap) ichki qiymatni qaytaradi. Biroq, agar qiymat Err
qiymati bo'lsa, bu metod kodni closure(yopish) ga chaqiradi, bu biz belgilab beradigan anonim funksiya bo'lib, unwrap_or_else
ga argument sifatida o'tkazamiz. Biz 13-bobda closure(yopilish)larni batafsil ko'rib chiqamiz. Hozircha siz shuni bilishingiz kerakki, unwrap_or_else
Err
ning ichki qiymatidan o‘tadi, bu holda biz 12-9-listga qo‘shgan "argumentlar yetarli emas"
statik qatori bo‘lib, bizning yopishimiz uchun Vertikal quvurlar(pipe) o'rtasida paydo bo'ladigan Err
argumenti. Yopishdagi(closure) kod ishlayotganida err
qiymatidan foydalanishi mumkin.
Biz standart kutubxonadan process
ni qamrab olish uchun yangi use
qatorini qo‘shdik. Xato holatida ishga tushiriladigan yopishdagi kod faqat ikkita qatordan iborat: biz err
qiymatini chop qilamiz va keyin process::exit
ni chaqiramiz. process::exit
funksiyasi dasturni darhol to'xtatadi va chiqish holati kodi sifatida berilgan raqamni qaytaradi. Bu biz 12-8 roʻyxatda qoʻllagan panic!
asosidagi ishlovga oʻxshaydi, ammo biz endi barcha qoʻshimcha natijalarni olmaymiz. Keling, sinab ko'raylik:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/minigrep`
Argumentlarni tahlil qilish muammosi: argumentlar yetarli emas
Ajoyib! Ushbu chiqish bizning foydalanuvchilarimiz uchun juda qulay.
main
dan mantiqni ajratib olish
Endi biz konfiguratsiyani tahlil qilishni qayta tiklashni tugatdik, keling, dastur mantig'iga murojaat qilaylik. "Binary loyihalar uchun vazifalarni ajratish" da aytib o'tganimizdek, biz konfiguratsiyani o'rnatish yoki xatolarni qayta ishlash bilan bog'liq bo'lmagan main
funksiyadagi barcha mantiqni ushlab turadigan run
nomli funksiyani chiqaramiz. Ishimiz tugagach, main
qisqa va tekshirish orqali tekshirish oson bo'ladi va biz boshqa barcha mantiqlar uchun testlarni yozishimiz mumkin bo'ladi.
12-11 ro'yxatda ajratilgan run
funksiyasi ko'rsatilgan. Hozircha biz funksiyani chiqarishni kichik, bosqichma-bosqich yaxshilashni amalga oshirmoqdamiz. Biz hali ham src/main.rs da funksiyani aniqlayapmiz.
Fayl nomi: 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::build(&args).unwrap_or_else(|err| {
println!("Argumentlarni tahlil qilish muammosi: {err}");
process::exit(1);
});
println!("{} qidirilmoqda", config.sorov);
println!("{} faylida", config.fayl_yoli);
run(config);
}
fn run(config: Config) {
let tarkib = fs::read_to_string(config.fayl_yoli)
.expect("Faylni o'qiy olishi kerak edi");
println!("Fayl tarkibi:\n{tarkib}");
}
// --snip--
struct Config {
sorov: String,
fayl_yoli: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
run
funksiyasi endi faylni o‘qishdan boshlab main
dan qolgan barcha mantiqni o‘z ichiga oladi. run
funksiyasi argument sifatida Config
misolini oladi.
run
funksiyasidan xatolarni qaytarish(return)
Qolgan dastur mantigʻi run
funksiyasiga ajratilgan boʻlsa, biz 12 9-ro'yxatdagi Config::build
bilan qilganimiz kabi, xatolarni boshqarishni yaxshilashimiz mumkin. Dasturni expect
deb chaqirish orqali panic qo‘yish o‘rniga, run
funksiyasi biror narsa noto‘g‘ri ketganda Result<T, E>
ni qaytaradi. Bu bizga foydalanuvchilarga qulay tarzda xatolarni main
ga qayta ishlash mantig'ini yanada mustahkamlash imkonini beradi. 12-12 roʻyxatda run
signaturesi va asosiy qismiga qilishimiz kerak boʻlgan oʻzgarishlar koʻrsatilgan.
Fayl nomi: 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::build(&args).unwrap_or_else(|err| {
println!("Argumentlarni tahlil qilish muammosi: {err}");
process::exit(1);
});
println!("{} qidirilmoqda", config.sorov);
println!("{} faylida", config.fayl_yoli);
run(config);
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let tarkib = fs::read_to_string(config.fayl_yoli)?;
println!("Fayl tarkibi:\n{tarkib}");
Ok(())
}
struct Config {
sorov: String,
fayl_yoli: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
Biz bu yerda uchta muhim o'zgarishlarni amalga oshirdik. Birinchidan, biz run
funksiyasining qaytish turini Result<(), Box<dyn Error>>
ga o'zgartirdik. Bu funksiya avval birlik(binary) turini qaytardi, ()
va biz buni Ok
holatida qaytarilgan qiymat sifatida saqlaymiz.
Xato turi uchun biz trait obyekti Box<dyn Error>
dan foydalandik (va biz std::error::Error
ni yuqori qismida use
statementi bilan qamrab oldik). Biz 17-bobda trait objectlarni ko'rib chiqamiz. Hozircha shuni bilingki, Box<dyn Error>
funksiya Error
traitini amalga oshiradigan turni qaytarishini bildiradi, lekin qaytariladigan qiymatning qaysi turini belgilashimiz shart emas. Bu bizga turli xil xato holatlarida har xil turdagi xato qiymatlarini qaytarish uchun moslashuvchanlikni beradi. dyn
kalit so'zi(keywordi) "dynamic(dinamik)" so'zining qisqartmasi.
Ikkinchidan, biz 9-bobda aytib o'tganimizdek, ?
operatori foydasiga expect
chaqiruvini olib tashladik. Xatoda panic!
o‘rniga, ?
murojat qiluvchiga ishlov berish uchun joriy funksiyadan xato qiymatini qaytaradi.
Uchinchidan, run
funksiyasi endi muvaffaqiyatli holatda Ok
qiymatini qaytaradi.
Biz signatureda run
funksiyasining muvaffaqiyat turini ()
deb e’lon qildik, ya’ni birlik turi qiymatini Ok
qiymatiga o‘rashimiz(wrap) kerak. Bu Ok(())
sintaksisi dastlab biroz g‘alati ko‘rinishi mumkin, ammo ()
dan foydalanish biz run
ni faqat uning yon ta’siri uchun chaqirayotganimizni bildirishning idiomatik usulidir; u bizga kerakli qiymatni qaytarmaydi.
Ushbu kodni ishga tushirganingizda, u kompilyatsiya qilinadi, lekin ogohlantirishni ko'rsatadi:
$ cargo run men olma.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: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
warning: `minigrep` (bin "minigrep") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.71s
Running `target/debug/minigrep the poem.txt`
men qidirilmoqda
olma.txt faylida
Fayl tarkibi:
Tanishaylik, men - olma,
Nomimga quloq solma.
Olaver, ikiklanmay,
Ishtahang bo'lsin karnay
Reklamaga hojat yo'q
Ta'mim rosa yoqimli.
Ortganini quritsang,
Qishda yeysan qoqimni
Men sizlarni olmangiz,
Xomligimda olmangiz!
Asilbekga o'xshab so'ng,
Voy qornim deb qolmangiz!
Bog'larda chiroymanda,
Vitaminga boymanda!
Pishganimda yemasangiz,
Qolasizda armonda!
Rust bizga kodimiz Result
qiymatini e'tiborsiz qoldirganligini va Result
qiymati xatolik yuz berganligini ko'rsatishi mumkinligini aytadi. Ammo biz xatolik bor yoki yo'qligini tekshirmayapmiz va kompilyator bu yerda xatoliklarni hal qilish uchun kodga ega bo'lishimiz kerakligini eslatadi! Keling, bu muammoni hozir tuzatamiz.
main
dagi run
dan qaytarilgan xatolarni qayta ishlash
Biz xatolarni tekshirib ko'ramiz va ularni 12-10-sonli ro'yxatdagi Config::build
bilan ishlatganimizga o'xshash metod yordamida hal qilamiz, lekin bir oz farq bilan:
Filename: 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::build(&args).unwrap_or_else(|err| {
println!("Argumentlarni tahlil qilish muammosi: {err}");
process::exit(1);
});
println!("{} qidirilmoqda", config.sorov);
println!("{} faylida", config.fayl_yoli);
if let Err(e) = run(config) {
println!("Dastur xatosi: {e}");
process::exit(1);
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let tarkib = fs::read_to_string(config.fayl_yoli)?;
println!("Fayl tarkibi:\n{tarkib}");
Ok(())
}
struct Config {
sorov: String,
fayl_yoli: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
run
Err
qiymatini qaytaradimi yoki yo‘qligini tekshirish uchun unwrap_or_else
o‘rniga if let
dan foydalanamiz va agar qaytarsa process::exit(1)
ni chaqiramiz. run
funksiyasi Config::build
Config
misolini qaytarganidek, biz unwrap
ni xohlagan qiymatni qaytarmaydi. Muvaffaqiyatli holatda run
()
ni qaytargani uchun biz faqat xatoni aniqlash haqida qayg'uramiz, shuning uchun o'ralgan(wrap) qiymatni qaytarish uchun unwrap_or_else
shart emas, bu faqat ()
bo`ladi.
if let
va unwrap_or_else
funksiyalarining tanasi ikkala holatda ham bir xil: biz xatoni chop qilamiz va chiqamiz.
Kodni kutubxona(library) cratesiga bo'lish
Bizning minigrep
loyihamiz hozircha yaxshi ko'rinmoqda! Endi biz src/main.rs faylini ajratamiz va src/lib.rs fayliga bir nechta kodni joylashtiramiz. Shunday qilib, biz kodni sinab ko'rishimiz va kamroq mas'uliyatli src/main.rs fayliga ega bo'lishimiz mumkin.
Keling, main
funksiya bo'lmagan barcha kodlarni src/main.rs dan src/lib.rs ga o'tkazamiz:
run
funksiyasi definitioni- Tegishli
use
statementlari Config
ning definitioniConfig::build
definitioni
src/lib.rs ning mazmuni 12-13 roʻyxatda koʻrsatilgan signaturelarga ega boʻlishi kerak (qisqalik uchun funksiyalarning qismlarini olib tashladik). E'tibor bering, biz 12-14 ro'yxatdagi src/main.rs ni o'zgartirmagunimizcha, bu kompilyatsiya qilinmaydi.
Fayl nomi: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub sorov: String,
pub fayl_yoli: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
// --snip--
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
// --snip--
let tarkib = fs::read_to_string(config.fayl_yoli)?;
println!("Fayl tarkibi:\n{tarkib}");
Ok(())
}
Biz pub
kalit so‘zidan erkin foydalandik: Config
da, uning maydonlari va build
metodida va run
funksiyasida. Endi bizda testdan o'tkazishimiz mumkin bo'lgan ommaviy(public) API mavjud kutubxona cratesi bor!
Endi biz src/lib.rs ga ko'chirilgan kodni 12-14 ro'yxatda ko'rsatilganidek src/main.rs dagi binary crate doirasiga olib kirishimiz kerak.
Fayl nomi: 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::build(&args).unwrap_or_else(|err| {
println!("Argumentlarni tahlil qilish muammosi: {err}");
process::exit(1);
});
println!("{} qidirilmoqda", config.sorov);
println!("{} faylida", config.fayl_yoli);
if let Err(e) = minigrep::run(config) {
// --snip--
println!("Dastur xatosi: {e}");
process::exit(1);
}
}
Kutubxona cratesidan Config
turini binary crate ko'lamiga olib kirish uchun use minigrep::Config
qatorini qo'shamiz va run
funksiyasiga crate nomimiz bilan prefix qo'shamiz. Endi barcha funksiyalar ulanishi va ishlashi kerak. Dasturni cargo run
bilan ishga tushiring va hamma narsa to'g'ri ishlashiga ishonch hosil qiling.
Vouv! Bu juda ko'p ish edi, lekin biz kelajakda muvaffaqiyatga erishdik. Endi xatolarni hal qilish ancha oson va biz kodni modulliroq qildik. Deyarli barcha ishlarimiz bundan buyon src/lib.rs da amalga oshiriladi.
Keling, eski kod bilan qiyin bo'lgan, ammo yangi kod bilan oson bo'lgan narsani qilish orqali ushbu yangi modullikdan foydalanaylik: biz bir nechta testlarni yozamiz!
Testga asoslangan ishlab chiqish bilan kutubxonaning funksionalligini rivojlantirish
Endi biz mantiqni src/lib.rs ga chiqardik va argumentlarni yig‘ish va xatolarni qayta ishlashni src/main.rs da qoldirdik, kodimizning asosiy funksionalligi uchun testlarni yozish ancha osonlashdi. Biz turli xil argumentlar bilan funksiyalarni to'g'ridan-to'g'ri chaqirishimiz va buyruq satridan binaryga murojaat qilmasdan qaytish(return) qiymatlarini tekshirishimiz mumkin.
Ushbu bo'limda biz quyidagi bosqichlar bilan test-driven development (TDD) jarayonidan foydalangan holda minigrep
dasturiga qidiruv mantig'ini qo'shamiz:
- Muvaffaqiyatsiz bo'lgan testni yozing va siz kutgan sabab tufayli muvaffaqiyatsiz bo'lishiga ishonch hosil qilish uchun uni ishga tushiring.
- Yangi testdan o'tish uchun yetarli kodni yozing yoki o'zgartiring.
- Siz qo'shgan yoki o'zgartirgan kodni qayta tiklang(refaktoring) va testlar o'tishda davom etayotganiga ishonch hosil qiling.
- Repeat from step 1!
Garchi bu dasturiy ta'minotni yozishning ko'p usullaridan biri bo'lsa-da, TDD kod dizaynini boshqarishga yordam beradi. Testdan o'tishni ta'minlaydigan kodni yozishdan oldin testni yozish jarayon davomida yuqori sinov qamrovini saqlashga yordam beradi.
Biz fayl tarkibidagi so'rovlar qatorini qidirishni amalga oshiradigan va so'rovga mos keladigan qatorlar ro'yxatini tuzadigan funksiyani amalga oshirishni sinovdan o'tkazamiz. Biz bu funksiyani qidiruv
funksiyasiga qo‘shamiz.
Muvaffaqiyatsiz test yozish
Bizga endi ular kerak emasligi sababli, dasturning harakatini tekshirish uchun foydalanilgan src/lib.rs va src/main.rs dan println!
statementlarini olib tashlaymiz. Keyin, src/lib.rs da, 11-bobda qilganimizdek, test funksiyasiga ega tests
modulini qo'shing. Test funksiyasi biz qidirish
funksiyasiga ega bo'lishini xohlagan xatti-harakatni belgilaydi: u so'rov va izlash uchun matnni oladi va u so'rovni o'z ichiga olgan matndan faqat satrlarni qaytaradi. 12-15 ro'yxatda ushbu test ko'rsatilgan, u hali kompilyatsiya bo'lmaydi.
Fayl nomi: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub sorov: String,
pub fayl_yoli: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let tarkib = fs::read_to_string(config.fayl_yoli)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn birinchi_natija() {
let sorov = "marali";
let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.";
assert_eq!(vec!["xavfsiz, tez, samarali."], qidiruv(sorov, tarkib));
}
}
Bu test marali
qatorini qidiradi.Biz izlayotgan matn uchta qatordan iborat bo‘lib, ulardan faqat bittasi marali
ni o‘z ichiga oladi (E’tibor bering, qo‘sh qo‘shtirnoqning ochilishidan keyingi teskari chiziq Rustga ushbu satr literalining boshiga yangi qator belgisini qo‘ymaslikni bildiradi). qidiruv
funksiyasidan qaytarilgan qiymat faqat biz kutgan qatorni o'z ichiga oladi, deb ta'kidlaymiz.
Biz hali bu testni bajara olmaymiz va uning muvaffaqiyatsizligini kuzata olmaymiz, chunki test hatto kompilyatsiya ham qilmaydi: qidiruv
funksiyasi hali mavjud emas! TDD tamoyillariga muvofiq, biz 12-16 roʻyxatda koʻrsatilganidek, har doim boʻsh vektorni qaytaruvchi qidiruv
funksiyasining definitionni qoʻshish orqali testni kompilyatsiya qilish va ishga tushirish uchun yetarli kodni qoʻshamiz. Keyin test kompilyatsiya qilinishi va muvaffaqiyatsiz bo'lishi kerak, chunki bo'sh vektor "xavfsiz, tez, samarali."
qatorini o'z ichiga olgan vektorga mos kelmaydi.
Fayl nomi: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub sorov: String,
pub fayl_yoli: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let tarkib = fs::read_to_string(config.fayl_yoli)?;
Ok(())
}
pub fn qidiruv<'a>(sorov: &str, tarkib: &'a str) -> Vec<&'a str> {
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn birinchi_natija() {
let sorov = "marali";
let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.";
assert_eq!(vec!["xavfsiz, tez, samarali."], qidiruv(sorov, tarkib));
}
}
E'tibor bering, biz qidiruv
signaturesida 'a
aniq lifetimeni belgilashimiz va bu lifetimeni tarkib
argumenti va qaytarish(return) qiymati bilan ishlatishimiz kerak. 10-bobda esda tutingki, lifetime parametrlari qaysi argumentning lifetime(ishlash muddati) qaytariladigan qiymatning lifetime bilan bog'liqligini belgilaydi. Bunday holda, qaytarilgan vektorda tarkib
argumentining bo'laklariga (sorov
argumenti o'rniga) reference qiluvchi string bo'laklari bo'lishi kerakligini ko'rsatamiz.
Boshqacha qilib aytganda, biz Rustga aytamizki, qidiruv
funksiyasi tomonidan qaytarilgan maʼlumotlar tarkib
argumentida qidiruv
funksiyasiga oʻtgan maʼlumotlar shuncha vaqtgacha yashaydi. Bu muhim! Murojaatlar haqiqiy bo'lishi uchun bo'laklar(slice) bo'yicha reference qilingan ma'lumotlar ham haqiqiy bo'lishi kerak; agar kompilyator biz tarkib
emas, balki sorov
ning satr bo'laklarini(string slice) yaratmoqda deb hisoblasa, u xavfsizlik tekshiruvini noto'g'ri bajaradi.
Agar biz lifetime izohlarni(annotation) unutib, ushbu funksiyani kompilyatsiya qilishga harakat qilsak, biz ushbu xatoni olamiz:
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:29:50
|
29 | pub fn qidiruv(sorov: &str, tarkib: &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 `sorov` or `tarkib`
help: consider introducing a named lifetime parameter
|
29 | pub fn qidiruv<'a>(sorov: &'a str, tarkib: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to previous error
warning: build failed, waiting for other jobs to finish...
error: could not compile `minigrep` (lib test) due to previous error
Rust bizga ikkita argumenning qaysi biri kerakligini bila olmaydi, shuning uchun biz buni aniq aytishimiz kerak. tarkib
barcha matnimizni o'z ichiga olgan argument bo'lgani uchun va biz ushbu matnning mos keladigan qismlarini qaytarmoqchi bo'lganimiz sababli, biz tarkib
lifetime sintaksisi yordamida qaytarish qiymatiga ulanishi kerak bo'lgan argument ekanligini bilamiz.
Boshqa dasturlash tillari signaturedagi qiymatlarni qaytarish uchun argumentlarni ulashni talab qilmaydi, ammo bu amaliyot vaqt o'tishi bilan osonlashadi. Siz ushbu misolni 10-bobdagi “Ma’lumotnomalarni lifetime bilan tekshirish” bo‘limi bilan solishtirishingiz mumkin.
Endi testni bajaramiz:
$ 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 src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 1 test
test tests::birinchi_natija ... FAILED
successes:
successes:
failures:
---- tests::birinchi_natija stdout ----
thread 'tests::birinchi_natija' panicked at 'assertion failed: `(left == right)`
left: `["xavfsiz, tez, samarali."]`,
right: `[]`', src/lib.rs:46:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::birinchi_natija
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`
Ajoyib, test biz kutganimizdek muvaffaqiyatsiz tugadi. Keling, testdan o'tamiz!
Testdan o'tish uchun kod yozish
Hozirda testimiz muvaffaqiyatsiz tugadi, chunki biz har doim bo'sh vektorni qaytaramiz. Buni tuzatish va qidiruv
ni amalga oshirish uchun dasturimiz quyidagi bosqichlarni bajarishi kerak:
tarkib
ning har bir satrini takrorlang.- Berilgan satrda siz izlayotgan qator mavjudligini tekshiring.
- Agar shunday bo'lsa, uni biz qaytaradigan qiymatlar ro'yxatiga qo'shing.
- Agar bunday bo'lmasa, hech narsa qilmang.
- Mos keladigan natijalar ro'yxatini qaytaring.
Keling, satrlarni takrorlashdan boshlab, har bir bosqichda ishlaylik.
lines
metodi bilan qatorlar bo'ylab takrorlash
Rust 12-17 ro'yxatda ko'rsatilganidek, qulay tarzda lines
deb nomlangan satrlarni qatorma-qator takrorlash uchun foydali metodga ega. E'tibor bering, bu hali kompilyatsiya qilinmaydi.
Fayl nomi: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub sorov: String,
pub fayl_yoli: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let tarkib = fs::read_to_string(config.fayl_yoli)?;
Ok(())
}
pub fn qidiruv<'a>(sorov: &str, tarkib: &'a str) -> Vec<&'a str> {
for line in tarkib.lines() {
// do something with line
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn birinchi_natija() {
let sorov = "marali";
let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.";
assert_eq!(vec!["xavfsiz, tez, samarali."], qidiruv(sorov, tarkib));
}
}
lines
metodi iteratorni qaytaradi.Biz iteratorlar haqida 13-bobda chuqurroq gaplashamiz, lekin esda tutingki, siz iteratordan foydalanishning bunday usulini 3-5-ro'yxatda ko'rgansiz, bu yerda biz to'plamdagi har bir elementda ba'zi kodlarni ishlatish uchun iterator bilan for
siklidan foydalanganmiz.
So'rov uchun har bir qatorni qidirish
Keyinchalik, joriy qatorda so'rovlar qatori mavjudligini tekshiramiz. Yaxshiyamki, satrlarda biz uchun buni amalga oshiradigan contains
deb nomlangan foydali metod mavjud! 12-18 roʻyxatda koʻrsatilganidek, qidiruv
funksiyasidagi contains
metodiga murojatni qoʻshing. E'tibor bering, bu hali kompilyatsiya qilinmaydi.
Fayl nomi: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub sorov: String,
pub fayl_yoli: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let tarkib = fs::read_to_string(config.fayl_yoli)?;
Ok(())
}
pub fn qidiruv<'a>(sorov: &str, tarkib: &'a str) -> Vec<&'a str> {
for line in tarkib.lines() {
if line.contains(sorov) {
// do something with line
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn birinchi_natija() {
let sorov = "marali";
let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.";
assert_eq!(vec!["xavfsiz, tez, samarali."], qidiruv(sorov, tarkib));
}
}
Ayni paytda biz funksionallikni yaratmoqdamiz. Uni kompilyatsiya qilish uchun biz funksiya signaturesida ko'rsatganimizdek, tanadan qiymatni qaytarishimiz kerak.
Mos keladigan qatorlarni saqlash
Ushbu funksiyani tugatish uchun bizga qaytarmoqchi bo'lgan mos keladigan satrlarni saqlash metodi kerak. Buning uchun biz for
siklidan oldin o'zgaruvchan vector yasashimiz va vectorda line
ni saqlash uchun push
metodini chaqirishimiz mumkin. for
siklidan so'ng, 12-19 ro'yxatda ko'rsatilganidek, vectorni qaytaramiz.
Fayl nomi: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub sorov: String,
pub fayl_yoli: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let tarkib = fs::read_to_string(config.fayl_yoli)?;
Ok(())
}
pub fn qidiruv<'a>(sorov: &str, tarkib: &'a str) -> Vec<&'a str> {
let mut natijalar = Vec::new();
for line in tarkib.lines() {
if line.contains(sorov) {
natijalar.push(line);
}
}
natijalar
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn birinchi_natija() {
let sorov = "marali";
let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.";
assert_eq!(vec!["xavfsiz, tez, samarali."], qidiruv(sorov, tarkib));
}
}
Endi qidiruv
funksiyasi faqat sorov
ni o'z ichiga olgan qatorlarni qaytarishi kerak va bizning testimiz o'tishi kerak. Keling, testni bajaramiz:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 0.37s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::birinchi_natija ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-54f36c611e701f9d)
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 muvaffaqiyatli o'tdi, shuning uchun u ishlayotganini bilamiz!
Shu nuqtada, biz bir xil funksionallikni saqlab qolish uchun testlarni o'tkazgan holda qidiruv funksiyasini amalga oshirishni qayta tiklash imkoniyatlarini ko'rib chiqishimiz mumkin. Qidiruv funksiyasidagi kod juda yomon emas, lekin u iteratorlarning ba'zi foydali xususiyatlaridan foydalanmaydi. Biz 13-bobda ushbu misolga qaytamiz, u yerda iteratorlarni batafsil o'rganamiz va uni qanday yaxshilashni ko'rib chiqamiz.
run
funksiyasidagi qidiruv
funksiyasidan foydalanish
Endi qidiruv
funksiyasi ishlayotgan va testdan o‘tgan bo‘lsa, run
funksiyamizdan qidiruv
ni chaqirishimiz kerak. Biz config.sorov
qiymatini va fayldan o'qiydigan tarkib
-ni qidiruv
funksiyasiga o'tkazishimiz kerak. Keyin run
qidiruv
dan qaytarilgan har bir qatorni chop etadi:
Fayl nomi: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub sorov: String,
pub fayl_yoli: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let tarkib = fs::read_to_string(config.fayl_yoli)?;
for line in qidiruv(&config.sorov, &tarkib) {
println!("{line}");
}
Ok(())
}
pub fn qidiruv<'a>(sorov: &str, tarkib: &'a str) -> Vec<&'a str> {
let mut natijalar = Vec::new();
for line in tarkib.lines() {
if line.contains(sorov) {
natijalar.push(line);
}
}
natijalar
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn birinchi_natija() {
let sorov = "marali";
let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.";
assert_eq!(vec!["xavfsiz, tez, samarali."], qidiruv(sorov, tarkib));
}
}
Biz qidiruv
dan har bir qatorni qaytarish va uni chop etish uchun for
siklidan foydalanmoqdamiz.
Endi butun dastur ishlashi kerak! Keling, buni sinab ko'raylik, avval Olma she'ridagi "karnay" ning aynan bir satrini qaytarishi kerak bo'lgan so'z bilan:
$ cargo run -- karnay olma.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/minigrep karnay olma.txt`
Ishtahang bo'lsin karnay
Ajoyib! Keling, bir nechta qatorga mos keladigan so'zni sinab ko'raylik, masalan, "olma":
$ cargo run -- olma olma.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep olma olma.txt`
Tanishaylik, men - olma,
Nomimga quloq solma.
Men sizlarni olmangiz,
Xomligimda olmangiz!
Voy qornim deb qolmangiz!
Va nihoyat, she’rning hech bir joyida bo‘lmagan so‘zni izlaganimizda, masalan, “mashina” kabi satrlar chiqmasligiga ishonch hosil qilaylik:
$ cargo run -- mashina olma.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/minigrep mashina olma.txt`
Ajoyib! Biz klassik dasturning o'z mini versiyasini yaratdik va ilovalarni qanday tuzish haqida ko'p narsalarni o'rgandik. Shuningdek, biz faylni kiritish(input) va chiqarish(output), lifetime, test va buyruq satrini tahlil qilish haqida bir oz o'rgandik.
Ushbu loyihani yakunlash uchun biz atrof-muhit(environment) o'zgaruvchilari bilan qanday ishlashni va standart xatoga qanday chop etishni qisqacha ko'rsatamiz, bu ikkalasi ham buyruq qatori dasturlarini yozishda foydalidir..
Environment(atrof-muhit) o'zgaruvchilari bilan ishlash
Biz minigrep
ni qo‘shimcha xususiyatni qo‘shish orqali yaxshilaymiz: foydalanuvchi environment orqali yoqishi mumkin bo‘lgan katta-kichik harflarni hisobga olmay qidirish imkoniyati.Biz bu xususiyatni buyruq qatori opsiyasiga aylantirishimiz va foydalanuvchilar uni har safar qo‘llashni xohlaganlarida kiritishlarini talab qilishimiz mumkin, lekin buning oʻrniga uni environment qilib, biz foydalanuvchilarga environmentni bir marta oʻrnatish va barcha qidiruvlarini terminal sessiyasida katta-kichik harflarga sezgir boʻlmasligiga ruxsat beramiz.
Katta-kichik harflarni sezmaydigan qidiruv
funksiyasi uchun muvaffaqiyatsiz test yozish
Biz birinchi navbatda yangi harflarga_etiborsiz_qidirish
funksiyasini qo'shamiz, u muhit o'zgaruvchisi qiymatga ega bo'lganda chaqiriladi. Biz TDD jarayonini kuzatishda davom etamiz, shuning uchun birinchi qadam yana muvaffaqiyatsiz testni yozishdir. Biz yangi harflarga_etiborsiz_qidirish
funksiyasi uchun yangi test qo‘shamiz va 12-20 ro‘yxatda ko‘rsatilganidek, ikkita test o‘rtasidagi farqlarni aniqlashtirish uchun eski testimiz nomini birinchi_natija
harflarga_etiborli
ga o‘zgartiramiz.
Fayl nomi: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub sorov : String,
pub fayl_yoli: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov , fayl_yoli })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let tarkib = fs::read_to_string(config.fayl_yoli)?;
for line in qidiruv(&config.sorov , &tarkib) {
println!("{line}");
}
Ok(())
}
pub fn qidiruv<'a>(sorov : &str, tarkib: &'a str) -> Vec<&'a str> {
let mut natijalar = Vec::new();
for line in tarkib.lines() {
if line.contains(sorov ) {
natijalar.push(line);
}
}
natijalar
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn harflarga_etiborli() {
let sorov = "marali";
let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.
Duct tape.";
assert_eq!(vec!["xavfsiz, tez, samarali."], qidiruv(sorov , tarkib));
}
#[test]
fn harflarga_etiborsiz() {
let sorov = "rUsT";
let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.
Menga ishoning.";
assert_eq!(
vec!["Rust:", "Menga ishoning."],
harflarga_etiborsiz_qidirish(sorov , tarkib)
);
}
}
Note that we’ve edited the old test’s contents
too. We’ve added a new line
with the text "Duct tape."
using a capital D that shouldn’t match the query
"duct"
when we’re searching in a case-sensitive manner. Changing the old test
in this way helps ensure that we don’t accidentally break the case-sensitive
search functionality that we’ve already implemented. This test should pass now
and should continue to pass as we work on the case-insensitive search.
The new test for the case-insensitive search uses "rUsT"
as its query. In
the search_case_insensitive
function we’re about to add, the query "rUsT"
should match the line containing "Rust:"
with a capital R and match the line
"Trust me."
even though both have different casing from the query. This is
our failing test, and it will fail to compile because we haven’t yet defined
the search_case_insensitive
function. Feel free to add a skeleton
implementation that always returns an empty vector, similar to the way we did
for the search
function in Listing 12-16 to see the test compile and fail.
Implementing the search_case_insensitive
Function
The search_case_insensitive
function, shown in Listing 12-21, will be almost
the same as the search
function. The only difference is that we’ll lowercase
the query
and each line
so whatever the case of the input arguments,
they’ll be the same case when we check whether the line contains the query.
Filename: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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)
);
}
}
First, we lowercase the query
string and store it in a shadowed variable with
the same name. Calling to_lowercase
on the query is necessary so no
matter whether the user’s query is "rust"
, "RUST"
, "Rust"
, or "rUsT"
,
we’ll treat the query as if it were "rust"
and be insensitive to the case.
While to_lowercase
will handle basic Unicode, it won’t be 100% accurate. If
we were writing a real application, we’d want to do a bit more work here, but
this section is about environment variables, not Unicode, so we’ll leave it at
that here.
Note that query
is now a String
rather than a string slice, because calling
to_lowercase
creates new data rather than referencing existing data. Say the
query is "rUsT"
, as an example: that string slice doesn’t contain a lowercase
u
or t
for us to use, so we have to allocate a new String
containing
"rust"
. When we pass query
as an argument to the contains
method now, we
need to add an ampersand because the signature of contains
is defined to take
a string slice.
Next, we add a call to to_lowercase
on each line
to lowercase all
characters. Now that we’ve converted line
and query
to lowercase, we’ll
find matches no matter what the case of the query is.
Let’s see if this implementation passes the tests:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 1.33s
Running unittests src/lib.rs (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 src/main.rs (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
Great! They passed. Now, let’s call the new search_case_insensitive
function
from the run
function. First, we’ll add a configuration option to the
Config
struct to switch between case-sensitive and case-insensitive search.
Adding this field will cause compiler errors because we aren’t initializing
this field anywhere yet:
Filename: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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)
);
}
}
We added the ignore_case
field that holds a Boolean. Next, we need the run
function to check the ignore_case
field’s value and use that to decide
whether to call the search
function or the search_case_insensitive
function, as shown in Listing 12-22. This still won’t compile yet.
Filename: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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)
);
}
}
Finally, we need to check for the environment variable. The functions for
working with environment variables are in the env
module in the standard
library, so we bring that module into scope at the top of src/lib.rs. Then
we’ll use the var
function from the env
module to check to see if any value
has been set for an environment variable named IGNORE_CASE
, as shown in
Listing 12-23.
Filename: src/lib.rs
use std::env;
// --snip--
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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)
);
}
}
Here, we create a new variable ignore_case
. To set its value, we call the
env::var
function and pass it the name of the IGNORE_CASE
environment
variable. The env::var
function returns a Result
that will be the
successful Ok
variant that contains the value of the environment variable if
the environment variable is set to any value. It will return the Err
variant
if the environment variable is not set.
We’re using the is_ok
method on the Result
to check whether the environment
variable is set, which means the program should do a case-insensitive search.
If the IGNORE_CASE
environment variable isn’t set to anything, is_ok
will
return false and the program will perform a case-sensitive search. We don’t
care about the value of the environment variable, just whether it’s set or
unset, so we’re checking is_ok
rather than using unwrap
, expect
, or any
of the other methods we’ve seen on Result
.
We pass the value in the ignore_case
variable to the Config
instance so the
run
function can read that value and decide whether to call
search_case_insensitive
or search
, as we implemented in Listing 12-22.
Let’s give it a try! First, we’ll run our program without the environment
variable set and with the query to
, which should match any line that contains
the word “to” in all lowercase:
$ 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!
Looks like that still works! Now, let’s run the program with IGNORE_CASE
set to 1
but with the same query to
.
$ IGNORE_CASE=1 cargo run -- to poem.txt
If you’re using PowerShell, you will need to set the environment variable and run the program as separate commands:
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
This will make IGNORE_CASE
persist for the remainder of your shell
session. It can be unset with the Remove-Item
cmdlet:
PS> Remove-Item Env:IGNORE_CASE
We should get lines that contain “to” that might have uppercase letters:
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
Excellent, we also got lines containing “To”! Our minigrep
program can now do
case-insensitive searching controlled by an environment variable. Now you know
how to manage options set using either command line arguments or environment
variables.
Some programs allow arguments and environment variables for the same configuration. In those cases, the programs decide that one or the other takes precedence. For another exercise on your own, try controlling case sensitivity through either a command line argument or an environment variable. Decide whether the command line argument or the environment variable should take precedence if the program is run with one set to case sensitive and one set to ignore case.
The std::env
module contains many more useful features for dealing with
environment variables: check out its documentation to see what is available.
Xato xabarlarini standart chiqish oqimi o'rniga xato oqimiga yozish
Ayni paytda biz println!
funksiyasidan foydalangan holda terminalga barcha
chiqishlarimizni yozib olamiz. Ko'pgina terminallarda ikkita chiqish turi
mavjud: umumiy ma'lumot uchun standart chiqish oqimi ( stdout
)
va xato xabarlari uchun standart xato oqimi (stderr
).
Ushbu farq foydalanuvchilarga dasturning muvaffaqiyatli chiqishini faylga yo'naltirishni
tanlashga imkon beradi, ammo shu bilan birga xato
xabarlarini ekranga chiqaradi.
println!
funktsiyasi (makrosi) faqat standart chiqishda chop etish mumkin,
shuning uchun biz standart xatolar oqimiga chop etish uchun boshqa misollarda
ko'rib chiqaylik.
Xatolar yozilgan joyni tekshirish
Birinchidan, keling, minigrep
-dan chop etilgan kontent hozirda standart chiqishga
qanday yozilishini, shu jumladan biz standart xato oqimiga yozmoqchi bo'lgan har qanday
xato xabarlarini ko'rib chiqaylik. Biz buni standart chiqish oqimini faylga yo'naltirish
va ataylab xatoga yo'l qo'yish orqali qilamiz. Biz standart xatolar oqimiga yo'naltirmaganimiz uchun,
standart xatolar oqimiga yuborilgan har qanday tarkib ekranda paydo bo'lishda davom etadi.
Buyruq qatorining (cmd) dasturlari xato xabarlarini standart xato oqimiga yuborishi kutilmoqda, shuning uchun biz standart chiqish oqimini faylga yo'naltirsak ham, ekrandagi xato xabarlarini ko'rishimiz mumkin. Bizning dasturimiz hozirda to'g'ri ishlamayapti: biz xato xabari chiqishini faylga saqlayotganini ko'ramiz.
Ushbu xatti-harakatni namoyish qilish uchun biz dasturni >
va output.txt fayl nomi
bilan ishga tushiramiz. Biz standart chiqish oqimini yo'naltirishni xohlaymiz.
Biz hech qanday dalil (argument) keltirmaymiz, bu xatoga olib kelishi kerak:
$ cargo run > output.txt
>
sintaksisi qobiqqa (shellga) output.txt-ga ekran o'rniga standart chiqish (standard output) tarkibini yozishni buyuradi.
Biz ekranda ko'rishni kutgan xato xabarini ko'rmadik, shuning uchun u faylda bo'lishi kerak.
Yuqorida keltirilgan output.txt-ning holati:
Problem parsing arguments: not enough arguments
Ha, bizning xato xabarimiz standart chiqishda (standard outputga) ko'rsatiladi. Bunday xato xabarlarini standart xatolar oqimiga kiritish ancha foydali, shuning uchun faqat muvaffaqiyatli ishga tushirish ma'lumotlari faylga kiradi. Biz buni keyinchalik o'zgartiramiz.
Xatolarni standard xato oqimiga chop etish (print qilish)
Xato xabarlarini chiqarish usulini o'zgartirish uchun biz 12-24-ro'yxatdagi koddan
foydalanamiz. Ushbu bobda ilgari qilgan refaktoring tufayli xato xabarlarini chop
etadigan barcha kodlar bitta main
funktsiyada joylashgan. Rust standart kutubxonasi eprintln!
makrosini
taqdim etadi va bu makro standart xato oqimiga kiradi, shuning uchun println!
bilan chaqirilgan joyda,
keling uning o'rniga eprintln!
makrosini ishlatamiz.
Fayl: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&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);
}
}
Keling, dasturni xuddi shu tarzda, hech qanday dalilsiz (argumentsiz) qayta ishga
tushiramiz va standart chiqishni (outputni) >
bilan qayta yo'naltiramiz:
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
Endi biz ekranda xatoni ko'rishimiz mumkin va output.txt esa bo'sh. Bu esa aynan buyruq qatoridan (cmd-dan) biz kutgan holat.
Keling, dasturni xatoga olib kelmaydigan argumentlar bilan qayta ishga tushiramiz, ammo baribir standart chiqish faylga yo'naltiradi, misol:
$ cargo run -- to poem.txt > output.txt
Biz terminalga hech qanday chiqishni ko'rmaymiz. Va output.txt esa quydagi natijalarni o'z ichiga oladi:
Fayl: output.txt
Are you nobody, too?
How dreary to be somebody!
Yani, biz vaziyatga qarab, muvaffaqiyatli chiqish (output) uchun standart chiqish oqimidan va xatolarni chiqarish uchun standart xato oqimidan foydalanamiz.
Xulosa
Ushbu bobda siz hozirgacha o'rgangan ba'zi asosiy tushunchalar takrorlangan va
Rustda muntazam I/O operatsiyalarini qanday bajarish kerakligi aytilgan.
Buyruq qatori argumentlari (command line argumentlari), fayllar,
atrof-muhit o'zgaruvchilari va println!
makrosi yordamida xatolarni
ishlatgan holda, endi siz buyruq qatori (CLI) dasturlarini yozishga tayyormiz.
Oldingi boblardagi tushunchalar bilan birgalikda sizning kodingiz yaxshi tartibga solinadi,
ma'lumotlarni tegishli tuzilmalarda samarali saqlaydi, xatolarni yaxshi qayta ishlaydi va yaxshi
sinovdan o'tkaziladi.
Keyinchalik, biz funktsional tillar ta'sirida bo'lgan Rust-ning ba'zi xususiyatlarini ko'rib chiqamiz: yopilishlar (closures) va iteratorlar.
Funksional til xususiyatlari: iteratorlar va closurelar
Rust dizayni ko'plab mavjud tillar va texnikalardan ilhomlangan va muhim ta'sirlardan biri funksional dasturlash. Funksional uslubda dasturlash ko'pincha funksiyalarni argumentlar orqali uzatish, ularni boshqa funksiyalardan qaytarish, keyinchalik bajarish uchun o'zgaruvchilarga tayinlash va hokazolar orqali qiymat sifatida foydalanishni o'z ichiga oladi.
Ushbu bobda biz funksional dasturlash nima yoki yo'qligi masalasini muhokama qilmaymiz, aksincha, Rustning ko'p tillardagi funksiyalarga o'xshash ba'zi xususiyatlarini muhokama qilamiz.
Aniqroq aytganda, biz quyidagilarni ko'rib chiqamiz:
- Closurelar, oʻzgaruvchida saqlashingiz mumkin boʻlgan funksiyaga oʻxshash konstruksiya
- Iteratorlar, bir qator elementlarni qayta ishlash usuli
- 12-bobdagi I/O loyihasini yaxshilash uchun closure va iteratorlardan qanday foydalanish kerak
- Closure va iteratorlarning ishlashi (Spoiler ogohlantirishi: ular siz o'ylagandan ham tezroq!)
Biz allaqachon Rustning boshqa ba'zi xususiyatlarini ko'rib chiqdik, masalan, pattern matching va enumlar, ular ham funksional uslubga ta'sir qiladi. Closure va iteratorlarni o'zlashtirish idiomatik, tezkor Rust kodini yozishning muhim qismi bo'lganligi sababli, biz ushbu bobni ularga bag'ishlaymiz.
Closurelar: Environmentni qamrab oladigan anonim funksiyalar
Rustning closureri - bu o'zgaruvchida saqlashingiz yoki boshqa funksiyalarga argument sifatida o'tishingiz mumkin bo'lgan anonim funktsiyalar. Closureni bir joyda yaratishingiz va keyin uni boshqa kontekstda baholash uchun boshqa joyga murojaat qilishingiz mumkin. Funksiyalardan farqli o'laroq, closurelar ular belgilangan doiradagi qiymatlarni olishlari mumkin. Ushbu closure xususiyatlari kodni qayta ishlatish va xatti-harakatlarni moslashtirishga(behavior customization) qanday imkon berishini ko'rsatamiz.
Environmentni closurelar bilan qo'lga olish
Avvalo, keyinchalik foydalanish uchun ular belgilangan muhitdan(environment) qiymatlarni olish uchun closurelardan qanday foydalanishimiz mumkinligini ko'rib chiqamiz.Bu senariy: Ko'pincha bizning futbolka kompaniyamiz reklama ro'yxatidagi kimgadir eksklyuziv, cheklangan nashrdagi futbolkani sovg'a sifatida taqdim etadi. Pochta ro'yxatidagi odamlar ixtiyoriy ravishda o'z profillariga sevimli ranglarini qo'shishlari mumkin. Agar bepul futbolka uchun tanlangan kishi o'zining sevimli ranglar to'plamiga ega bo'lsa, u rangdagi futbolkani oladi. Agar biror kishi sevimli rangni ko'rsatmagan bo'lsa, u kompaniyada eng ko'p bo'lgan rangni oladi.
Buni amalga oshirishning ko'plab usullari mavjud. Ushbu misol uchun biz Qizil
va Moviy
variantlariga ega FutbolkaRangi
nomli enumdan foydalanamiz (oddiylik uchun mavjud ranglar sonini cheklaydi). Biz kompaniya inventarini Inventarizatsiya
strukturasi bilan ifodalaymiz, unda futbolkalar
deb nomlangan maydon mavjud bo‘lib, unda hozirda mavjud bo‘lgan futbolka ranglarini ifodalovchi Vec<FutbolkaRangi>
mavjud.
Inventarizatsiya
da belgilangan yutuq
metodi bepul futbolka g‘olibining ixtiyoriy futbolka rangini afzal ko‘radi va odam oladigan futbolka rangini qaytaradi. Ushbu sozlash 13-1 ro'yxatda ko'rsatilgan:
Fayl nomi: src/main.rs
#[derive(Debug, PartialEq, Copy, Clone)]
enum FutbolkaRangi {
Qizil,
Moviy,
}
struct Inventarizatsiya {
futbolkalar: Vec<FutbolkaRangi>,
}
impl Inventarizatsiya {
fn yutuq(&self, foydalanuvchi_afzalligi: Option<FutbolkaRangi>) -> FutbolkaRangi {
foydalanuvchi_afzalligi.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> FutbolkaRangi {
let mut qizil_raqam = 0;
let mut moviy_raqam = 0;
for rang in &self.futbolkalar {
match rang {
FutbolkaRangi::Qizil => qizil_raqam += 1,
FutbolkaRangi::Moviy => moviy_raqam += 1,
}
}
if qizil_raqam > moviy_raqam {
FutbolkaRangi::Qizil
} else {
FutbolkaRangi::Moviy
}
}
}
fn main() {
let store = Inventarizatsiya {
futbolkalar: vec![FutbolkaRangi::Moviy, FutbolkaRangi::Qizil, FutbolkaRangi::Moviy],
};
let user_pref1 = Some(FutbolkaRangi::Qizil);
let yutuq1 = store.yutuq(user_pref1);
println!(
"{:?} afzalligi bilan foydalanuvchi {:?} oladi",
user_pref1, yutuq1
);
let user_pref2 = None;
let yutuq2 = store.yutuq(user_pref2);
println!(
"{:?} afzalligi bilan foydalanuvchi {:?} oladi",
user_pref2, yutuq2
);
}
main
boʻlimida belgilangan dokon
ikkita moviy futbolka va bitta qizil futbolka qolgan. Qizil ko'ylakni afzal ko'rgan foydalanuvchi va hech qanday imtiyozsiz foydalanuvchi uchun yutuq
metodini chaqiramiz.
Shunga qaramay, ushbu kod ko'p jihatdan amalga oshirilishi mumkin va bu yerda, closurelarga e'tibor qaratish uchun biz siz allaqachon o'rgangan tushunchalarga yopishib oldik, closuredan foydalanadigan yutuq
metodidan tashqari. yutuq
metodida biz Option<FutbolkaRangi>
turidagi parametr sifatida foydalanuvchi imtiyozini olamiz va foydalanuvchi_afzalligi
da unwrap_or_else
metodini chaqiramiz. Option<T>
da unwrap_or_else
metodi standart kutubxona tomonidan aniqlanadi. Buning uchun bitta argument kerak bo‘ladi: T
qiymatini qaytaruvchi hech qanday argumentsiz closure (Option<T>
enumning Some
variantida, bizning holatimizda FutbolkaRangi
da tugaydigan qiymat turiga aylantiriladi). Agar Option<T>
Some
varianti bo'lsa, unwrap_or_else
qiymatini Some
ichidan qaytaradi. Agar Option<T>
None
varianti bo'lsa, unwrap_or_else
closureni chaqiradi va closure orqali qaytarilgan qiymatni qaytaradi.
Biz closure ifodasini belgilaymiz || self.most_stocked()
ni unwrap_or_else
argumenti sifatida. Bu hech qanday parametrlarni o'zi qabul qilmaydigan closuredir (agar closure parametrlari bo'lsa, ular ikkita vertikal chiziq orasida paydo bo'ladi). Closurening asosiy qismi self.most_stocked()
ni chaqiradi. Biz bu yerda closureni aniqlayapmiz va unwrap_or_else
ni amalga oshirish, agar natija kerak bo‘lsa, keyinroq closureni baholaydi.
Ushbu kodni ishga tushirsak quyidagi natijani chop etadi:
$ 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
Qiziqarli tomoni shundaki, biz joriy Inventarizatsiya
misolida self.most_stocked()
deb nomlanuvchi closuredan o‘tdik. Standart kutubxona biz belgilagan Inventarizatsiya
yoki FutbolkaRangi
turlari yoki biz ushbu senariyda foydalanmoqchi bo'lgan mantiq haqida hech narsa bilishi shart emas edi. Closure self
Inventarizatsiya
misoliga o'zgarmas(immutable) referenceni oladi va uni biz belgilagan kod bilan unwrap_or_else
metodiga uzatadi. Funksiyalar esa o'z muhitini(environmentini) shu tarzda ushlab tura olmaydi.
Closure typi Inference va Annotation
Funksiyalar va closurelar o'rtasida ko'proq farqlar mavjud. Closurelar odatda parametrlar turlarini yoki fn
funksiyalari kabi qaytarish qiymatini(return value) izohlashni talab qilmaydi. Funksiyalar uchun tur annotationlari talab qilinadi, chunki turlar foydalanuvchilarga ochiq interfeysning bir qismidir. Ushbu interfeysni qat'iy belgilash, har bir kishi funksiya qanday turdagi qiymatlardan foydalanishi va qaytarishi(return) haqida kelishib olishini ta'minlash uchun muhimdir. Boshqa tomondan, closurelar bu kabi ochiq interfeysda ishlatilmaydi: ular o'zgaruvchilarda saqlanadi va ularni nomlamasdan va kutubxonamiz foydalanuvchilariga ko'rsatmasdan foydalaniladi.
Closurelar odatda qisqa va har qanday ixtiyoriy senariyda emas, faqat tor kontekstda tegishli. Ushbu cheklangan kontekstlarda kompilyator ko'pgina o'zgaruvchilarning turlarini qanday aniqlashga qodir bo'lganiga o'xshab, parametrlarning turlarini va qaytish turini taxmin qilishi mumkin (kompilyatorga closure turi annotationlari ham kerak bo'lgan kamdan-kam holatlar mavjud).
O'zgaruvchilarda bo'lgani kabi, agar biz aniqlik va ravshanlikni oshirishni xohlasak, zarur bo'lgandan ko'ra batafsilroq bo'lish uchun turdagi annotationlarni qo'shishimiz mumkin. Closure uchun turlarga izoh(annotation) qo'yish 13-2 ro'yxatda ko'rsatilgan definitionga o'xshaydi. Ushbu misolda biz closureni aniqlaymiz va uni 13-1 ro'yxatda bo'lgani kabi argument sifatida topshirgan joyda closureni belgilash o'rniga uni o'zgaruvchida saqlaymiz.
Fayl nomi: 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); }
Turga annotationlar qoʻshilishi bilan closure sintaksisi funksiyalar sintaksisiga koʻproq oʻxshaydi. Bu yerda biz taqqoslash uchun parametriga 1 qo'shadigan funksiyani va bir xil xatti-harakatlarga ega bo'lgan closureni aniqlaymiz. Tegishli qismlarni bir qatorga qo'yish uchun bir nechta bo'shliqlar qo'shdik. Bu pipelardan foydalanish va ixtiyoriy bo'lgan sintaksis miqdori bundan mustasno, closure sintaksisi funksiya sintaksisiga qanchalik o'xshashligini ko'rsatadi:
fn bitta_v1_qoshish (x: u32) -> u32 { x + 1 }
let bitta_v2_qoshish = |x: u32| -> u32 { x + 1 };
let bitta_v3_qoshish = |x| { x + 1 };
let bitta_v4_qoshish = |x| x + 1 ;
Birinchi qatorda funksiya taʼrifi(definition), ikkinchi qatorda esa toʻliq izohlangan closure definitioni koʻrsatilgan. Uchinchi qatorda biz closure definitiondan turdagi annotationlarni olib tashlaymiz. To'rtinchi qatorda biz qavslarni olib tashlaymiz, ular ixtiyoriy, chunki closure tanas(body) faqat bitta ifodaga(expression) ega. Bularning barchasi to'g'ri definitionlar bo'lib, ular chaqirilganda bir xil xatti-harakatlarni keltirib chiqaradi. bitta_v3_qoshish
va bitta_v4_qoshish
qatorlari kompilyatsiya qilish uchun closurelarni baholashni talab qiladi, chunki turlar ulardan foydalanishdan kelib chiqadi. Bu let v = Vec::new();
ga o'xshash bo'lib, Rust turini aniqlay olishi uchun Vec
ga turiga izohlar(annotation) yoki ba'zi turdagi qiymatlar kiritilishi kerak.
Closure definitionlari uchun kompilyator ularning har bir parametri va ularning qaytish(return) qiymati uchun bitta aniq turdagi xulosa chiqaradi. Masalan, 13-3 ro'yxatda parametr sifatida qabul qilingan qiymatni qaytaradigan qisqa closure definitioni ko'rsatilgan. Ushbu closure ushbu misol maqsadlaridan tashqari juda foydali emas. E'tibor bering, biz definitionga hech qanday annotation qo'shmaganmiz.
Hech qanday turdagi annotationlar mavjud emasligi sababli, biz bu yerda birinchi marta String
bilan qilgan har qanday turdagi closureni chaqirishimiz mumkin. Agar biz namuna_closure
ni butun(integer) son bilan chaqirishga harakat qilsak, xatoga yo'l qo'yamiz.
Fayl nomi: src/main.rs
fn main() {
let namuna_closure = |x| x;
let s = namuna_closure(String::from("salom"));
let n = namuna_closure(5);
}
Kompilyator bizga quyidagi xatoni beradi:
$ cargo run
Compiling namuna_closure v0.1.0 (file:///projects/namuna_closure)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = namuna_closure(5);
| --------------- ^- help: try using a conversion method: `.to_string()`
| | |
| | expected struct `String`, found integer
| arguments to this function are incorrect
|
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let namuna_closure = |x| x;
| ^
For more information about this error, try `rustc --explain E0308`.
error: could not compile `namuna_closure` due to previous error
Birinchi marta namuna_closure
String
qiymati bilan chaqirilganda, kompilyator x
turini va closurening qaytish turini String
deb hisoblaydi. Keyin bu turlar(type) namuna_closure
bo'limida yopiladi va biz bir xil closure(yopilish) bilan boshqa turdan foydalanishga uringanimizda xatoga duch kelamiz.
Malumot olish yoki Egalik(Ownership) huquqini ko'chirish
Closurelar o'z muhitidan qiymatlarni uchta usulda olishlari mumkin, ular to'g'ridan-to'g'ri funksiya parametr olishi mumkin bo'lgan uchta usulga mos keladi: immutably borrowing (o'zgarmas borrowing(qarz olish)), mutably borrowing (o'zgaruvchan borrowing(qarz olish)) va egalik qilish(ownership). Closure funksiya tanasi(body) olingan qiymatlar bilan nima qilishiga qarab ulardan qaysi birini ishlatishni hal qiladi.
13-4 ro'yxatda biz list
deb nomlangan vectorga immutable(o'zgarmas) referencei qamrab oluvchi closureni aniqlaymiz, chunki u qiymatni chop etish uchun faqat immutable referencega muhtoj:
Fayl nomi: src/main.rs
fn main() { let list = vec![1, 2, 3]; println!("Closureni belgilashdan oldin: {:?}", list); let faqat_borrow = || println!("Closuredan: {:?}", list); println!("Closureni chaqirishdan oldin: {:?}", list); faqat_borrow(); println!("Chaqirilgandan keyin closure: {:?}", list); }
Ushbu misol, shuningdek, o'zgaruvchining closure definitioniga bog'lanishi mumkinligini ko'rsatadi va biz keyinchalik o'zgaruvchi nomi va qavslar yordamida o'zgaruvchi nomi funksiya nomiga o'xshab yopishni chaqirishimiz mumkin.
Biz bir vaqtning o'zida bir nechta immutable(o'zgarmas) referencelarga ega bo'lishimiz mumkin bo'lgan list
uchun, list
closure definitionidan oldin, closure definitionidan keyin, lekin closure chaqirilishidan oldin va closure chaqirilgandan keyin hali ham koddan foydalanish mumkin. Ushbu kod kompilyatsiya bo'ladi, ishlaydi va chop etadi:
$ cargo run
Compiling namuna_closure v0.1.0 (file:///projects/namuna_closure)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/namuna_closure`
Closureni belgilashdan oldin: [1, 2, 3]
Closureni chaqirishdan oldin: [1, 2, 3]
Closuredan: [1, 2, 3]
Chaqirilgandan keyin closure: [1, 2, 3]
Keyinchalik, 13-5 ro'yxatda biz closure bodysini list
vectoriga element qo'shishi uchun o'zgartiramiz. Closure endi mutable(o'zgaruvchan) referenceni oladi:
Fayl nomi: src/main.rs
fn main() { let mut list = vec![1, 2, 3]; println!("Closureni aniqlashdan oldin: {:?}", list); let mut ozgaruvchan_borrow = || list.push(7); ozgaruvchan_borrow(); println!("Chaqirilgandan keyin closure: {:?}", list); }
Ushbu kod kompilyatsiya bo'ladi, ishlaydi va chop etadi:
$ cargo run
Compiling namuna_closure v0.1.0 (file:///projects/namuna_closure)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/namuna_closure`
Closureni aniqlashdan oldin: [1, 2, 3]
Chaqirilgandan keyin closure: [1, 2, 3, 7]
E'tibor bering, ozgaruvchan_borrow
closurening ta'rifi(definition) va chaqiruvi o'rtasida endi println!
belgisi yo'q: ozgaruvchan_borrow
aniqlanganda, u list
ga o'zgaruvchan(mutable) referenceni oladi. Closure chaqirilgandan keyin biz closureni qayta ishlatmaymiz, shuning uchun mutable borrow(o'zgaruvchan qarz) tugaydi. Closure definationi va closure chaqiruvi o'rtasida chop etish uchun immutable(o'zgarmas) borrowga ruxsat berilmaydi, chunki mutable borrow mavjud bo'lganda boshqa borrowlarga ruxsat berilmaydi. Qaysi xato xabari borligini bilish uchun u yerga println!
qo'shib ko'ring!
Agar closurening asosiy qismi ownershipga(egalik) muhtoj bo'lmasa ham, uni environmentda foydalanadigan qiymatlarga ownershiplik qilishga harakat qilmoqchi bo'lsangiz, parametrlar ro'yxatidan oldin move
kalit so'zidan foydalanishingiz mumkin.
Ushbu uslub asosan ma'lumotlarni yangi threadga tegishli bo'lishi uchun ko'chirish uchun yangi threadga closureni o'tkazishda foydalidir. Biz 16-bobda parallellik(concurrency) haqida gapirganda, thereadlarni va nima uchun ulardan foydalanishni xohlashingizni batafsil muhokama qilamiz, ammo hozircha move
kalit so'ziga muhtoj bo'lgan closure yordamida yangi threadni yaratishni qisqacha ko'rib chiqamiz. 13-6 ro'yxat vektorni asosiy thredda emas, balki yangi threadda chop etish uchun o'zgartirilgan 13-4 ro'yxatini ko'rsatadi:
Fayl nomi: src/main.rs
use std::thread; fn main() { let list = vec![1, 2, 3]; println!("Closureni aniqlashdan oldin: {:?}", list); thread::spawn(move || println!("{:?} threaddan", list)) .join() .unwrap(); }
Biz argument sifatida ishlash uchun threadni yopish(closure) imkonini berib, yangi threadni yaratamiz. Closure tanasi(body) listni chop etadi. Roʻyxat 13-4, closure faqat oʻzgarmas(immutable) reference yordamida list
ni yozib oldi, chunki bu uni chop etish uchun zarur boʻlgan list
ga kirishning eng kam miqdori. Ushbu misolda, closure tanasi(body) hali ham faqat o'zgarmas(immutable) referencega muhtoj bo'lsa ham, biz closure definationing boshiga move
kalit so'zini qo'yish orqali list
closurega ko'chirilishi kerakligini ko'rsatishimiz kerak. Yangi thread asosiy threadning qolgan qismi tugashidan oldin tugashi yoki asosiy thread birinchi bo'lib tugashi mumkin. Agar asosiy thread list
ga ownershiplikni saqlab qolgan boʻlsa-da, lekin yangi thread paydo boʻlishidan oldin tugasa va list
ni tashlab qoʻysa, threaddagi immutable(oʻzgarmas) reference yaroqsiz boʻladi. Shuning uchun, kompilyator list
ni yangi threadga berilgan closurega ko'chirishni talab qiladi, shuning uchun reference haqiqiy bo'ladi. Kompilyatorda qanday xatolarga yo'l qo'yganingizni ko'rish uchun closure aniqlangandan so'ng, move
kalit so'zini olib tashlang yoki asosiy threaddagi list
dan foydalaning!
Qabul qilingan qiymatlarni closuredan va Fn
traitlaridan ko'chirish
Closure ma'lumotnomani qo'lga kiritgandan so'ng(shunday qilib, agar biror narsa bo'lsa, closurega ko'chirilgan narsaga ta'sir qiladi) yoki closure aniqlangan environmentdan qiymatga ownershiplikni qo'lga kiritgandan so'ng,(agar biror narsa bo'lsa, closuredan ko'chirilgan narsaga ta'sir qiladi) closurening asosiy qismidagi kod closure keyinroq baholanganda referencelar yoki qiymatlar bilan nima sodir bo'lishini belgilaydi.
Closure tanasi(body) quyidagilardan birini amalga oshirishi mumkin: olingan qiymatni closuredan tashqariga ko'chirish(move), olingan qiymatni mutatsiyalash, qiymatni ko'chirish yoki mutatsiyalash yoki boshlash uchun environmentdan hech narsa olmaslik.
Yopishning environmentdan handlelarni ushlash(capture) va boshqarish usuli closure implementlarining qaysi traitlariga ta'sir qiladi va traitlar funksiyalar va structlar qanday closure turlaridan foydalanishi mumkinligini ko'rsatishi mumkin. Closurelar ushbu Fn
belgilarining bittasi, ikkitasi yoki uchtasini avtomatik ravishda qo'shimcha usulda, closure tanasi qiymatlarni(value) qanday boshqarishiga qarab implement qilinadi:
FnOnce
bir marta chaqirilishi mumkin bo'lgan closurelar uchun amal qiladi. Barcha closurelar hech bo'lmaganda ushbu traitni amalga oshiradi(implement qiladi), chunki barcha closurelar chaqirilishi mumkin. Qabul qilingan qiymatlarni(value) tanasidan tashqariga ko'chiradigan closure faqatFnOnce
ni implement qiladi va boshqaFn
traitlarining hech birini implement qilmaydi, chunki uni faqat bir marta chaqirish mumkin.FnMut
qo'lga kiritilgan qiymatlarni(value) tanasidan tashqariga olib chiqmaydigan, lekin olingan qiymatlarni o'zgartirishi mumkin bo'lgan closurelarga nisbatan qo'llaniladi.Ushbu closurelarni bir necha marta chaqirish mumkin.Fn
qo'lga kiritilgan qiymatlarni tanasidan tashqariga chiqarmaydigan va olingan qiymatlarni o'zgartirmaydigan closurelar, shuningdek, environmentdan hech narsani ushlab(capture) turmaydigan closurelar uchun amal qiladi. Ushbu closurelar environmentni o'zgartirmasdan bir necha marta chaqirilishi mumkin, bu bir vaqtning o'zida bir necha marta closureni chaqirish kabi holatlarda muhimdir.
Keling, 13-1 ro'yxatda biz qo'llagan Option<T>
bo'yicha unwrap_or_else
metodining definitionini ko'rib chiqaylik:
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(),
}
}
}
Eslatib oʻtamiz, T
Option
ning Some
variantidagi qiymat turini ifodalovchi umumiy turdir(generic type). Bu T
turi, shuningdek, unwrap_or_else
funksiyasining qaytish(return) turidir: masalan, Option<String>
da unwrap_or_else
ni chaqiruvchi kod, String
oladi.
Keyin, unwrap_or_else
funksiyasi qo'shimcha F
umumiy turdagi parametrga ega ekanligiga e'tibor bering. F
turi f
nomli parametrning turi(type) bo'lib, biz unwrap_or_else
ga chaqiruv(call) qilganimizda ta`minlovchi closuredir.
Generic F
turida belgilangan belgi FnOnce() -> T
bo'lib, bu F
bir marta chaqirilishi, hech qanday argumentga ega bo'lmasligi va T
qaytarilishini bildiradi. Trait bound-da FnOnce
dan foydalanish unwrap_or_else
faqat bir marta f
ni chaqirishi mumkin bo'lgan cheklovni ifodalaydi. unwrap_or_else
matnida biz Option
Some
bo‘lsa, f
chaqirilmasligini ko‘rishimiz mumkin. Agar Option
None
bo'lsa, f
bir marta chaqiriladi. Barcha closurelar FnOnce
ni implement qilganligi sababli, unwrap_or_else
eng har xil turdagi closurelarni qabul qiladi va imkon qadar moslashuvchan.
Eslatma: Funksiyalar uchta
Fn
traitlarini ham implement qilishi mumkin. Agar biz qilmoqchi bo'lgan narsa environmentdan qiymat olishni(*capture value) talab qilmasa, bizFn
traitlaridan birini implement qiladigan narsa kerak bo'lganda closure o'rniga funksiya nomidan foydalanishimiz mumkin. Masalan,Option<Vec<T>>
qiymatida, agar qiymatNone
bo'lsa, yangi, bo'sh vektorni olish uchununwrap_or_else(Vec::new)
ni chaqirishimiz mumkin.
Endi keling, slicelarda aniqlangan standart kutubxona metodini ko‘rib chiqamiz, bu unwrap_or_else
dan qanday farq qilishini va nima uchun sort_by_key
trait bound uchun FnOnce
o‘rniga FnMut
dan foydalanishini ko‘raylik. Closure ko'rib chiqilayotgan qismdagi joriy elementga reference ko'rinishida bitta argument oladi va order qilinishi mumkin bo'lgan K
turidagi qiymatni qaytaradi. Ushbu funksiya har bir elementning ma'lum bir atributi bo'yicha sliceni saralashni xohlaganingizda foydalidir. 13-7 ro'yxatda bizda Kvadrat
misollar listi mavjud va biz ularni kenglik
atributi bo'yicha pastdan yuqoriga tartiblash uchun sort_by_key
dan foydalanamiz:
Fayl nomi: src/main.rs
#[derive(Debug)] struct Kvatrat { kengligi: u32, balandligi: u32, } fn main() { let mut list = [ Kvatrat { kengligi: 10, balandligi: 1 }, Kvatrat { kengligi: 3, balandligi: 5 }, Kvatrat { kengligi: 7, balandligi: 12 }, ]; list.sort_by_key(|r| r.kengligi); println!("{:#?}", list); }
Ushbu kod quyidagi natijani chop etadi:
$ cargo run
Compiling Kvadrats v0.1.0 (file:///projects/Kvadrats)
Finished dev [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/Kvadratlar`
[
Kvadrat {
kengligi: 3,
balandligi: 5,
},
Kvadrat {
kengligi: 7,
balandligi: 12,
},
Kvadrat {
kengligi: 10,
balandligi: 1,
},
]
sort_by_key
FnMut
closureni olish uchun aniqlanganining sababi shundaki, u closureni bir necha marta chaqiradi: slicedagi har bir element uchun bir marta. |r| r.kengligi
o'z environmentidan hech narsani ushlamaydi(capture), mutatsiyaga uchramaydi yoki boshqa joyga ko'chirmaydi, shuning uchun u trait bound bo'lgan talablarga javob beradi.
Bundan farqli o'laroq, 13-8 ro'yxat faqat FnOnce
traitini amalga oshiradigan closure misolini ko'rsatadi, chunki u qiymatni environmentdan tashqariga ko'chiradi. Kompilyator bu closureni sort_by_key
bilan ishlatishimizga ruxsat bermaydi:
Fayl nomi: src/main.rs
#[derive(Debug)]
struct Rectangle {
kengligi: u32,
balandligi: u32,
}
fn main() {
let mut list = [
Rectangle { kengligi: 10, balandligi: 1 },
Rectangle { kengligi: 3, balandligi: 5 },
Rectangle { kengligi: 7, balandligi: 12 },
];
let mut saralash_operatsiyalari = vec![];
let qiymat = String::from("chaqirilgan kalit orqali");
list.sort_by_key(|r| {
saralash_operatsiyalari.push(qiymat);
r.kengligi
});
println!("{:#?}", list);
}
Bu list
ni saralashda sort_by_key
necha marta chaqirilishini hisoblashning oʻylab topilgan (bu ishlamaydi) usulidir. Ushbu kod closure environmentidan qiymat
—a String
ni saralash_operatsiyalari
vektoriga surish(push) orqali hisoblashni amalga oshirishga harakat qiladi. Closure qiymat
ni ushlaydi, so‘ngra qiymat
ownershipligini saralash_operatsiyalari
vektoriga o‘tkazish orqali qiymat
ni closuredan chiqaradi. Ushbu closureni bir marta chaqirish mumkin; uni ikkinchi marta chaqirishga urinish ishlamaydi, chunki qiymat
endi saralash_operatsiyalari
ga push qilinadigan environmentda(muhitda) bo'lmaydi! Shuning uchun, bu closure faqat FnOnce
ni amalga oshiradi(implement qiladi). Ushbu kodni kompilyatsiya qilmoqchi bo'lganimizda, biz qiymat
ni closuredan chiqarib bo'lmaydigan xatoni olamiz, chunki closure FnMut
ni implement qilishi kerak:
$ cargo run
Compiling kvadratlar v0.1.0 (file:///projects/kvadratlar)
error[E0507]: cannot move out of `qiymat`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let qiymat = String::from("chaqirilgan kalit orqali");
| ----- captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | saralash_operatsiyalari.push(qiymat);
| ^^^^^ move occurs because `qiymat` has type `String`, which does not implement the `Copy` trait
For more information about this error, try `rustc --explain E0507`.
error: could not compile `kvadratlar` due to previous error
Xato qiymat
ni environmentdan tashqariga olib chiqadigan closure tanasidagi(body) chiziqqa(line) ishora qiladi. Buni tuzatish uchun biz closure tanasini qiymatlarni environmentdan ko'chirmasligi uchun o'zgartirishimiz kerak. sort_by_key
necha marta chaqirilishini hisoblash uchun hisoblagichni(counter) environment saqlash va uning qiymatini closure tanasida oshirish buni hisoblashning yanada sodda usuli hisoblanadi. 13-9 ro'yxatdagi closure sort_by_key
bilan ishlaydi, chunki u faqat raqam_saralash_operatsiyalari
counteriga mutable(o'zgaruvchan) referenceni oladi va shuning uchun uni bir necha marta chaqirish mumkin:
Fayl nomi: src/main.rs
#[derive(Debug)] struct Kvadrat { kengligi: u32, balandligi: u32, } fn main() { let mut list = [ Kvadrat { kengligi: 10, balandligi: 1 }, Kvadrat { kengligi: 3, balandligi: 5 }, Kvadrat { kengligi: 7, balandligi: 12 }, ]; let mut raqam_saralash_operatsiyalari = 0; list.sort_by_key(|r| { raqam_saralash_operatsiyalari += 1; r.kengligi }); println!("{:#?}, {raqam_saralash_operatsiyalari} operatsiyalarida tartiblangan", list); }
Fn
traitlari closurelardan foydalanadigan funksiyalar yoki turlarni belgilash yoki ishlatishda muhim ahamiyatga ega. Keyingi bo'limda biz iteratorlarni muhokama qilamiz. Ko'pgina iterator metodlari closure argumentlarini oladi, shuning uchun biz davom etayotganda ushbu closure tafsilotlarini(details) yodda tuting!
Iteratorlar yordamida elementlar ketma-ketligini qayta ishlash
Iterator pattern sizga navbat bilan elementlarning ketma-ketligi bo'yicha ba'zi vazifalarni(task) bajarishga imkon beradi. Iterator har bir elementni takrorlash va ketma-ketlik qachon tugashini aniqlash mantiqi uchun javobgardir. Iteratorlardan foydalanganda, bu mantiqni(logic) o'zingiz takrorlashingiz shart emas.
Rust-da iteratorlar dangasa, ya'ni iteratorni ishlatish uchun ishlatadigan metodlarni chaqirmaguningizcha ular hech qanday ta'sir ko'rsatmaydi. Masalan, 13-10-Ro'yxatdagi kod Vec<T>
da belgilangan iter
metodini chaqirish orqali v1
vektoridagi elementlar ustidan iterator yaratadi. Ushbu kod o'z-o'zidan hech qanday foydali ish qilmaydi.
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }
Iterator v1_iter
o'zgaruvchisida saqlanadi. Biz iteratorni yaratganimizdan so'ng, biz uni turli usullarda ishlatishimiz mumkin. 3-bobdagi 3-5 ro'yxatda biz arrayning har bir elementida ba'zi kodlarni bajarish uchun for
loop siklidan foydalangan holda uni takrorladik. Korpus ostida bu bilvosita yaratgan va keyin iteratorni ishlatgan, ammo biz hozirgacha uning qanday ishlashini ko'rib chiqdik.
13-11 Ro'yxatdagi misolda biz iteratorni yaratishni for
loop siklidagi iteratordan foydalanishdan ajratamiz. for
loop sikli v1_iter
da iterator yordamida chaqirilganda, iteratordagi har bir element loop siklning bir iteratsiyasida ishlatiladi, bu esa har bir qiymatni chop etadi.
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("{} : Olingan", val); } }
Standart kutubxonalari tomonidan taqdim etilgan iteratorlarga ega bo'lmagan tillarda siz xuddi shu funksiyani o'zgaruvchini 0 indeksidan boshlab yozishingiz mumkin, qiymat olish uchun vektorga indekslash uchun ushbu o'zgaruvchidan foydalanish va vektordagi elementlarning umumiy soniga yetgunga qadar sikldagi o'zgaruvchi qiymatini oshirish.
Iteratorlar siz uchun barcha mantiqni(logic) boshqaradi, siz chalkashtirib yuborishingiz mumkin bo'lgan takroriy kodni qisqartiradi. Iteratorlar vektorlar kabi indekslash mumkin bo'lgan ma'lumotlar tuzilmalari(data structure) emas, balki turli xil ketma-ketliklar(sequence) bilan bir xil mantiqdan foydalanish uchun ko'proq moslashuvchanlikni beradi. Keling, iteratorlar buni qanday qilishini ko'rib chiqaylik.
Iterator
traiti va next
metodi
Barcha iteratorlar standart kutubxonada(standard library) aniqlangan Iterator
nomli traitni implement qiladilar. Traitning definitioni quyidagicha ko'rinadi:
#![allow(unused)] fn main() { pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // default implement qilingan metodlar bekor qilindi } }
Eʼtibor bering, bu definitionda baʼzi yangi sintaksislar qoʻllangan: type Item
va Self::Item
bu trait bilan bogʻlangan turni(associated type) belgilaydi. Bog'langan turlar haqida 19-bobda batafsil gaplashamiz. Hozircha siz bilishingiz kerak bo'lgan narsa shuki, ushbu kodda aytilishicha, Iterator
traitini implement qilish uchun siz Item
turini ham belgilashingiz kerak bo'ladi va bu Item
turi next
metodining qaytarish(return) turida qo'llaniladi. Boshqacha qilib aytganda, Item
turi iteratordan qaytarilgan tur bo'ladi.
Iterator
traiti amalga oshiruvchilardan(implementorlar) faqat bitta metodni belgilashni talab qiladi: next
metod, u bir vaqtning o'zida Some
ga o'ralgan(wrapped) iteratorning bir elementini qaytaradi va takrorlash(iteratsiya) tugagach, None
ni qaytaradi.
Biz iteratorlarda next
metodini to'g'ridan-to'g'ri chaqirishimiz mumkin; Ro'yxat 13-12 vektordan yaratilgan iteratorda next
ga takroriy chaqiruvlardan qanday qiymatlar qaytarilishini ko'rsatadi.
Fayl nomi: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn iterator_demonstratsiyasi() {
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);
}
}
Esda tutingki, biz v1_iter
ni o'zgaruvchan(mutable) qilishimiz kerak edi: iteratorda next
metodini chaqirish iterator ketma-ketlikda(sequence) qayerdaligini kuzatish uchun foydalanadigan ichki holatni(internal state) o'zgartiradi. Boshqacha qilib aytganda, bu kod iteratorni iste'mol qiladi(consumes) yoki ishlatadi. next
ga har bir chaqiruv(call) iteratordan biror elementni olib tashlaydi. Biz for
loop siklidan foydalanganda v1_iter
ni o‘zgaruvchan(mutable) qilishimiz shart emas edi, chunki sikl v1_iter
ga ownership(egalik) qildi va uni sahna ortida o‘zgaruvchan qildi.
Shuni ham yodda tutingki, biz next
ga chaiqruvlardan oladigan qiymatlar vektordagi qiymatlarga o'zgarmas(immutable) referencelardir. iter
metodi immutable(o'zgarmas) referencelar ustida iterator hosil qiladi. Agar biz v1
ga ownershiplik(egalik) qiluvchi va tegishli qiymatlarni qaytaruvchi iterator yaratmoqchi bo'lsak, iter
o‘rniga into_iter
ni chaqirishimiz mumkin. Xuddi shunday, agar biz mutable(o'zgaruvchan) referencelarni takrorlashni xohlasak, iter
o'rniga iter_mut
ni chaqirishimiz mumkin.
Iteratorni consume qiladigan metodlar
Iterator
traiti standart kutubxona(standard library) tomonidan taqdim etilgan default implementationlar bilan bir qator turli metodlarga ega; ushbu metodlar haqida Iterator
traiti uchun standart kutubxona API texnik hujjatlarini ko'rib chiqish orqali bilib olishingiz mumkin. Ushbu metodlarning ba'zilari o'z definitionlarida next
metodni chaqiradi, shuning uchun Iterator
tratini implement qilishda next
metodni qo'llash talab qilinadi.
next
ni chaqiruvchi metoflar consuming adaptorlar deb ataladi, chunki ularni chaqirish iteratordan foydalanadi. Bitta misol, iteratorga ownership(egalik) qiladigan va next
deb qayta-qayta chaqirish orqali elementlarni takrorlaydigan, shu bilan iteratorni consume qiladigan sum
metodidir. U takrorlanayotganda, u har bir elementni ishlayotgan jamiga qo'shadi va takrorlash tugagach, jamini qaytaradi. 13-13 ro'yxatda sum
metodidan foydalanishni ko'rsatadigan test mavjud:
Fayl nomi: 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);
}
}
Bizga sum
chaqiruvidan keyin v1_iter
dan foydalanishga ruxsat berilmagan, chunki sum
biz chaqiruvchi iteratorga ownershiplik(egalik) qiladi.
Boshqa iteratorlarni yaratuvchi metodlar
Iterator adaptorlari iteratorni consume(iste'mol) qilmaydigan Iterator
traiti bo'yicha aniqlangan metoddir. Buning o'rniga, ular asl iteratorning ba'zi jihatlarini o'zgartirib, turli iteratorlarni ishlab chiqaradilar.
13-14 ro'yxatda iterator adapter metodini map
deb chaqirish misoli ko'rsatilgan, bunda elementlar takrorlanganda(iteratsiya) har bir elementga chaqiruv(call) qilish yopiladi.
map
metodi o'zgartirilgan elementlarni ishlab chiqaradigan yangi iteratorni qaytaradi. Bu yerda closure vektorning har bir elementi 1 ga oshiriladigan yangi iteratorni yaratadi:
Fayl nomi: src/main.rs
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
Biroq, bu kod ogohlantirish(warning) ishlab chiqaradi:
$ 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: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
warning: `iterators` (bin "iterators") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
13-14 ro'yxatdagi kod hech narsa qilmaydi; biz belgilagan closure hech qachon chaqirilmaydi. Ogohlantirish(warning) bizga nima uchun eslatib turadi: iterator adapterlari dangasa va biz bu yerda iteratorni consume(ishlatish) qilishimiz kerak.
Ushbu ogohlantirishni tuzatish va iteratorni consume qilish uchun biz 12-bobda env::args
bilan 12-1 ro'yxatda qo'llagan collect
metodian foydalanamiz. Ushbu metod iteratorni consume qiladi va natijada olingan qiymatlarni ma'lumotlar to'plamiga(data type) to'playdi.
13-15 ro'yxatda biz vektorga map
-ga chaqiruvdan qaytgan iterator bo'yicha takrorlash natijalarini yig'amiz. Ushbu vektor 1 ga oshirilgan asl vektorning har bir elementini o'z ichiga oladi.
Fayl nomi: 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]); }
map
yopilganligi sababli, biz har bir elementda bajarmoqchi bo'lgan har qanday operatsiyani belgilashimiz mumkin. Bu Iterator
traiti taʼminlaydigan iteratsiya xatti-harakatlarini(behavior) qayta ishlatishda closurelar sizga qandaydir behaviorlarni sozlash imkonini berishining ajoyib namunasidir.
Murakkab harakatlarni(complex action) o'qilishi mumkin bo'lgan tarzda bajarish uchun iterator adapterlariga bir nechta chaiquvlarni zanjirlashingiz(chain) mumkin. Ammo barcha iteratorlar dangasa bo'lgani uchun, iterator adapterlariga chaqiruvlardan natijalarni olish uchun consuming adapter metodlaridan birini chaqirishingiz kerak.
Environmentni qamrab oladigan(capture) closurelardan foydalanish
Ko'pgina iterator adapterlari closurelarni argument sifatida qabul qiladilar va odatda biz iterator adapterlariga argument sifatida ko'rsatadigan closurelar ularning environmentini oladigan closurelar bo'ladi.
Ushbu misol uchun biz closureni oladigan filter
metodidan foydalanamiz. Closure iteratordan element oladi va bool
ni qaytaradi. Agar closure true
qiymatini qaytarsa, qiymat filtr
tomonidan ishlab chiqarilgan iteratsiyaga kiritiladi. Agar closure false
bo'lsa, qiymat kiritilmaydi.
13-16 roʻyxatda biz Poyabzal
structi misollari toʻplamini iteratsiya qilish uchun uning environmentidan poyabzal_olchami
oʻzgaruvchisini ushlaydigan(capture) closure bilan filtr
dan foydalanamiz. U faqat belgilangan o'lchamdagi poyabzallarni qaytaradi.
Fayl nomi: src/lib.rs
#[derive(PartialEq, Debug)]
struct Poyabzal {
olchami: u32,
uslub: String,
}
fn olcham_boyicha_poyabzal(poyabzal: Vec<Poyabzal>, poyabzal_olchami: u32) -> Vec<Poyabzal> {
poyabzal.into_iter().filter(|s| s.size == poyabzal_olchami).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn olcham_boyicha_filterlash() {
let poyabzal = vec![
Poyabzal {
olchami: 10,
uslub: String::from("krossovka"),
},
Poyabzal {
olchami: 13,
uslub: String::from("sandal"),
},
Poyabzal {
olchami: 10,
uslub: String::from("etik"),
},
];
let in_my_size = olcham_boyicha_poyabzal(poyabzal, 10);
assert_eq!(
in_my_size,
vec![
Poyabzal {
olchami: 10,
uslub: String::from("krossovka")
},
Poyabzal {
olchami: 10,
uslub: String::from("etik")
},
]
);
}
}
olcham_boyicha_poyabzal
funksiyasi parametr sifatida poyabzal vektori va poyabzal o'lchamiga egalik qiladi. U faqat belgilangan o'lchamdagi poyabzallarni o'z ichiga olgan vektorni qaytaradi.
olcham_boyicha_poyabzal
bodysida(tanasida) vektorga ownershiplik(egalik) qiluvchi iterator yaratish uchun into_iter
ni chaqiramiz. Keyin biz ushbu iteratorni faqat closure true
ni qaytaradigan elementlarni o'z ichiga olgan yangi iteratorga moslashtirish uchun filter
ni chaqiramiz.
Closure muhitdan poyabzal_olchami
parametrini oladi va qiymatni har bir poyabzal o'lchami bilan solishtiradi, faqat belgilangan o'lchamdagi poyabzallarni saqlaydi. Nihoyat, collect
ni chaqirish moslashtirilgan iterator tomonidan qaytarilgan qiymatlarni funksiya tomonidan qaytariladigan vektorga to'playdi.
Test shuni ko'rsatadiki, biz olcham_boyicha_poyabzal
deb ataganimizda, biz faqat biz ko'rsatgan qiymat bilan bir xil o'lchamdagi poyabzallarni qaytarib olamiz.
I/O loyihamizni takomillashtirish
Iteratorlar haqidagi yangi bilimlar bilan biz koddagi joylarni aniqroq va ixchamroq qilish uchun iteratorlardan foydalangan holda 12-bobdagi I/O(input/output) loyihasini yaxshilashimiz mumkin. Keling, iteratorlar Config::build
va qidiruv
funksiyalarini amalga implement qilishni qanday yaxshilashi mumkinligini ko'rib chiqaylik.
Iterator yordamida clone
ni olib tashlash
12-6 roʻyxatda biz String
qiymatlari boʻlagini olgan kodni qoʻshdik va boʻlimga indekslash va qiymatlarni klonlash orqali Config
strukturasining namunasini yaratdik, Config
strukturasiga ushbu qiymatlarga ownershiplik(egalik) qilish imkonini berdi. 13-17 ro'yxatda biz 12-23 ro'yxatdagi kabi Config::build
funksiyasining bajarilishini takrorladik:
Fayl nomi: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub sorov: String,
pub fayl_yoli: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
sorov,
fayl_yoli,
ignore_case,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let tarkib = fs::read_to_string(config.fayl_yoli)?;
let natijalar = if config.ignore_case {
harflarga_etiborsiz_qidirish(&config.sorov, &tarkib)
} else {
qidiruv(&config.sorov, &tarkib)
};
for line in natijalar {
println!("{line}");
}
Ok(())
}
pub fn qidiruv<'a>(sorov: &str, tarkib: &'a str) -> Vec<&'a str> {
let mut natijalar = Vec::new();
for line in tarkib.lines() {
if line.contains(sorov) {
natijalar.push(line);
}
}
natijalar
}
pub fn harflarga_etiborsiz_qidirish<'a>(
sorov: &str,
tarkib: &'a str,
) -> Vec<&'a str> {
let sorov = sorov.to_lowercase();
let mut natijalar = Vec::new();
for line in tarkib.lines() {
if line.to_lowercase().contains(&sorov) {
natijalar.push(line);
}
}
natijalar
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn harflarga_etiborli() {
let sorov = "duct";
let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.
Duct tape.";
assert_eq!(vec!["xavfsiz, tez, samarali."], qidiruv(sorov, tarkib));
}
#[test]
fn harflarga_etiborsiz() {
let sorov = "rUsT";
let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.
Menga ishoning.";
assert_eq!(
vec!["Rust:", "Menga ishoning."],
harflarga_etiborsiz_qidirish(sorov, tarkib)
);
}
}
O'shanda biz samarasiz clone
chaqiruvlari(call) haqida qayg'urmaslikni aytdik, chunki kelajakda ularni olib tashlaymiz. Xo'sh, bu vaqt hozir!
Bizga bu yerda clone
kerak edi, chunki bizda args
parametrida String
elementlari bo‘lgan slice bor, lekin build
funksiyasi args
ga ega emas. Config
namunasiga ownershiplikni(egalik) qaytarish uchun Config
ning sorov
va fayl_yoli
maydonlaridagi qiymatlarni klonlashimiz kerak edi, shunda Config
namunasi o‘z qiymatlariga ega bo‘lishi mumkin.
Iteratorlar haqidagi yangi bilimlarimiz bilan biz build
funksiyasini oʻzgartirib, bir sliceni olish oʻrniga iteratorga argument sifatida ownershiplik qilishimiz mumkin.
Biz slice uzunligini tekshiradigan kod o'rniga iterator funksiyasidan foydalanamiz va ma'lum joylarga ko'rsatamiz. Bu Config::build
funksiyasi nima qilayotganini aniqlaydi, chunki iterator qiymatlarga kira oladi.
Config::build
iteratorga ownershiplik qilib, borrow qilingan indekslash operatsiyalaridan foydalanishni to'xtatgandan so'ng, biz clone
deb chaqirish va yangi ajratish(allocation) o'rniga String
qiymatlarini iteratordan Config
ga ko'chirishimiz mumkin.
Qaytarilgan(return) iteratordan to'g'ridan-to'g'ri foydalanish
I/O loyihangizning src/main.rs faylini oching, u quyidagicha ko'rinishi kerak:
Fayl nomi: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Argumentlarni tahlil qilish muammosi: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = minigrep::run(config) {
eprintln!("Dastur xatosi: {e}");
process::exit(1);
}
}
Biz birinchi navbatda 12-24-Ro'yhatdagi main
funksiyaning boshlanishini 13-18-Ro'yxatdagi kodga almashtiramiz, bu safar iteratordan foydalanadi. Biz Config::build
ni ham yangilamagunimizcha, bu kompilyatsiya qilinmaydi.
Fayl nomi: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Argumentlarni tahlil qilish muammosi: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = minigrep::run(config) {
eprintln!("Dastur xatosi: {e}");
process::exit(1);
}
}
env::args
funksiyasi iteratorni qaytaradi! Iterator qiymatlarini(value) vectorga yig'ib, keyin sliceni(bo'lak) Config::build
ga o'tkazish o'rniga, endi biz env::args
dan qaytarilgan(return) iteratorga ownershiplik(egalik) huquqini to'g'ridan-to'g'ri Config::build
ga o'tkazmoqdamiz.
Keyinchalik, Config::build
definitioni yangilashimiz kerak. I/O loyihangizning src/lib.rs faylida keling, Config::build
signaturesni 13-19-raqamli roʻyxatga oʻxshatib oʻzgartiraylik. Bu hali ham kompilyatsiya qilinmaydi, chunki biz funksiya bodysini(tanasi) yangilashimiz kerak.
Fayl nomi: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(
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 file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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)
);
}
}
The standard library documentation for the env::args
function shows that the
type of the iterator it returns is std::env::Args
, and that type implements
the Iterator
trait and returns String
values.
We’ve updated the signature of the Config::build
function so the parameter
args
has a generic type with the trait bounds impl Iterator<Item = String>
instead of &[String]
. This usage of the impl Trait
syntax we discussed in
the “Traits as Parameters” section of Chapter 10
means that args
can be any type that implements the Iterator
type and
returns String
items.
Because we’re taking ownership of args
and we’ll be mutating args
by
iterating over it, we can add the mut
keyword into the specification of the
args
parameter to make it mutable.
Using Iterator
Trait Methods Instead of Indexing
Next, we’ll fix the body of Config::build
. Because args
implements the
Iterator
trait, we know we can call the next
method on it! Listing 13-20
updates the code from Listing 12-23 to use the next
method:
Fayl nomi: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(
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 file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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)
);
}
}
Remember that the first value in the return value of env::args
is the name of
the program. We want to ignore that and get to the next value, so first we call
next
and do nothing with the return value. Second, we call next
to get the
value we want to put in the query
field of Config
. If next
returns a
Some
, we use a match
to extract the value. If it returns None
, it means
not enough arguments were given and we return early with an Err
value. We do
the same thing for the file_path
value.
Making Code Clearer with Iterator Adaptors
We can also take advantage of iterators in the search
function in our I/O
project, which is reproduced here in Listing 13-21 as it was in Listing 12-19:
Fayl nomi: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub sorov: String,
pub fayl_yoli: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("argumentlar yetarli emas");
}
let sorov = args[1].clone();
let fayl_yoli = args[2].clone();
Ok(Config { sorov, fayl_yoli })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let tarkib = fs::read_to_string(config.fayl_yoli)?;
Ok(())
}
pub fn qidiruv<'a>(sorov: &str, tarkib: &'a str) -> Vec<&'a str> {
let mut natijalar = Vec::new();
for line in tarkib.lines() {
if line.contains(sorov) {
natijalar.push(line);
}
}
natijalar
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn birinchi_natija() {
let sorov = "marali";
let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.";
assert_eq!(vec!["xavfsiz, tez, samarali."], qidiruv(sorov, tarkib));
}
}
We can write this code in a more concise way using iterator adaptor methods.
Doing so also lets us avoid having a mutable intermediate results
vector. The
functional programming style prefers to minimize the amount of mutable state to
make code clearer. Removing the mutable state might enable a future enhancement
to make searching happen in parallel, because we wouldn’t have to manage
concurrent access to the results
vector. Listing 13-22 shows this change:
Fayl nomi: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(
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 file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
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)
);
}
}
Eslatib o'tamiz, qidiruv
funksiyasining maqsadi tarkib
dagi sorov
ni o'z ichiga olgan barcha qatorlarni qaytarishdir(return). 13-16 Roʻyxatdagi filter
misoliga oʻxshab, bu kod filter
adapteridan faqat line.contains(sorov)
uchun true
qaytaradigan satrlarni saqlash uchun foydalanadi. Keyin mos keladigan qatorlarni collect
bilan boshqa vectorga yig'amiz. Juda oddiyroq! harflarga_etiborsiz_qidirish
funksiyasida ham iterator metodlaridan foydalanish uchun xuddi shunday o'zgartirish kiriting.
Looplar yoki iteratorlar o'rtasida tanlash
Keyingi mantiqiy savol - o'z kodingizda qaysi uslubni tanlashingiz kerakligi va nima uchun: 13-21-Ro'yxatdagi asl dastur yoki 13-22-Ro'yxatdagi iteratorlardan foydalangan holda versiya. Aksariyat Rust dasturchilari iterator uslubidan foydalanishni afzal ko'rishadi. Avvaliga o'rganish biroz qiyinroq, lekin siz turli xil iterator adapterlari va ular nima qilishini his qilganingizdan so'ng, iteratorlarni tushunish osonroq bo'ladi. Kod aylanishning turli bitlari va yangi vectorlarni yaratish o'rniga, loop siklning yuqori darajadagi(high-level) maqsadiga e'tibor qaratadi. Bu ba'zi oddiy kodlarni abstrakt qiladi, shuning uchun ushbu kodga xos bo'lgan tushunchalarni, masalan, iteratordagi har bir element o'tishi kerak bo'lgan filtrlash shartini ko'rish osonroq bo'ladi.
Ammo ikkita dastur haqiqattan ham ekvivalentmi? Intuitiv taxmin shundan iboratki, low-leveldagi loop tezroq bo'ladi. Keling, performance haqida gapiraylik.
Ishlash samaradorligini(Performance) solishtirish: Looplar va iteratorlar
Looplar yoki iteratorlardan foydalanishni aniqlash uchun siz qaysi implement qilish tezroq ekanligini bilishingiz kerak: qidiruv
funksiyasining aniq for
lop sikliga ega versiyasi yoki iteratorli versiya.
Ser Arthur Conan Doylening Sherlok Xolmsning sarguzashtlari(The Adventures of Sherlock Holmes) asarining to‘liq mazmunini String
ga yuklash va mazmundan the so‘zini izlash orqali sinovdan o‘tkizdik. Mana, for
loop siklidan foydalangan holda qidiruv
versiyasi va iteratorlardan foydalangan holda sinov natijalari:
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
Iterator versiyasi biroz tezroq edi! Biz bu yerda benchmark kodini tushuntirmaymiz, chunki bu ikkita versiyaning ekvivalentligini isbotlash emas, balki ushbu ikki dasturning ishlash jihatidan qanday taqqoslanishi haqida umumiy tushunchaga ega bo'lishdir.
Batafsilroq tahlil qilish uchun siz tarkib
sifatida har xil oʻlchamdagi turli matnlarni, sorov
sifatida turli uzunlikdagi turli soʻz va soʻzlarni va boshqa har xil oʻzgarishlardan foydalanishni tekshirishingiz kerak. Gap shundaki: iteratorlar, garchi yuqori darajadagi(high-level) abstraksiya bo'lsa ham, xuddi quyi darajadagi(lower-level) kodni o'zingiz yozganingizdek, taxminan bir xil kodga kompilyatsiya qilinadi. Iteratorlar Rustning zero-cost(nol xarajatli) abstraksiyalaridan biri bo‘lib, bu orqali biz abstraktsiyadan foydalanish qo‘shimcha runtime yukini talab qilmaydi. Bu C++ tilining asl dizayneri va amalga oshiruvchisi(implementori) Bjarne Stroustrupning “C++ asoslari (Foundations of C++)” (2012) asarida zero-overhead belgilashiga o‘xshaydi:
Umuman olganda, C++ ilovalari zero overhead(nol qo'shimcha xarajatlar) printsipiga bo'ysunadi: Siz foydalanmayotgan narsangiz uchun pul to'lamaysiz. Va yana: Siz foydalanadigan narsadan yaxshiroq kodlash mumkin emas.
Yana bir misol sifatida, quyidagi kod audio dekoderdan olingan. Dekodlash algoritmi oldingi namunalarning chiziqli(linear) funksiyasi asosida kelajakdagi qiymatlarni baholash uchun linear prediction(taxmin qilish) qilish matematik operatsiyasidan foydalanadi. Ushbu kod uchta o'zgaruvchi bo'yicha matematikani amalga oshirish uchun iterator zanjiridan foydalanadi: ma'lumotlarning bufer
qismi, 12 koeffitsient
massivi va qlp_shift
da ma'lumotlarni o'zgartirish uchun miqdor. Biz ushbu misolda o'zgaruvchilarni e'lon qildik, lekin ularga hech qanday qiymat bermadik; Garchi bu kod o'z kontekstidan tashqarida unchalik katta ma'noga ega bo'lmasa-da, bu Rust yuqori darajadagi g'oyalarni low-leveldagi kodga qanday tarjima qilishining qisqacha, haqiqiy misolidir.
let buffer: &mut [i32];
let koeffitsient: [i64; 12];
let qlp_shift: i16;
for i in 12..buffer.len() {
let taxmin_qilish = koeffitsient.iter()
.zip(&buffer[i - 12..i])
.map(|(&c, &s)| c * s as i64)
.sum::<i64>() >> qlp_shift;
let delta = buffer[i];
buffer[i] = taxmin_qilish as i32 + delta;
}
taxmin_qilish
qiymatini hisoblash uchun ushbu kod koeffitsient
dagi 12 ta qiymatning har birini iteratsiya(takrorlaydi) qiladi va koeffitsient qiymatlarini bufer
dagi oldingi 12 ta qiymat bilan bog‘lash uchun zip
metodidan foydalanadi. Keyin, har bir juftlik uchun biz qiymatlarni ko'paytiramiz, barcha natijalarni yig'amiz va qlp_shift
bitlari yig'indisidagi bitlarni o'ngga siljitamiz.
Audio dekoderlar kabi ilovalardagi hisob-kitoblar ko'pincha performenceni(ish samaradorligi) birinchi o'ringa qo'yadi. Bu yerda biz iterator yaratamiz, ikkita adapterdan foydalanamiz va keyin qiymatni consume(iste'mol) qilamiz. Ushbu Rust kodi qaysi assembly kodini kompilyatsiya qiladi? Xo'sh, ushbu yozuvdan boshlab, u siz qo'lda yozadigan assemblyga kopilatsiya qiladi. koeffitsient
dagi qiymatlar ustidagi iteratsiyaga mos keladigan loop sikl umuman yo‘q: Rust 12 ta iteratsiya(takrorlash) borligini biladi, shuning uchun u loop siklni unrolls
qiladi. Unrolling - bu optimallashtirish bo'lib, u loop siklni boshqarish kodining qo'shimcha yukini olib tashlaydi va buning o'rniga loop siklning har bir iteratsiyasi uchun takroriy kod hosil qiladi.
Barcha koeffitsientlar registrlarda saqlanadi, ya'ni qiymatlarga kirish juda tez. Runtimeda massivga kirishda chegaralar yo'q. Rust qo'llashi mumkin bo'lgan barcha optimallashtirishlar natijada olingan kodni juda samarali qiladi. Endi siz buni bilganingizdan so'ng, siz iteratorlar va closurelardan qo'rqmasdan foydalanishingiz mumkin! Ular kodni yuqori darajadagi(high-level) ko'rinishga olib keladi, lekin buning uchun runtime ishlashi uchun jazo qo'llamaydi.
Xulosa
Closure va iteratorlar - bu funksional dasturlash tili g'oyalaridan ilhomlangan Rust xususiyatlari(feature). Ular Rustning low-leveldagi ishlashda high-leveldagi g'oyalarni aniq ifodalash qobiliyatiga hissa qo'shadilar. Closure va iteratorlarni implement qilish runtimening ishlashiga ta'sir qilmaydi. Bu Rustning zero-cost(nol xarajatli) abstraksiyalarni taqdim etishga intilish maqsadining bir qismidir.
Endi biz I/O loyihamizning ifodaliligini yaxshilaganimizdan so‘ng, keling, loyihani dunyo bilan baham ko‘rishimizga yordam beradigan cargo
ning yana bir qancha xususiyatlarini ko‘rib chiqaylik.
Cargo va Crates.io haqida ko'proq
Hozirgacha biz kodimizni yaratish(build), ishga tushirish(run) va sinab ko'rish(test) uchun faqat Cargo-ning eng asosiy xususiyatlaridan foydalanganmiz, ammo u ko'proq narsani qila oladi. Ushbu bobda biz sizga quyidagi amallarni bajarishni ko'rsatish uchun uning boshqa, yanada ilg'or xususiyatlarini muhokama qilamiz:
- Reliz profillari bilan buildarni customizatsiya qilish
- Kutubxonalarni(library) crates.io saytida nashr eting
- Workspacelar bilan yirik loyihalarni tashkil qiling
- crates.io dan binary(ikkilik) fayllarni o'rnating
- Maxsus buyruqlar yordamida cargoni kengaytiring
Cargo biz ushbu bobda ko'rib chiqiladigan funksiyalardan ham ko'proq narsani qila oladi, shuning uchun uning barcha xususiyatlarini to'liq tushuntirish uchun uning texnik hujjatlariga qarang.
Reliz profillari bilan buildarni customizatsiya qilish
Rust-da release profillari turli xil konfiguratsiyalarga ega bo'lgan oldindan belgilangan va sozlanishi mumkin bo'lgan profillar bo'lib, ular dasturchiga kodni kompilyatsiya qilish uchun turli xil variantlarni ko'proq nazorat qilish imkonini beradi. Har bir profil boshqalardan mustaqil ravishda configuratsiya qilingan.
Cargo ikkita asosiy profilga ega: cargo build
ni ishga tushirganingizda dev
cargo profili va cargo build --release
ni ishga tushirganingizda release
cargo profilidan foydalanadi. dev
profili ishlab chiqish(development) uchun yaxshi standart sozlamalar bilan belgilangan va release
profili relizlar uchun yaxshi standart parametrlarga ega.
Ushbu profil nomlari sizning buildlaringiz natijalaridan tanish bo'lishi mumkin:
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
$ cargo build --release
Finished release [optimized] target(s) in 0.0s
dev
va release
- bu kompilyator tomonidan ishlatiladigan turli xil profillar.
Loyihaning Cargo.toml fayliga [profile.*]
boʻlimlarini aniq qoʻshmagan boʻlsangiz, Cargo har bir profil uchun standart(default) sozlamalarga ega.
Moslashtirmoqchi(customizatsiya) boʻlgan har qanday profil uchun [profile.*]
boʻlimlarini qoʻshish orqali siz default sozlamalarning har qanday quyi toʻplamini bekor qilasiz. Masalan, dev
va release
profillari uchun opt-level
sozlamalari uchun default qiymatlar:
Fayl nomi: Cargo.toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
opt-level
sozlama Rust kodingizga qo'llaniladigan optimallashtirishlar sonini nazorat qiladi, 0 dan 3 gacha. Ko'proq optimallashtirishni qo'llash kompilyatsiya vaqtini uzaytiradi, shuning uchun agar siz tez-tez ishlab chiqayotgan bo'lsangiz va kodingizni kompilyatsiya qilsangiz, natijada olingan kod sekinroq ishlayotgan bo'lsa ham, tezroq kompilyatsiya qilishni kamroq optimallashtirishni xohlaysiz. Shunday qilib, dev
uchun default opt-level
0
dir. Kodni releasega chiqarishga tayyor bo'lganingizda, kompilyatsiya qilish uchun ko'proq vaqt sarflaganingiz ma'qul. Siz release rejimida faqat bir marta kompilyatsiya qilasiz, lekin kompilyatsiya qilingan dasturni ko'p marta ishga tushirasiz, shuning uchun release rejimi tradelari tezroq ishlaydigan kod uchun kompilyatsiya vaqtini uzaytiradi. Shuning uchun release
profili uchun default opt-level
3
dir.
Siz Cargo.toml da boshqa qiymat qoʻshish orqali default sozlamani bekor qilishingiz mumkin. Misol uchun, agar biz development profilida optimallashtirish darajasi 1 dan foydalanmoqchi bo'lsak, loyihamizning Cargo.toml fayliga ushbu ikki qatorni qo'shishimiz mumkin:
Fayl nomi: Cargo.toml
[profile.dev]
opt-level = 1
Bu kod default 0
sozlamasini bekor qiladi. Now when we run cargo build
,
Cargo dev
profili uchun default sozlamalardan hamda opt-level
ga moslashtirishimizdan foydalanadi. Biz opt-level
ni 1
ga o‘rnatganimiz sababli, Cargo defaultdan ko‘ra ko‘proq optimallashtirishni qo‘llaydi, lekin release builddagi kabi emas.
Har bir profil uchun konfiguratsiya opsiyalari va standart sozlamalarning to'liq ro'yxati uchun Cargo texnik hujjatlariga qarang.
Crateni Crates.io-ga nashr qilish
Biz crates.io paketlaridan loyihamizga dependency sifatida foydalandik, lekin siz oʻz paketlaringizni nashr(publish) qilish orqali kodingizni boshqa odamlar bilan ham baham koʻrishingiz mumkin. crates.io saytidagi crate registri paketlaringizning manba kodini tarqatadi, shuning uchun u birinchi navbatda open source kodni saqlaydi.
Rust va Cargoda publish etilgan paketingizni odamlar topishi va undan foydalanishini osonlashtiradigan funksiyalar mavjud. Biz ushbu xususiyatlarning ba'zilari haqida keyin gaplashamiz va keyin paketni qanday nashr(publish) qilishni tushuntiramiz.
Foydali hujjatlarga(documentation) sharhlar(comment) qo'yish
Paketlaringizni to'g'ri hujjatlashtirish boshqa foydalanuvchilarga ulardan qanday va qachon foydalanishni bilishga yordam beradi, shuning uchun texnik hujjatlarni yozish uchun vaqt sarflashga arziydi. 3-bobda biz Rust kodini ikkita slash //
yordamida qanday izohlashni(comment) muhokama qildik. Rust shuningdek, HTML hujjatlarini yaratadigan documentation comment deb nomlanuvchi hujjatlar uchun o'ziga xos izohga ega. HTML sizning cratengiz qanday impelemnent qilinganidan farqli o'laroq, sizning cratengizdan qanday foydalanishni bilishga qiziqqan dasturchilar uchun mo'ljallangan umumiy API elementlari uchun hujjat sharhlari mazmunini ko'rsatadi.
Hujjatlarga sharhlar ikkita o'rniga uchta slashdan foydalaniladi, ///
va matnni formatlash uchun Markdown notationni qo'llab-quvvatlaydi. Hujjatlarga sharhlarni ular hujjatlashtirilayotgan element oldiga qo'ying. 14-1 Ro'yxatda my_crate
nomli cratedagi bir_qoshish
funksiyasi uchun hujjat sharhlari ko'rsatilgan.
Fayl nomi: src/lib.rs
/// Berilgan raqamga bitta qo'shadi.
///
/// # Misollar
///
/// ```
/// let argument = 5;
/// let javob = my_crate::bir_qoshish(argument);
///
/// assert_eq!(6, javob);
/// ```
pub fn bir_qoshish(x: i32) -> i32 {
x + 1
}
Bu yerda biz bir_qoshish
funksiyasi nima qilishini tavsiflab beramiz, Misollar
sarlavhasi bilan bo‘limni boshlaymiz, so‘ngra bir_qoshish
funksiyasidan qanday foydalanishni ko‘rsatadigan kodni taqdim etamiz. Biz ushbu hujjat sharhidan HTML hujjatlarini cargo doc
ni ishga tushirish orqali yaratishimiz mumkin. Bu buyruq Rust bilan tarqatilgan rustdoc
toolini ishga tushiradi va yaratilgan HTML hujjatlarini target/doc jildiga joylashtiradi.
Qulaylik uchun cargo doc --open
ni ishga tushirish joriy crate hujjatlari uchun HTML-ni yaratadi (shuningdek, cratengizning barcha dependencilari uchun hujjatlar) va natijani veb-brauzerda ochadi. bir_qoshish
funksiyasiga o‘ting va 14-1-rasmda ko‘rsatilganidek, hujjat sharhlaridagi matn qanday ko‘rsatilishini ko‘rasiz:
Tez-tez ishlatiladigan bo'limlar
Biz HTML-da Misollar
sarlavhali bo'lim yaratish uchun 14-1 ro'yxatdagi # Misollar
Markdown sarlavhasidan foydalandik. Mualliflar o'z hujjatlarida tez-tez foydalanadigan boshqa bo'limlar:
- Panics: Hujjat yozilayotan funksiya senariylari panic qo'zg'atishi mumkin. O'z dasturlari panic qo'zg'ashini istamaydigan funksiyaning murojaat qiluvchilari bunday holatlarda funksiyani chaqirmasliklariga ishonch hosil qilishlari kerak.
- Errors: Agar funksiya
Result
ni qaytarsa, yuzaga kelishi mumkin bo'lgan xatolar turlarini tavsiflash va bu xatolar qaytarilishiga qanday sharoitlar sabab bo'lishi mumkinligi murojaat qiluvchilar uchun foydali bo'lishi mumkin, shuning uchun ular turli xil xatolarni turli yo'llar bilan hal qilish uchun kod yozishlari mumkin. - Safety: Agar funksiya murojaat qilish uchun
unsafe
bo'lsa (biz 19-bobda xavfsizlikni muhokama qilamiz), funksiya nima uchun xavfli ekanligini tushuntiruvchi bo'lim bo'lishi kerak va funksiya murojaat qiluvchilar qo'llab-quvvatlashini kutayotgan o'zgarmaslarni qamrab oladi.
Ko'pgina hujjatlar sharhlari ushbu bo'limlarning barchasiga muhtoj emas, ammo bu sizning kodingiz foydalanuvchilari bilishni qiziqtiradigan jihatlarni eslatish uchun yaxshi nazorat ro'yxati.
Texnik hujjatlarga sharhlar test sifatida
Hujjatlarga sharhlaringizga(documentation comment) misol kod bloklarini qo'shish kutubxonangizdan(library) qanday foydalanishni ko'rsatishga yordam beradi va bu qo'shimcha bonusga ega bo'ladi: cargo test
ishga tushirish hujjatlaringizdagi kod misollarini test sifatida ishga tushiradi! Hech narsa misollar bilan hujjatlashtirishdan yaxshiroq emas. Lekin hech narsa ishlamaydigan misollardan ko'ra yomonroq emas, chunki hujjatlar yozilgandan beri kod o'zgargan. Agar biz 14-1 roʻyxatdagi bir_qoshish
funksiyasi uchun hujjatlar bilan cargo test
oʻtkazsak, test natijalarida quyidagi boʻlimni koʻramiz:
Doc-tests my_crate
running 1 test
test src/lib.rs - bir_qoshish (line 5) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
Endi funksiyani yoki misolni, misoldagi assert_eq!
panic qo'zg'atadigan tarzda o'zgartirsak va yana cargo test
ishga tushirsak, hujjat testlari misol va kod bir-biri bilan sinxronlanmaganligini aniqlaymiz.!
O'z ichiga olgan elementlarni sharhlash
Hujjat sharhining uslubi //!
hujjatni sharhlardan keyingi elementlarga emas, balki sharhlarni o'z ichiga olgan elementga qo'shadi. Biz odatda bu doc izohlaridan cratening ildiz(root) faylida (odatda src/lib.rs) yoki modul ichida crateni yoki butun modulni hujjatlash uchun foydalanamiz.
Masalan, bir_qoshish
funksiyasini o'z ichiga olgan my_crate
cratesi maqsadini tavsiflovchi hujjatlarni qo'shish uchun biz src/lib.rs faylining boshiga //!
bilan boshlanadigan hujjat sharhlarini qo`shamiz, 14-2 ro'yxatda ko'rsatilganidek:
Fayl nomi: src/lib.rs
//! # Mening Crateyim
//!
//! `my_crate` - muayyan hisob-kitoblarni bajarishni qulayroq qilish uchun
//! yordamchi dasturlar to'plami.
/// Berilgan raqamga bitta qo'shadi.
// --snip--
///
/// # Misollar
///
/// ```
/// let argument = 5;
/// let javob = my_crate::bir_qoshish(argument);
///
/// assert_eq!(6, javob);
/// ```
pub fn bir_qoshish(x: i32) -> i32 {
x + 1
}
E'tibor bering, //!
bilan boshlanadigan oxirgi qatordan keyin hech qanday kod yo'q. Fikrlarni ///
o'rniga //!
bilan boshlaganimiz sababli, biz ushbu sharhdan keyingi elementni emas, balki ushbu sharhni o'z ichiga olgan elementni hujjatlashtirmoqdamiz. Bunday holda, bu element crate ildizi(root) bo'lgan src/lib.rs faylidir. Ushbu sharhlar butun crateni tasvirlaydi.
cargo doc --open
ni ishga tushirganimizda, bu izohlar 14-2-rasmda ko‘rsatilganidek, my_crate
hujjatlarining birinchi sahifasida cratedagi public itemlar ro‘yxati ustida ko'rsatiladi:
Elementlar ichidagi hujjat sharhlari, ayniqsa, cratelar va modullarni tavsiflash uchun foydalidir. Foydalanuvchilarga cratening tashkil etilishini tushunishlariga yordam berish uchun konteynerning umumiy maqsadini tushuntirish uchun ulardan foydalaning.
pub use
bilan qulay Public APIni eksport qilish
Public API structi crateni nashr qilishda muhim ahamiyatga ega. Sizning cratengizdan foydalanadigan odamlar structureni sizdan ko'ra kamroq bilishadi va agar sizning cratengiz katta modul ierarxiyasiga ega bo'lsa, ular foydalanmoqchi bo'lgan qismlarni topishda qiyinchiliklarga duch kelishlari mumkin.
7-bobda biz pub
kalit so‘zi yordamida itemlarni qanday qilib hammaga ochiq(public) qilish va use
kalit so‘zi bilan obyektlarni qamrovga(scope) kiritishni ko‘rib chiqdik.Biroq, crateni ishlab chiqishda sizga mantiqiy bo'lgan structure foydalanuvchilaringiz uchun unchalik qulay bo'lmasligi mumkin. Siz structlaringizni bir nechta darajalarni o'z ichiga olgan ierarxiyada tartibga solishni xohlashingiz mumkin, ammo keyin siz ierarxiyada chuqur aniqlagan turdan foydalanmoqchi bo'lgan odamlar ushbu tur mavjudligini aniqlashda muammolarga duch kelishlari mumkin.
Ular, shuningdek, use
my_crate::FoydaliTur;
o'rniga use
my_crate::biror_modul::boshqa_modul::FoydaliTur;
ni kiritishlari kerakligidan bezovtalanishi mumkin.
Yaxshi xabar shundaki, agar sturcture boshqa kutubxonadan(library) foydalanishi uchun qulay bo'lmasa, ichki organizationgizni o'zgartirishingiz shart emas: Buning o'rniga, pub use
dan foydalanib, private structuredan farq qiladigan public structure yaratish uchun itemlarni qayta eksport qilishingiz mumkin. Qayta eksport qilish public ob'ektni bir joyda oladi va uni boshqa joyda hammaga ochiq(public) qiladi, go'yo u boshqa joyda aniqlangandek.
Masalan, badiiy tushunchalarni modellashtirish uchun rassom
nomli kutubxona(library) yaratdik, deylik.
Ushbu kutubxona ichida ikkita modul mavjud: 14-3 roʻyxatda koʻrsatilganidek, AsosiyRang
va IkkilamchiRang
nomli ikkita raqamni oʻz ichiga olgan turlar
moduli va aralashtirish
nomli funksiyani oʻz ichiga olgan yordamchi
moduli:
Fayl nomi: src/lib.rs
//! # Rassom
//!
//! Badiiy tushunchalarni modellashtirish uchun kutubxona.
pub mod turlar {
/// RYB rang modeliga muvofiq asosiy ranglar.
pub enum AsosiyRang {
Qizil,
Sariq,
Kok,
}
/// RYB rang modeliga muvofiq ikkinchi darajali ranglar.
pub enum IkkilamchiRang {
Qovoqrang,
Yashil,
Siyohrang,
}
}
pub mod yordamchi {
use crate::turlar::*;
/// Ikkilamchi rang yaratish uchun ikkita asosiy rangni teng
/// miqdorda birlashtiradi.
pub fn aralashtirish(c1: AsosiyRang, c2: AsosiyRang) -> IkkilamchiRang {
// --snip--
unimplemented!();
}
}
14-3-rasmda cargo doc
tomonidan yaratilgan ushbu crate uchun hujjatlarning bosh sahifasi qanday ko'rinishi ko'rsatilgan:
E'tibor bering, AsosiyRang
va IkkilamchiRang
turlari birinchi sahifada ko'rsatilmagan va aralashtirish
funksiyasi ham mavjud emas. Ularni ko'rish uchun turlar
va yordamchi
ni bosishimiz kerak.
Ushbu kutubxonaga bog'liq bo'lgan boshqa cratega rassom
dan elementlarni qamrab oladigan, hozirda aniqlangan modul stryucturedan ko'rsatadigan use
statementlari kerak bo'ladi. 14-4 roʻyxatda rassom
cratesidagi AsosiyRang
va aralashtirish
elementlaridan foydalanadigan crate misoli koʻrsatilgan:
Fayl nomi: src/main.rs
use art::turlar::AsosiyRang;
use rassom::yordamchi::aralashtirish;
fn main() {
let qizil = AsosiyRang::Qizil;
let yellow = AsosiyRang::Sariq;
aralashtirish(qizil, sariq);
}
rassom
cratesidan foydalanadigan 14-4-Ro'yxatdagi kod muallifi AsosiyRang
turlar
modulida, aralashtirish
esa yordamchi
modulida ekanligini aniqlashi kerak edi. rassom
cratening modul stucturesi undan foydalanadiganlarga qaraganda rassom
crate ustida ishlayotgan developerlarga ko'proq mos keladi. The internal
structure doesn’t contain any useful information for someone trying to
understand how to use the art
crate, but rather causes confusion because
developers who use it have to figure out where to look, and must specify the
module names in the use
statements.
Ichki stuctureda rassom
cratesidan qanday foydalanishni tushunishga urinayotganlar uchun foydali ma'lumotlar mavjud emas, aksincha, chalkashliklarga sabab bo'ladi, chunki undan foydalanadigan developerlar qayerga qarash kerakligini aniqlashlari kerak va use
statementlarida modul nomlarini ko'rsatishi kerak.
Ichki organizationni public API’dan olib tashlash uchun biz 14-5 ro‘yxatda ko‘rsatilganidek, top leveldagi elementlarni qayta eksport qilish uchun pub use
statementlarini qo‘shish uchun 14-3 ro‘yxatdagi rassom
crate kodini o‘zgartirishimiz mumkin:
Fayl nomi: src/lib.rs
//! # Rassom
//!
//! Badiiy tushunchalarni modellashtirish uchun kutubxona.
pub use self::turlar::AsosiyRang;
pub use self::turlar::IkkilamchiRang;
pub use self::yordamchi::aralashtirish;
pub mod turlar {
// --snip--
/// RYB rang modeliga muvofiq asosiy ranglar.
pub enum AsosiyRang {
Qizil,
Sariq,
Kok,
}
/// RYB rang modeliga muvofiq ikkinchi darajali ranglar.
pub enum IkkilamchiRang {
Qovoqrang,
Yashil,
Siyohrang,
}
}
pub mod yordamchi {
// --snip--
use crate::turlar::*;
/// Ikkilamchi rang yaratish uchun ikkita asosiy rangni teng
/// miqdorda birlashtiradi.
pub fn aralashtirish(c1: AsosiyRang, c2: AsosiyRang) -> IkkilamchiRang {
IkkilamchiRang::Qovoqrang
}
}
Ushbu crate uchun cargo doc
yaratadigan API hujjatlari Endi 14-4-rasmda ko'rsatilganidek, re-exportlarni birinchi sahifada listga oling va bog'lang, bu AsosiyRang
va IkkilamchiRang
turlarini va aralashtirish
funksiyasini topishni osonlashtiradi.
rassom
crate foydalanuvchilari hali ham 14-4 roʻyxatda koʻrsatilganidek, 14-3 roʻyxatdagi ichki(internal) structureni koʻrishlari va foydalanishlari mumkin yoki ular 14-6 roʻyxatda koʻrsatilganidek, 14-5 roʻyxatdagi qulayroq structuredan foydalanishlari mumkin:
Fayl nomi: src/main.rs
use rassom::aralashtirish;
use rassom::AsosiyRang;
fn main() {
// --snip--
let qizil = AsosiyRang::qizil;
let sariq = AsosiyRang::Sariq;
mix(qizil, sariq);
}
Ko'plab ichki modullar mavjud bo'lsa, pub use
bilan top leveldagi turlarni qayta eksport(re-export) qilish cratedan foydalanadigan foydalanuvchilar tajribasida sezilarli o'zgarishlarga olib kelishi mumkin. pub use
ning yana bir keng tarqalgan qoʻllanilishi bu crate deifinationlarini cratengizning public API qismiga aylantirish uchun joriy cratedagi dependency definitionlarini qayta eksport qilishdir.
Foydali public API stucturesini yaratish fandan ko'ra ko'proq san'atdir va siz foydalanuvchilaringiz uchun eng mos keladigan APIni topish uchun takrorlashingiz mumkin. pub use
ni tanlash sizga cratengizni ichki stuctureda moslashuvchanlikni beradi va bu ichki stuctureni foydalanuvchilarga taqdim etgan narsadan ajratadi. O'rnatgan ba'zi cratelar kodlarini ko'rib chiqing, ularning ichki tuzilishi(internal structure) public APIdan farq qiladimi yoki yo'qmi.
Crates.io da account sozlash
Har qanday cratelarni nashr qilishdan oldin crates.io saytida hisob(account) yaratishingiz va API tokenini olishingiz kerak.
Buning uchun crates.io saytidagi bosh sahifaga tashrif buyuring va GitHub hisob qaydnomasi(account) orqali tizimga kiring. (GitHub hisobi hozirda talab hisoblanadi, ammo sayt kelajakda hisob yaratishning boshqa usullarini qo'llab-quvvatlashi mumkin.), Tizimga kirganingizdan so'ng,https://crates.io/me/ sahifasidagi hisob sozlamalariga tashrif buyuring va API kalitingizni(key) oling. Keyin API kalitingiz bilan cargo login
buyrug'ini bajaring, masalan:
$ cargo login abcdefghijklmnopqrstuvwxyz012345
Bu buyruq Cargoga API tokeningiz haqida xabar beradi va uni ~/.cargo/credentials da saqlaydi. E'tibor bering, bu token secret: uni boshqa hech kim bilan baham ko'rmang. Agar biron-bir sababga ko'ra uni kimdir bilan baham ko'rsangiz, uni bekor qilishingiz va crates.io saytida yangi token yaratishingiz kerak.
Yangi cratega metadata qo'shish
Aytaylik, sizda nashr qilmoqchi bo'lgan cratengiz bor. Nashr qilishdan oldin cratening Cargo.toml faylining [package]
boʻlimiga metamaʼlumotlar qoʻshishingiz kerak boʻladi.
Sizning cratengizga noyob nom kerak bo'ladi. Mahalliy(local) miqyosda crate ustida ishlayotganingizda, cratega xohlaganingizcha nom berishingiz mumkin. Biroq, crates.io saytidagi crate nomlari birinchi kelganda, birinchi navbatda beriladi. Crate nomi olingandan so'ng, boshqa hech kim bu nom bilan crateni nashr eta olmaydi. Crateni nashr etishga urinishdan oldin foydalanmoqchi bo'lgan nomni qidiring. Agar nom ishlatilgan bo'lsa, nashr qilish uchun yangi nomdan foydalanish uchun boshqa nom topib, Cargo.toml faylida [package]
bo'limi ostidagi name
maydonini tahrirlashingiz kerak bo'ladi:
Fayl nomi: Cargo.toml
[package]
name = "kalkulyator"
Noyob nom tanlagan bo'lsangiz ham, ushbu nuqtada crateni nashr qilish uchun cargo publish
ni ishga tushirganingizda, siz ogohlantirish va keyin xatolikni olasiz:
$ 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 xato, chunki sizda ba'zi muhim ma'lumotlar yetishmayapti: tavsif(description) va litsenziya talab qilinadi, shunda foydalanuvchilar sizning cratengiz nima qilishini va undan qanday shartlar ostida foydalanishlari mumkinligini bilishlari mumkin. Cargo.toml ga bir yoki ikki jumladan iborat tavsif(description) qo'shing, chunki u qidiruv natijalarida cratengiz bilan birga ko'rinadi. license
maydoni uchun siz litsenziya identifikatorining qiymatini berishingiz kerak. Linux
Foundation’s Software Package Data Exchange (SPDX) ushbu qiymat uchun foydalanishingiz mumkin bo'lgan identifikatorlarni sanab o'tadi. Masalan, MIT litsenziyasidan foydalangan holda cratengizni litsenziyalaganingizni ko'rsatish uchun MIT
identifikatorini qo'shing:
Fayl nomi: Cargo.toml
[package]
name = "kalkulyator"
license = "MIT"
Agar siz SPDX da ko'rinmaydigan litsenziyadan foydalanmoqchi bo'lsangiz, ushbu litsenziya matnini faylga joylashtirishingiz, faylni loyihangizga kiritishingiz kerak, va keyin license
kalitidan foydalanish oʻrniga oʻsha fayl nomini koʻrsatish uchun license-file
dan foydalaning.
Loyihangiz uchun qaysi litsenziya to'g'ri kelishi haqidagi ko'rsatmalar ushbu kitob doirasidan tashqarida. Rust hamjamiyatidagi(community) ko'p odamlar o'z loyihalarini Rust bilan bir xil tarzda MIT OR Apache-2.0
qo'sh litsenziyasidan foydalangan holda litsenziyalashadi. Ushbu amaliyot shuni ko'rsatadiki, loyihangiz uchun bir nechta litsenziyaga ega bo'lish uchun OR
bilan ajratilgan bir nechta litsenziya identifikatorlarini ham belgilashingiz mumkin.
Noyob nom, versiya, tavsif(description) va litsenziya qoʻshilgan holda nashr etishga tayyor boʻlgan loyiha uchun Cargo.toml fayli quyidagicha koʻrinishi mumkin:
Fayl nomi: Cargo.toml
[package]
name = "kalkulyator"
version = "0.1.0"
edition = "2021"
description = "Sanoq tizimlari bilan ishlaydigan kalkulyator"
license = "MIT OR Apache-2.0"
[dependencies]
Cargo hujjatlarida boshqalar sizning cratengizni osongina topishi va undan foydalanishi uchun siz belgilashingiz mumkin bo'lgan boshqa metama'lumotlar tasvirlangan.
Crates.io-da nashr qilish
Endi siz hisob(account) yaratdingiz, API tokeningizni saqladingiz, cratengiz uchun nom tanladingiz va kerakli metamaʼlumotlarni koʻrsatdingiz, siz nashr(publish) qilishga tayyorsiz! Crateni nashr qilish boshqalar foydalanishi uchun crates.io saytiga ma'lum bir versiyani yuklaydi.
Ehtiyot bo'ling, chunki nashr doimiydir(permanent). Versiyani hech qachon qayta yozib bo'lmaydi va kodni o'chirib bo'lmaydi.crates.io -ning asosiy maqsadlaridan biri doimiy kod arxivi bo'lib xizmat qilishdir, shunda crates.io-dan cratelarga bog'liq bo'lgan barcha loyihalar o'z ishini davom ettiradi. Versiyani o'chirishga ruxsat berish bu maqsadni amalga oshirishni imkonsiz qiladi. Biroq, siz nashr(publish) etishingiz mumkin bo'lgan crate versiyalari soniga cheklov yo'q.
cargo publish
buyrug'ini qayta ishga tushiring. Endi u muvaffaqiyatli bo'lishi kerak:
$ cargo publish
Updating crates.io index
Packaging kalkulyator v0.1.0 (file:///projects/kalkulyator)
Verifying kalkulyator v0.1.0 (file:///projects/kalkulyator)
Compiling kalkulyator v0.1.0
(file:///projects/kalkulyator/target/package/kalkulyator-0.1.0)
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
Uploading kalkulyator v0.1.0 (file:///projects/kalkulyator)
Tabriklaymiz! Siz endi kodingizni Rust hamjamiyatiga(community) ulashdingiz va har kim o'z loyihasiga dependency sifatida cratengizni osongina qo'shishi mumkin.
Mavjud cratening yangi versiyasini nashr qilish
Cratengizga oʻzgartirishlar kiritib, yangi versiyani chiqarishga tayyor boʻlgach, Cargo.toml faylida koʻrsatilgan version
qiymatini oʻzgartirasiz va qayta nashr qilasiz. Siz kiritgan o'zgartirishlar turiga qarab keyingi versiya raqami qanday bo'lishini aniqlash uchun semantik versiya qoidalaridan foydalaning.
Keyin yangi versiyani yuklash uchun cargo publish
ni ishga tushiring.
Crates.io-dan cargo yank
bilan eskirgan versiyalar
Cratening oldingi versiyalarini olib tashlamasangiz ham, kelajakdagi loyihalarni ularni yangi dependency sifatida qo'shishning oldini olishingiz mumkin. Bu crate versiyasi bir yoki boshqa sabablarga ko'ra buzilganda foydalidir. Bunday vaziyatlarda Cargo crate versiyasini yanking(tortib) olishni qo'llab-quvvatlaydi.
Versiyani yanking o'zgartirish yangi loyihalarning ushbu versiyaga bog'lanishiga to'sqinlik qiladi, lekin shunga qaramay, unga bog'liq bo'lgan barcha mavjud loyihalarni ishlashni davom ettirishga imkon beradi. Aslini olganda, yank degani Cargo.lock bilan barcha loyihalar buzilmasligini va kelajakda yaratilgan Cargo.lock fayllari yanked versiyasidan foydalanmasligini anglatadi.
Cratening versiyasini tortib olish uchun siz avval nashr qilgan crate jildida cargo yank
ni ishga tushiring va qaysi versiyani yank qilishni belgilang. Misol uchun, agar biz kalkulyator
nomli 1.0.1 versiyasini chop etgan bo'lsak va biz uni yank qilib olmoqchi bo'lsak, kalkulyator
loyihasi jildida biz quyidagi amllarni bajaramiz:
$ cargo yank --vers 1.0.1
Updating crates.io index
Yank kalkulyator@1.0.1
Buyruqga --undo
ni qo'shish orqali siz yank
ni bekor qilishingiz va loyihalarni versiyaga qarab qaytadan boshlashga ruxsat berishingiz mumkin:
$ cargo yank --vers 1.0.1 --undo
Updating crates.io index
Unyank kalkulyator@1.0.1
Yank hech qanday kodni o'chirmaydi. U, masalan, tasodifan yuklangan secretlarni o'chira olmaydi. Agar bu sodir bo'lsa, siz ushbu secretlarni darhol tiklashingiz kerak.
Cargo Workspacelar
12-bobda biz bianry crate va kutubxona cratesini o'z ichiga olgan paketni yaratdik. Loyihangiz rivojlanib borgan sari, kutubxona(library) cratesi kattalashib borishini va paketingizni bir nechta kutubxona cratelariga bo'lishni xohlayotganingizni ko'rishingiz mumkin. Cargo tandemda ishlab chiqilgan bir nechta tegishli paketlarni boshqarishga yordam beradigan workspaces deb nomlangan xususiyatni taklif etadi.
Workspace yaratish
workspace - bu bir xil Cargo.lock va output(chiqish) jildiga ega bo'lgan paketlar to'plami. Keling, workspcedan foydalangan holda loyiha yarataylik - biz workspacening tuzilishiga e'tibor qaratishimiz uchun arzimas koddan foydalanamiz. Workspaceni tuzishning bir necha yo'li mavjud, shuning uchun biz faqat bitta umumiy usulni ko'rsatamiz. Binary(ikkilik) va ikkita kutubxonani o'z ichiga olgan workspacega ega bo'lamiz. Asosiy funksionallikni ta'minlaydigan binary ikkita kutubxonaga bog'liq bo'ladi. Bitta kutubxona bitta_qoshish
funksiyasini, ikkinchi kutubxona esa ikkita_qoshish
funksiyasini taqdim etadi.
Ushbu uchta crate bir xil workspacening bir qismi bo'ladi. Biz workspaceni uchun yangi jild yaratishdan boshlaymiz:
$ mkdir qoshish
$ cd qoshish
Keyinchalik, qoshuvchi jildida biz butun workspaceni sozlaydigan Cargo.toml faylini yaratamiz. Bu faylda [package]
boʻlimi boʻlmaydi.
Buning o'rniga, u binary(ikkilik) crate yordamida paketga yo'lni ko'rsatib, workspacega a'zolar qo'shish imkonini beruvchi [workspace]
bo'limidan boshlanadi; bu holda, bu yo'l qoshuvchi:
Fayl nomi: Cargo.toml
[workspace]
members = [
"qoshuvchi",
]
Keyin, qoshuvchi jilida cargo new
ni ishga tushirish orqali qoshuvchi
binary cratesini yaratamiz:
$ cargo new qoshuvchi
Created binary (application) `qoshuvchi` package
Ushbu nuqtada biz cargo build
ni ishga tushirish orqali worksoaceni qurishimiz mumkin. Sizning qoshuvchi jildingiz quyidagicha ko'rinishi kerak:
├── Cargo.lock
├── Cargo.toml
├── qoshuvchi
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
Workspaceda kompilyatsiya qilingan artefaktlar joylashtiriladigan top leveldagi bitta target jildi mavjud; qoshuvchi
paketi o'zining target jildiga ega emas. Agar biz qoshuvchi jildi ichidan cargo build
ni ishga tushirsak ham, kompilyatsiya qilingan artefaktlar hali ham qoshish/qoshuvchi/target emas, balki qoshish/target da tugaydi. Cargo workspacedagi target jildini shunday tuzadi, chunki workspacedagi cratelar bir-biriga bog'liq bo'lishi kerak. Agar har bir crate o'zining target jildiga ega bo'lsa, har bir crate artefaktlarni o'zining target jildiga joylashtirish uchun workspacedagi boshqa cratelarning har birini qayta kompilyatsiya qilishi kerak edi. Bitta target jildini baham ko'rish(share) orqali cratelar keraksiz rebuildingdan qochishi mumkin.
Workspaceda ikkinchi paketni yaratish
Keyinchalik, workspaceda boshqa a'zolar(member) paketini yaratamiz va uni bitta_qoshish
deb nomlaymiz. members
ro'yxatida bitta_qoshish yo'lini belgilash uchun top leveldagi Cargo.toml ni o'zgartiring:
Fayl nomi: Cargo.toml
[workspace]
members = [
"qoshuvchi",
"bitta_qoshish",
]
Keyin bitta_qoshish
nomli yangi kutubxonalibrary cratesini yarating:
$ cargo new bitta_qoshish --lib
Created library `bitta_qoshish` package
Sizning qoshish jildingizda endi quyidagi jild va fayllar bo‘lishi kerak:
├── Cargo.lock
├── Cargo.toml
├── bitta_qoshish
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── qoshuvchi
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
bitta_qoshish/src/lib.rs fayliga bitta_qoshish
funksiyasini qo'shamiz:
Fayl nomi: bitta_qoshish/src/lib.rs
pub fn bitta_qoshish(x: i32) -> i32 {
x + 1
}
Endi biz kutubxonamizga ega bo'lgan bitta_qoshish
paketiga bog'liq bo'lgan qoshuvchi
paketiga ega bo'lishimiz mumkin. Birinchidan, biz qoshuvchi/Cargo.toml ga bitta_qoshish
yo'liga bog'liqlikni qo'shishimiz kerak.
Fayl nomi: qoshuvchi/Cargo.toml
[dependencies]
bitta_qoshish = { path = "../bitta_qoshish" }
Cargo workspacedagi cratelar bir-biriga bog'liq bo'ladi deb o'ylamaydi, shuning uchun biz qaramlik munosabatlari(relationship) haqida aniq bo'lishimiz kerak.
Keyin, keling, qoshuvchi
cratedagi bitta_qoshish
funksiyasidan (bitta_qoshish
cratesidan) foydalanamiz. qoshuvchi/src/main.rs faylini oching va yangi bitta_qoshish
kutubxona cratesini qamrab olish uchun tepaga use
qatorini qo'shing. Keyin 14-7 roʻyxatdagi kabi bitta_qoshish
funksiyasini chaqirish uchun main
funksiyani oʻzgartiring.
Fayl nomi: qoshuvchi/src/main.rs
use bitta_qoshish;
fn main() {
let raqam = 10;
println!("Salom, Rust! {raqam} plyus bir {} ga teng!", bitta_qoshish::bitta_qoshish(raqam));
}
Keling, yuqori darajadagi qoshish jildida cargo build
ni ishga tushirish orqali workspaceni build qilaylik!
$ cargo build
Compiling bitta_qoshish v0.1.0 (file:///projects/qoshish/bitta_qoshish)
Compiling qoshuvchi v0.1.0 (file:///projects/qoshish/qoshuvchi)
Finished dev [unoptimized + debuginfo] target(s) in 0.68s
Binary crateni qoshish jildidan ishga tushirish uchun biz -p
argumenti va cargo run
bilan paket nomidan foydalanib workspaceda qaysi paketni ishga tushirishni belgilashimiz mumkin:
$ cargo run -p qoshuvchi
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/qoshuvchi`
Salom, Rust! 10 plyus bir 11 ga teng!
Bu kodni qoshuvchi/src/main.rs da ishga tushiradi, bu bitta_qoshish
cratesiga bog'liq.
Workspacedagi tashqi(external) paketga bog'liqlik
E'tibor bering, workspaceda har bir crate jildida Cargo.lock emas, balki top leveldagi faqat bitta Cargo.lock fayli mavjud. Bu barcha cratelar barcha depencilarning(bog'liqlik) bir xil versiyasidan foydalanishini ta'minlaydi. Agar biz qoshuvchi/Cargo.toml va bitta_qoshish/Cargo.toml fayllariga rand
paketini qo'shsak, Cargo ikkalasini ham rand
ning bitta versiyasida hal qiladi va buni bitta Cargo.lockda qayd etadi. Workspacedagi barcha cratelarni bir xil depensilardan foydalanishga aylantirish, cratelarning har doim bir-biriga mos kelishini anglatadi. Keling, bitta_qoshish/Cargo.toml faylidagi [dependencies]
bo'limiga rand
cratesini qo'shamiz, shunda biz bitta_qoshish
cratesida rand
cratesidan foydalanishimiz mumkin:
Fayl nomi: bitta_qoshish/Cargo.toml
[dependencies]
rand = "0.8.5"
Endi biz bitta_qoshish/src/lib.rs fayliga use rand;
ni qo'shishimiz mumkin va qoshish jildida cargo build
-ni ishga tushirish orqali butun workspaceni build qilish rand
cratesini olib keladi va kompilyatsiya qiladi. Biz bitta ogohlantirish olamiz, chunki biz qamrab olgan rand
ni nazarda tutmayapmiz:
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling bitta_qoshish v0.1.0 (file:///projects/qoshish/bitta_qoshish)
warning: unused import: `rand`
--> bitta_qoshish/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: `bitta_qoshish` (lib) generated 1 warning
Compiling qoshuvchi v0.1.0 (file:///projects/qoshish/qoshuvchi)
Finished dev [unoptimized + debuginfo] target(s) in 10.18s
Top leveldagi Cargo.lock endi bitta_qoshish
rand
ga bog'liqligi(dependency) haqida ma'lumotni o'z ichiga oladi. Biroq, workspacening biror joyida rand
ishlatilsa ham, ularning Cargo.toml fayllariga rand
qo'shmagunimizcha, biz uni workspacedagi boshqa cratelarda ishlata olmaymiz. Masalan, agar biz qoshuvchi
paketi uchun qoshuvchi/src/main.rs fayliga use rand;
qo'shsak, xatoga duch kelamiz:
$ cargo build
--snip--
Compiling qoshuvchi v0.1.0 (file:///projects/qoshish/qoshuvchi)
error[E0432]: unresolved import `rand`
--> qoshuvchi/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
Buni tuzatish uchun qoshuvchi
paketi uchun Cargo.toml faylini tahrirlang va rand
ham unga dependency(bog'liqligini) ekanligini ko'rsating. qoshuvchi
paketini yaratish Cargo.lock dagi qoshuvchi
uchun depensiar ro'yxatiga rand
qo'shadi, lekin rand
ning qo'shimcha nusxalari yuklab olinmaydi. Cargo rand
paketidan foydalangan holda workspacedagi har bir cratedagi har bir crate bir xil versiyadan foydalanishini taʼminladi, bu bizga joyni tejaydi va workspacedagi cratelar bir-biriga mos kelishini taʼminlaydi.
Workspacega test qo'shish
Yana bir yaxshilanish uchun, keling, bitta_qoshish
cratesidagi bitta_qoshish::bitta_qoshish
funksiyasi testini qo'shamiz:
Fayl nomi: bitta_qoshish/src/lib.rs
pub fn bitta_qoshish(x: i32) -> i32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ishlamoqda() {
assert_eq!(3, bitta_qoshish(2));
}
}
Top-leveldagi qoshish jildida cargo test
-ni ishga tushiring. Shunga o'xshash tuzilgan workspaceda cargo test
ni o'tkazish workspacedagi barcha cratelar uchun testlarni o'tkazadi:
$ cargo test
Compiling bitta_qoshish v0.1.0 (file:///projects/qoshish/bitta_qoshish)
Compiling adder v0.1.0 (file:///projects/qoshish/qoshuvchi)
Finished test [unoptimized + debuginfo] target(s) in 0.27s
Running unittests src/lib.rs (target/debug/deps/bitta_qoshish-f0253159197f7841)
running 1 test
test tests::ishlamoqda ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/qoshuvchi-49979ff40686fa8e)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests bitta_qoshish
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Chiqishning(output) birinchi qismida bitta_qoshish
cratesidagi ishlamoqda
testi muvaffaqiyatli o'tganligi ko'rsatilgan. Keyingi bo'limda qoshuvchi
cratesida nol testlar topilganligi ko'rsatilgan, so'ngra oxirgi bo'lim bitta_qoshish
cratesida nol hujjat testlari topilganligini ko'rsatadi.
Bundan tashqari, biz top leveldagi jilddan -p
flagidan foydalanib va biz test qilib ko'rmoqchi bo'lgan crate nomini ko'rsatib, workspacedagi ma'lum bir crate uchun testlarni o'tkazishimiz mumkin:
$ cargo test -p bitta_qoshish
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/bitta_qoshish-b3235fea9a156f74)
running 1 test
test tests::ishlamoqda ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests bitta_qoshish
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Bu chiqishda cargo test
koʻrsatilgan, faqat bitta_qoshish
cratesi uchun stestlar oʻtkazilgan va qoshuvchi
cratesi testlari oʻtkazilmagan.
Agar siz workspacedagi cratelarni crates.io-ga nashr(publish) qilsangiz, workspacedagi har bir crate alohida nashr etilishi kerak bo'ladi.
cargo test
singari, biz p
flagidan foydalanib va nashr qilmoqchi bo'lgan crate nomini ko'rsatib, workspacemizda ma'lum bir crateni nashr qilishimiz mumkin.
Qo'shimcha mashq qilish uchun ushbu workspacega bitta_qoshish
cratesga o'xshash ikkita_qoshish
cratesini qo'shing!
Loyihangiz o'sib ulg'aygan sayin, workspacedan foydalanishni o'ylab ko'ring: bitta katta kod blokidan ko'ra kichikroq, individual komponentlarni tushunish osonroq. Bundan tashqari, cratelarni workspaceda saqlash, agar ular bir vaqtning o'zida tez-tez almashtirilsa, cratelar orasidagi muvofiqlashtirishni osonlashtirishi mumkin.
Binary(ikkilik) fayllarni cargo install
bilan o'rnatish
cargo install
buyrug'i binary cratelarni mahalliy(local) sifatida o'rnatish va ishlatish imkonini beradi. Bu tizim paketlarini almashtirish uchun mo'ljallanmagan; Bu Rust dasturchilari uchun crates.io saytida boshqalar baham ko'rgan toollarni o'rnatishning qulay usuli bo'lishi kerak. E'tibor bering, siz faqat binary targetlarga ega bo'lgan paketlarni o'rnatishingiz mumkin. binary target bu o'z-o'zidan ishga tushirilmaydigan, lekin mos bo'lgan kutubxona(library) targetdidan farqli o'laroq, src/main.rs fayli yoki cratening bir qismi sifatida bajariladigan boshqa faylni o'z ichiga olgan bajariladigan dastur. Boshqa dasturlarga kiritish uchun. Odatda, cratelar README faylida crate va kutubxona ekanligi, binary targetli yoki har ikkalasi haqida ma'lumotga ega.
cargo install
bilan o'rnatilgan barcha binary fayllar o'rnatish ildizining(root) bin jildida saqlanadi. Rust-ni rustup.rs yordamida o'rnatgan bo'lsangiz va hech qanday maxsus konfiguratsiyaga ega bo'lmasangiz, bu jild $HOME/.cargo/bin bo'ladi. cargo install
bilan oʻrnatgan dasturlarni ishga tushirish uchun jildingiz $PATH
da ekanligiga ishonch hosil qiling.
Masalan, 12-bobda biz fayllarni qidirish uchun ripgrep
deb nomlangan grep
toolining Rust ilovasi mavjudligini eslatib o'tdik. Keling ripgrep
ni o'rnatish uchun biz quyidagilarni ishga tushirishimiz mumkin:
$ cargo install ripgrep
Updating crates.io index
Downloaded ripgrep v13.0.0
Downloaded 1 crate (243.3 KB) in 0.88s
Installing ripgrep v13.0.0
--snip--
Compiling ripgrep v13.0.0
Finished release [optimized + debuginfo] target(s) in 3m 10s
Installing ~/.cargo/bin/rg
Installed package `ripgrep v13.0.0` (executable `rg`)
Chiqishning ikkinchidan oxirgi qatori o'rnatilgan binary faylning joylashuvi va nomini ko'rsatadi, bu ripgrep
holatida rg
dir. O'rnatish jildi $PATH
da bo'lsa, avval aytib o'tilganidek, siz rg --help
ni ishga tushirishingiz va fayllarni qidirish uchun tezroq, rustda kuchidan foydalanib yozilgan tooldan foydalanishni boshlashingiz mumkin!
Maxsus buyruqlar bilan cargoni kengaytirish
Cargo cargoni o'zini o'zgartirmasdan, uni yangi kichik buyruqlar bilan kengaytirishingiz uchun mo'ljallangan. Agar $PATH
-dagi binary fayl cargo-birnarsa
deb nomlangan bo'lsa, uni cargo birnarsasi
ni ishga tushirish orqali uni Cargo kichik buyrug'i kabi ishga tushirishingiz mumkin. Bu kabi maxsus buyruqlar cargo --list
ishga tushirilganda ham ro'yxatga olinadi. Kengaytmalarni(extension) o'rnatish va keyin ularni xuddi o'rnatilgan Cargo toollari kabi ishga tushirish uchun cargo install
dan foydalanish imkoniyati Cargo dizaynining o'ta qulay afzalligidir!
Xulosa
Cargo va crates.io bilan kod almashish Rust ekotizimini turli xil vazifalar uchun foydali qiladigan dizaynning bir qismidir. Rustning standart kutubxonasi(standard library) kichik va barqaror(stable), lekin cratelarni almashish(share), ishlatish va tildagidan farqli vaqt jadvalida yaxshilash oson. crates.io saytida sizga foydali bo'lgan kodni baham(share) ko'rishdan uyalmang, ehtimol u boshqa birovga ham foydali bo'lishi mumkin!
Smart Pointerlar
Pointer bu xotirada biror manzilni o'z ichiga olgan o'zgaruvchi uchun umumiy
tushuncha. Ushbu manzil biror boshqa ma'lumotga ishora qiladi. Rustda eng keng
tarqalgan pointer turi bu siz 4-bobda o'rgangan reference hisoblanadi.
Referencelar &
belgisi bilan ko'rsatiladi va qaratilgan qiymatni qarzga oladi.
Ularning ma'lumotga murojaat qilishdan boshqa hech qanday maxsus qobiliyati
bo'lmaydi.
Smart pointerlar esa pointer kabi ishlaydigan, lekin qo'shimcha metama'lumotlar va imkoniyatlarga ega ma'lumot tuzilmalaridir. Smart pointerlar tushunchasi faqatgina Rustda yagona emas: smart pointerlar C++da paydo bo'lgan va boshqa tillarda ham mavjud. Rust standart kutubxonasi taqdim etadigan turli xil smart pointerlarga ega bo'lib, ular referencelardan tashqari funksionallikni ta'minlaydi. Umumiy tushunchani o'rganish uchun biz smart pointerlarning bir nechta turli misollarini ko'rib chiqamiz, jumladan reference hisoblash smart pointer turi. Ushbu pointer sizga egalar sonini kuzatish orqali ma'lumotning bir nechta egalari bo'lish imkonini beradi va egalari qolmaganda ma'lumotni tozalaydi.
Rust o'zining egalik va qarz olish tushunchasi bilan referencelar va smart pointerlar o'rtasida qo'shimcha farqga ega: referencelar faqat ma'lumotlarni qarzga olsa, ko'p hollarda smart pointerlar ma'lumotga egalik qiladilar.
Garchi biz ularni o‘sha paytda shunday deb atamagan bo‘lsak-da, biz ushbu
kitobda bir nechta smart pointerlarga duch keldik, jumladan, 8-bobdagi String
va Vec<T>
. Bu ikkala tur ham smart pointer hisoblanadi, chunki u biror
xotiraga egalik qiladi va sizga uni manipulyatsiya qilish imkonini beradi.
Shuningdek, ularda metama'lumot va qo'shimcha imkoniyatlar yoki kafolatlar
bo'ladi. Masalan, String
o'z sig'imini metama'lumot sifatida saqlaydi va uning
ma'lumoti har doim UTF-8 bo'lishini ta'minlash uchun qo'shimcha xususiyatga ega.
Smart pointerlar odatda structlar yordamida amalga oshiriladi. Oddiy structdan
farqli o'laroq, smart pointerlar Deref
va Drop
traitlarini amalga oshiradi.
Deref
traiti smart pointer struct-ning instancesiga reference kabi o'zini
tutish imkonini beradi, shunday qilib siz ikkala referencelar yoki smart
pointerlar bilan ishlash uchun kod yozishingiz mumkin. Drop
traiti smart
pointerning instancesi scopedan chiqib ketganda ishga tushadigan kodni
moslashtirish imkonini beradi. Ushbu bobda biz ikkala traitni muhokama qilamiz
va nima uchun ular smart pointerlar uchun muhimligini ko'rsatamiz.
Smart pointer patterni Rustda tez-tez ishlatiladigan umumiy dizayn patterni ekanligini hisobga olsak, bu bobda mavjud bo'lgan barcha smart pointerlar qamrab olinmaydi. Ko'pgina kutubxonalarda o'zlarining smart pointerlari mavjud va siz hatto o'zingiznikini yozishingiz mumkin. Biz standart kutubxonadagi eng keng tarqalgan smart pointerlarni ko'rib chiqamiz:
Box<T>
heapga qiymatlarni joylashtirish uchunRc<T>
, bir nechta egalik qilish imkonini beruvchi referencelar hisoblash turiRef<T>
vaRefMut<T>
,RefCell<T>
orqali kiriladi, bu qarz olish qoidalariga kompilyatsiya vaqti o'rniga runtimeda rioya qilishga majburlovchi tur
Bundan tashqari, biz ichki o'zgaruvchanlik patternini ko'rib chiqamiz, bunda o'zgarmas tur ichki qiymatni o'zgartirish uchun APIni ochib beradi. Shuningdek, biz reference davrlarini muhokama qilamiz: ular xotirani oqishiga olib kelishi va ularni qanday oldini olish mumkin.
Keling sho'ng'iymiz!
Heapdagi ma'lumotlarni ko'rsatish uchun Box<T>
dan foydalanish
Eng sodda smart pointer bu box bo'lib, uning turi Box<T>
deb yoziladi.
Boxlar sizga ma'lumotlarni stackda emas, balki heapda saqlashga imkon beradi.
Stackda esa heapdagi ma'lumotlariga pointer qoladi. Stack va heap o'rtasidagi
farqni ko'rib chiqish uchun 4-bobga qarang.
Boxlar o'z ma'lumotlarini stackda emas, balki heapda saqlashdan tashqari, ishlash bo'yicha qo'shimcha xarajatlarga ega emas. Lekin ularda ko'p qo'shimcha imkoniyatlar ham yo'q. Siz ulardan ko'pincha quyidagi holatlarda foydalanasiz:
- Agar sizda kompilyatsiya vaqtida o'lchami noma'lum bo'lgan tur mavjud bo'lsa va siz aniq o'lchamni talab qiladigan kontekstda ushbu turdagi qiymatdan foydalanmoqchi bo'lsangiz
- Agar sizda katta hajmdagi maʼlumotlar mavjud boʻlsa va siz egalik huquqini oʻtkazganingizda maʼlumotlardan nusxa olinmasligiga ishonch hosil qilmoqchi bo'lsangiz
- Agar siz biror qiymatga egalik qilmoqchi bo'lsangiz va siz uni ma'lum bir turda bo'lishiga emas, balki ma'lum bir traitni implement qiluvchi tur bo'lishi haqida qayg'ursangiz
Birinchi holatni “Rekursiv turlarni Boxlar bilan qo'llash” bo‘limida ko‘rsatamiz. Ikkinchi holatda, katta hajmdagi ma'lumotlarga egalik huquqini o'tkazish uzoq vaqt talab qilishi mumkin, chunki ma'lumotlar stackdan ko'chiriladi. Bunday vaziyatda ishlashni yaxshilash uchun biz katta hajmdagi ma'lumotlarni heapda box ichida saqlashimiz mumkin. Shundan so'ng, pointer ma'lumotlarining faqat kichik miqdori stackdan ko'chiriladi, heapdagi u reference qilingan ma'lumotlar esa bir joyda qoladi. Uchinchi holat trait object sifatida tanilgan va butun 17-bob shu mavzuga bag'ishlangan, “Turli turdagi qiymatlarga ruxsat beruvchi Trait Objectlaridan foydalanish” o'sha mavzu. Shunday qilib, bu erda o'rgangan narsalaringizni 17-bobda yana qo'llaysiz!
Heapda ma'lumotlarni saqlash uchun Box<T>
dan foydalanish
Box<T>
uchun heap xotiradan foydalanish holatini muhokama qilishdan oldin, biz
sintaksisni va Box<T>
ichida saqlangan qiymatlar bilan qanday o'zaro aloqa
qilishni ko'rib chiqamiz.
15-1 ro'yxatda i32
qiymatini heapda saqlash uchun boxdan qanday foydalanish
ko'rsatilgan:
Fayl nomi: src/main.rs
fn main() { let b = Box::new(5); println!("b = {}", b); }
Biz b
o'zgaruvchini heapda joylashgan 5
ga point qiluvchi Box
qiymatiga ega
bo'lishi uchun e'lon qilamiz. Ushbu dastur b = 5
ni chop etadi; bu holda biz
boxdagi ma'lumotlarga, stackda bo'lgani kabi kirishimiz mumkin. main
ning
oxiridagi b
kabi boxlar scopedan chiqib ketganda, xuddi egalik qilingan
qiymatlarga o'xshab, u ham xotiradan o'chiriladi. O'chirilish ham box (stackda
saqlanuvchi) uchun va u point qiluvchi ma'lumotlar (heapda saqlanuvchi) uchun
ham sodir bo'ladi.
Heapda bitta qiymat saqlash unchalik foydali emas, shuning uchun boxlarni o'zini
bu tarzda ko'pincha ishlatmaysiz. Ko'pincha holatlarda bitta i32
kabi
qiymatlarni stackda saqlash maqsadga muvofiq bo'ladi. Keling, boxlar, agar bizda
boxlar bo'lmasa, ruxsat berilmaydigan turlarni e'lon qilishga imkon beradigan
holatni ko'rib chiqaylik.
Rekursiv turlarni Boxlar bilan qo'llash
Rekursiv turning qiymati o'zining bir qismi sifatida bir xil turdagi boshqa qiymatga ega bo'lishi mumkin. Rekursiv turlar muammo tug'diradi, chunki kompilyatsiya vaqtida Rust tur qancha joy egallashini bilishi kerak. Biroq, rekursiv turdagi qiymatlarni joylashtirish nazariy jihatdan cheksiz davom etishi mumkin, shuning uchun Rust qiymat uchun qancha joy kerakligini bilmaydi. Boxlar ma'lum o'lchamga ega bo'lganligi sababli, biz rekursiv tur ta'rifiga box kiritish orqali rekursiv turlarni qo'llashimiz mumkin.
Rekursiv turga misol sifatida keling, cons listni o'rganamiz. Bu funktsional dasturlash tillarida keng tarqalgan ma'lumotlar turi hisoblanadi. Biz e'lon qiladigan cons list turi rekursiyadan tashqari sodda; shuning uchun biz ishlaydigan misoldagi tushunchalar rekursiv turlarni o'z ichiga olgan murakkab vaziyatlarga tushganingizda foydali bo'ladi.
Cons List haqida batafsil ma'lumot
Cons list Lisp dasturlash tili va uning dialektlaridan kelib chiqqan va
ichma-ich juftliklardan tashkil topgan maʼlumotlar strukturasi boʻlib, linked
listning Lispdagi versiyasi hisoblanadi. Uning nomi Lispdagi cons
funktsiyasidan (“construct function” uchun qisqartma) kelib chiqqan bo'lib,
uning ikkita argumentidan yangi juftlik yaratadi. Qiymat va boshqa juftlikdan
iborat bo'lgan juftlikda cons
ni chaqirish orqali biz rekursiv juftliklardan
iborat bo'lgan cons list tuzishimiz mumkin.
Misol uchun, bu yerda 1, 2, 3 ro'yxatini o'z ichiga olgan cons listining psevdokod ko'rinishi, har bir juft qavs ichida:
(1, (2, (3, Nil)))
Cons listdagi har bir element ikkita elementni o'z ichiga oladi: shu elementning
qiymati va keyingi element. Ro'yxatning oxirgi elementida keyingi elementsiz
faqat Nil
deb nomlangan qiymatdan iborat bo'ladi. Cons list cons
funksiyasini rekursiv chaqirish orqali hosil qilinadi. Rekursiyaning tubidagi
holatini bildiruvchi qoidaga aylangan nom Nil
hisoblanadi. E'tibor bering, bu
6-bobdagi “null” yoki “nil” tushunchasi bilan bir xil emas, ya'ni noto'g'ri yoki
yo'q qiymat.
Cons list ma'lumotlar tuzilmasi Rust-da tez-tez ishlatilmaydi. Ko'pincha Rust-da
sizga elementlar ro'yxati kerak bo'lsa, Vec<T>
foydalanish uchun yaxshiroq
tanlovdir. Boshqa, murakkabroq rekursiv ma'lumot turlaridan foyladanish bir
qancha vaziyatlarda foydalidir, ammo ushbu bobdagi cons listdan boshlab, boxlar
qanday qilib, rekursiv ma'lumot turini e'lon qilishga imkon berishini
o'rganishimiz mumkin.
Ro'yxat 15-2 cons list uchun enum ko'rinishini o'z ichiga oladi. E'tibor bering,
ushbu kod hali kompilyatsiya qilinmaydi, chunki List
turi ma'lum hajmga ega
emas, biz buni tushuntiramiz.
Fayl nomi: src/main.rs
enum List {
Cons(i32, List),
Nil,
}
fn main() {}
Eslatma: Biz ushbu misol maqsadlari uchun faqat
i32
qiymatlarini o'z ichiga olgan cons listni amalga oshirmoqdamiz. Biz 10-bobda muhokama qilganimizdek, har qanday turdagi qiymatlarni saqlashi mumkin bo'lgan cons list turini generiklar yordamida e'lon qilishimiz mumkin edi.
List
turidan foydalanib 1, 2, 3
roʻyxatini saqlash 15-3 ro'yxat kabi
bo'ladi:
Fayl nomi: 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)));
}
Birinchi Cons
qiymati 1
va boshqa List
qiymatiga ega. Bu List
qiymati
2
va boshqa List
qiymatiga ega bo'lgan boshqa Cons
qiymatidir. Ushbu
List
qiymati 3
ni o'z ichiga olgan yana bitta Cons
qiymati va List
qiymati, nihoyat Nil
, ro'yxat oxirini bildiruvchi rekursiv bo'lmagan variant.
Agar biz 15-3 ro'yxatdagi kodni kompilyatsiya qilishga harakat qilsak, biz 15-4 ro'yxatda ko'rsatilgan xatoni olamiz:
$ 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 {
| ^^^^^^^^^
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
For more information about this error, try `rustc --explain E0072`.
error: could not compile `cons-list` due to previous error
Xato ushbu tur “cheksiz o'lchamga ega” ekanligini ko'rsatadi. Buning sababi
shundaki, biz List
ni rekursiv variant bilan e'lon qildik: u bevosita o'zining
boshqa qiymatini saqlaydi. Natijada, Rust List
qiymatini saqlash uchun qancha
joy kerakligini aniqlay olmaydi. Keling, nima uchun bu xatoga duch kelganimizni
qismlarga ajratamiz. Birinchidan, Rust rekursiv bo'lmagan turdagi qiymatni
saqlash uchun qancha joy kerakligini qanday hal qilishini ko'rib chiqamiz.
Rekursiv bo'lmagan turning o'lchamini hisoblash
6-bobda enum ta'riflarini muhokama qilganimizdagi 6-2 ro'yxatda e'lon qilingan
Xabar
enumini eslang:
enum Xabar { Chiqish, Kochirish { x: i32, y: i32 }, Yozish(String), RangTanlash(i32, i32, i32), } fn main() {}
Xabar
qiymati uchun qancha joy ajratish kerakligini aniqlash uchun Rust har
bir variantni ko'rib chiqadi va qaysi variantga ko'proq joy kerakligini
aniqlaydi. Rust Xabar::Chiqish
uchun hech qanday joy kerak emasligini,
Xabar::Kochirish
ikkita i32
qiymatini saqlash uchun yetarli joy kerakligini
aniqlaydi va hokazo. Faqat bitta variant qo'llanilishi sababli, Xabar
qiymatiga kerak bo'ladigan eng ko'p joy-bu uning eng katta variantini saqlash
uchun zarur bo'lgan joy hisoblanadi.
Rust 15-2 roʻyxatdagi List
enum kabi rekursiv turga qancha boʻsh joy
kerakligini aniqlashga harakat qilganda nima sodir boʻlishini bu bilan
taqqoslang. Kompilyator i32
turidagi qiymat va List
turidagi qiymatga ega
bo'lgan Cons
variantini ko'rib chiqishdan boshlaydi. Shuning uchun, Cons
uchun i32
va List
o'lchamiga teng bo'sh joy kerak bo'ladi. List
turiga
qancha xotira kerakligini aniqlash uchun kompilyator Cons
variantidan boshlab
variantlarni ko'rib chiqadi. Cons
variantida i32
turidagi qiymat va List
turidagi qiymat mavjud va bu jarayon 15-1-rasmda ko'rsatilganidek, cheksiz davom
etadi.
O'lchami ma'lum bo'lgan rekursiv turni e'lon qilish uchun Box<T>
dan foydalanish
Rust rekursiv e'lon qilingan turlar uchun qancha joy ajratish kerakligini aniqlay olmaganligi sababli, kompilyator ushbu foydali taklif bilan xatolik beradi:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
|
2 | Cons(i32, Box<List>),
| ++++ +
Ushbu taklifda "indirection" qiymatni to'g'ridan-to'g'ri saqlash o'rniga, biz pointerni qiymatga saqlash orqali qiymatni bilvosita saqlash uchun ma'lumotlar strukturasini o'zgartirishimiz kerakligini anglatadi.
Box<T>
pointer bo'lgani uchun Rust har doim Box<T>
uchun qancha joy
kerakligini biladi: pointerning o'lchami u ko'rsatayotgan ma'lumotlar miqdoriga
qarab o'zgarmaydi. Bu shuni anglatadiki, biz to'g'ridan-to'g'ri boshqa List
qiymati o'rniga Cons
variantiga Box<T>
qo'yishimiz mumkin. Box<T>
keyingi
List
qiymatiga ishora qiladi, bu qiymat Cons
varianti ichida emas, balki
heapda bo'ladi. G'oyaga ko'ra, bizda hali ham boshqa ro'yxatlarni o'z ichiga
olgan ro'yxatlar bilan yaratilgan ro'yxat mavjud, ammo bu amalga oshirish endi
elementlarni bir-birining ichiga emas, balki bir-birining yoniga joylashtirishga
o'xshaydi.
We can change the definition of the List
enum in Listing 15-2 and the usage of
the List
in Listing 15-3 to the code in Listing 15-5, which will compile:
Biz 15-2 ro'yxatidagi List
enumni va 15-3 ro'yxatidagi List
ning
qo'llanishini 15-5 ro'yxatidagi kodga o'zgartirishimiz mumkin, bu kompilyatsiya
bo'ladi:
Fayl nomi: 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)))))); }
Cons
variantiga i32
o'lchamiga va boxdagi pointer ma'lumotlarini saqlash
uchun bo'sh joy kerak. Nil
varianti hech qanday qiymatlarni saqlamaydi,
shuning uchun u Cons
variantiga qaraganda kamroq joy talab qiladi. Endi
bilamizki, har qanday List
qiymati i32
o‘lchamini va boxdagi pointer
ma’lumotlari hajmini egallaydi. Boxdan foydalanib, biz cheksiz, rekursiv
zanjirni buzdik, shuning uchun kompilyator List
qiymatini saqlash uchun
kerakli hajmni aniqlay oladi. 15-2-rasmda Cons
varianti hozir qanday
ko'rinishi ko'rsatilgan.
Boxlar faqat bilvosita va heapda joylashuvni ta'minlaydi; ularda biz boshqa smart pointerlarda ko'radigan boshqa maxsus imkoniyatlar yo'q. Ular, shuningdek, ushbu maxsus imkoniyatlarga ega bo'lgan ishlash xarajatlariga ega emaslar, shuning uchun ular bizga kerak bo'lgan yagona xususiyat bo'lgan cons list kabi holatlarda foydali bo'lishi mumkin. Biz 17-bobda boxlar uchun ko'proq foydalanish holatlarini ko'rib chiqamiz.
Box<T>
turi smart pointerdir, chunki u Deref
xususiyatini amalga oshiradi,
bu esa Box<T>
qiymatlariga reference kabi qarashga imkonini beradi. Box<T>
qiymati scopedan chiqib ketganda, box ko'rsatayotgan heapdagi ma'lumotlar ham
Drop
xususiyatini amalga oshirish tufayli tozalanadi. Ushbu ikki xususiyat biz
ushbu bobning qolgan qismida muhokama qiladigan boshqa smart pointer turlari
tomonidan taqdim etilgan funksionallik uchun yanada muhimroq bo'ladi. Keling,
ushbu ikki xususiyatni batafsil ko'rib chiqaylik.
Smart Pointerlarni Deref
Xususiyati Bilan Oddiy Havolalar Kabi Ishlatish
Deref
xususiyatini qo'llash, dereference operatorining *
(ko'paytirish
yoki glob operatori bilan adashtirmaslik kerak) xulq-atvorini sozlashga imkon
beradi. Smart pointerlarni Deref
xususiyati bilan oddiy havolalar kabi
qo'llasangiz, siz havolalar ustida ishlaydigan kod yozishingiz, shuningdek,
ushbu kodni smart pointerlar bilan ishlatishingiz mumkin bo'ladi.
Keling, avvalo, dereference operatori oddiy havolalar bilan qanday ishlashini
ko'rib chiqaylik. Keyin biz Box<T>
kabi maxsus turni e'lon qilishga harakat
qilamiz va dereference operatori nega bizning yangi e'lon qilgan turimizdagi
havola kabi ishlamayotganini ko'ramiz. Biz Deref
xususiyatini amalga oshirish
smart pointerlarning havolalarga o'xshash tarzda ishlashiga qanday imkon
berishini ko'rib chiqamiz. Keyin biz Rustning deref coercion xususiyatini va
u bizga havolalar yoki smart pointerlar bilan ishlashga qanday imkon berishini
ko'rib chiqamiz.
Eslatma: biz qurmoqchi bo'lgan
MyBox<T>
turi va haqiqiyBox<T>
o‘rtasida bitta katta farq bor: bizning versiyamiz o‘z ma’lumotlarini heapda saqlamaydi. Biz ushbu misolda e'tiborimizniDeref
ga qaratmoqdamiz, shuning uchun ma'lumotlarning qayerda saqlanishi pointerga o'xshash xatti-harakatlardan kamroq ahamiyatga ega.
Pointerni Qiymatga bog'lash
Muntazam havola pointerning bir turi bo'lib, pointerni boshqa joyda saqlangan
qiymatga o'q kabi tasavvur qilishning bir usuli. 15-6 ro'yxatda biz i32
qiymatiga havola yaratamiz va keyin qiymatga havolani bog'lash uchun dereference
operatoridan foydalanamiz:
Fayl nomi: src/main.rs
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
x
o'zgaruvchisi i32
turidagi 5
qiymatiga ega. Biz y
ni x
ning
havolasiga tenglashtiramiz. Biz x
5
ga teng ekanligini solishtirishimiz
mumkin. Ammo, agar biz y
dagi qiymatni solishtirmoqchi bo'lsak, kompilyator
haqiqiy qiymatni solishtirishi uchun *y
dan foydalanib, u havola qilgan
qiymatga (ya'ni, dereference) murojaat qilishimiz kerak. y
da dereference
qo'llaganimizdan so'ng, y
ishora qilib turgan butun son qiymatiga kirish
imkoniga ega bo'lamiz, bu 5
bilan solishtirishimizga imkon beradi.
Agar assert_eq!(5, y);
yozishga harakat qilganimizda, ushbu kompilyatsiya
xatoligini olgan bo'lar edik:
$ 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}`
= help: the following other types implement trait `PartialEq<Rhs>`:
f32
f64
i128
i16
i32
i64
i8
isize
and 6 others
= 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
Raqam va raqamga havola bilan solishtirishga yo'l qo'yilmaydi, chunki ular har xil turlar. Biz havola qilingan qiymatga murojaat qilish uchun dereference operatoridan foydalanishimiz kerak.
Box<T>
ni Havola Kabi Ishlatish
15-6 ro'yxatdagi kodni havola o'rniga Box<T>
ishlatgan holda qayta yozishimiz
mumkin; 15-7 ro'yxatdagi funksiyalarida Box<T>
da ishlatiladigan dereference
operatori 15-6 ro'yxatidagi havolada ishlatilgan dereference operatori
bilan bir xil tarzda ishlatiladi:
Fayl nomi: src/main.rs
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
15-7 va 15-6 ro'yxat o'rtasidagi asosiy farq shundaki, biz bu yerda y
ni x
qiymatiga havola emas, balki x
ning ko'chirilgan qiymatiga ishora qiluvchi
Box<T>
ning misoli qilib belgiladik. Oxirgi solishtiruvda biz dereference
operatoridan Box<T>
ko'rsatgichiga murojat qilish uchun xuddi y
havola
bo'lganida qilganimizdek bajarishimiz mumkin. Keyin biz Box<T>
ning o'ziga xos
xususiyatlarini o'rganamiz, bu bizga o'z turimizni e'lon qilish orqali
dereference operatoridan foydalanishga imkon beradi.
O'zimizning Aqlli Ko'rsatgichimizni E'lon Qilish
Keling, aqlli ko'rsatgichlar havolalardan qanday farq qilishini bilish uchun
standart kutubxona tomonidan taqdim etilgan Box<T>
turiga o'xshash aqlli
ko'rsatgichni yarataylik. Keyin biz dereference operatoridan foydalanish
qobiliyatini qanday qo'shishni ko'rib chiqamiz.
Box<T>
turi oxir-oqibat bitta elementga ega bo'lgan tuple struct sifatida
aniqlanadi, 15-8 ro'yxatda xuddi shu tarzda MyBox<T>
turini belgilaydi.
Shuningdek, Box<T>
da belgilangan new
funksiyaga mos keladigan new
funksiyani aniqlaymiz.
Fayl nomi: src/main.rs
struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() {}
Biz MyBox
nomli structni aniqlaymiz va T
generic parametrini e'lon qilamiz,
chunki biz turimiz istalgan turdagi qiymatlarni ushlab turishini xohlaymiz.
MyBox
turi T
turidagi bitta elementga ega bo'lgan tuple structdir.
MyBox::new
funksiyasi T
turidagi bitta parametrni oladi va berilgan qiymatni
ushlab turuvchi MyBox
misolini qaytaradi.
15-7 ro'yxatdagi main
funksiyasini 15-8 ro'yxatiga qo'shib, Box<T>
o'rniga
biz belgilagan MyBox<T>
turidan foydalanish uchun o'zgartirishga harakat
qilaylik. 15-9 ro'yxatdagi kod kompilyatsiya qilinmaydi, chunki Rust MyBox
ni
qanday qilib dereference qilishni bilmaydi.
Fayl nomi: 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);
}
Natijada kompilyatsiya xatosi:
$ 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
Bizning MyBox<T>
turini dereference qilib bo'lmaydi, chunki biz bu qobiliyatni
o'z turimizda qo'llamaganmiz. *
operatori yordamida dereference qilishni
yoqish uchun biz Deref
traitini qo`llaymiz.
Deref
Traitni Amalga Oshirish Orqali Turga Havola Kabi Munosabatda Bo'lish
10-bobning “Turga xos Traitni amalga
oshirish” boʻlimida muhokama
qilinganidek, traitni amalga oshirish uchun biz traitning talab qilinadigan
usullarini amalga oshirishimiz kerak. Standart kutubxona tomonidan taqdim
etilgan Deref
xususiyati bizdan self
qarz oladigan va ichki ma'lumotlarga
havolani qaytaradigan deref
nomli metodni qo'llashimizni talab qiladi. 15-10
ro'yxat MyBox
ta'rifiga qo'shish uchun Deref
amalga oshirilishini o'z ichiga
oladi:
Fayl nomi: 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); }
Type Target = T;
sintaksisi foydalanish uchun Deref
xususiyati uchun
bog`langan turni belgilaydi. Bog'langan turlar generic parametrni e'lon
qilishning biroz boshqacha usulidir, ammo hozircha ular haqida
tashvishlanishingiz shart emas; biz ularni 19-bobda batafsil yoritamiz.
Biz deref
metodining tanasini &self.0
bilan to'ldiramiz, shuning uchun
deref
biz *
operatori bilan kirmoqchi bo'lgan qiymatga havolani qaytaradi;
5-bobning “Har xil turlarni yaratish uchun nomli maydonlarsiz tuplelardan
foydalanish” boʻlimidan .0
tuple structidagi
birinchi qiymatga kirishini esga oling. MyBox<T>
qiymatida *
ni chaqiruvchi
15-9 ro'yxatdagi main
funksiya endi kompilyatsiya qilinadi va solishtiruvlar
o`tadi!
Deref
traitisiz kompilyator faqat &
havolalarini dereference qilishi mumkin.
deref
metodi kompilyatorga Deref
ni qo'llaydigan har qanday turdagi qiymatni
olish va deref
usulini chaqirish va &
havolasini olish imkoniyatini beradi.
15-9 ro'yxatda *y
ga kirganimizda, Rust sahna ortida ushbu kodni ishga
tushirdi:
*(y.deref())
Rust *
operatorini deref
metodini chaqirish va keyin oddiy dereference bilan
almashtiradi, shuning uchun deref
metodini chaqirish kerakligi haqida
o'ylamasligimiz kerak. Ushbu Rust xususiyati bizga oddiy havola yoki Deref
ni
qo'llaydigan turga ega bo'ladimi, bir xil ishlaydigan kod yozish imkonini
beradi.
deref
metodi qiymatga havolani qaytarishining sababi va *(y.deref())
qavslar
tashqarisidagi oddiy dereference hali ham zarur bo'lishi ownership tizimi bilan
bog'liq. Agar deref
usuli qiymatga havola o'rniga to'g'ridan-to'g'ri qiymatni
qaytargan bo'lsa, qiymat o'zidan
o'chiriladi. Biz MyBox<T>
ichidagi ichki
qiymatga egalik qilishni istamaymiz, bu holatda yoki ko'p hollarda biz
dereference operatoridan foydalanamiz.
Esda tutingki, *
operatori deref
metodini chaqirish va keyin *
operatorini
faqat bir marta chaqirish bilan almashtiriladi, har safar kodimizda *
dan
foydalanamiz. *
operatorini almashtirish cheksiz takrorlanmasligi sababli, biz
15-9 ro'yxatdagi assert_eq!
dagi 5
ga mos keladigan i32
turidagi
ma'lumotlarga ega bo`lamiz.
Funksiya va Metodlar bilan Yashirin Deref Coercion'lar
Deref coercion havolani Deref
xususiyatini boshqa turga havolada amalga
oshiradigan turga aylantiradi. Masalan, deref coercion &String
ni &str
ga
aylantirishi mumkin, chunki String
Deref
traitini amalga oshiradi va u
&str
ni qaytaradi. Deref coercion - bu Rust funksiyalar va metodlarga
argumentlar bo'yicha bajaradigan qulaylik va faqat Deref
traitini amalga
oshiradigan turlarda ishlaydi. Bu funksiya yoki metod ta'rifidagi parametr turiga
mos kelmaydigan funksiya yoki metodga argument sifatida ma'lum bir turdagi
qiymatga havolani uzatganimizda avtomatik ravishda sodir boʻladi. Deref
metodiga chaqiruvlar ketma-ketligi biz taqdim etgan turni parametr kerak
bo'lgan turga aylantiradi.
Rustga deref coercion qo'shildi, shuning uchun dasturchilar funktsiya va metod
chaqiruvlarini yozish uchun &
va *
bilan ko'p aniq havolalar va
dereferencelarni qo'shishlari shart emas. Deref coercion xususiyati bizga
havolalar yoki aqlli ko'rsatkichlar uchun ishlashi mumkin bo'lgan ko'proq kod
yozish imkonini beradi.
Deref coercionni amalda ko'rish uchun biz 15-8 ro'yxatda belgilagan MyBox<T>
turini hamda 15-10 ro'yxatiga qo'shgan Deref
ni amalga oshirishdan
foydalanamiz. Ro'yxat 15-11 string slice parametriga ega bo'lgan funksiyaning
ta'rifini ko'rsatadi:
Fayl nomi: src/main.rs
fn salom(nom: &str) { println!("Salom, {nom}!"); } fn main() {}
Biz salom
funksiyasini argument sifatida string slice bilan chaqirishimiz
mumkin, masalan, salom("Rust");
. Deref coercion 15-12 ro'yxatda
ko'rsatilganidek, MyBox<String>
turidagi qiymatga havola bilan salom
ni
chaqirish imkonini beradi:
Fayl nomi: 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 salom(nom: &str) { println!("Salom, {nom}!"); } fn main() { let m = MyBox::new(String::from("Rust")); salom(&m); }
Bu yerda biz &m
argumenti bilan salom
funksiyasini chaqiramiz, bu
MyBox<String>
qiymatiga havola. Biz Deref
traitini MyBox<T>
uchun
15-10 ro'yxatda amalga oshirganimiz uchun Rust deref
ni chaqirish orqali
&MyBox<String>
ni &String
ga aylantirishi mumkin. Standart kutubxona String
da Deref
ning amalga oshirilishini ta'minlaydi, bu string slice qaytaradi va
bu Deref
uchun API hujjatlarida. Rust &String
ni &str
ga aylantirish uchun
yana deref
ni chaqiradi, bu salom
funksiyasi ta`rifiga mos keladi.
Agar Rust deref coercionni amalga oshirmagan bo'lganida, biz &MyBox<String>
tipidagi qiymat bilan salom
ni chaqirish uchun 15-12 ro'yxatdagi kod
o'rniga 15-13 ro'yxatdagi kodini yozishimiz kerak edi.
Fayl nomi: 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 salom(nom: &str) { println!("Salom, {nom}!"); } fn main() { let m = MyBox::new(String::from("Rust")); salom(&(*m)[..]); }
(*m)
MyBox<String>
ni String
ga yo'naltiradi. Keyin &
va [..]
salom
belgisiga mos kelishi uchun butun stringga teng bo'lgan String
string sliceni
oladi. Deref coercionsiz ushbu kodni o'qish, yozish va tushunish ushbu belgilar
bilan qiyinroq. Deref coercion Rustga ushbu konversiyalarni biz uchun avtomatik
tarzda boshqarishga imkon beradi.
"Deref" traiti jalb qilingan turlar uchun aniqlanganda, Rust turlarni tahlil
qiladi va parametr turiga mos keladigan havolani olish uchun kerak bo'lganda
Deref::deref
dan foydalanadi. Deref::deref
qo'shilishi kerak bo'lgan vaqtlar
soni kompilyatsiya vaqtida hal qilinadi, shuning uchun deref coerciondan
foydalanganlik uchun ishga tushirish vaqtida jarima yo'q!
Deref Coercion O'zgaruvchanlik bilan Qanday O'zaro Ta'sir Qilishi
O'zgarmas havolalarda *
operatorini rad qilish uchun Deref
traitidan
foydalanishga o'xshab, o'zgaruvchan havolalarda *
operatorini rad
qilish uchun DerefMut
traitidan foydalanishingiz mumkin.
Rust ushbu uchta holatda tur va traitni amalga oshirishlarni topsa, deref coercionni amalga oshiradi
&T
dan&U
gacha,T: Deref<Target=U>
&mut T
dan&mut U
gachaT: DerefMut<Target=U>
&mut T
dan&U
gachaT: Deref<Target=U>
Birinchi ikkita holat bir-biri bilan bir xil, faqat ikkinchisi o'zgaruvchanlikni
amalga oshiradi. Birinchi holatda aytilishicha, agar sizda &T
bo'lsa va T
Deref
ni U
turiga qo'llasa, shaffof tarzda &U
ni olishingiz mumkin.
Ikkinchi holatda aytilishicha, xuddi shunday deref coercion o'zgaruvchan
havolalar uchun sodir bo'ladi.
Uchinchi holat qiyinroq: Rust o'zgarmasga o'zgaruvchan havolani ham majbur qiladi. Ammo buning teskarisi mumkin emas: o'zgarmas havolalar hech qachon o'zgaruvchan havolalarga majburlamaydi. Qarz olish qoidalari tufayli, agar sizda o'zgaruvchan havola bo'lsa, bu o'zgaruvchan havola ma'lumot uchun yagona havola bo'lishi kerak (aks holda dastur kompilyatsiya qilinmaydi). Bitta o'zgaruvchan havolani bitta o'zgarmas havolaga aylantirish hech qachon qarz olish qoidalarini buzmaydi. O'zgarmas havolani o'zgaruvchan havolaga aylantirish uchun dastlabki o'zgarmas havola ushbu ma'lumotga yagona o'zgarmas havola bo'lishini talab qiladi, ammo qarz olish qoidalari bunga kafolat bermaydi. Shu sababli, Rust o'zgarmas havolani o'zgaruvchan havolaga aylantirish mumkin deb taxmin qila olmaydi.
Drop
Trait bilan tozalash uchun kodni yuritish
Agar qiymat o‘z doirasidan chiqqanda uni o‘zgartirish imkonini beradigan ikkinchi muhim sanalgan smart pointer namunasidan biri bu Drop
dir. Siz Drop
traitini implementatsiya qilish uchun xohlagan turdan foydalanishingiz mumkin, va kodni fayl yoki tarmoqlarni ulash resurslarini yaratish uchun ham ishlatilishi mumkin
Drop
ni smart pointerlar kontekstida ishlatishimizning sababi Drop
traiti smart pointerni implementatsiyasida deyarli har doim ishlatiladi. Masalan, qachonki Box<T>
tashlab yuborilganda u quti ko‘rsatayotgan heapdan joy ajratadi.
Ayrim dasturlash tillarida ayrim turlar uchun dasturchi xotirani bo‘shatish uchun yoki har safar resurslar o‘sha tur instancedan ishlatib bo‘lmagungacha kodni chaqirishi kerak. Fayl handlelari, soketlar va locklar bunga misol bo‘la oladi. Agar ular kodni chaqirishni unitsalar, tizimda haddan tashqari yuklanish yuzaga keladi va tizim ishdan chiqadi. Rustda agar qiymat o‘z doirasidan chiqqanda siz kodning ma’lum bir qismi ishga tushirishni belgilashingiz mumkin, kompilyator avtomatik ravishda kodni kiritadi. Natijada, ma’lum bir turdagi instance tugagan dasturning hamma joyiga tozalovchi kodni joylashtirishdan xavotir olmasangiz ham bo‘ladi va siz resurslarni sizib ketishini oldini olgan bo‘lasiz!
Siz Drop
traiti implementatsiyasi yordamida agar qiymat doirasidan chiqqan holda kodni run qilish uchun belgilashingiz mumkin. Drop
traiti sizdan self
dan referens oluvchi drop
nomli metodni implementatsiya qilishni talab qiladi. Rustda drop
qachon chaqirilishini ko‘rish uchun, drop
ni println!
yordamida implementatsiya qilib ko‘raylik.
15-14 ni ko‘rib chiqadigan bo‘lsak, Rustda qachon drop
funksiyasi ishlashini ko‘rish uchun faqat o‘ziga tegishli bo‘lganCustomSmartPointer
structi faqat agar instance o‘z doirasidan chiqqanda Dropping CustomSmartPointer!
ni print qiladi.
Fayl nomi: src/main.rs
struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("CustomSmartPointerni `{}` ma'lumot bilan Drop qilish!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("menga tegishli"), }; let d = CustomSmartPointer { data: String::from("boshqaga tegishli"), }; println!("CustomSmartPointerlar yaratildi."); }
Drop
traiti o‘z ichiga preludeni oladi, shuning uchun biz uni scopeni ichiga olishimiz shart emas. Biz CustomSmartPointer
da Drop
ni implementatsiya qilamiz va drop
metodi implementatisyasi uchun println!
ni chaqiramiz. drop
funksiyasining tana (body) qismi bu sizning turdagi instance o‘z doirasidan (scope) chiqib ketgandagi ayrim bir logikaga ega koddir. Rustda qachon drop
chaqirilishini ko‘rish uchun biz ozgina tekstni print qilamiz.
main
da biz 2ta CustomSmartPointer
instancelarini yaratamiz va keyin CustomSmartPointers yaratildi
ni print qilamiz. main
ning oxirida CustomSmartPointer
doiradan (scope) chiqib ketadi va Rust yakuniy xabarni print qilib, biz kodga qo‘ygan drop
metodini chaqiradi. E’tibor bering biz drop
metodini to‘g‘ridan-to‘g‘ri chaqririshimiz shart emas.
Agar biz dasturni run qilsak, quyidagi outputni ko‘ramiz:
$ 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`!
Rust avtomatik ravishda bizning o‘rnimizga biz ko‘rsatgan kodni instance doiradan (scope) chiqqanda drop
ni chaqirdi. O‘zgaruvchilar yaratilish paytida teskari tartibda tushib qoldiriladi (drop qilinadi), shuning uchun d
c
dan oldin tushib qoldirildi (drop qilindi). Ushbu misolning maqsadi sizga drop
metodining qanday ishlashining vizual ko‘rinishini berishdir; odatda xabarni print qilishning o‘rniga siz sizning turingizni ishga tushirish (run qilish) uchun tozalash kodini ko‘rsatasiz.
std::mem::drop
yordamida Qiymatni Erta Drop qilish
Afsuski, avtomatik drop
funksiyasini o‘chirish oson emas. Odatda drop
ni o‘chirish zarur emas; Drop
ning asosiy mohiyati uning avtomatik ravishda hal qilishidir. Ba’zi paytlarda siz qiymatni erta tozalashga duch kelishingiz mumkin. Lockalarni boshqaruvchi smart pointerlarni ishlatishga bir misol bo‘la oladi: bir doirada (scope)da boshqa kodni olish uchun siz lockni chaqiradigan drop
metodini majburiy ravishda ishlatishingiz mumkin. Rust sizga Drop
traitidagi drop
metodini qo‘lda tushurishga qo‘ymaydi; agar siz qiymatni o‘z doirani (scope) tugashidan oldin majburiy drop bo‘lishini xohlasangiz. uning uchun siz standart kutubxona tomonidan taqdim etilgan std::mem::drop
ni ishlatishingiz mumkin.
Agar biz 15-14dagi ilovaga qo‘lda Drop
traitining drop
metodi yordamida main
ga o‘zgaritirish kiratigan bo‘lsak, 15-15 ilovada ko‘rsatilgan kompilyator xatosini ko‘ramiz:
Fayl nomi: src/main.rs
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("CustomSmartPointerni `{}` ma'lumot bilan Drop qilish!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("ma'lumot"),
};
println!("CustomSmartPointer yaratildi.");
c.drop();
println!("main tugashidan oldin CustomSmartPointer drop qilindi.");
}
Ushbu kodni komplilyatsiya qilganimizda quyidagi xatolikni ko‘ramiz:
$ 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
Ushbu xatolikdagi xabarda drop
ni to‘g‘ridan-to‘g‘ri chaqira olmasligizni ko‘rsatadi. Xatolikdagi xabar instanceni tozalovchi umumiy dasturlash atamasi bo‘lgan funksiya, ya’ni destructor
ni ishlatadi. destructor
constructor
ga o‘xshash bo‘lib, instancelarni yaratadi. Rustda drop
funksiyasi alohida bir destructordir.
Rust bizga drop
ni to‘g‘ridan-to‘g‘ri chaqrishga qo‘ymaydi chunki Rust qiymatni avtomatik ravishda baribir main
ni oxirida drop
ni chaqiradi. Ushbu holat double free xatoligini keltirib chiqarishi mumkin chunki Rust bitta qiymatni ikki marta tozalashga xarakat qiladi.
Agar qiymat o‘z doirasidan (scope) chiqqanda biz drop
ni avtomatik kiritishini o‘chirib qo‘ya olmaymiz va drop
metodini to‘g‘ridan-to‘g‘ri chaqira olmaymiz. Shuning uchun agar bizga majburiy ravishda qiymat tozalanishini xoxlasak, biz std::mem::drop
funksiyasini ishlatamiz
std::mem::drop
funksiyasi Drop
traitidagi drop
metodidan farq qiladi. Biz buni majburan tozalash (drop) qilishni xohlagan qiymatni argument sifatida berish deb ataymiz. Funksiya preludeda, va 15-16 ro'yxatda ko‘rsatilgandek biz 15-15 ro'yxatdagi main
da drop
funkisyasini chaqirish uchun o‘zgartirish kiritishimiz mumkin:
Fayl nomi: src/main.rs
struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("CustomSmartPointerni `{}` ma'lumot bilan Drop qilish!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("ma'lumot"), }; println!("CustomSmartPointer yaratildi."); drop(c); println!("main tugashidan oldin CustomSmartPointer drop qilindi."); }
Run qilingan kod quyidagini print qiladi:
$ 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.
c
nuqtasida tozalash (drop qilish) uchun drop
metodi kodini chaqirishini ko‘rsatish uchunCustomSmartPointerdagi ma’lumot bilan tozalash (drop qilish)
CustomSmartPointer yaratildi
va CustomSmartPointer main tugashidan oldin tozalandi (drop qilindi)
matnlari orasida print qilindi.
Siz Drop
traiti implementatsiyasida ko‘rsatilgan koddan har xil turda tozalashni qulay va xavfsiz qilishingiz mumkin: masalan, siz uni o‘zingizning xotira taqsimlagichni yaratish uchun ishlatsangiz bo‘ladi. Drop
traiti va Rustning ownership tizimi bilan tozalash uchun bosh qotirmasangiz ham bo‘ladi chunki Rust buni avtomatik ravishda qiladi.
Siz ishlatilib turgan qiymatlarni bexosdan tozalanib ketish muammolaridan xavotir olmasangiz bo‘ladi: ownership tizimi referencelarni doim to‘g‘riligiga hamda drop
qiymat bir marta chaqrilib boshqa ishlatilmasligini ta’minlaydi.
Hozirda biz Box<T>
ni va smart pointerlarni ba’zi bir xususiyatlarini tekshirib oldik, keling standart kutubxonada keltirilgan boshqa smart pointerlarni ham ko‘rib chiqaylik.
Rc<T>
, reference hisoblangan Smart Pointer
Ko‘p hollarda, ownership ochiq, ya’ni tushunarli holda bo’ladi: sizga qaysi o‘zgaruvchi qaysi berilgan qiymatga egalik qilishini aniq bilasiz. Ammo, ayrim hollarda bitta qiymat ko‘p egalarga ega bo‘lishi mumkin. Masalan, grafik ma’lumotlar tuzilmasida (data structures), bir nechta edgelar bitta nodeni point qilishi mumkin, vas shu node unga point qilingan barcha edgelar tomonidan egalik qilinadi. Nodega edgelar point qilinmagungacha, shuningdek egalari bo‘lmagungacha tozalanishi mumkin emas.
Rustning refenceni hisoblovchi Rc
Rc<T>
ni zaldagi televizor sifatida tasavvur qiling. Agar bir kishi televizor ko‘rish uchun xonaga kirsa, u televizorni yoqadi. Boshqalar esa shunchaki xonaga kirib tomosha qilsalar bo‘ladi. Xonadan oxirgi odam chiqib ketayotganda, ular televizorni o‘chirib ketishadi chunki televizor boshqa ishlatilmaydi. Agar bir kishi boshqalar televizorni tomosha qilib o‘tirganida o‘chirsa boshqalar uchun g‘alati bo‘lishi mumkin.
Biz Rc<T>
ni ma’lumotni heapda dasturning ko‘p qismlarini o‘qishi uchun ajratishni hohlasaganimizda foydalanamiz va biz kompilyatsiya vaqti qaysi qism ma’lumotini oxirgi foydalanishni yakunlaganini bila olmaymiz. Agar biz ma’lumotni qaysi qismi oxirida to‘xtashini bilganimizda, biz shu qismni ma’lumotni egasi sifatida tayinlar edik va kompilyatsiya vaqtida qo‘llaniladigan oddiy egalik (ownership) qoidalari kuchga kirar edi.
Shuni yodda tutish kerakki Rc<T>
yakka-thread holatlardagina ishlatiladi. Biz 16-bo‘limda parralellik haqida suhlashganimizda, biz ko‘p threadli dasturlarda referenceni hisoblashni qanday qilishni o‘rganamiz.
Rc<T>
ni Ma'lumotni Ulashish uchun ishlatish
Keling kamchiligi bor bo‘lgan 15-5 ro‘yxat misolimizga qaytaylik. Esingizda bo‘lsa biz Box<T>
ni ishlatishni ko‘rsatib o‘tgan edik. Bu safar, biz 2ta ro‘yxat ham egalikni (ownership) 3-ro‘yxat bilan ulashadigan ro‘yxat yaratamiz. Aniq qilib aytadigan bo‘lsak, 15-3 shaklga o‘xshashdir:
Biz 5 va 10 dan iborat bo‘lgan a
ro‘yxatni yaratamiz. Keyin yana ikkita ro‘yxatni ham yaratamiz, ya’ni 3dan boshlanadigan b
va 4dan boshlanadigan c
. b
va c
ro‘yxatlari 5 va 10dan iborat bo‘lgan a
ro‘yxatda davom etadi. Boshqacha qilib aytganda, ro‘yxatlar birinchi 5 va 10dan iborat bo‘lgan birinchi ro‘yxat bilan ulashishadi.
15-17-ro‘yxatda ko‘rsatilgandek, bizning ssenariy bo‘yicha Box<T>
bilan List
dagi ta’rifimiz yordamida implement qilishga urunsak ishga tushmaydi:
Fayl-nomi: 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));
}
Ushbu kodni kompilyatsiya qilsak, biz yuqoridagi xatolikni ko'ramiz:
$ 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
variantlari o'zlariga tegishli bo'lgan ma'lumotlargagina egalik (own) qila oladi, shuninguchun biz b
ro'yxatini yaratganimizda, a
b
ga o'tadi va b
a
ga egalik qiladi.Undan keyin, c
ni yaratish uchun a
dan foydalanmoqchi bo'lganizmizda bizga ruxsat bermaydi chunki a
ko'chib ketganligi uchun.
Buning o'rniga havolalarni ushlab turish uchun Cons
ta'rifini o'zgartirishimiz mumkin, lekin keyin biz layvtaym parametrlarini ko'rsatishimiz kerak bo'ladi. Layvtaym parametrlarini belgilash orqali, biz ro'yxatdagi har bir elementning yashashini ko'rsatamiz. 15-17 ro'yxatda ko'rsatilganidek bu elementlar va ro'yxatlarga tegishli, lekin har doim gam emas.
15-18-ro'yxatda ko'rsatilganidek, Box<T>
ning o'rnigaRc<T>
ni ishlatish uchun biz bizning Ro'yxat
imizning mazmunini o'zgartiramiz. Har bir Cons
varianti qiymatni o'zida ushlab turadi va Rc<T>
Ro'yxat
ni ko'rsatadi. a
ning egaligini olishning o'rniga b
ni yaratganimizda, a
ni ushlab turgan Rc<List>
ni klonlaymiz, shu bilan birga referenslar sonini birdan ikkiga ko'paytiramiz va Rc<List>
dagi ma'lumotlarning egaligini ulashish uchun a
va b
ga ruxsat beramiz. referenslar sonini ikkidan uchga ko'paytirgan holda, c
ni yaratayotganimizda a
ni ham klonlaymiz. Rc::clone
ni har safar chaqirganimizda, Rc<List>
tarkibidagi ma'lumotlarining referenslari soni oshiriladi, zero referenslar paydo bo'lmagungacha ma'lumotlar tozalanmaydi.
Filename: 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)); }
Biz scopeni ichiga Rc<T>
ni kiritsh uchun use
statementini qo'shishimiz kerak
chunki u muqaddimani ichida bo'lmagani uchun. main
ni ichida 5 va 10ni saqlovch ro'yxatni
yaratamiz va uni a
ga tegishli yangi Rc<List>
ga joylashtiramiz. Keyin esa b
va c
yaratganimizda, Rc::clone
funksiyasini chaqiramiz va argument sifatida a
ga tegishli bo'lgan Rc<List>
ga o'tkazib yuboramiz.
Rc::clone(&a)
ning o'rniga biz a.clone()
chaqirishimiz mumkin edi, lekin Rust
qoidalariga muofiq ushbu holatda Rc::clone
ishlatgan ma'qul. Rc::clone
ning implementatsiyasi clone
ning ko'p implementatsiya turiga o'xshab ma'lumotlarni to'liq
nusxa olmaydi. Rc::clone
ning chaqirilishi referenslarning sonini ko'paytiradi, va shuning uchun ham ko'p vaqt olmaydi. Ma'lumotlarni to'liq nusxalash esa ko'p vaqt talab etadi. Referenslarni hisoblash uchun Rc::clone
ni ishlatsak, biz to'liq nusxalangan bilan referenslar soni ortgan nusxalar orasidagi farqni tasavvur qilishimiz mumkin. Kodning unumdorlik muammolarini qidirayotganimizda, bizga faqat to'liq nusxalangan nusxalarga e'tibor qaratib, Rc::clone
ning chaqirilishini e'tiborga olmasak ham bo'ladi.
Rc<T>
ni nusxalash Referenslar hisobini orttiradi
a
da referenslarni yaratib va uni Rc<List>
ga drop qilib referenslar sonini ortayotganligini keling bizning ishlab turgan 15-18-ro'yxatimizdagi misolga o'zgartirish kiritib ko'raylik,
15-19-ro'yxatda, main
ni o'zgartirib ko'raylik chunki uning ichki scope(doirasi) c
ro'yxatining atrofida shundan so'ng biz c
scope(doirasida) chiqqanda qanday qilib referenslar soni ortayotganini ko'ramiz.
Fayl-nomi: 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)); }
Dasturda referens soni o'zgargan har bir nuqtada, Rc::strong_count
funksiyasini chaqirish yordamida biz referens sonini print qilamiz. Ushbu funksiyani nomicount
deb emas strong_count
deb nomlanadi, chunki Rc<T>
turida weak_count
ham bor; biz weak_count
ni "Referenslar siklini oldini olish: Rc<T>
ni Weak<T>
ga aylantirish" bo'limida ko'ramiz.
Ushbu kod quyidagini print qiladi:
$ 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
da Rc<List>
ning referens soni 1 ekanligini ko'rishimiz mumkin; keyinchalik har safar biz clone
ni chaqirganimizda, son 1ga ortib boraveradi. c
o'z scope(doirasi)dan chiqib ketganida,referens soni 1ga kamayadi. Rc::clone
funksiyasi yordamida referens sonini ortirish uchun chaqirganimizdek referens soni kamaytirish uchun hech qanday funksiyasini chaqirishimiz kerak bo'lmaydi, chunki Drop
traitining implementatsiyasi Rc<T>
qiymati o'z scope(doirasi)dan chiqqanda avtomatik ravishda referens sonini kamaytiradi.
main
oxirida b
va keyin a
larning scope(doirasidan) chiqib, son/hisob 0ga tenglashib, Rc<List>
to'liq tozalanganligini biz ushbu misolda ko'ra olmaymiz. Rc<T>
ni ishlatish yordamida bitta qiymat ko'p egalarga ega bo'lishi mumkin, hamda son/hisob qiymat egalaridan biri bor bo'lgunga qadar yaroqliligini tekshirib turadi.
O'zgarmas o'zgaruvchilar yordamida, Rc<T>
o'qish uchun ma'lumotlarni dasturning ko'p joylari orasida ulashish imkonini beradi. Agar Rc<T>
ko'p o'zgaruvchan referenslarga ega bo'lish imkonini bergan bo'lsa, siz 4-Bo'limda ko'rsatilganidek borrowing qoidalarini birini buzishingiz mumkin: ko'p o'zgaruchilar bir xil joyga borrow qilib data race va nomuvofiqliklarga sabab bo'lishi mumkin. Lekin ma'lumotni o'zrgatira olish foydalidir! keyingi bo'limda, biz ichki o'zgaruvchanlik shakli(pattern) va RefCell<T>
turi bilan Rc<T>
yordamida o'zgarmaslik cheklovi bilan ishlash ko'rib chiqamiz.
RefCell<T>
va Ichki O'zgaruvchanlik Shakli(pattern)
Ichki o'zgaruvchanlik bu Rustda ma'lumotlarga o'zgarmas referenslar mavjud bo'lganda ham ma'lumotni o'zgartirish imkonini beruvchi (dizayn) shakli/patternidir: odatda bu borrowing qoidalari bo'yicha esa taqiqlangan. Ma'lumotni o'zgaruvchan qilish uchun shakl/pattern Rustning o'zgaruvchanlik va borrowingni boshqaruchi oddiy qoidalarini chetlab o'tish uchun ma'lumotlar strukturasi ichiga unsafe
kod ishlatiladi. Xavfsiz bo'lmagan kod bizning o'rnimizga kompilyatorga qoidalarni kompliyator yordamisiz tekshirayotganimizni ko'rsatadi; xavfsiz bo'lmagan kod haqida 19-bo'limda o'rganib chiqamiz.
Biz ichki o'zgaruvchanlik shakli/patterni ishlatadigan turlardan faqatgina borrowing qoidalari runtimeda amal qilingaligi paytida ishlatishimiz mumkin, kompilyator bunga kafolat bera olmaydi. Keyin unsafe
kod xavfsiz APIga ulanadi va tashqi tur o'zgarmasligicha qoladi.
Keling ushbu tushunchani ichki o'zgaruvchanlik shakliga amal qiluvchi quyidagi RefCell<T>
turiga qarab ko'rib chiqaylik.
Enforcing Borrowing Rules at Runtime with RefCell<T>
yordamida Borrowing qoidalarini Runtime vaqtida kuch bilan ishlatish
Rc<T>
lidan faqrli o'laroq, RefCell<T>
turi o'zi egalik qilib turgan ma'lumotda yagona egalikni namoyish etadi. Xo'sh, RefCell<T>
turi Box<T>
turidan nimasi bilan farq qiladi? 4-bo'limda o'tilgan borrowing qoidalarini esga olaylik:
- Xohlagan belgilangan vaqtda, siz yoki (ikkalasini bir vaqtda ega bo'lish mumkin emas)bitta o'zgaruvchan referens yoki xohlagan sondagi o'zgarmas referenslarga ega bo'lishingiz mumkin.
- Referenslar har doim yaroqli bo'lishi shart
Referenslar va Box<T>
bilan, borrowing qoidalarining kompilyatsiya vaqtida o'zgarmaslar kuchga kiradi. RefCell<T>
bilan esa ushbu o'zgarmaslar runtime paytida kuchga kiradi. Referenslar bilan, agar siz ushbu qoidalarni buzsangiz, sizda kompilyator xatoligi yuzaga keladi. RefCell<T>
bilan suhbu qoidalarni buzganingizda, sizning dasturingizda panic vujudga kelib, dastur chiqib ketadi.
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.
Borrowing qoidalarini kompilyatsiay vaqtida tekshirishning yaxshi tarafi xatolarni development vaqtida tezroq topishdir, va runtime unumdorligiga ta'sir ko'rsatmaydi chunki hamma analizlar oldindan qilingan bo'ladi. Ko'p hollarda borrowing qoidalarini kompilyatsiya vaqtida tekshirish eng yaxshi tanlovdir, sababi ushbu xususiyat Rustda odatiy xususiyatidir.
Borrowing qoidalarini runtime vaqtida tekshrishning afzalligi shundaki, kompilyatsiya vaqtidagi tekshiruvlar tomonidan ruxsat etilmaganda ba'zi xotira uchun xavfsizlik ssenariylarga ruxsat beriladi. Rust kompilyatoriga o'xshagan statik analizlar o'z-o'zidan konservativdir. Kodni tahlil qilayotganda kodning ba'zi bir xususiyatlarini aniqlash qiyindir: bunga Halting Problem mashxur misol bo'la oladi, bu kitob doirasidan tashqarida bo'lsada lekin izlanib o'rganish uchun qiziq mavzu
Agar Rust kompilyatori egalik (ownership) qoidalari asosida kompilyatsiya qilayotganini aqiqlay olmasa, bu to'g'ri, ya'ni ishlab turgan dasturni rad etishi mumkin, shuning uchun ham konservativ hisoblanadi va bu ba'zi tahlillar uchun qiyindir. Agar Rust xatolikka ega bo'lgan dasturni qabul qilsa, foydalanuvchilar Rust beradigan kafolatlarga ishona olmaydilar. Agarda, Rust ishlab turgan dasturni rad etsa, dasturchi uchun noqulaylik tug'diradi, lekin hech qanday qo'rqinchli narsa bo'lmaydi. RefCell<T>
turi sizning kodingiz borrowing qoidlariga amal qilayotganiga ishonchingiz komil bo'lganda lekin kompilyator buni tushuna olmayotganda va kafolat bera olmaganda foydalidir.
RefCell<T>
Rc<T>
ga o'xshab bitta potokli (oqimli) ssenariylarda ishlatilinadi va agar siz ko'p potokli (oqimli) holatda ishlatsangiz kompilyatsiya vaqtidagi xatolikni yuzaga keltiradi. Biz RefCell<T>
ni ko'p potokli (oqimli) dasturda qanday qilib funksionalligini olishni 16-bo'limda ko'rib chiqamiz.
Quyida takrorlash uchun Box<T>
, Rc<T>
, yoki RefCell<T>
ni tanlash sabablari:
Rc<T>
bitta ma'lumotga ko'p egalarga ega bo'lish imkonini beradi;Box<T>
vaRefCell<T>
esa yagona egaga egadirlar;Box<T>
kompilyatsiya vaqtida o'zgaruvchan va o'zgarmas borrowlarni tekshrilishini ta'minlaydi;Rc<T>
kompilyatsiya vaqtida faqat o'zgarmas borrowlarni tekshrilishini ta'minlaydi;RefCell<T>
runtimeda o'zgaruvchan va o'zgarmas borrowlarni tekshrilishini ta'minlaydi.- Because
RefCell<T>
runtimeda o'zgaruvchan borrowlar tekshirilishi ta'minlaydi, agarRefCell<T>
o'zgarmas bo'lsadaRefCell<T>
ichida qiymatni o'zgaruvchan qilishingiz mumkin.
Qiymatni o'zgarmas qiymat ichida o'zgaruvchan qilish ichki o'zgaruvchanlik shaklidir (pattern). Keling ichki o'zgaruvchanlikni foydali ekanligini va bu qanday sodir bo'lishini misollarda ko'rib chiqaylik.
Ichki o'zgaruvchanlik: O'zgaruvchan Borrowdan O'zgarmas Qiymatga
Borrowing qoilari natijasida, o'zgarmas qiyamtga ega bo'lganingizda, siz o'zgaruvchan borrow qila olmaysiz. Masalan, ushbu kod kompilyatsiya qilinmaydi:
fn main() {
let x = 5;
let y = &mut x;
}
Agar siz ushbu kodni kompilyatsiya qilishga harakat qilsangiz, quyidagi xatolik kelib chiqadi:
$ 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
Shunday vaziyatlar borki qiymat o'zini-o'zi o'zining metodlarida o'zgarivchan qilishi
foydali hisoblanadi, lekin boshqa kodda o'zgarmas shaklda bo'ladi. Kodning doirasidan
tashqaridagi qiymat metodi qiymatni o'zgaruvchan qila olmaydi. RefCell<T>
ni ishlatish ichki
o'zgaruvchanlikga ega bo'lishning bir yo'li hisoblanadi, lekin RefCell<T>
borrowing
qoidalarini to'liq aylanib o'tmaydi: kompilyatorda borrow tekshiruvchisi shu ichki o'zgaruvchanlikka
ruxsat beradi, va borrowing qoidalari runtimes tekshiriladi. Agar qoidalarni rad etsangiz,
kompilyator xatoligini o'rniga panic!
ko'rasiz.
RefCell<T>
ni o'zgarmas qiymatni o'zgaruvchanga aylantirishni, hamda nimaga RefCell<T>
ni
ishlatish foydali ekanligini amaliy misollarda ko'rib chiqaylik.
Ichki o'zgaruvchanlik uchun foydalanish holati/misoli: Soxta Obyektlar
Ayrim hollarda test vaqti dasturchi boshqa turni o'rniga kerakli hatti-harakatni kuzatish uchun va to'g'ri kompilyatsiya amalga oshirilganligini tasdiqlash uchun boshqa bir turni ishlatib ko'radi. Ushbu to'ldiruvchi tur test double deb ataladi. Qiyin bo'lgan sahna ko'rinishida aktyorning o'rniga chiqib, sahna ko'rinishi amalga oshirib beruvchi, ya'nikino yaratishda "kaskadyor" misolida ko'rib chiqaylik. Test doublelari boshqa turlarda test o'tkazayotganimizda xizmat qiladi. Soxta obyektlar test paytida nimalar sodir bo'lishini qayd etuvchi test doublelar o'ziga xos turlardan biri bo'lib, siz to'g'ri amallar amalga oshirilayotganini ko'zdan kechirishingiz mumkin.
Rustda boshqa dasturlash tillari kabi bir xil ma'noli obyektlarga ega emas, va soxta obyekt funksionalligini olgan standart kutubxonasi yo'q. Aksincha, soxta obyektlar kabi ish bajaruvchi struct yaratishingiz mumkin.
Ushbu ssenariyni ko'rib chiqaylik: qiymatni maksimal qiymatga nisbatan kuzatuvchi kutubxona yaratamiz va joriy qiymat maksimal qiymatga qanchalik yaqinligiga qarab bizga xabar jo'natib turadi. Ushbu kutubxona foydalanuvchi uchun ruxsat etilgan API 'call'lar sonini kuzatib borish uchun ishlatilishi mumkin, bu ishlatish mumkin bo'lgan bir misol.
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!");
}
}
}
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);
}
}
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...
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);
}
}
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);
}
}
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 src/lib.rs (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 'tests::it_sends_an_over_75_percent_warning_message' 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); }
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. Note that RefCell<T>
does not work for multithreaded code!
Mutex<T>
is the thread-safe version of RefCell<T>
and we’ll discuss
Mutex<T>
in Chapter 16.
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() {}
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()); }
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 b
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.
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)]), }); }
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()); }
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), ); }
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.
Fearless Concurrency
Bir vaqtning o'zida dasturlashni(concurrent programming) xavfsiz va samarali boshqarish Rustning asosiy maqsadlaridan biridir. Dasturning turli qismlari mustaqil ravishda bajariladigan(execute) concurrent programming va dasturning turli qismlari bir vaqtning o'zida bajariladigan parallel dasturlash ko'proq kompyuterlar o'zlarining bir nechta protsessorlaridan foydalanishlari sababli tobora muhim ahamiyat kasb etmoqda. Tarixiy jihatdan, ushbu kontekstlarda dasturlash qiyin va xatolarga moyil bo'lgan: Rust buni o'zgartirishga umid qilmoqda.
Dastlab, Rust jamoasi xotira xavfsizligini ta'minlash va parallel muammolarning oldini olish turli usullar bilan hal qilinishi kerak bo'lgan ikkita alohida muammo deb o'ylagan. Vaqt o'tishi bilan jamoa egalik(ownership) va turdagi tizimlar(type system) xotira xavfsizligi va parallellik muammolarini boshqarishga yordam beradigan kuchli vositalar to'plami ekanligini aniqladi! Ownership(egalik) va turlarni tekshirishdan(type checking) foydalangan holda, ko'plab parallellik xatolar runtimedagi xatolardan ko'ra Rustda kompilyatsiya vaqtidagi xatolardir. Shuning uchun, runtime bilan bir vaqtda xatolik yuzaga kelgan aniq holatlarni takrorlash uchun ko'p vaqt sarflashdan ko'ra, noto'g'ri kod kompilyatsiya qilishni rad etadi va muammoni tushuntiruvchi xatoni taqdim etadi. Natijada, siz kodingizni ishlab chiqarishga(production) yuborilgandan keyin emas, balki uning ustida ishlayotganingizda tuzatishingiz mumkin. Biz Rustning bu jihatini fearless concurrency deb nomladik. Fearless concurrency sizga nozik xatolarsiz kod yozish imkonini beradi va yangi xatolarni kiritmasdan qayta tiklash oson.
Eslatma: Oddiylik uchun biz ko'p muammolarni concurrent va yoki parallel deb aniqroq bo'lishdan ko'ra concurrent deb ataymiz. Agar bu kitob concurrency va yoki parallellik haqida bo'lsa, biz aniqroq bo'lardik. Ushbu bo'lim uchun, iltimos, biz concurrent ishlatganimizda, parallel va/yoki concurrentni aqliy ravishda almashtiring.
Ko'pgina tillar bir vaqtda muammolarni hal qilish(concurrent problem) uchun taklif qiladigan yechimlar haqida dogmatikdir. Misol uchun, Erlang xabarlarni bir vaqtda uzatish uchun oqlangan funksiyaga ega, ammo threadlar orasidagi holatni(state) almashishning noaniq usullariga ega. Mumkin bo'lgan yechimlarning faqat bir qismini qo'llab-quvvatlash high-leveldagi tillar uchun oqilona strategiyadir, chunki high-leveldagi til mavhumlikni qo'lga kiritish uchun ba'zi nazoratdan voz kechishdan foyda va'da qiladi. Biroq, low-leveldagi tillar har qanday vaziyatda eng yaxshi samaradorlik bilan yechimni ta'minlashi va hardwarega(qurilma) nisbatan kamroq abstraktsiyalarga ega bo'lishi kutilmoqda. Shu sababli, Rust sizning vaziyatingiz va talablaringizga mos keladigan tarzda muammolarni modellashtirish uchun turli xil vositalarni taklif qiladi.
Mana biz ushbu bobda muhokama qiladigan mavzular:
- Bir vaqtning o'zida bir nechta kod qismlarini ishlatish uchun threadlarni qanday yaratish kerak
- Message-passing Xabarlarni uzatish concurrency, bu yerda kanallar threadlar o'rtasida xabarlar yuboradi
- Shared-state bir vaqtning o'zida bir nechta thereadlar(multiple thread) ma'lumotlarning bir qismiga kirish huquqiga ega
Sync
vaSend
traitlari, Rustning parallellik kafolatlarini foydalanuvchi tomonidan belgilangan turlarga hamda standart kutubxona tomonidan taqdim etilgan turlarga kengaytiradi.
Kodni bir vaqtning o'zida ishga tushirish uchun threadlardan foydalanish
Kodni bir vaqtning o'zida ishga tushirish(simultaneously) uchun threadlardan foydalanish hozirgi operatsion tizimlarning ko'pchiligida bajarilgan(execute) dastur kodi process ishga tushiriladi va operatsion tizim bir vaqtning o'zida bir nechta processlarni boshqaradi.Dastur doirasida siz bir vaqtning o'zida ishlaydigan(simultaneously) mustaqil qismlarga(independent part) ham ega bo'lishingiz mumkin. Ushbu mustaqil qismlarni boshqaradigan xususiyatlar threadlar deb ataladi. Masalan, veb-server bir vaqtning o'zida bir nechta so'rovlarga(requestlar) javob berishi uchun bir nechta(multiple) threadlarga ega bo'lishi mumkin.
Bir vaqtning o'zida bir nechta vazifalarni(multiple task) bajarish uchun dasturingizdagi hisoblashni bir nechta(multiple) threadlarga bo'lish unumdorlikni oshirishi mumkin, ammo bu murakkablikni ham oshiradi. Theredlar bir vaqtning o'zida(simultaneously) ishlashi mumkinligi sababli, kodingizning turli xil ish threadlaridagi qismlari qaysi tartibda ishlashi haqida hech qanday kafolat yo'q. Bu muammolarga olib kelishi mumkin, masalan:
- Race conditionlari, bu yerda threadlar ma'lumotlar yoki resurslarga mos kelmaydigan tartibda kirishadi
- Deadlock, bu yerda ikkita thread bir-birini kutib, ikkala threadning davom etishiga to'sqinlik qiladi
- Faqat ma'lum holatlarda yuzaga keladigan va qayta ishlab chiqarish va ishonchli tarzda tuzatish qiyin bo'lgan xatolar
Rust threadlardan foydalanishning salbiy ta'sirini yumshatishga harakat qiladi, lekin multithreadli kontekstda dasturlash hali ham ehtiyotkorlik bilan o'ylashni talab qiladi va bitta threadda ishlaydigan dasturlardan farq qiladigan kod tuzilishini talab qiladi.
Dasturlash tillari threadlarni turli yo'llar bilan amalga oshiradi(impelement qiladi) va ko'pgina operatsion tizimlar yangi threadlarni yaratish uchun til chaqirishi mumkin bo'lgan API-ni taqdim etadi. Rust standart kutubxonasi(standard library) 1:1 threadni amalga oshirish modelidan foydalanadi, bunda dastur har bir til uchun bitta operatsion tizim threadidan foydalanadi. 1:1 modeliga turli xil o'zgarishlarni keltirib chiqaradigan boshqa theredlar modellarini amalga oshiradigan(implement qiladigan) cratelar mavjud.
spawn
yordamida yangi thread yaratish
Yangi thread yaratish uchun biz thread::spawn
funksiyasini chaqiramiz va unga yangi threadda ishga tushirmoqchi bo'lgan kodni o'z ichiga olgan closureni (biz 13-bobda closurelar haqida gapirgan edik) o'tkazamiz. 16-1 ro'yxatdagi misol asosiy(main) threaddagi ba'zi matnni va yangi threaddagi boshqa matnni chop etadi:
Fayl nomi: src/main.rs
use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { for i in 1..10 { println!("salom ochilgan threaddan {}-raqam!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("salom, main threaddan {}-raqam!", i); thread::sleep(Duration::from_millis(1)); } }
Esda tutingki, Rust dasturining asosiy ishi tugagach, barcha ochilgan threadlar ishlashni tugatgan yoki tugatmaganidan qat'i nazar, o'chiriladi. Ushbu dasturning chiqishi har safar bir oz boshqacha bo'lishi mumkin, ammo u quyidagilarga o'xshaydi:
salom, main threaddan 1-raqam!
salom, ochilgan threaddan 1-raqam!
salom, main threaddan 2-raqam!
salom, ochilgan threaddan 2-raqam!
salom, main threaddan 3-raqam!
salom, ochilgan threaddan 3-raqam!
salom, main threaddan 4-raqam!
salom, ochilgan threaddan 4-raqam!
salom, ochilgan threaddan 5-raqam!
thread::sleep
ga chaqiruvlar threadni qisqa muddatga uning bajarilishini to'xtatishga majbur qiladi, bu esa boshqa threadning ishlashiga imkon beradi. Ehtimol, threadlar navbatma-navbat bo'ladi, lekin bu kafolatlanmaydi: bu sizning operatsion tizimingiz threadlarni qanday rejalashtirishiga(schedule) bog'liq. Bu ishga tushirishda birinchi bo'lib main thread chop etiladi, garchi ishlab chiqarilgan threadning chop etish bayonoti kodda birinchi bo'lib paydo bo'lsa ham. Va biz paydo bo'lgan thredga i
9 bo'lguncha chop etishni aytgan bo'lsak ham, asosiy thread yopilishidan oldin u 5 ga yetdi.
Agar siz ushbu kodni ishga tushirsangiz va faqat main threaddan olingan ma'lumotlarni ko'rsangiz yoki hech qanday o'xshashlikni ko'rmasangiz, operatsion tizimning threadlar o'rtasida almashishi uchun ko'proq imkoniyatlar yaratish uchun diapazonlardagi raqamlarni oshirib ko'ring.
join
handlerlari yordamida barcha threadlar tugashini kutilmoqda
16-1 ro'yxatidagi kod ko'pincha main thread tugashi tufayli paydo bo'lgan threadni muddatidan oldin to'xtatibgina qolmay, balki threadlarning ishlash tartibiga kafolat yo'qligi sababli, biz ham yangi ochilgangan threadning umuman ishga tushishiga kafolat bera olmaymiz!
O'zgaruvchida thread::spawn
ning qaytish(return) qiymatini saqlash orqali ochilgangan threadning ishlamasligi yoki muddatidan oldin tugashi muammosini hal qilishimiz mumkin. thread::spawn
ning qaytish turi(return type) JoinHandle
dir. JoinHandle
- bu tegishli qiymat bo'lib, biz join
metodini chaqirganimizda, uning threadi tugashini kutamiz. 16-2 ro'yxatda biz 16-1 ro'yxatda yaratgan JoinHandle
dan qanday foydalanish va join
ni chaqirish orqali yaratilgan thread main
chiqishdan oldin tugashini ko'rsatadi:
Fayl nomi: src/main.rs
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("salom ochilgan threaddan {}-raqam!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("salom, main threaddan {}-raqam!", i); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap(); }
Handleda join
ni chaqirish, handle bilan ifodalangan thread tugaguncha ishlayotgan threadni bloklaydi. Threadni bloklash uning ish bajarishi yoki chiqishining oldini olish degani. Biz chaqiruvni(call) main threadning foor
loop siklidan keyin qo'yganimiz sababli, 16-2 ro'yxatini ishga tushirish shunga o'xshash natijani berishi kerak:
salom, main threaddan 1-raqam!
salom, main threaddan 2-raqam!
salom, ochilgan threaddan 1-raqam!
salom, main threaddan 3-raqam!
salom, ochilgan threaddan 2-raqam!
salom, main threaddan 4-raqam!
salom, ochilgan threaddan 3-raqam!
salom, ochilgan threaddan 4-raqam!
salom, ochilgan threaddan 5-raqam!
salom, ochilgan threaddan 6-raqam!
salom, ochilgan threaddan 7-raqam!
salom, ochilgan threaddan 8-raqam!
salom, ochilgan threaddan 9-raqam!
Ikki thread almashishda davom etadi, lekin main thread handle.join()
chaqiruvi tufayli kutadi va hosil qilingan thread tugamaguncha tugamaydi.
Ammo keling, main
da for
loop siklidan oldin handle.join()
ni ko‘chirsak nima bo‘lishini ko‘rib chiqamiz, masalan:
Fayl nomi: src/main.rs
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("salom ochilgan threaddan {}-raqam!", i); thread::sleep(Duration::from_millis(1)); } }); handle.join().unwrap(); for i in 1..5 { println!("salom, main threaddan {}-raqam!", i); thread::sleep(Duration::from_millis(1)); } }
Main thread ochilgan thread tugashini kutadi va keyin for
loop siklini ishga tushiradi, shuning uchun bu yerda ko'rsatilganidek, chiqish boshqa qo'shilmaydi:
salom, ochilgan threaddan 1-raqam!
salom, ochilgan threaddan 2-raqam!
salom, ochilgan threaddan 3-raqam!
salom, ochilgan threaddan 4-raqam!
salom, ochilgan threaddan 5-raqam!
salom, ochilgan threaddan 6-raqam!
salom, ochilgan threaddan 7-raqam!
salom, ochilgan threaddan 8-raqam!
salom, ochilgan threaddan 9-raqam!
salom, main threaddan 1-raqam!
salom, main threaddan 2-raqam!
salom, main threaddan 3-raqam!
salom, main threaddan 4-raqam!
Kichik tafsilotlar(detail), masalan, join
deb ataladigan joy, sizning threadlaringiz bir vaqtning o'zida ishlashi yoki ishlamasligiga ta'sir qilishi mumkin.
Threadlar bilan move
closuredan foydalanish
Biz tez-tez closurelar thread::spawn
ga o'tiladigan move
kalit so'zidan foydalanamiz, chunki closure keyinchalik environmentdan foydalanadigan qiymatlarga ownershiplik(egalik) qiladi va shu tariqa bu qiymatlarga ownershiplik huquqini bir threaddan ikkinchisiga o'tkazadi. 13-bobning “Ma’lumotnomalarni qo‘lga kiritish yoki ownershiplik huquqini ko‘chirish” bo‘limida biz closure kontekstida move
ni muhokama qildik. Endi biz move
va thread::spawn
o'rtasidagi o'zaro ta'sirga ko'proq e'tibor qaratamiz.
Ro'yxat 16-1da e'tibor bering, biz thread::spawn
ga o'tadigan closure hech qanday argument talab qilmaydi: biz ochilgan thread kodidagi main threaddan hech qanday ma'lumotdan foydalanmayapmiz. Tugallangan threaddagi main threaddan ma'lumotlarni ishlatish uchun ochilgan threadning yopilishi kerakli qiymatlarni olishi kerak. 16-3 ro'yxatda main threadda vector yaratish va uni ishlab ochilgan threadda ishlatishga urinish ko'rsatilgan. Biroq, bu hali ishlamaydi, buni birozdan keyin ko'rasiz.
Fayl nomi: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Mana vektor: {:?}", v);
});
handle.join().unwrap();
}
Closure v
dan foydalanadi, shuning uchun u v
ni oladi va uni closure environmentining bir qismiga aylantiradi. Chunki thread::spawn
bu closureni yangi threadda ishga tushiradi, biz ushbu yangi thread ichidagi v
ga kirishimiz kerak. Ammo biz ushbu misolni kompilatsiya qilganimizda, biz quyidagi xatoni olamiz:
$ 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 v
ni qanday capture qilishni infers(xulosa) qiladi va println!
faqat v
ga reference kerakligi sababli, closure v
ni olishga harakat qiladi. Biroq, muammo bor: Rust ochilgan threrad qancha vaqt ishlashini ayta olmaydi, shuning uchun v
ga reference har doim haqiqiy(valiq yaroqli) bo'lishini bilmaydi.
16-4 ro'yxatda v
ga reference bo'lishi mumkin bo'lgan senariy ko'rsatilgan va u yaroqli(valiq) bo'lmaydi:
Fayl nomi: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Mana vektor: {:?}", v);
});
drop(v); // oh no!
handle.join().unwrap();
}
Agar Rust bizga ushbu kodni ishga tushirishga ruxsat bergan bo'lsa, ochilgan thread umuman ishlamasdan darhol fonga qo'yilishi mumkin. Ochilgan thread ichida v
ga reference bor, lekin main thread darhol v
ni tushiradi, biz 15-bobda muhokama qilgan drop
funksiyasidan foydalangan holda. Keyin, ochilgan thread bajarila boshlaganda, v
endi haqiqiy(valiq) emas, shuning uchun unga referense ham yaroqsiz. Oh yo'q!
16-3 ro'yxatdagi kompilyator xatosini tuzatish uchun xato xabari maslahatidan foydalanishimiz mumkin:
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 || {
| ++++
Closuredan oldin move
kalit so‘zini qo‘shish orqali biz closureni Rustga qiymatlarni olishi kerak degan xulosaga(infer) kelishga ruxsat berishdan ko‘ra, u foydalanadigan qiymatlarga ownershiklik qilishga majburlaymiz. 16-5 ro'yxatda ko'rsatilgan 16-3 ro'yxatga kiritilgan o'zgartirish biz xohlagan tarzda kompilatsiya bo'ladi va ishlaydi:
Fayl nomi: src/main.rs
use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Mana vektor: {:?}", v); }); handle.join().unwrap(); }
Kodni 16 4 ro'yxatida tuzatish uchun xuddi shu narsani sinab ko'rishimiz mumkin, bu yerda main thread move
closuresi orqali drop
deb ataladi. Biroq, bu tuzatish ishlamaydi, chunki 16-4-raqamli roʻyxat boshqa sabablarga koʻra amalga oshirilmaydi. Agar biz closurega move
ni qo‘shsak, biz v
ni closure environmentiga o'tkazamiz va biz main threadda endi drop
ni chaqira olmaymiz. Buning o'rniga biz ushbu kompilyator xatosini olamiz:
$ 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
Rustning ownershiplik(egalik) qoidalari bizni yana qutqardi! 16-3 roʻyxatdagi koddan xatoga yoʻl qoʻydik, chunki Rust konservativ boʻlib, thread uchun faqat v
harfini oldi, bu esa main thread nazariy jihatdan ochilgangan threadning referenceni bekor qilishi mumkinligini anglatadi. Rustga v
ownershiplik huquqini ochilgan threadga o'tkazishni aytish orqali biz Rustga main thread endi v
dan foydalanmasligiga kafolat beramiz. Agar biz 16-4 ro'yxatni xuddi shunday o'zgartirsak, main threadda v
dan foydalanmoqchi bo'lganimizda ownershiplik qoidalarini buzgan bo'lamiz. move
kalit so'zi Rustning borrowing olishning konservativ defaultini bekor qiladi; ownershiplik qoidalarini buzishimizga yo'l qo'ymaydi.
Threadar va thread API haqida asosiy tushunchaga ega bo'lgan holda, keling, threadlar bilan nima qilishimiz mumkinligini ko'rib chiqaylik.
Threadlar orasidagi ma'lumotlarni uzatish uchun Message Passing(xabar uzatish)dan foydalanish
Xavfsiz concurrencyni ta'minlashning tobora ommalashib borayotgan yondashuvlaridan biri bu message passing bo'lib, bu yerda threadlar yoki actorlar bir-biriga ma'lumotlarni o'z ichiga olgan xabarlarni yuborish orqali muloqot qilishadi. Go tili texnik hujjatlaridagi shiordagi g‘oya: “Xotirani almashish(share) orqali muloqot qilmang; Buning o'rniga, muloqot(communication) orqali xotirani share qiling."
Message-sending(xabar yuborish) concurrencyni amalga oshirish uchun Rustning standart kutubxonasi channels amalga oshirishni ta'minlaydi. Channel(kanal) - bu umumiy dasturlash tushunchasi bo'lib, uning yordamida ma'lumotlar bir threaddan ikkinchisiga yuboriladi.
Dasturlashdagi kanalni(channel) oqim yoki daryo kabi suvning yo'naltirilgan kanali kabi tasavvur qilishingiz mumkin. Agar siz daryoga rezina o'rdak kabi narsalarni qo'ysangiz, u suv yo'lining oxirigacha pastga tushadi.
Kanalning ikkita yarmi bor: uzatuvchi(transmitte) va qabul qiluvchi(receiver). Transmitterning yarmi daryoga rezina o'rdak qo'yadigan yuqori oqim joyidir va qabul qiluvchining yarmi rezina o'rdak quyi oqimga tushadi. Kodingizning bir qismi siz yubormoqchi bo'lgan ma'lumotlarga ega bo'lgan transmitterdagi metodlarni chaqiradi, boshqa qismi esa kelgan xabarlarni qabul qiluvchi tomonni tekshiradi. Agar transmitter yoki receiverning yarmi tushib qolsa, kanal closed(yopiq) deyiladi.
Bu yerda biz qiymatlarni yaratish va ularni kanalga yuborish uchun bitta threadga ega bo'lgan dasturni va qiymatlarni qabul qiladigan va ularni chop etadigan boshqa threadni ishlab chiqamiz. Featureni tasvirlash uchun kanal yordamida threadlar orasidagi oddiy qiymatlarni yuboramiz. Texnika bilan tanishganingizdan so'ng, siz bir-biringiz bilan aloqa o'rnatishingiz kerak bo'lgan har qanday threadlar uchun kanallardan foydalanishingiz mumkin, masalan, chat tizimi yoki ko'p threadlar hisoblash qismlarini bajaradigan va qismlarni natijalarni jamlaydigan bitta threadga yuboradigan tizim.
Birinchidan, 16-6 ro'yxatda biz channel(kanal) yaratamiz, lekin u bilan hech narsa qilmaymiz. E'tibor bering, bu hali kompilyatsiya qilinmaydi, chunki Rust kanal orqali qanday turdagi qiymatlarni yuborishimizni ayta olmaydi.
Fayl nomi: src/main.rs
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
Biz mpsc::channel
funksiyasidan foydalanib yangi kanal yaratamiz; mpsc
multiple producer, single consumer degan maʼnoni anglatadi. Qisqa qilib aytganda, Rustning standart kutubxonasi kanallarni implement qilish usuli kanalda qiymatlarni ishlab chiqaradigan bir nechta sending(jo'natish) uchlari bo'lishi mumkin, ammo bu qiymatlarni qabul qiladigan consumer faqat bitta receiving(qabul qiluvchi) end bo'lishi mumkinligini anglatadi. Tasavvur qiling-a, bir nechta daryolar birlashib, bitta katta daryoga quyiladi: har qanday oqim oxirida bitta daryoga tushadi. Hozircha biz bitta ishlab chiqaruvchidan(single producer) boshlaymiz, lekin biz ushbu misol ishlaganda bir nechta producerlarni(multiple producer) qo'shamiz.
mpsc::channel
funksiyasi tupleni qaytaradi, uning birinchi elementi jo'natuvchi end - transmitter va ikkinchi element - receiver end - qabul qiluvchidir. tx
va rx
qisqartmalari an'anaviy ravishda ko'plab sohalarda mos ravishda transmitter va receiver uchun ishlatiladi, shuning uchun biz har bir endni ko'rsatish uchun o'zgaruvchilarimizni shunday nomlaymiz. Biz tuplelarni destruksiya pattern let
statementdan foydalanmoqdamiz; Biz 18-bobda let
statementlarida patternlardan foydalanish va destruksiyani muhokama qilamiz. Hozircha shuni bilingki, let
statementdan shu tarzda foydalanish mpsc::channel
tomonidan qaytarilgan tuple qismlarini ajratib olishning qulay usuli hisoblanadi.
16-7 ro'yxatda ko'rsatilganidek, transmitter uchini ochilgan threadga o'tkazamiz va u bitta threadni yuborsin, shunda hosil qilingan thread main thread bilan bog'lanadi. Bu daryoning yuqori oqimiga rezina o'rdak qo'yish yoki bir threaddan ikkinchisiga chat xabarini yuborishga o'xshaydi.
Fayl nomi: src/main.rs
use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("salom"); tx.send(val).unwrap(); }); }
Shunga qaramay, biz yangi thread yaratish uchun thread::spawn
dan foydalanamiz va keyin move
yordamida tx
ni yopishga(close) o'tkazamiz, shunda hosil qilingan thread tx
ga ega bo'ladi. Kanal orqali xabarlarni jo'natish uchun ochilgan thread transmitterga ega bo'lishi kerak. Transmitterda biz jo'natmoqchi bo'lgan qiymatni qabul qiluvchi send
metodi mavjud. send
metodi Result<T, E>
typeni qaytaradi, shuning uchun agar qabul qiluvchi(receiver) allaqachon drop qilingan bo'lsa va qiymatni yuborish uchun joy bo'lmasa, yuborish operatsiyasi xatolikni qaytaradi. Ushbu misolda biz xatolik yuz berganda panic qo'yish uchun unwrap
ni chaqiramiz. Ammo haqiqiy dasturda biz uni to'g'ri hal qilamiz: xatolarni to'g'ri hal qilish strategiyalarini ko'rib chiqish uchun 9-bobga qayting.
16-8 ro'yxatda biz main threaddagi qabul qiluvchidan(receive) qiymatni olamiz. Bu daryoning oxiridagi suvdan rezina o'rdakni olish yoki suhbat xabarini olish kabi.
Fayl nomi: src/main.rs
use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("salom"); tx.send(val).unwrap(); }); let received = rx.recv().unwrap(); println!("Tushundim: {}", received); }
Receiverda ikkita foydali metod mavjud: recv
va try_recv
. Biz recv
dan foydalanmoqdamiz, bu receive(qabul qilish) ning qisqartmasi bo'lib, u main threadning bajarilishini bloklaydi va kanalga qiymat yuborilguncha kutadi. Qiymat yuborilgach, recv
uni Result<T, E>
shaklida qaytaradi. Transmitter yopilganda, recv
boshqa qiymatlar kelmasligini bildirish uchun xatolikni qaytaradi.
try_recv
metodi bloklanmaydi, aksincha darhol Result<T, E>
ni qaytaradi: Ok
qiymati, agar mavjud bo‘lsa, xabarni ushlab turadi va bu safar hech qanday xabar bo‘lmasa, Err
qiymati. try_recv
dan foydalanish, agar ushbu thread xabarlarni kutayotganda boshqa ishi boʻlsa foydali boʻladi: biz tez-tez try_recv
ni chaqiradigan, agar mavjud bo'lsa, xabarni ko'rib chiqadigan va boshqasi qayta tekshirilgunga qadar biroz vaqt ishlaydigan sikl yozishimiz mumkin.
Biz ushbu misolda soddalik uchun recv
dan foydalandik; bizda main thread uchun xabarlarni kutishdan boshqa ishimiz yo'q, shuning uchun main threadi bloklash maqsadga muvofiqdir.
Kodni 16-8 ro'yxatda ishga tushirganimizda, biz main threaddan chop etilgan qiymatni ko'ramiz:
Tushundim: salom
Mukammal! Perfect!
Kanallar va ownershiplik(egalkik) huquqini o'tkazish
Ownershiplik qoidalari xabarlarni jo'natishda muhim rol o'ynaydi, chunki ular xavfsiz, bir vaqtda kod yozishga yordam beradi. Bir vaqtning o'zida dasturlashda(concurrent programming) xatolarning oldini olish Rust dasturlarida ownershiplik haqida o'ylashning afzalligi hisoblanadi. Muammolarning oldini olish uchun kanallar va ownershiplik qanday ishlashini ko‘rsatish uchun tajriba o‘tkazamiz: biz kanalga yuborganimizdan so‘ng val
qiymatidan foydalanishga harakat qilamiz. Nima uchun bu kodga ruxsat berilmaganligini bilish uchun 16-9-raqamdagi kodni kompilyatsiya qilib ko'ring:
Fayl nomi: src/main.rs
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("salom");
tx.send(val).unwrap();
println!("qandaysan {}", val);
});
let received = rx.recv().unwrap();
println!("Tushundim: {}", received);
}
Bu yerda biz tx.send
orqali kanalga yuborganimizdan so‘ng val
ni chop etishga harakat qilamiz.
Bunga ruxsat berish noto'g'ri fikr bo'ladi: qiymat boshqa threadga yuborilgandan so'ng, biz qiymatni qayta ishlatishdan oldin uni o'zgartirishi yoki tashlab yuborishi(drop) mumkin. Potensial ravishda, boshqa threadning o'zgartirishlari nomuvofiq yoki mavjud bo'lmagan ma'lumotlar tufayli xatolar yoki kutilmagan natijalarga olib kelishi mumkin. Biroq, agar biz 16-9 ro'yxatdagi kodni kompilyatsiya qilmoqchi bo'lsak, Rust bizga xato qiladi:
$ 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` which comes from the expansion of the macro `println` (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
Bizning concurrency xatomiz kompilyatsiya vaqtida xatolikka olib keldi. send
funksiyasi oʻz parametriga ownershiplik qiladi va qiymat koʻchirilganda qabul qiluvchi(receiver) unga ownershiplik qiladi. Bu bizni qiymatni yuborgandan keyin tasodifan qayta ishlatishdan to'xtatadi; ownershiplik tizimi hamma narsa yaxshi ekanligini tekshiradi.
Bir nechta qiymatlarni yuborish va qabul qiluvchining(receiver) kutayotganini ko'rish
16-8 ro'yxatdagi kod kompilatsiya bo'ldi va ishga tushirildi, lekin u bizga ikkita alohida thread kanal orqali bir-biri bilan gaplashayotganini aniq ko'rsatmadi. 16-10-ro'yxatda biz 16-8-ro'yxatdagi kod bir vaqtda ishlayotganini tasdiqlovchi ba'zi o'zgartirishlar kiritdik: ochilgan thread endi bir nechta xabarlarni yuboradi va har bir xabar o'rtasida bir soniya pauza qiladi.
Fayl nomi: 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("qandaysan"),
String::from("otabek"),
String::from("vodiyga"),
String::from("ketti"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Tushundim: {}", received);
}
}
Bu safar, ochilgan threadda biz main threadga yubormoqchi bo'lgan satrlar vektori mavjud. Biz ularni takrorlaymiz, har birini alohida yuboramiz va 1 soniyalik Duration
qiymati bilan thread::sleep
funksiyasini chaqirish orqali har biri o‘rtasida pauza qilamiz.
Main threadda biz endi recv
funksiyasini aniq chaqirmayapmiz: buning o'rniga biz rx
ni iterator sifatida ko'rib chiqamiz. Qabul qilingan har bir qiymat uchun biz uni chop etamiz. Kanal yopilganda(close), iteratsiya tugaydi.
16-10 ro'yxatdagi kodni ishga tushirganda, har bir satr orasida 1 soniyalik pauza bilan quyidagi chiqishni ko'rishingiz kerak:
Tushundim: qandaysan
Tushundim: otabek
Tushundim: vodiyga
Tushundim: ketti
Bizda main threaddagi for
siklida pauza yoki kechikishlar keltirib chiqaradigan kod yo‘qligi sababli, biz main thread hosil qilingan threaddan qiymatlarni olishni kutayotganini aytishimiz mumkin.
Transmitterni klonlash orqali bir nechta producerlarni yaratish
Avvalroq mpsc
multiple producer, single consumer degan qisqartma ekanligini eslatib o'tgan edik. Keling, 16-10 ro'yxatdagi kodni ishlatish va kengaytirish uchun mpsc
ni qo'yaylik va barchasi bir xil qabul qiluvchiga(receiver) qiymatlarni yuboradigan bir nechta threadlarni yaratamiz. Biz buni 16-11 ro'yxatda ko'rsatilganidek, transmitterni klonlash orqali amalga oshirishimiz mumkin:
Fayl nomi: 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("salom"),
String::from("threaddan"),
String::from("siz"),
String::from("uchun"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
thread::spawn(move || {
let vals = vec![
String::from("ko'plab"),
String::from("habarlar"),
String::from("hammasi"),
String::from("ishlayapti"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Tushundim: {}", received);
}
// --snip--
}
Bu safar, birinchi ochilgan threadni yaratishdan oldin, biz transmitterda clone
deb nomlaymiz. Bu bizga yangi transmitterni beradi, biz birinchi ochilgan threadga o'tishimiz mumkin. Biz asl transmitterni ikkinchi ochilgan threadga o'tkazamiz.
Bu bizga ikkita thread beradi, ularning har biri bitta qabul qiluvchiga(receiver) turli xabarlar yuboradi.
Kodni ishga tushirganingizda, chiqishingiz quyidagicha ko'rinishi kerak:
Tushundim: salom
Tushundim: threaddan
Tushundim: siz
Tushundim: uchun
Tushundim: ko'plab
Tushundim: habarlar
Tushundim: hammasi
Tushundim: ishlayapti
Tizimingizga qarab qiymatlarni boshqa tartibda ko'rishingiz mumkin. Bu concurrencyni qiziqarli va qiyin qiladi. Agar siz thread::sleep
bilan tajriba o'tkazsangiz, unga turli threadlarda turli qiymatlar bersangiz, har bir ishga tushirish aniqroq bo'lmaydi va har safar har xil chiqish hosil qiladi.
Endi biz kanallar qanday ishlashini ko'rib chiqdik, keling, boshqa concurrency usulini ko'rib chiqaylik.
Concurrencyda Shared-State
Message passing(Xabarni uzatish) - bu concurrencyni boshqarishning yaxshi usuli, ammo bu yagona emas. Yana bir usul bir nechta(multiple) threadlar bir xil umumiy ma'lumotlarga(shared data) kirishlari mumkin. Go tilidagi texnik hujjatlardagi shiorning ushbu qismini yana bir bor ko'rib chiqing: "xotirani almashish(sharing memory) orqali muloqot(comminicate) qilmang."
Xotirani almashish(sharing memory) orqali muloqot(comminication) qanday ko'rinishga ega bo'lar edi? Bundan tashqari, nima uchun message-passing enthusiastlar memory sharingdan foydalanmaslik haqida ogohlantiradilar?
Qaysidir ma'noda, har qanday dasturlash tilidagi kanallar bitta ownershiplik huquqiga o'xshaydi, chunki qiymatni kanalga o'tkazganingizdan so'ng, siz boshqa qiymatdan foydalanmasligingiz kerak. Shared memory concurrencyda bir nechta ownershiplik huquqiga o'xshaydi: concurrencyda bir nechta threadlar bir xil xotira joyiga(memory location) kirishi mumkin. 15-bobda ko'rganingizdek, smart pointerlar bir nechta ownershiplik qilish imkoniyatini yaratdi, bir nechta(multiple) ownershiplik murakkablikni oshirishi mumkin, chunki bu turli ownerlarni boshqarish kerak. Rust type tizimi va ownershiplik qoidalari ushbu boshqaruvni to'g'ri bajarishga katta yordam beradi. Misol uchun, shared memory uchun eng keng tarqalgan concurrency primitivlaridan biri bo'lgan mutexlarni ko'rib chiqaylik.
Bir vaqtning o'zida bitta threaddan ma'lumotlarga kirishga ruxsat berish uchun mutexlardan foydalanish
Mutex bu mutual exclusion ning qisqartmasi boʻlib, mutex istalgan vaqtda baʼzi maʼlumotlarga faqat bitta threadga kirish imkonini beradi. Mutexdagi ma'lumotlarga kirish uchun thread birinchi navbatda mutexning *lock(qulf)*ni olishni so'rab kirishni xohlashini bildirishi kerak. Lock(qulf) - bu mutexning bir qismi bo'lgan ma'lumotlar tuzilmasi bo'lib, u hozirda ma'lumotlarga kimning eksklyuziv kirish huquqiga ega ekanligini kuzatib boradi. Shuning uchun, mutex qulflash tizimi(locking system) orqali o'zida mavjud bo'lgan ma'lumotlarni himoya qilish(guarding) sifatida tavsiflanadi.
Mutexlardan foydalanish qiyinligi bilan mashhur, chunki siz ikkita qoidani eslab qolishingiz kerak:
- Ma'lumotlardan foydalanishdan oldin siz qulfni olishga harakat qilishingiz kerak.
- Mutex himoya qiladigan ma'lumotlar bilan ishlashni tugatgandan so'ng, boshqa threadlar qulfni(lock) olishi uchun ma'lumotlarni qulfdan chiqarishingiz(unlock) kerak.
Mutexni tushunish uchun bitta mikrofon bilan konferensiyada guruh muhokamasining haqiqiy hayotiy misolini tasavvur qiling. Panel ishtirokchisi gapirishdan oldin mikrofondan foydalanishni xohlashini so'rashi yoki signal berishi kerak. Mikrofonni olishganda, ular xohlagancha gaplashishi mumkin va keyin mikrofonni gapirishni so'ragan keyingi ishtirokchiga beradi. Agar panel ishtirokchisi mikrofon bilan ishlashni tugatgandan so'ng uni o'chirishni unutib qo'ysa, boshqa hech kim gapira olmaydi. Agar umumiy mikrofonni boshqarish noto'g'ri bo'lsa, panel rejalashtirilganidek ishlamaydi!
Mutexlarni boshqarish juda qiyin bo'lishi mumkin, shuning uchun ko'p odamlar kanallarga(channel) ishtiyoq bilan qarashadi. Biroq, Rust type tizimi va ownershiplik qoidalari tufayli siz qulflash(locking) va qulfni noto'g'ri ochishingiz(unlocking) mumkin emas.
Mutex<T>
API
Mutexdan qanday foydalanishga misol sifatida, keling, 16-12 ro'yxatda ko'rsatilganidek, bitta threadli kontekstda mutexdan foydalanishdan boshlaylik:
Fayl nomi: src/main.rs
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut raqam = m.lock().unwrap(); *raqam = 6; } println!("m = {:?}", m); }
Ko'pgina turlarda(type) bo'lgani kabi, biz bog'langan new
funksiyasidan foydalangan holda Mutex<T>
ni yaratamiz.
Mutex ichidagi ma'lumotlarga kirish uchun biz qulfni olish uchun lock
metodidan foydalanamiz. Bu chaqiruv joriy threadni bloklaydi, shuning uchun u bizni qulflash navbati kelmaguncha hech qanday ishni bajara olmaydi.
Qulfni ushlab turgan boshqa thread panic qo'zg'atsa, lock
chaqiruvi muvaffaqiyatsiz bo'ladi. Bunday holda, hech kim qulfni qo'lga kirita olmaydi, shuning uchun biz unwrap
ni tanladik va agar shunday vaziyatda bo'lsak, bu threadni panic qo'yishni tanladik.
Qulfni qo'lga kiritganimizdan so'ng, biz bu holatda num
deb nomlangan return qiymatini ichidagi ma'lumotlarga o'zgaruvchan reference sifatida ko'rib chiqishimiz mumkin. Tur(type) tizimi m
dagi qiymatni ishlatishdan oldin qulfni olishimizni ta'minlaydi. m
turi i32
emas, Mutex<i32>
, shuning uchun biz i32
qiymatidan foydalanish uchun lock
ni chaqirishimiz kerak. Biz unuta olmaymiz; aks holda turdagi tizim bizga ichki i32
ga kirishga ruxsat bermaydi.
As you might suspect, Mutex<T>
is a smart pointer. More accurately, the call
to lock
returns a smart pointer called MutexGuard
, wrapped in a
LockResult
that we handled with the call to unwrap
. The MutexGuard
smart
pointer implements Deref
to point at our inner data; the smart pointer also
has a Drop
implementation that releases the lock automatically when a
MutexGuard
goes out of scope, which happens at the end of the inner scope. As
a result, we don’t risk forgetting to release the lock and blocking the mutex
from being used by other threads, because the lock release happens
automatically.
After dropping the lock, we can print the mutex value and see that we were able
to change the inner i32
to 6.
Sharing a Mutex<T>
Between Multiple Threads
Now, let’s try to share a value between multiple threads using Mutex<T>
.
We’ll spin up 10 threads and have them each increment a counter value by 1, so
the counter goes from 0 to 10. The next example in Listing 16-13 will have
a compiler error, and we’ll use that error to learn more about using
Mutex<T>
and how Rust helps us use it correctly.
Fayl nomi: 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());
}
We create a counter
variable to hold an i32
inside a Mutex<T>
, as we did
in Listing 16-12. Next, we create 10 threads by iterating over a range of
numbers. We use thread::spawn
and give all the threads the same closure: one
that moves the counter into the thread, acquires a lock on the Mutex<T>
by
calling the lock
method, and then adds 1 to the value in the mutex. When a
thread finishes running its closure, num
will go out of scope and release the
lock so another thread can acquire it.
In the main thread, we collect all the join handles. Then, as we did in Listing
16-2, we call join
on each handle to make sure all the threads finish. At
that point, the main thread will acquire the lock and print the result of this
program.
We hinted that this example wouldn’t compile. Now let’s find out why!
$ 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
The error message states that the counter
value was moved in the previous
iteration of the loop. Rust is telling us that we can’t move the ownership
of lock counter
into multiple threads. Let’s fix the compiler error with a
multiple-ownership method we discussed in Chapter 15.
Multiple Ownership with Multiple Threads
In Chapter 15, we gave a value multiple owners by using the smart pointer
Rc<T>
to create a reference counted value. Let’s do the same here and see
what happens. We’ll wrap the Mutex<T>
in Rc<T>
in Listing 16-14 and clone
the Rc<T>
before moving ownership to the thread.
Fayl nomi: 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());
}
Once again, we compile and get... different errors! The compiler is teaching us a lot.
$ 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:36
|
11 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `[closure@src/main.rs:11:36: 11:43]`
| | |
| | required by a bound introduced by this call
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
|
= help: within `[closure@src/main.rs:11:36: 11:43]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
note: required because it's used within this closure
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/src/thread/mod.rs:704:8
|
= note: required by this bound in `spawn`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` due to previous error
Wow, that error message is very wordy! Here’s the important part to focus on:
`Rc<Mutex<i32>>` cannot be sent between threads safely
. The compiler is
also telling us the reason why: the trait `Send` is not implemented for `Rc<Mutex<i32>>`
. We’ll talk about Send
in the next section: it’s one of
the traits that ensures the types we use with threads are meant for use in
concurrent situations.
Unfortunately, Rc<T>
is not safe to share across threads. When Rc<T>
manages the reference count, it adds to the count for each call to clone
and
subtracts from the count when each clone is dropped. But it doesn’t use any
concurrency primitives to make sure that changes to the count can’t be
interrupted by another thread. This could lead to wrong counts—subtle bugs that
could in turn lead to memory leaks or a value being dropped before we’re done
with it. What we need is a type exactly like Rc<T>
but one that makes changes
to the reference count in a thread-safe way.
Atomic Reference Counting with Arc<T>
Fortunately, Arc<T>
is a type like Rc<T>
that is safe to use in
concurrent situations. The a stands for atomic, meaning it’s an atomically
reference counted type. Atomics are an additional kind of concurrency
primitive that we won’t cover in detail here: see the standard library
documentation for std::sync::atomic
for more
details. At this point, you just need to know that atomics work like primitive
types but are safe to share across threads.
You might then wonder why all primitive types aren’t atomic and why standard
library types aren’t implemented to use Arc<T>
by default. The reason is that
thread safety comes with a performance penalty that you only want to pay when
you really need to. If you’re just performing operations on values within a
single thread, your code can run faster if it doesn’t have to enforce the
guarantees atomics provide.
Let’s return to our example: Arc<T>
and Rc<T>
have the same API, so we fix
our program by changing the use
line, the call to new
, and the call to
clone
. The code in Listing 16-15 will finally compile and run:
Fayl nomi: 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()); }
This code will print the following:
Result: 10
We did it! We counted from 0 to 10, which may not seem very impressive, but it
did teach us a lot about Mutex<T>
and thread safety. You could also use this
program’s structure to do more complicated operations than just incrementing a
counter. Using this strategy, you can divide a calculation into independent
parts, split those parts across threads, and then use a Mutex<T>
to have each
thread update the final result with its part.
Note that if you are doing simple numerical operations, there are types simpler
than Mutex<T>
types provided by the std::sync::atomic
module of the
standard library. These types provide safe, concurrent,
atomic access to primitive types. We chose to use Mutex<T>
with a primitive
type for this example so we could concentrate on how Mutex<T>
works.
Similarities Between RefCell<T>
/Rc<T>
and Mutex<T>
/Arc<T>
You might have noticed that counter
is immutable but we could get a mutable
reference to the value inside it; this means Mutex<T>
provides interior
mutability, as the Cell
family does. In the same way we used RefCell<T>
in
Chapter 15 to allow us to mutate contents inside an Rc<T>
, we use Mutex<T>
to mutate contents inside an Arc<T>
.
Another detail to note is that Rust can’t protect you from all kinds of logic
errors when you use Mutex<T>
. Recall in Chapter 15 that using Rc<T>
came
with the risk of creating reference cycles, where two Rc<T>
values refer to
each other, causing memory leaks. Similarly, Mutex<T>
comes with the risk of
creating deadlocks. These occur when an operation needs to lock two resources
and two threads have each acquired one of the locks, causing them to wait for
each other forever. If you’re interested in deadlocks, try creating a Rust
program that has a deadlock; then research deadlock mitigation strategies for
mutexes in any language and have a go at implementing them in Rust. The
standard library API documentation for Mutex<T>
and MutexGuard
offers
useful information.
We’ll round out this chapter by talking about the Send
and Sync
traits and
how we can use them with custom types.
Extensible Concurrency with the Sync
and Send
Traits
Interestingly, the Rust language has very few concurrency features. Almost every concurrency feature we’ve talked about so far in this chapter has been part of the standard library, not the language. Your options for handling concurrency are not limited to the language or the standard library; you can write your own concurrency features or use those written by others.
However, two concurrency concepts are embedded in the language: the
std::marker
traits Sync
and Send
.
Allowing Transference of Ownership Between Threads with Send
The Send
marker trait indicates that ownership of values of the type
implementing Send
can be transferred between threads. Almost every Rust type
is Send
, but there are some exceptions, including Rc<T>
: this cannot be
Send
because if you cloned an Rc<T>
value and tried to transfer ownership
of the clone to another thread, both threads might update the reference count
at the same time. For this reason, Rc<T>
is implemented for use in
single-threaded situations where you don’t want to pay the thread-safe
performance penalty.
Therefore, Rust’s type system and trait bounds ensure that you can never
accidentally send an Rc<T>
value across threads unsafely. When we tried to do
this in Listing 16-14, we got the error the trait Send is not implemented for Rc<Mutex<i32>>
. When we switched to Arc<T>
, which is Send
, the code
compiled.
Any type composed entirely of Send
types is automatically marked as Send
as
well. Almost all primitive types are Send
, aside from raw pointers, which
we’ll discuss in Chapter 19.
Allowing Access from Multiple Threads with Sync
The Sync
marker trait indicates that it is safe for the type implementing
Sync
to be referenced from multiple threads. In other words, any type T
is
Sync
if &T
(an immutable reference to T
) is Send
, meaning the reference
can be sent safely to another thread. Similar to Send
, primitive types are
Sync
, and types composed entirely of types that are Sync
are also Sync
.
The smart pointer Rc<T>
is also not Sync
for the same reasons that it’s not
Send
. The RefCell<T>
type (which we talked about in Chapter 15) and the
family of related Cell<T>
types are not Sync
. The implementation of borrow
checking that RefCell<T>
does at runtime is not thread-safe. The smart
pointer Mutex<T>
is Sync
and can be used to share access with multiple
threads as you saw in the “Sharing a Mutex<T>
Between Multiple
Threads” section.
Implementing Send
and Sync
Manually Is Unsafe
Because types that are made up of Send
and Sync
traits are automatically
also Send
and Sync
, we don’t have to implement those traits manually. As
marker traits, they don’t even have any methods to implement. They’re just
useful for enforcing invariants related to concurrency.
Manually implementing these traits involves implementing unsafe Rust code.
We’ll talk about using unsafe Rust code in Chapter 19; for now, the important
information is that building new concurrent types not made up of Send
and
Sync
parts requires careful thought to uphold the safety guarantees. “The
Rustonomicon” has more information about these guarantees and how to
uphold them.
Summary
This isn’t the last you’ll see of concurrency in this book: the project in Chapter 20 will use the concepts in this chapter in a more realistic situation than the smaller examples discussed here.
As mentioned earlier, because very little of how Rust handles concurrency is part of the language, many concurrency solutions are implemented as crates. These evolve more quickly than the standard library, so be sure to search online for the current, state-of-the-art crates to use in multithreaded situations.
The Rust standard library provides channels for message passing and smart
pointer types, such as Mutex<T>
and Arc<T>
, that are safe to use in
concurrent contexts. The type system and the borrow checker ensure that the
code using these solutions won’t end up with data races or invalid references.
Once you get your code to compile, you can rest assured that it will happily
run on multiple threads without the kinds of hard-to-track-down bugs common in
other languages. Concurrent programming is no longer a concept to be afraid of:
go forth and make your programs concurrent, fearlessly!
Next, we’ll talk about idiomatic ways to model problems and structure solutions as your Rust programs get bigger. In addition, we’ll discuss how Rust’s idioms relate to those you might be familiar with from object-oriented programming.
Object-Oriented Programming Features of Rust
Object-oriented programming (OOP) is a way of modeling programs. Objects as a programmatic concept were introduced in the programming language Simula in the 1960s. Those objects influenced Alan Kay’s programming architecture in which objects pass messages to each other. To describe this architecture, he coined the term object-oriented programming in 1967. Many competing definitions describe what OOP is, and by some of these definitions Rust is object-oriented, but by others it is not. In this chapter, we’ll explore certain characteristics that are commonly considered object-oriented and how those characteristics translate to idiomatic Rust. We’ll then show you how to implement an object-oriented design pattern in Rust and discuss the trade-offs of doing so versus implementing a solution using some of Rust’s strengths instead.
Characteristics of Object-Oriented Languages
There is no consensus in the programming community about what features a language must have to be considered object-oriented. Rust is influenced by many programming paradigms, including OOP; for example, we explored the features that came from functional programming in Chapter 13. Arguably, OOP languages share certain common characteristics, namely objects, encapsulation, and inheritance. Let’s look at what each of those characteristics means and whether Rust supports it.
Objects Contain Data and Behavior
The book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley Professional, 1994), colloquially referred to as The Gang of Four book, is a catalog of object-oriented design patterns. It defines OOP this way:
Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data. The procedures are typically called methods or operations.
Using this definition, Rust is object-oriented: structs and enums have data,
and impl
blocks provide methods on structs and enums. Even though structs and
enums with methods aren’t called objects, they provide the same
functionality, according to the Gang of Four’s definition of objects.
Encapsulation that Hides Implementation Details
Another aspect commonly associated with OOP is the idea of encapsulation, which means that the implementation details of an object aren’t accessible to code using that object. Therefore, the only way to interact with an object is through its public API; code using the object shouldn’t be able to reach into the object’s internals and change data or behavior directly. This enables the programmer to change and refactor an object’s internals without needing to change the code that uses the object.
We discussed how to control encapsulation in Chapter 7: we can use the pub
keyword to decide which modules, types, functions, and methods in our code
should be public, and by default everything else is private. For example, we
can define a struct AveragedCollection
that has a field containing a vector
of i32
values. The struct can also have a field that contains the average of
the values in the vector, meaning the average doesn’t have to be computed
on demand whenever anyone needs it. In other words, AveragedCollection
will
cache the calculated average for us. Listing 17-1 has the definition of the
AveragedCollection
struct:
Filename: src/lib.rs
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
The struct is marked pub
so that other code can use it, but the fields within
the struct remain private. This is important in this case because we want to
ensure that whenever a value is added or removed from the list, the average is
also updated. We do this by implementing add
, remove
, and average
methods
on the struct, as shown in Listing 17-2:
Filename: 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;
}
}
The public methods add
, remove
, and average
are the only ways to access
or modify data in an instance of AveragedCollection
. When an item is added
to list
using the add
method or removed using the remove
method, the
implementations of each call the private update_average
method that handles
updating the average
field as well.
We leave the list
and average
fields private so there is no way for
external code to add or remove items to or from the list
field directly;
otherwise, the average
field might become out of sync when the list
changes. The average
method returns the value in the average
field,
allowing external code to read the average
but not modify it.
Because we’ve encapsulated the implementation details of the struct
AveragedCollection
, we can easily change aspects, such as the data structure,
in the future. For instance, we could use a HashSet<i32>
instead of a
Vec<i32>
for the list
field. As long as the signatures of the add
,
remove
, and average
public methods stay the same, code using
AveragedCollection
wouldn’t need to change. If we made list
public instead,
this wouldn’t necessarily be the case: HashSet<i32>
and Vec<i32>
have
different methods for adding and removing items, so the external code would
likely have to change if it were modifying list
directly.
If encapsulation is a required aspect for a language to be considered
object-oriented, then Rust meets that requirement. The option to use pub
or
not for different parts of code enables encapsulation of implementation details.
Inheritance as a Type System and as Code Sharing
Inheritance is a mechanism whereby an object can inherit elements from another object’s definition, thus gaining the parent object’s data and behavior without you having to define them again.
If a language must have inheritance to be an object-oriented language, then Rust is not one. There is no way to define a struct that inherits the parent struct’s fields and method implementations without using a macro.
However, if you’re used to having inheritance in your programming toolbox, you can use other solutions in Rust, depending on your reason for reaching for inheritance in the first place.
You would choose inheritance for two main reasons. One is for reuse of code:
you can implement particular behavior for one type, and inheritance enables you
to reuse that implementation for a different type. You can do this in a limited
way in Rust code using default trait method implementations, which you saw in
Listing 10-14 when we added a default implementation of the summarize
method
on the Summary
trait. Any type implementing the Summary
trait would have
the summarize
method available on it without any further code. This is
similar to a parent class having an implementation of a method and an
inheriting child class also having the implementation of the method. We can
also override the default implementation of the summarize
method when we
implement the Summary
trait, which is similar to a child class overriding the
implementation of a method inherited from a parent class.
The other reason to use inheritance relates to the type system: to enable a child type to be used in the same places as the parent type. This is also called polymorphism, which means that you can substitute multiple objects for each other at runtime if they share certain characteristics.
Polymorphism
To many people, polymorphism is synonymous with inheritance. But it’s actually a more general concept that refers to code that can work with data of multiple types. For inheritance, those types are generally subclasses.
Rust instead uses generics to abstract over different possible types and trait bounds to impose constraints on what those types must provide. This is sometimes called bounded parametric polymorphism.
Inheritance has recently fallen out of favor as a programming design solution in many programming languages because it’s often at risk of sharing more code than necessary. Subclasses shouldn’t always share all characteristics of their parent class but will do so with inheritance. This can make a program’s design less flexible. It also introduces the possibility of calling methods on subclasses that don’t make sense or that cause errors because the methods don’t apply to the subclass. In addition, some languages will only allow single inheritance (meaning a subclass can only inherit from one class), further restricting the flexibility of a program’s design.
For these reasons, Rust takes the different approach of using trait objects instead of inheritance. Let’s look at how trait objects enable polymorphism in Rust.
Using Trait Objects That Allow for Values of Different Types
In Chapter 8, we mentioned that one limitation of vectors is that they can
store elements of only one type. We created a workaround in Listing 8-9 where
we defined a SpreadsheetCell
enum that had variants to hold integers, floats,
and text. This meant we could store different types of data in each cell and
still have a vector that represented a row of cells. This is a perfectly good
solution when our interchangeable items are a fixed set of types that we know
when our code is compiled.
However, sometimes we want our library user to be able to extend the set of
types that are valid in a particular situation. To show how we might achieve
this, we’ll create an example graphical user interface (GUI) tool that iterates
through a list of items, calling a draw
method on each one to draw it to the
screen—a common technique for GUI tools. We’ll create a library crate called
gui
that contains the structure of a GUI library. This crate might include
some types for people to use, such as Button
or TextField
. In addition,
gui
users will want to create their own types that can be drawn: for
instance, one programmer might add an Image
and another might add a
SelectBox
.
We won’t implement a fully fledged GUI library for this example but will show
how the pieces would fit together. At the time of writing the library, we can’t
know and define all the types other programmers might want to create. But we do
know that gui
needs to keep track of many values of different types, and it
needs to call a draw
method on each of these differently typed values. It
doesn’t need to know exactly what will happen when we call the draw
method,
just that the value will have that method available for us to call.
To do this in a language with inheritance, we might define a class named
Component
that has a method named draw
on it. The other classes, such as
Button
, Image
, and SelectBox
, would inherit from Component
and thus
inherit the draw
method. They could each override the draw
method to define
their custom behavior, but the framework could treat all of the types as if
they were Component
instances and call draw
on them. But because Rust
doesn’t have inheritance, we need another way to structure the gui
library to
allow users to extend it with new types.
Defining a Trait for Common Behavior
To implement the behavior we want gui
to have, we’ll define a trait named
Draw
that will have one method named draw
. Then we can define a vector that
takes a trait object. A trait object points to both an instance of a type
implementing our specified trait and a table used to look up trait methods on
that type at runtime. We create a trait object by specifying some sort of
pointer, such as a &
reference or a Box<T>
smart pointer, then the dyn
keyword, and then specifying the relevant trait. (We’ll talk about the reason
trait objects must use a pointer in Chapter 19 in the section “Dynamically
Sized Types and the Sized
Trait.”) We can
use trait objects in place of a generic or concrete type. Wherever we use a
trait object, Rust’s type system will ensure at compile time that any value
used in that context will implement the trait object’s trait. Consequently, we
don’t need to know all the possible types at compile time.
We’ve mentioned that, in Rust, we refrain from calling structs and enums
“objects” to distinguish them from other languages’ objects. In a struct or
enum, the data in the struct fields and the behavior in impl
blocks are
separated, whereas in other languages, the data and behavior combined into one
concept is often labeled an object. However, trait objects are more like
objects in other languages in the sense that they combine data and behavior.
But trait objects differ from traditional objects in that we can’t add data to
a trait object. Trait objects aren’t as generally useful as objects in other
languages: their specific purpose is to allow abstraction across common
behavior.
Listing 17-3 shows how to define a trait named Draw
with one method named
draw
:
Filename: src/lib.rs
pub trait Draw {
fn draw(&self);
}
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.
Filename: src/lib.rs
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
On the Screen
struct, we’ll define a method named run
that will call the
draw
method on each of its components
, as shown in Listing 17-5:
Filename: 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();
}
}
}
This works differently from defining a struct that uses a generic type
parameter with trait bounds. A generic type parameter can only be substituted
with one concrete type at a time, whereas trait objects allow for multiple
concrete types to fill in for the trait object at runtime. For example, we
could have defined the Screen
struct using a generic type and a trait bound
as in Listing 17-6:
Filename: 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();
}
}
}
This restricts us to a Screen
instance that has a list of components all of
type Button
or all of type TextField
. If you’ll only ever have homogeneous
collections, using generics and trait bounds is preferable because the
definitions will be monomorphized at compile time to use the concrete types.
On the other hand, with the method using trait objects, one Screen
instance
can hold a Vec<T>
that contains a Box<Button>
as well as a
Box<TextField>
. Let’s look at how this works, and then we’ll talk about the
runtime performance implications.
Implementing the Trait
Now we’ll add some types that implement the Draw
trait. We’ll provide the
Button
type. Again, actually implementing a GUI library is beyond the scope
of this book, so the draw
method won’t have any useful implementation in its
body. To imagine what the implementation might look like, a Button
struct
might have fields for width
, height
, and label
, as shown in Listing 17-7:
Filename: 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
}
}
The width
, height
, and label
fields on Button
will differ from the
fields on other components; for example, a TextField
type might have those
same fields plus a placeholder
field. Each of the types we want to draw on
the screen will implement the Draw
trait but will use different code in the
draw
method to define how to draw that particular type, as Button
has here
(without the actual GUI code, as mentioned). The Button
type, for instance,
might have an additional impl
block containing methods related to what
happens when a user clicks the button. These kinds of methods won’t apply to
types like TextField
.
If someone using our library decides to implement a SelectBox
struct that has
width
, height
, and options
fields, they implement the Draw
trait on the
SelectBox
type as well, as shown in Listing 17-8:
Filename: 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() {}
Our library’s user can now write their main
function to create a Screen
instance. To the Screen
instance, they can add a SelectBox
and a Button
by putting each in a Box<T>
to become a trait object. They can then call the
run
method on the Screen
instance, which will call draw
on each of the
components. Listing 17-9 shows this implementation:
Filename: 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();
}
When we wrote the library, we didn’t know that someone might add the
SelectBox
type, but our Screen
implementation was able to operate on the
new type and draw it because SelectBox
implements the Draw
trait, which
means it implements the draw
method.
This concept—of being concerned only with the messages a value responds to
rather than the value’s concrete type—is similar to the concept of duck
typing in dynamically typed languages: if it walks like a duck and quacks
like a duck, then it must be a duck! In the implementation of run
on Screen
in Listing 17-5, run
doesn’t need to know what the concrete type of each
component is. It doesn’t check whether a component is an instance of a Button
or a SelectBox
, it just calls the draw
method on the component. By
specifying Box<dyn Draw>
as the type of the values in the components
vector, we’ve defined Screen
to need values that we can call the draw
method on.
The advantage of using trait objects and Rust’s type system to write code similar to code using duck typing is that we never have to check whether a value implements a particular method at runtime or worry about getting errors if a value doesn’t implement a method but we call it anyway. Rust won’t compile our code if the values don’t implement the traits that the trait objects need.
For example, Listing 17-10 shows what happens if we try to create a Screen
with a String
as a component:
Filename: src/main.rs
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
We’ll get this error because String
doesn’t implement the Draw
trait:
$ 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`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast from `String` 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
This error lets us know that either we’re passing something to Screen
we
didn’t mean to pass and so should pass a different type or we should implement
Draw
on String
so that Screen
is able to call draw
on it.
Trait Objects Perform Dynamic Dispatch
Recall in the “Performance of Code Using Generics” section in Chapter 10 our discussion on the monomorphization process performed by the compiler when we use trait bounds on generics: the compiler generates nongeneric implementations of functions and methods for each concrete type that we use in place of a generic type parameter. The code that results from monomorphization is doing static dispatch, which is when the compiler knows what method you’re calling at compile time. This is opposed to dynamic dispatch, which is when the compiler can’t tell at compile time which method you’re calling. In dynamic dispatch cases, the compiler emits code that at runtime will figure out which method to call.
When we use trait objects, Rust must use dynamic dispatch. The compiler doesn’t know all the types that might be used with the code that’s using trait objects, so it doesn’t know which method implemented on which type to call. Instead, at runtime, Rust uses the pointers inside the trait object to know which method to call. This lookup incurs a runtime cost that doesn’t occur with static dispatch. Dynamic dispatch also prevents the compiler from choosing to inline a method’s code, which in turn prevents some optimizations. However, we did get extra flexibility in the code that we wrote in Listing 17-5 and were able to support in Listing 17-9, so it’s a trade-off to consider.
Implementing an Object-Oriented Design Pattern
The state pattern is an object-oriented design pattern. The crux of the pattern is that we define a set of states a value can have internally. The states are represented by a set of state objects, and the value’s behavior changes based on its state. We’re going to work through an example of a blog post struct that has a field to hold its state, which will be a state object from the set "draft", "review", or "published".
The state objects share functionality: in Rust, of course, we use structs and traits rather than objects and inheritance. Each state object is responsible for its own behavior and for governing when it should change into another state. The value that holds a state object knows nothing about the different behavior of the states or when to transition between states.
The advantage of using the state pattern is that, when the business requirements of the program change, we won’t need to change the code of the value holding the state or the code that uses the value. We’ll only need to update the code inside one of the state objects to change its rules or perhaps add more state objects.
First, we’re going to implement the state pattern in a more traditional object-oriented way, then we’ll use an approach that’s a bit more natural in Rust. Let’s dig in to incrementally implementing a blog post workflow using the state pattern.
The final functionality will look like this:
- A blog post starts as an empty draft.
- When the draft is done, a review of the post is requested.
- When the post is approved, it gets published.
- Only published blog posts return content to print, so unapproved posts can’t accidentally be published.
Any other changes attempted on a post should have no effect. For example, if we try to approve a draft blog post before we’ve requested a review, the post should remain an unpublished draft.
Listing 17-11 shows this workflow in code form: this is an example usage of the
API we’ll implement in a library crate named blog
. This won’t compile yet
because we haven’t implemented the blog
crate.
Filename: 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());
}
We want to allow the user to create a new draft blog post with Post::new
. We
want to allow text to be added to the blog post. If we try to get the post’s
content immediately, before approval, we shouldn’t get any text because the
post is still a draft. We’ve added assert_eq!
in the code for demonstration
purposes. An excellent unit test for this would be to assert that a draft blog
post returns an empty string from the content
method, but we’re not going to
write tests for this example.
Next, we want to enable a request for a review of the post, and we want
content
to return an empty string while waiting for the review. When the post
receives approval, it should get published, meaning the text of the post will
be returned when content
is called.
Notice that the only type we’re interacting with from the crate is the Post
type. This type will use the state pattern and will hold a value that will be
one of three state objects representing the various states a post can be
in—draft, waiting for review, or published. Changing from one state to another
will be managed internally within the Post
type. The states change in
response to the methods called by our library’s users on the Post
instance,
but they don’t have to manage the state changes directly. Also, users can’t
make a mistake with the states, like publishing a post before it’s reviewed.
Defining Post
and Creating a New Instance in the Draft State
Let’s get started on the implementation of the library! We know we need a
public Post
struct that holds some content, so we’ll start with the
definition of the struct and an associated public new
function to create an
instance of Post
, as shown in Listing 17-12. We’ll also make a private
State
trait that will define the behavior that all state objects for a Post
must have.
Then Post
will hold a trait object of Box<dyn State>
inside an Option<T>
in a private field named state
to hold the state object. You’ll see why the
Option<T>
is necessary in a bit.
Filename: 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 {}
The State
trait defines the behavior shared by different post states. The
state objects are Draft
, PendingReview
, and Published
, and they will all
implement the State
trait. For now, the trait doesn’t have any methods, and
we’ll start by defining just the Draft
state because that is the state we
want a post to start in.
When we create a new Post
, we set its state
field to a Some
value that
holds a Box
. This Box
points to a new instance of the Draft
struct.
This ensures whenever we create a new instance of Post
, it will start out as
a draft. Because the state
field of Post
is private, there is no way to
create a Post
in any other state! In the Post::new
function, we set the
content
field to a new, empty String
.
Storing the Text of the Post Content
We saw in Listing 17-11 that we want to be able to call a method named
add_text
and pass it a &str
that is then added as the text content of the
blog post. We implement this as a method, rather than exposing the content
field as pub
, so that later we can implement a method that will control how
the content
field’s data is read. The add_text
method is pretty
straightforward, so let’s add the implementation in Listing 17-13 to the impl Post
block:
Filename: 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 {}
The add_text
method takes a mutable reference to self
, because we’re
changing the Post
instance that we’re calling add_text
on. We then call
push_str
on the String
in content
and pass the text
argument to add to
the saved content
. This behavior doesn’t depend on the state the post is in,
so it’s not part of the state pattern. The add_text
method doesn’t interact
with the state
field at all, but it is part of the behavior we want to
support.
Ensuring the Content of a Draft Post Is Empty
Even after we’ve called add_text
and added some content to our post, we still
want the content
method to return an empty string slice because the post is
still in the draft state, as shown on line 7 of Listing 17-11. For now, let’s
implement the content
method with the simplest thing that will fulfill this
requirement: always returning an empty string slice. We’ll change this later
once we implement the ability to change a post’s state so it can be published.
So far, posts can only be in the draft state, so the post content should always
be empty. Listing 17-14 shows this placeholder implementation:
Filename: 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 {}
With this added content
method, everything in Listing 17-11 up to line 7
works as intended.
Requesting a Review of the Post Changes Its State
Next, we need to add functionality to request a review of a post, which should
change its state from Draft
to PendingReview
. Listing 17-15 shows this code:
Filename: 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
}
}
We give Post
a public method named request_review
that will take a mutable
reference to self
. Then we call an internal request_review
method on the
current state of Post
, and this second request_review
method consumes the
current state and returns a new state.
We add the request_review
method to the State
trait; all types that
implement the trait will now need to implement the request_review
method.
Note that rather than having self
, &self
, or &mut self
as the first
parameter of the method, we have self: Box<Self>
. This syntax means the
method is only valid when called on a Box
holding the type. This syntax takes
ownership of Box<Self>
, invalidating the old state so the state value of the
Post
can transform into a new state.
To consume the old state, the request_review
method needs to take ownership
of the state value. This is where the Option
in the state
field of Post
comes in: we call the take
method to take the Some
value out of the state
field and leave a None
in its place, because Rust doesn’t let us have
unpopulated fields in structs. This lets us move the state
value out of
Post
rather than borrowing it. Then we’ll set the post’s state
value to the
result of this operation.
We need to set state
to None
temporarily rather than setting it directly
with code like self.state = self.state.request_review();
to get ownership of
the state
value. This ensures Post
can’t use the old state
value after
we’ve transformed it into a new state.
The request_review
method on Draft
returns a new, boxed instance of a new
PendingReview
struct, which represents the state when a post is waiting for a
review. The PendingReview
struct also implements the request_review
method
but doesn’t do any transformations. Rather, it returns itself, because when we
request a review on a post already in the PendingReview
state, it should stay
in the PendingReview
state.
Now we can start seeing the advantages of the state pattern: the
request_review
method on Post
is the same no matter its state
value. Each
state is responsible for its own rules.
We’ll leave the content
method on Post
as is, returning an empty string
slice. We can now have a Post
in the PendingReview
state as well as in the
Draft
state, but we want the same behavior in the PendingReview
state.
Listing 17-11 now works up to line 10!
Adding approve
to Change the Behavior of content
The approve
method will be similar to the request_review
method: it will
set state
to the value that the current state says it should have when that
state is approved, as shown in Listing 17-16:
Filename: 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
}
}
We add the approve
method to the State
trait and add a new struct that
implements State
, the Published
state.
Similar to the way request_review
on PendingReview
works, if we call the
approve
method on a Draft
, it will have no effect because approve
will
return self
. When we call approve
on PendingReview
, it returns a new,
boxed instance of the Published
struct. The Published
struct implements the
State
trait, and for both the request_review
method and the approve
method, it returns itself, because the post should stay in the Published
state in those cases.
Now we need to update the content
method on Post
. We want the value
returned from content
to depend on the current state of the Post
, so we’re
going to have the Post
delegate to a content
method defined on its state
,
as shown in Listing 17-17:
Filename: 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
}
}
Because the goal is to keep all these rules inside the structs that implement
State
, we call a content
method on the value in state
and pass the post
instance (that is, self
) as an argument. Then we return the value that’s
returned from using the content
method on the state
value.
We call the as_ref
method on the Option
because we want a reference to the
value inside the Option
rather than ownership of the value. Because state
is an Option<Box<dyn State>>
, when we call as_ref
, an Option<&Box<dyn State>>
is returned. If we didn’t call as_ref
, we would get an error because
we can’t move state
out of the borrowed &self
of the function parameter.
We then call the unwrap
method, which we know will never panic, because we
know the methods on Post
ensure that state
will always contain a Some
value when those methods are done. This is one of the cases we talked about in
the “Cases In Which You Have More Information Than the
Compiler” section of Chapter 9 when we
know that a None
value is never possible, even though the compiler isn’t able
to understand that.
At this point, when we call content
on the &Box<dyn State>
, deref coercion
will take effect on the &
and the Box
so the content
method will
ultimately be called on the type that implements the State
trait. That means
we need to add content
to the State
trait definition, and that is where
we’ll put the logic for what content to return depending on which state we
have, as shown in Listing 17-18:
Filename: 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
}
}
We add a default implementation for the content
method that returns an empty
string slice. That means we don’t need to implement content
on the Draft
and PendingReview
structs. The Published
struct will override the content
method and return the value in post.content
.
Note that we need lifetime annotations on this method, as we discussed in
Chapter 10. We’re taking a reference to a post
as an argument and returning a
reference to part of that post
, so the lifetime of the returned reference is
related to the lifetime of the post
argument.
And we’re done—all of Listing 17-11 now works! We’ve implemented the state
pattern with the rules of the blog post workflow. The logic related to the
rules lives in the state objects rather than being scattered throughout Post
.
Why Not An Enum?
You may have been wondering why we didn’t use an
enum
with the different possible post states as variants. That’s certainly a possible solution, try it and compare the end results to see which you prefer! One disadvantage of using an enum is every place that checks the value of the enum will need amatch
expression or similar to handle every possible variant. This could get more repetitive than this trait object solution.
Trade-offs of the State Pattern
We’ve shown that Rust is capable of implementing the object-oriented state
pattern to encapsulate the different kinds of behavior a post should have in
each state. The methods on Post
know nothing about the various behaviors. The
way we organized the code, we have to look in only one place to know the
different ways a published post can behave: the implementation of the State
trait on the Published
struct.
If we were to create an alternative implementation that didn’t use the state
pattern, we might instead use match
expressions in the methods on Post
or
even in the main
code that checks the state of the post and changes behavior
in those places. That would mean we would have to look in several places to
understand all the implications of a post being in the published state! This
would only increase the more states we added: each of those match
expressions
would need another arm.
With the state pattern, the Post
methods and the places we use Post
don’t
need match
expressions, and to add a new state, we would only need to add a
new struct and implement the trait methods on that one struct.
The implementation using the state pattern is easy to extend to add more functionality. To see the simplicity of maintaining code that uses the state pattern, try a few of these suggestions:
- Add a
reject
method that changes the post’s state fromPendingReview
back toDraft
. - Require two calls to
approve
before the state can be changed toPublished
. - Allow users to add text content only when a post is in the
Draft
state. Hint: have the state object responsible for what might change about the content but not responsible for modifying thePost
.
One downside of the state pattern is that, because the states implement the
transitions between states, some of the states are coupled to each other. If we
add another state between PendingReview
and Published
, such as Scheduled
,
we would have to change the code in PendingReview
to transition to
Scheduled
instead. It would be less work if PendingReview
didn’t need to
change with the addition of a new state, but that would mean switching to
another design pattern.
Another downside is that we’ve duplicated some logic. To eliminate some of the
duplication, we might try to make default implementations for the
request_review
and approve
methods on the State
trait that return self
;
however, this would violate object safety, because the trait doesn’t know what
the concrete self
will be exactly. We want to be able to use State
as a
trait object, so we need its methods to be object safe.
Other duplication includes the similar implementations of the request_review
and approve
methods on Post
. Both methods delegate to the implementation of
the same method on the value in the state
field of Option
and set the new
value of the state
field to the result. If we had a lot of methods on Post
that followed this pattern, we might consider defining a macro to eliminate the
repetition (see the “Macros” section in Chapter 19).
By implementing the state pattern exactly as it’s defined for object-oriented
languages, we’re not taking as full advantage of Rust’s strengths as we could.
Let’s look at some changes we can make to the blog
crate that can make
invalid states and transitions into compile time errors.
Encoding States and Behavior as Types
We’ll show you how to rethink the state pattern to get a different set of trade-offs. Rather than encapsulating the states and transitions completely so outside code has no knowledge of them, we’ll encode the states into different types. Consequently, Rust’s type checking system will prevent attempts to use draft posts where only published posts are allowed by issuing a compiler error.
Let’s consider the first part of main
in Listing 17-11:
Filename: 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());
}
We still enable the creation of new posts in the draft state using Post::new
and the ability to add text to the post’s content. But instead of having a
content
method on a draft post that returns an empty string, we’ll make it so
draft posts don’t have the content
method at all. That way, if we try to get
a draft post’s content, we’ll get a compiler error telling us the method
doesn’t exist. As a result, it will be impossible for us to accidentally
display draft post content in production, because that code won’t even compile.
Listing 17-19 shows the definition of a Post
struct and a DraftPost
struct,
as well as methods on each:
Filename: 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);
}
}
Both the Post
and DraftPost
structs have a private content
field that
stores the blog post text. The structs no longer have the state
field because
we’re moving the encoding of the state to the types of the structs. The Post
struct will represent a published post, and it has a content
method that
returns the content
.
We still have a Post::new
function, but instead of returning an instance of
Post
, it returns an instance of DraftPost
. Because content
is private
and there aren’t any functions that return Post
, it’s not possible to create
an instance of Post
right now.
The DraftPost
struct has an add_text
method, so we can add text to
content
as before, but note that DraftPost
does not have a content
method
defined! So now the program ensures all posts start as draft posts, and draft
posts don’t have their content available for display. Any attempt to get around
these constraints will result in a compiler error.
Implementing Transitions as Transformations into Different Types
So how do we get a published post? We want to enforce the rule that a draft
post has to be reviewed and approved before it can be published. A post in the
pending review state should still not display any content. Let’s implement
these constraints by adding another struct, PendingReviewPost
, defining the
request_review
method on DraftPost
to return a PendingReviewPost
, and
defining an approve
method on PendingReviewPost
to return a Post
, as
shown in Listing 17-20:
Filename: 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,
}
}
}
The request_review
and approve
methods take ownership of self
, thus
consuming the DraftPost
and PendingReviewPost
instances and transforming
them into a PendingReviewPost
and a published Post
, respectively. This way,
we won’t have any lingering DraftPost
instances after we’ve called
request_review
on them, and so forth. The PendingReviewPost
struct doesn’t
have a content
method defined on it, so attempting to read its content
results in a compiler error, as with DraftPost
. Because the only way to get a
published Post
instance that does have a content
method defined is to call
the approve
method on a PendingReviewPost
, and the only way to get a
PendingReviewPost
is to call the request_review
method on a DraftPost
,
we’ve now encoded the blog post workflow into the type system.
But we also have to make some small changes to main
. The request_review
and
approve
methods return new instances rather than modifying the struct they’re
called on, so we need to add more let post =
shadowing assignments to save
the returned instances. We also can’t have the assertions about the draft and
pending review posts’ contents be empty strings, nor do we need them: we can’t
compile code that tries to use the content of posts in those states any longer.
The updated code in main
is shown in Listing 17-21:
Filename: 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());
}
The changes we needed to make to main
to reassign post
mean that this
implementation doesn’t quite follow the object-oriented state pattern anymore:
the transformations between the states are no longer encapsulated entirely
within the Post
implementation. However, our gain is that invalid states are
now impossible because of the type system and the type checking that happens at
compile time! This ensures that certain bugs, such as display of the content of
an unpublished post, will be discovered before they make it to production.
Try the tasks suggested at the start of this section on the blog
crate as it
is after Listing 17-21 to see what you think about the design of this version
of the code. Note that some of the tasks might be completed already in this
design.
We’ve seen that even though Rust is capable of implementing object-oriented design patterns, other patterns, such as encoding state into the type system, are also available in Rust. These patterns have different trade-offs. Although you might be very familiar with object-oriented patterns, rethinking the problem to take advantage of Rust’s features can provide benefits, such as preventing some bugs at compile time. Object-oriented patterns won’t always be the best solution in Rust due to certain features, like ownership, that object-oriented languages don’t have.
Summary
No matter whether or not you think Rust is an object-oriented language after reading this chapter, you now know that you can use trait objects to get some object-oriented features in Rust. Dynamic dispatch can give your code some flexibility in exchange for a bit of runtime performance. You can use this flexibility to implement object-oriented patterns that can help your code’s maintainability. Rust also has other features, like ownership, that object-oriented languages don’t have. An object-oriented pattern won’t always be the best way to take advantage of Rust’s strengths, but is an available option.
Next, we’ll look at patterns, which are another of Rust’s features that enable lots of flexibility. We’ve looked at them briefly throughout the book but haven’t seen their full capability yet. Let’s go!
Patterns and Matching
Patterns are a special syntax in Rust for matching against the structure of
types, both complex and simple. Using patterns in conjunction with match
expressions and other constructs gives you more control over a program’s
control flow. A pattern consists of some combination of the following:
- Literals
- Destructured arrays, enums, structs, or tuples
- Variables
- Wildcards
- Placeholders
Some example patterns include x
, (a, 3)
, and Some(Color::Red)
. In the
contexts in which patterns are valid, these components describe the shape of
data. Our program then matches values against the patterns to determine whether
it has the correct shape of data to continue running a particular piece of code.
To use a pattern, we compare it to some value. If the pattern matches the
value, we use the value parts in our code. Recall the match
expressions in
Chapter 6 that used patterns, such as the coin-sorting machine example. If the
value fits the shape of the pattern, we can use the named pieces. If it
doesn’t, the code associated with the pattern won’t run.
This chapter is a reference on all things related to patterns. We’ll cover the valid places to use patterns, the difference between refutable and irrefutable patterns, and the different kinds of pattern syntax that you might see. By the end of the chapter, you’ll know how to use patterns to express many concepts in a clear way.
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, {color}, as the background"); } 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"); } }
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); } }
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); } }
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); }
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);
}
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);
| ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
| |
| 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() {}
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); }
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.
Refutability: Whether a Pattern Might Fail to Match
Patterns come in two forms: refutable and irrefutable. Patterns that will match
for any possible value passed are irrefutable. An example would be x
in the
statement let x = 5;
because x
matches anything and therefore cannot fail
to match. Patterns that can fail to match for some possible value are
refutable. An example would be Some(x)
in the expression if let Some(x) = a_value
because if the value in the a_value
variable is None
rather than
Some
, the Some(x)
pattern will not match.
Function parameters, let
statements, and for
loops can only accept
irrefutable patterns, because the program cannot do anything meaningful when
values don’t match. The if let
and while let
expressions accept
refutable and irrefutable patterns, but the compiler warns against
irrefutable patterns because by definition they’re intended to handle possible
failure: the functionality of a conditional is in its ability to perform
differently depending on success or failure.
In general, you shouldn’t have to worry about the distinction between refutable and irrefutable patterns; however, you do need to be familiar with the concept of refutability so you can respond when you see it in an error message. In those cases, you’ll need to change either the pattern or the construct you’re using the pattern with, depending on the intended behavior of the code.
Let’s look at an example of what happens when we try to use a refutable pattern
where Rust requires an irrefutable pattern and vice versa. Listing 18-8 shows a
let
statement, but for the pattern we’ve specified Some(x)
, a refutable
pattern. As you might expect, this code will not compile.
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value;
}
If some_option_value
was a None
value, it would fail to match the pattern
Some(x)
, meaning the pattern is refutable. However, the let
statement can
only accept an irrefutable pattern because there is nothing valid the code can
do with a None
value. At compile time, Rust will complain that we’ve tried to
use a refutable pattern where an irrefutable pattern is required:
$ 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: `Option<i32>` defined here
--> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:518:1
|
= note:
/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:522:5: not covered
= 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 | let x = if let Some(x) = some_option_value { x } else { todo!() };
| ++++++++++ ++++++++++++++++++++++
help: alternatively, you might want to use let else to handle the variant that isn't matched
|
3 | let Some(x) = some_option_value else { todo!() };
| ++++++++++++++++
For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` due to previous error
Because we didn’t cover (and couldn’t cover!) every valid value with the
pattern Some(x)
, Rust rightfully produces a compiler error.
If we have a refutable pattern where an irrefutable pattern is needed, we can
fix it by changing the code that uses the pattern: instead of using let
, we
can use if let
. Then if the pattern doesn’t match, the code will just skip
the code in the curly brackets, giving it a way to continue validly. Listing
18-9 shows how to fix the code in Listing 18-8.
fn main() { let some_option_value: Option<i32> = None; if let Some(x) = some_option_value { println!("{}", x); } }
We’ve given the code an out! This code is perfectly valid, although it means we
cannot use an irrefutable pattern without receiving an error. If we give if let
a pattern that will always match, such as x
, as shown in Listing 18-10,
the compiler will give a warning.
fn main() { if let x = 5 { println!("{}", x); }; }
Rust complains that it doesn’t make sense to use if let
with an irrefutable
pattern:
$ 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: this pattern will always match, so the `if let` is useless
= help: consider replacing the `if let` with a `let`
= note: `#[warn(irrefutable_let_patterns)]` on by default
warning: `patterns` (bin "patterns") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.39s
Running `target/debug/patterns`
5
For this reason, match arms must use refutable patterns, except for the last
arm, which should match any remaining values with an irrefutable pattern. Rust
allows us to use an irrefutable pattern in a match
with only one arm, but
this syntax isn’t particularly useful and could be replaced with a simpler
let
statement.
Now that you know where to use patterns and the difference between refutable and irrefutable patterns, let’s cover all the syntax we can use to create patterns.
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 = {y}", x); }
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); }
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); }
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})"); } } }
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
.
Remember that a match
expression stops checking arms once it has found the
first matching pattern, so even though Point { x: 0, y: 0}
is on the x
axis
and the y
axis, this code would only print On the x axis at 0
.
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 {x} and in the y direction {y}"); } Message::Write(text) => { println!("Text message: {text}"); } Message::ChangeColor(r, g, b) => { println!("Change the color to red {r}, green {g}, and blue {b}",) } } }
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 color to red {r}, green {g}, and blue {b}"); } Message::ChangeColor(Color::Hsv(h, s, v)) => { println!("Change color to hue {h}, saturation {s}, value {v}") } _ => (), } }
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); }
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); }
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}") } } }
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; }
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);
}
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); }
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), } }
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}"); } } }
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)
},
}
}
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 => (), } }
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 = {y}", x); }
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"), } }
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), } }
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.
Advanced Features
By now, you’ve learned the most commonly used parts of the Rust programming language. Before we do one more project in Chapter 20, we’ll look at a few aspects of the language you might run into every once in a while, but may not use every day. You can use this chapter as a reference for when you encounter any unknowns. The features covered here are useful in very specific situations. Although you might not reach for them often, we want to make sure you have a grasp of all the features Rust has to offer.
In this chapter, we’ll cover:
- Unsafe Rust: how to opt out of some of Rust’s guarantees and take responsibility for manually upholding those guarantees
- Advanced traits: associated types, default type parameters, fully qualified syntax, supertraits, and the newtype pattern in relation to traits
- Advanced types: more about the newtype pattern, type aliases, the never type, and dynamically sized types
- Advanced functions and closures: function pointers and returning closures
- Macros: ways to define code that defines more code at compile time
It’s a panoply of Rust features with something for everyone! Let’s dive in!
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
union
s
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; }
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; }
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); } }
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]); }
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);
}
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); }
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) }; }
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)); } }
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 creating a wholeextern
block, we add theextern
keyword and specify the ABI to use just before thefn
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 requireunsafe
.
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); }
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); } }
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() {}
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>;
}
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>;
}
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
.
Associated types also become part of the trait’s contract: implementors of the trait must provide a type to stand in for the associated type placeholder. Associated types often have a name that describes how the type will be used, and documenting the associated type in the API documentation is good practice.
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 } ); }
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))
}
}
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() {}
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(); }
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(); }
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()); }
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());
}
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[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
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()); }
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 the
standard library trait 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() {}
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); }
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. In other words, if we
mix up Kilometers
and i32
values somewhere, the compiler will not give us
an error.
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(|| ()) } }
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(|| ()) } }
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!("Raqamni topish o'yini!");
let yashirin_raqam = rand::thread_rng().gen_range(1..=100);
println!("Yashirin raqam: {yashirin_raqam}");
loop {
println!("Iltimos, taxminingizni kiriting.");
let mut taxmin = String::new();
// --snip--
io::stdin()
.read_line(&mut taxmin)
.expect("Satrni o‘qib bo‘lmadi");
let taxmin: u32 = match taxmin.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("Sizning taxminingiz: {taxmin}");
// --snip--
match taxmin.cmp(&yashirin_raqam) {
Ordering::Less => println!("Raqam Kichik!"),
Ordering::Greater => println!("Raqam katta!"),
Ordering::Equal => {
println!("Siz yutdingiz!");
break;
}
}
}
}
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!
Advanced Functions and Closures
This section explores some advanced features related to functions and closures, including function pointers and returning closures.
Function Pointers
We’ve talked about how to pass closures to functions; you can also pass regular
functions to functions! This technique is useful when you want to pass a
function you’ve already defined rather than defining a new closure. Functions
coerce to the type fn
(with a lowercase f), not to be confused with the Fn
closure trait. The fn
type is called a function pointer. Passing functions
with function pointers will allow you to use functions as arguments to other
functions.
The syntax for specifying that a parameter is a function pointer is similar to
that of closures, as shown in Listing 19-27, where we’ve defined a function
add_one
that adds one to its parameter. The function do_twice
takes two
parameters: a function pointer to any function that takes an i32
parameter
and returns an i32
, and one i32 value
. The do_twice
function calls the
function f
twice, passing it the arg
value, then adds the two function call
results together. The main
function calls do_twice
with the arguments
add_one
and 5
.
Filename: 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); }
This code prints The answer is: 12
. We specify that the parameter f
in
do_twice
is an fn
that takes one parameter of type i32
and returns an
i32
. We can then call f
in the body of do_twice
. In main
, we can pass
the function name add_one
as the first argument to do_twice
.
Unlike closures, fn
is a type rather than a trait, so we specify fn
as the
parameter type directly rather than declaring a generic type parameter with one
of the Fn
traits as a trait bound.
Function pointers implement all three of the closure traits (Fn
, FnMut
, and
FnOnce
), meaning you can always pass a function pointer as an argument for a
function that expects a closure. It’s best to write functions using a generic
type and one of the closure traits so your functions can accept either
functions or closures.
That said, one example of where you would want to only accept fn
and not
closures is when interfacing with external code that doesn’t have closures: C
functions can accept functions as arguments, but C doesn’t have closures.
As an example of where you could use either a closure defined inline or a named
function, let’s look at a use of the map
method provided by the Iterator
trait in the standard library. To use the map
function to turn a vector of
numbers into a vector of strings, we could use a closure, like this:
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(); }
Or we could name a function as the argument to map
instead of the closure,
like this:
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(); }
Note that we must use the fully qualified syntax that we talked about earlier
in the “Advanced Traits” section because
there are multiple functions available named to_string
. Here, we’re using the
to_string
function defined in the ToString
trait, which the standard
library has implemented for any type that implements Display
.
Recall from the “Enum values” section of Chapter 6 that the name of each enum variant that we define also becomes an initializer function. We can use these initializer functions as function pointers that implement the closure traits, which means we can specify the initializer functions as arguments for methods that take closures, like so:
fn main() { enum Status { Value(u32), Stop, } let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect(); }
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.
Returning Closures
Closures are represented by traits, which means you can’t return closures
directly. In most cases where you might want to return a trait, you can instead
use the concrete type that implements the trait as the return value of the
function. However, you can’t do that with closures because they don’t have a
concrete type that is returnable; you’re not allowed to use the function
pointer fn
as a return type, for example.
The following code tries to return a closure directly, but it won’t compile:
fn returns_closure() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
The compiler error is as follows:
$ 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:8]`, 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
The error references the Sized
trait again! Rust doesn’t know how much space
it will need to store the closure. We saw a solution to this problem earlier.
We can use a trait object:
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
This code will compile just fine. For more about trait objects, refer to the section “Using Trait Objects That Allow for Values of Different Types” in Chapter 17.
Next, let’s look at macros!
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 thederive
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
}
};
}
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.
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 {
}
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();
}
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)
}
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
)
}
)
}
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()
}
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!
Final Project: Building a Multithreaded Web Server
It’s been a long journey, but we’ve reached the end of the book. In this chapter, we’ll build one more project together to demonstrate some of the concepts we covered in the final chapters, as well as recap some earlier lessons.
For our final project, we’ll make a web server that says “hello” and looks like Figure 20-1 in a web browser.
Here is our plan for building the web server:
- Learn a bit about TCP and HTTP.
- Listen for TCP connections on a socket.
- Parse a small number of HTTP requests.
- Create a proper HTTP response.
- Improve the throughput of our server with a thread pool.
Before we get started, we should mention one detail: the method we’ll use won’t be the best way to build a web server with Rust. Community members have published a number of production-ready crates available on crates.io that provide more complete web server and thread pool implementations than we’ll build. However, our intention in this chapter is to help you learn, not to take the easy route. Because Rust is a systems programming language, we can choose the level of abstraction we want to work with and can go to a lower level than is possible or practical in other languages. We’ll therefore write the basic HTTP server and thread pool manually so you can learn the general ideas and techniques behind the crates you might use in the future.
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 Hypertext Transfer Protocol (HTTP) and 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 local 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!"); } }
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 so
our server is unlikely to conflict with any other web server you might have
running on your machine, 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 function is called bind
because, 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 it’s
possible for binding to 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. Binding also wouldn’t work, for example,
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); }
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 until we get a line that is 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, which means it is asking for
information.
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(); }
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 the 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>
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(); }
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. At the moment, our server is very limited and
does not do 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 } }
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(); } }
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>
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(); }
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(); }
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 techniques we could use to avoid requests backing 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, then, we’ll have a fixed number of
threads waiting in the pool. Requests that come in are 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 up
to 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, the single-threaded async I/O model, or the multi-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. Before we do that, however, we’ll explore the technique we’re not going to use as a starting point.
Spawning 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 to get a working multithreaded server first. Then we’ll add the
thread pool as an improvement, and contrasting the two solutions will be
easier. 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(); }
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. However, as
we mentioned, this will eventually overwhelm the system because you’d be making
new threads without any limit.
Creating 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();
}
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 ThreadPool
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:11:16
|
11 | 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/main.rs:12:28
|
12 | 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/main.rs:17:14
|
17 | 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 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,
{
}
}
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 change new
into build
and return a Result
like we did with Config::build
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 function named build
with the following
signature to compare with the new
function:
pub fn build(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 the struct. 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,
{
}
}
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. The with_capacity
function 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. The Worker picks up code that needs to be run and runs the
code in the Worker’s thread. 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 fulfilling 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.
Here is the new process that will happen 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:
- Define a
Worker
struct that holds anid
and aJoinHandle<()>
. - Change
ThreadPool
to hold a vector ofWorker
instances. - Define a
Worker::new
function that takes anid
number and returns aWorker
instance that holds theid
and a thread spawned with an empty closure. - In
ThreadPool::new
, use thefor
loop counter to generate anid
, create a newWorker
with thatid
, 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 }
}
}
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.
Note: If the operating system can’t create a thread because there aren’t enough system resources,
thread::spawn
will panic. That will cause our whole server to panic, even though the creation of some threads might succeed. For simplicity’s sake, this behavior is fine, but in a production thread pool implementation, you’d likely want to usestd::thread::Builder
and itsspawn
method that returnsResult
instead.
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
The next problem we’ll tackle is 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 the code to run from
a queue held in the ThreadPool
and send that code to its thread to run.
The channels we learned about in Chapter 16—a simple way to communicate between
two threads—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:
- The
ThreadPool
will create a channel and hold on to the sender. - Each
Worker
will hold on to the receiver. - We’ll create a new
Job
struct that will hold the closures we want to send down the channel. - The
execute
method will send the job it wants to execute through the sender. - 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 }
}
}
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 }
}
}
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:26:42
|
21 | let (sender, receiver) = mpsc::channel();
| -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
26 | 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 }
}
}
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 for
ease of use. 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 }
}
}
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 }
}
}
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 }
}
}
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. However, this implementation can also result in the lock being held
longer than intended if we aren’t mindful of 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.
Graceful Shutdown and Cleanup
The code in Listing 20-20 is responding to requests asynchronously through the
use of a thread pool, as we intended. We get some warnings about the workers
,
id
, and thread
fields that we’re not using in a direct way that reminds us
we’re not cleaning up anything. When we use the less elegant ctrl-c method to halt the main thread, all other
threads are stopped immediately as well, even if they’re in the middle of
serving a request.
Next, then, we’ll implement the Drop
trait to call join
on each of the
threads in the pool so they can finish the requests they’re working on before
closing. Then we’ll implement a way to tell the threads they should stop
accepting new requests and shut down. To see this code in action, we’ll modify
our server to accept only two requests before gracefully shutting down its
thread pool.
Implementing the Drop
Trait on ThreadPool
Let’s start with implementing Drop
on our thread pool. When the pool is
dropped, our threads should all join to make sure they finish their work.
Listing 20-22 shows a first attempt at a Drop
implementation; this code won’t
quite work yet.
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();
}
}
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 }
}
}
First, we loop through each of the thread pool workers
. We use &mut
for
this because self
is a mutable reference, and we also need to be able to
mutate worker
. For each worker, we print a message saying that this
particular worker is shutting down, and then we call join
on that worker’s
thread. If the call to join
fails, we use unwrap
to make Rust panic and go
into an ungraceful shutdown.
Here is the error we get when we compile this code:
$ 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();
| ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call
| |
| move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait
|
note: this function takes ownership of the receiver `self`, which moves `worker.thread`
--> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/src/thread/mod.rs:1581:17
For more information about this error, try `rustc --explain E0507`.
error: could not compile `hello` due to previous error
The error tells us we can’t call join
because we only have a mutable borrow
of each worker
and join
takes ownership of its argument. To solve this
issue, we need to move the thread out of the Worker
instance that owns
thread
so join
can consume the thread. We did this in Listing 17-15: if
Worker
holds an Option<thread::JoinHandle<()>>
instead, we can call the
take
method on the Option
to move the value out of the Some
variant and
leave a None
variant in its place. In other words, a Worker
that is running
will have a Some
variant in thread
, and when we want to clean up a
Worker
, we’ll replace Some
with None
so the Worker
doesn’t have a
thread to run.
So we know we want to update the definition of Worker
like this:
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();
}
}
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 }
}
}
Now let’s lean on the compiler to find the other places that need to change. Checking this code, we get two errors:
$ 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<()>>`
|
note: the method `join` exists on the type `JoinHandle<()>`
--> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/src/thread/mod.rs:1581:5
help: consider using `Option::expect` to unwrap the `JoinHandle<()>` value, panicking if the value is an `Option::None`
|
52 | worker.thread.expect("REASON").join().unwrap();
| +++++++++++++++++
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, thread: 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
Let’s address the second error, which points to the code at the end of
Worker::new
; we need to wrap the thread
value in Some
when we create a
new Worker
. Make the following changes to fix this error:
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();
}
}
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),
}
}
}
The first error is in our Drop
implementation. We mentioned earlier that we
intended to call take
on the Option
value to move thread
out of worker
.
The following changes will do so:
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();
}
}
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),
}
}
}
As discussed in Chapter 17, the take
method on Option
takes the Some
variant out and leaves None
in its place. We’re using if let
to destructure
the Some
and get the thread; then we call join
on the thread. If a worker’s
thread is already None
, we know that worker has already had its thread
cleaned up, so nothing happens in that case.
Signaling to the Threads to Stop Listening for Jobs
With all the changes we’ve made, our code compiles without any warnings.
However, the bad news is this code doesn’t function the way we want it to yet.
The key is the logic in the closures run by the threads of the Worker
instances: at the moment, we call join
, but that won’t shut down the threads
because they loop
forever looking for jobs. If we try to drop our
ThreadPool
with our current implementation of drop
, the main thread will
block forever waiting for the first thread to finish.
To fix this problem, we’ll need a change in the ThreadPool
drop
implementation and then a change in the Worker
loop.
First, we’ll change the ThreadPool
drop
implementation to explicitly drop
the sender
before waiting for the threads to finish. Listing 20-23 shows the
changes to ThreadPool
to explicitly drop sender
. We use the same Option
and take
technique as we did with the thread to be able to move sender
out
of ThreadPool
:
Filename: 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),
}
}
}
Dropping sender
closes the channel, which indicates no more messages will be
sent. When that happens, all the calls to recv
that the workers do in the
infinite loop will return an error. In Listing 20-24, we change the Worker
loop to gracefully exit the loop in that case, which means the threads will
finish when the ThreadPool
drop
implementation calls join
on them.
Filename: 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),
}
}
}
To see this code in action, let’s modify main
to accept only two requests
before gracefully shutting down the server, as shown in Listing 20-25.
Filename: 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();
}
You wouldn’t want a real-world web server to shut down after serving only two requests. This code just demonstrates that the graceful shutdown and cleanup is in working order.
The take
method is defined in the Iterator
trait and limits the iteration
to the first two items at most. The ThreadPool
will go out of scope at the
end of main
, and the drop
implementation will run.
Start the server with cargo run
, and make three requests. The third request
should error, and in your terminal you should see output similar to this:
$ 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
You might see a different ordering of workers and messages printed. We can see
how this code works from the messages: workers 0 and 3 got the first two
requests. The server stopped accepting connections after the second connection,
and the Drop
implementation on ThreadPool
starts executing before worker 3
even starts its job. Dropping the sender
disconnects all the workers and
tells them to shut down. The workers each print a message when they disconnect,
and then the thread pool calls join
to wait for each worker thread to finish.
Notice one interesting aspect of this particular execution: the ThreadPool
dropped the sender
, and before any worker received an error, we tried to join
worker 0. Worker 0 had not yet gotten an error from recv
, so the main thread
blocked waiting for worker 0 to finish. In the meantime, worker 3 received a
job and then all threads received an error. When worker 0 finished, the main
thread waited for the rest of the workers to finish. At that point, they had
all exited their loops and stopped.
Congrats! We’ve now completed our project; we have a basic web server that uses a thread pool to respond asynchronously. We’re able to perform a graceful shutdown of the server, which cleans up all the threads in the pool.
Here’s the full code for reference:
Filename: 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();
}
Filename: 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),
}
}
}
We could do more here! If you want to continue enhancing this project, here are some ideas:
- Add more documentation to
ThreadPool
and its public methods. - Add tests of the library’s functionality.
- Change calls to
unwrap
to more robust error handling. - Use
ThreadPool
to perform some task other than serving web requests. - Find a thread pool crate on crates.io and implement a similar web server using the crate instead. Then compare its API and robustness to the thread pool we implemented.
Summary
Well done! You’ve made it to the end of the book! We want to thank you for joining us on this tour of Rust. You’re now ready to implement your own Rust projects and help with other peoples’ projects. Keep in mind that there is a welcoming community of other Rustaceans who would love to help you with any challenges you encounter on your Rust journey.
Ilova
Quyidagi bo'limlarda siz o'zingiz uchun foydali bo'lishi mumkin bo'lgan ma'lumotnomalar mavjud Rust sayohati.
Ilova A: Kalit so'zlar
Quyidagi ro'yxatda Rust tili tomonidan joriy yoki kelajakda foydalanish uchun ajratilgan kalit so'zlar mavjud. Shunday qilib, ularni identifikator sifatida ishlatib bo'lmaydi (raw identifikatorlar bo'limida muhokama qilinadigan “Raw identifikatorlardan” tashqari) Identifikatorlar - bu funksiyalar, o'zgaruvchilar, parametrlar, struct field, modullar, cratelar, konstantalar, macroslar, statik qiymatlar, atributlar, turlar, traitlar yoki lifetimelarining nomlari.
Hozirda foydalanilayotgan kalit so'zlar
Quyida hozirda foydalanilayotgan kalit so‘zlar ro‘yxati keltirilgan, ularning funksiyalari tasvirlangan.
as
- primitiv castingni amalga oshiring, elementni o'z ichiga olgan o'ziga xos traitni ajrating yokiuse
statementaridagi elementlarning nomini o'zgartiringasync
- joriy threadni bloklash o'rnigaFuture
ni return qilingawait
-Future
natijasi tayyor bo'lgunga qadar executionni to'xtatib turingbreak
- zudlik bilan loopdan chiqingconst
- konstanta elementlarni yoki konstanta raw pointerlarni aniqlangcontinue
- keyingi sikl iteratsiyasiga davom etingcrate
- modul yo'lida, crate rootga ishora qiladidyn
- trait objectga dinamik jo'natishelse
-if
vaif let
uchun zaxira control flow konstruksiyalarienum
- enumerationni aniqlashextern
- tashqi funksiya yoki o‘zgaruvchini bog‘lashfalse
- Boolean false(noto'g'ri) so'zfn
- funksiya yoki funktsiya pointer turini aniqlangfor
- iteratordagi elementlarni ko'rib chiqing, implement trait yoki yuqori darajali lifetimeni belgilangif
- shartli expression natijasiga asoslangan branchimpl
- o'ziga inherent yoki trait funksionalligini implementin
-for
sikl sintaksisining bir qismilet
- o'zgaruvchini bog'lashloop
- shartsiz loopmatch
- qiymatni patternlarga moslashtirishmod
- modulni aniqlashmove
- make a closure take ownership of all its capturesmut
- reference, raw pointerlar yoki pattern bindingdagi o'zgaruvchanlikni bildiradipub
- struct fieldlarida,impl
bloklarida yoki modullarda ommaviy ko'rinishni bildiradiref
- reference orqali bog'lashreturn
- funksiyadan qaytish(return)Self
- biz belgilayotgan yoki implement qilayotgan tur uchun turdagi aliasself
- metod mavzusi yoki joriy modulstatic
- global o'zgaruvchi yoki butun dasturning bajarilishi uchun lifetimestruct
- structurani aniqlashsuper
- joriy modulning parent modulitrait
- traitni aniqlashtrue
- Boolean true(to'g'ri) so'ztype
- turdagi alias yoki associated turni aniqlashunion
- define a union; is only a keyword when used in a union declarationunsafe
- xavfli kod, funksiyalar, traitlar yoki implementationlarni bildiradiuse
- bring symbols into scopewhere
- denote clauses that constrain a typewhile
- expression natijasi asosida shartli ravishda sikl
Kelajakda foydalanish uchun ajratilgan kalit so'zlar
Quyidagi kalit so'zlar hali hech qanday funksiyaga ega emas, lekin kelajakda foydalanish uchun Rust tomonidan zahiralangan.
abstract
become
box
do
final
macro
override
priv
try
typeof
unsized
virtual
yield
Raw identifikatorlar
Raw identifikatorlar odatda ruxsat berilmaydigan kalit so'zlardan foydalanish imkonini beruvchi sintaksisdir. Kalit so‘z oldiga r#
qo‘yish orqali raw identifikatordan foydalanasiz.
Masalan, match
kalit so'zdir. Agar siz o'z nomi sifatida match
dan foydalanadigan quyidagi funksiyani kompilyatsiya qilmoqchi bo'lsangiz:
Fayl nomi: src/main.rs
fn match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
you’ll get this error:
error: expected identifier, found keyword `match`
--> src/main.rs:4:4
|
4 | fn match(needle: &str, haystack: &str) -> bool {
| ^^^^^ expected identifier, found keyword
Xato, funksiya identifikatori sifatida match
kalit so‘zidan foydalana olmasligingizni ko‘rsatadi. Funksiya nomi sifatida match
dan foydalanish uchun siz raw identifikator sintaksisidan foydalanishingiz kerak, masalan:
Fayl nomi: src/main.rs
fn r#match(needle: &str, haystack: &str) -> bool { haystack.contains(needle) } fn main() { assert!(r#match("foo", "foobar")); }
Ushbu kod hech qanday xatosiz kompilyatsiya qilinadi. Funksiya nomidagi r#
prefiksi uning taʼrifida, shuningdek, funksiya main
da chaqirilgan joyiga eʼtibor bering.
Raw identifikatorlari identifikator sifatida tanlagan har qanday so'zdan foydalanishga imkon beradi, hatto bu so'z zahiradagi kalit so'z bo'lsa ham. Bu bizga identifikator nomlarini tanlashda ko'proq erkinlik beradi, shuningdek, bu so'zlar kalit so'zlar bo'lmagan tilda yozilgan dasturlar bilan integratsiyalashish imkonini beradi. Bunga qo'shimcha ravishda, raw identifikatorlar sizga cratetagidan boshqa Rust nashrida yozilgan kutubxonalardan foydalanish imkonini beradi. Misol uchun, try
2015 yilgi nashrda kalit so'z emas, balki 2018 yilgi nashrda mavjud. Agar siz 2015-yil nashri yordamida yozilgan kutubxonaga bogʻliq boʻlsangiz va try
funksiyasiga ega boʻlsangiz, bu funksiyani 2018-yilgi nashr kodingizdan chaqirish uchun r#try
raw identifikator sintaksisidan foydalanishingiz kerak boʻladi.
Nashrlar haqida qo'shimcha ma'lumot E ilovasiga qarang.
B-ilova: Operatorlar va Belgilar
Ushbu qo'shimchada Rust sintaksisining lug'ati, shu jumladan o'z-o'zidan yoki yo'llar (paths), umumlashmalar (generiklar), turlar, makroslar, atributlar, sharhlar, katakchalar (tuples) va qavslar kontekstida paydo bo'ladigan operatorlar va boshqa belgilar mavjud.
Operatorlar
B-1 jadvalida Rust tili operatorlari, operator qanday kontekstda ko'rinishi, qisqa tushuntirish va ushbu operator yuklanishi mumkinmi (overload) yoki yo'qmi ko'rsatilgan. Agar operator yuklanishi mumkin bo'lsa, bu operatorni yuklash uchun foydalanish kerak bo'lgan tegishli trait keltirilgan.
Operator | Misol | Tushuntirish | Yuklanishi mumkinmi? |
---|---|---|---|
! | ident!(...) , ident!{...} , ident![...] | Makros chaqiruvi | |
! | !expr | Bit yoki mantiqiy inkor | Not |
!= | expr != expr | Tengsizlik taqqoslash | PartialEq |
% | expr % expr | Bo'linishning qolg'i | Rem |
%= | var %= expr | Bo'linishning qolg'i va tayinlash | RemAssign |
& | &expr , &mut expr | Qarz olish | |
& | &type , &mut type , &'a type , &'a mut type | Ushbu tur qarzga olinganligini ko'rsatadi | |
& | expr & expr | Bitlik VA | BitAnd |
&= | var &= expr | Bitlik VA va tayinlash | BitAndAssign |
&& | expr && expr | Mantiqiy VA | |
* | expr * expr | Arifmetik ko'paytirish | Mul |
*= | var *= expr | Arifmetik ko'paytirish va tayinlash | MulAssign |
* | *expr | Havolani bekor qilish | Deref |
* | *const type , *mut type | Ushbu tur xom ko'rsatkich ekanligini ko'rsatadi | |
+ | trait + trait , 'a + trait | Murakkab turdagi cheklov | |
+ | expr + expr | Arifmetik qo'shish | Add |
+= | var += expr | Arifmetik qo'shish va tayinlash | AddAssign |
, | expr, expr | Argument va element ajratuvchisi | |
- | - expr | Arifmetik inkor | Neg |
- | expr - expr | Arifmetik ayirish | Sub |
-= | var -= expr | Arifmetik ayirish va tayinlash | SubAssign |
-> | fn(...) -> type , |...| -> type | Funktsiya va yopilishning qaytish turi | |
. | expr.ident | A'zoga (elementga) kirish | |
.. | .. , expr.. , ..expr , expr..expr | O'ngdan tashqari raqamlar oralig'ini bildiradi | PartialOrd |
..= | ..=expr , expr..=expr | Raqamlar oralig'ini, shu jumladan o'ng tomonni bildiradi | PartialOrd |
.. | ..expr | Strukturani yangilash sintaksisi | |
.. | variant(x, ..) , struct_type { x, .. } | "Va boshqa hamma narsa"ni bog'lash | |
... | expr...expr | (Eskirgan, yangi sintaksisdan foydalaning ..= ) Inklyuziv diapazonni aniqlashda ishlatiladi | |
/ | expr / expr | Arifmetik bo'lish | Div |
/= | var /= expr | Arifmetik bo'lish va tayinlash | DivAssign |
: | pat: type , ident: type | Turlarning cheklovlari | |
: | ident: expr | Tuzilish maydonini ishga tushirish | |
: | 'a: loop {...} | Tsikl yorlig'i | |
; | expr; | Ko'rsatma va elementning oxiri belgisi | |
; | [...; len] | Qat'iy o'lchamdagi massiv sintaksisining qismi | |
<< | expr << expr | Bitlik chapga surish | Shl |
<<= | var <<= expr | Bitlik chapga surish va tayinlash | ShlAssign |
< | expr < expr | "Kamroq" taqqoslash | PartialOrd |
<= | expr <= expr | "Kamroq yoki teng" taqqoslash | PartialOrd |
= | var = expr , ident = type | Tayinlash/ekvivalentlik | |
== | expr == expr | Tenglik taqqoslash | PartialEq |
=> | pat => expr | Moslama qolipining qismi | |
> | expr > expr | "Kattaroq" taqqoslash | PartialOrd |
>= | expr >= expr | "Kattaroq yoki teng" taqqoslash | PartialOrd |
>> | expr >> expr | Bitlik o'ngga surish | Shr |
>>= | var >>= expr | Bitlik o'ngga surish va tayinlash | ShrAssign |
@ | ident @ pat | Naqshni bog'lash (Pattern binding) | |
^ | expr ^ expr | Bitlik istisno YOKI | BitXor |
^= | var ^= expr | Bitlik istisno YOKI va tayinlash | BitXorAssign |
| | pat | pat | Muqobil naqshlar | |
| | expr | expr | Bitlik YOKI | BitOr |
|= | var |= expr | Bitlik YOKI va tayinlash | BitOrAssign |
|| | expr || expr | Qisqa mantiqiy YOKI | |
? | expr? | Xato qaytarish |
Operator bo'lmagan Belgilar
Quyidagi ro'yxatda operator sifatida ishlamaydigan barcha belgilar mavjud; ya'ni ular funktsiya yoki usul chaqiruvi kabi harakat qilmaydi.
B-2 jadvali o'z-o'zidan paydo bo'ladigan va turli joylarda qabul qilinadigan belgilarni ko'rsatadi.
Belgi | Tushuntirish |
---|---|
'ident | Nomlangan umrbod yoki tsikl yorlig'i |
...u8 , ...i32 , ...f64 , ...usize , va h.k. | Ma'lum turdagi sonli literal |
"..." | Qator (String) literal |
r"..." , r#"..."# , r##"..."## , va h.k. | Qochish belgilarini qayta ishlamaydigan xom satrli literal |
b"..." | Bayt string literal; string o'rniga bayt massivini hosil qiladi |
br"..." , br#"..."# , br##"..."## , va h.k. | Xom satr baytli harf, xom va baytli harflarning kombinatsiyasi |
'...' | Belgilar literal |
b'...' | ASCII bayt literal |
|...| expr | Yopilish |
! | Har doim bo'sh pastki tur divergiruvchi funksiyalar uchun |
_ | “E'tiborsiz” pattern binding; shuningdek, butun sonli literalni o'qilishi uchun ishlatiladi |
Jadval B-3 modul ierarxiyasi orqali elementga yo'l kontekstida ko'rinadigan belgilarni ko'rsatadi.
Belgi | Tushuntirish |
---|---|
ident::ident | Nomlar maydoni yo'li |
::path | Crate ildiziga nisbatan yo'l (ya'ni, aniq absolyut yo'l) |
self::path | Joriy modulga nisbatan yo'l (ya'ni, aniq nisbiy yo'l) |
super::path | Joriy modulning ota moduliga nisbatan yo'l |
type::ident , <type as trait>::ident | Tegishli konstantalar, funksiyalar va turlar |
<type>::... | To'g'ridan-to'g'ri nomlanishi mumkin bo'lmagan turga tegishli element (masalan, <&T>::... , <[T]>::... , va hokazo.) |
trait::method(...) | Usul chaqiruvini aniqlashtirish uchun usulni aniqlagan traitni nomlash |
type::method(...) | Usul chaqiruvini aniqlashtirish uchun usul aniqlangan tur nomini ko'rsatish |
<type as trait>::method(...) | Usul chaqiruvini aniqlashtirish uchun trait va tur nomini ko'rsatish |
Jadval B-4 generik turdagi parametrlarni ishlatish kontekstida ko'rinadigan belgilarni ko'rsatadi.
Belgi | Tushuntirish |
---|---|
path<...> | Turdagi umumlashtirilgan parametrlar uchun parametrlarni belgilaydi (masalan, Vec<u8> ) |
path::<...> , method::<...> | Ifodada generik tur, funksiya yoki usul parametrlari; ko'pincha turbofish deb ataladi (masalan, "42".parse::<i32>() ) |
fn ident<...> ... | Umumlashtirilgan funksiyani aniqlash |
struct ident<...> ... | Umumlashtirilgan tuzilmani aniqlash |
enum ident<...> ... | Umumlashtirilgan enum aniqlash |
impl<...> ... | Umumlashtirilgan amalga oshirishning ta'rifi |
for<...> type | Yuqori darajadagi umrbod cheklovlar |
type<ident=type> | Bir yoki bir nechta tegishli turlarga aniq tayinlangan generik tur (masalan, Iterator<Item=T> ) |
Jadval b-5 ko'rsatadi belgilar turi cheklangan umumlashtirilgan parametr turlaridan foydalanish kontekstida paydo bo'ladi
Belgi | Tushuntirish |
---|---|
T: U | T Umumlashtirilgan parametri U ni amalga oshiruvchi turlar bilan cheklangan |
T: 'a | T Umumlashtirilgan turi a umrbodidan uzoqroq bo'lishi kerak (ya'ni, tur hech qanday a dan qisqaroq bo'lgan ko'rsatkichlarga ega bo'lmasligi kerak) |
T: 'static | T Umumlashtirilgan turi faqat static umrbod ko'rsatkichlarini o'z ichiga oladi |
'b: 'a | 'b umrbodi a umrbodidan uzoqroq bo'lishi kerak |
T: ?Sized | Umumlashtirilgan tur parametri dinamik o'lchamli tur bo'lishiga ruxsat beradi |
'a + trait , trait + trait | Murakkab tur cheklovi |
Jadval B-6 makrolarni chaqirish yoki aniqlash va elementga atributlarni belgilash kontekstida ko'rinadigan belgilarni ko'rsatadi.
Belgi | Tushuntirish |
---|---|
#[meta] | Tashqi atribut |
#![meta] | Ichki atribut |
$ident | Makro almashtirish |
$ident:kind | Makro qo'lga olish |
$(…)… | Makro takrorlash |
ident!(...) , ident!{...} , ident![...] | Makro chaqiruvi |
B-7 jadvalida sharhlar yaratadigan belgilar ko'rsatilgan.
Belgi | Tushuntirish |
---|---|
// | Ichki bir qatorli hujjat sharhi |
//! | Tashqi bir qatorli hujjat sharhi |
/// | Tashqi chiziqli doc kommentariya |
/*...*/ | Ko'p qatorli sharh |
/*!...*/ | Ichki ko'p qatorli hujjat sharhi |
/**...*/ | Tashqi ko'p qatorli hujjat sharhi |
B-8 jadvalida katakchalardan (tuple-lardan) foydalanish kontekstida paydo bo'ladigan belgilar ko'rsatilgan.
Belgi | Tushuntirish |
---|---|
() | Bo'sh tupl (ya'ni, birlik), literal va tur |
(expr) | Qavs ichidagi ifoda |
(expr,) | Yagona elementli tupl ifodasi |
(type,) | Yagona elementli tupl turi |
(expr, ...) | Tupl ifodasi |
(type, ...) | Tupl turi |
expr(expr, ...) | Funksiya chaqiruvi ifodasi; shuningdek, tupl strukturasi va tupl enum variantlarini boshlash uchun ishlatiladi |
expr.0 , expr.1 , va h.k. | Tupl indekslash |
B-9 jadvali jingalak qavslardan ("{}") foydalanadigan kontekstlarni ko'rsatadi.
Kontekst | Tushuntirish |
---|---|
{...} | Blok ifodasi |
Type {...} | struct literal |
Jadval B-10 to'rtburchak qavslar ishlatiladigan kontekstlarni ko'rsatadi.
Kontekst | Tushuntirish |
---|---|
[...] | Array (yig'ilma) literal |
[expr; len] | len nusxalarini o'z ichiga olgan array literal |
[type; len] | len nusxalarini o'z ichiga olgan array turi |
expr[expr] | To'plamni indekslash. Yuklanishi mumkin (Index , IndexMut ) |
expr[..] , expr[a..] , expr[..b] , expr[a..b] | To'plamni kesishga o'xshash qilib indekslash, Range , RangeFrom , RangeTo yoki RangeFull ni "indeks" sifatida ishlatish |
Appendix C: Derivable Traits
In various places in the book, we’ve discussed the derive
attribute, which
you can apply to a struct or enum definition. The derive
attribute generates
code that will implement a trait with its own default implementation on the
type you’ve annotated with the derive
syntax.
In this appendix, we provide a reference of all the traits in the standard
library that you can use with derive
. Each section covers:
- What operators and methods deriving this trait will enable
- What the implementation of the trait provided by
derive
does - What implementing the trait signifies about the type
- The conditions in which you’re allowed or not allowed to implement the trait
- Examples of operations that require the trait
If you want different behavior from that provided by the derive
attribute,
consult the standard library documentation
for each trait for details of how to manually implement them.
These traits listed here are the only ones defined by the standard library that
can be implemented on your types using derive
. Other traits defined in the
standard library don’t have sensible default behavior, so it’s up to you to
implement them in the way that makes sense for what you’re trying to accomplish.
An example of a trait that can’t be derived is Display
, which handles
formatting for end users. You should always consider the appropriate way to
display a type to an end user. What parts of the type should an end user be
allowed to see? What parts would they find relevant? What format of the data
would be most relevant to them? The Rust compiler doesn’t have this insight, so
it can’t provide appropriate default behavior for you.
The list of derivable traits provided in this appendix is not comprehensive:
libraries can implement derive
for their own traits, making the list of
traits you can use derive
with truly open-ended. Implementing derive
involves using a procedural macro, which is covered in the
“Macros” section of Chapter 19.
Debug
for Programmer Output
The Debug
trait enables debug formatting in format strings, which you
indicate by adding :?
within {}
placeholders.
The Debug
trait allows you to print instances of a type for debugging
purposes, so you and other programmers using your type can inspect an instance
at a particular point in a program’s execution.
The Debug
trait is required, for example, in use of the assert_eq!
macro.
This macro prints the values of instances given as arguments if the equality
assertion fails so programmers can see why the two instances weren’t equal.
PartialEq
and Eq
for Equality Comparisons
The PartialEq
trait allows you to compare instances of a type to check for
equality and enables use of the ==
and !=
operators.
Deriving PartialEq
implements the eq
method. When PartialEq
is derived on
structs, two instances are equal only if all fields are equal, and the
instances are not equal if any fields are not equal. When derived on enums,
each variant is equal to itself and not equal to the other variants.
The PartialEq
trait is required, for example, with the use of the
assert_eq!
macro, which needs to be able to compare two instances of a type
for equality.
The Eq
trait has no methods. Its purpose is to signal that for every value of
the annotated type, the value is equal to itself. The Eq
trait can only be
applied to types that also implement PartialEq
, although not all types that
implement PartialEq
can implement Eq
. One example of this is floating point
number types: the implementation of floating point numbers states that two
instances of the not-a-number (NaN
) value are not equal to each other.
An example of when Eq
is required is for keys in a HashMap<K, V>
so the
HashMap<K, V>
can tell whether two keys are the same.
PartialOrd
and Ord
for Ordering Comparisons
The PartialOrd
trait allows you to compare instances of a type for sorting
purposes. A type that implements PartialOrd
can be used with the <
, >
,
<=
, and >=
operators. You can only apply the PartialOrd
trait to types
that also implement PartialEq
.
Deriving PartialOrd
implements the partial_cmp
method, which returns an
Option<Ordering>
that will be None
when the values given don’t produce an
ordering. An example of a value that doesn’t produce an ordering, even though
most values of that type can be compared, is the not-a-number (NaN
) floating
point value. Calling partial_cmp
with any floating point number and the NaN
floating point value will return None
.
When derived on structs, PartialOrd
compares two instances by comparing the
value in each field in the order in which the fields appear in the struct
definition. When derived on enums, variants of the enum declared earlier in the
enum definition are considered less than the variants listed later.
The PartialOrd
trait is required, for example, for the gen_range
method
from the rand
crate that generates a random value in the range specified by a
range expression.
The Ord
trait allows you to know that for any two values of the annotated
type, a valid ordering will exist. The Ord
trait implements the cmp
method,
which returns an Ordering
rather than an Option<Ordering>
because a valid
ordering will always be possible. You can only apply the Ord
trait to types
that also implement PartialOrd
and Eq
(and Eq
requires PartialEq
). When
derived on structs and enums, cmp
behaves the same way as the derived
implementation for partial_cmp
does with PartialOrd
.
An example of when Ord
is required is when storing values in a BTreeSet<T>
,
a data structure that stores data based on the sort order of the values.
Clone
and Copy
for Duplicating Values
The Clone
trait allows you to explicitly create a deep copy of a value, and
the duplication process might involve running arbitrary code and copying heap
data. See the “Ways Variables and Data Interact:
Clone” section in
Chapter 4 for more information on Clone
.
Deriving Clone
implements the clone
method, which when implemented for the
whole type, calls clone
on each of the parts of the type. This means all the
fields or values in the type must also implement Clone
to derive Clone
.
An example of when Clone
is required is when calling the to_vec
method on a
slice. The slice doesn’t own the type instances it contains, but the vector
returned from to_vec
will need to own its instances, so to_vec
calls
clone
on each item. Thus, the type stored in the slice must implement Clone
.
The Copy
trait allows you to duplicate a value by only copying bits stored on
the stack; no arbitrary code is necessary. See the “Stack-Only Data:
Copy” section in Chapter 4 for more
information on Copy
.
The Copy
trait doesn’t define any methods to prevent programmers from
overloading those methods and violating the assumption that no arbitrary code
is being run. That way, all programmers can assume that copying a value will be
very fast.
You can derive Copy
on any type whose parts all implement Copy
. A type that
implements Copy
must also implement Clone
, because a type that implements
Copy
has a trivial implementation of Clone
that performs the same task as
Copy
.
The Copy
trait is rarely required; types that implement Copy
have
optimizations available, meaning you don’t have to call clone
, which makes
the code more concise.
Everything possible with Copy
you can also accomplish with Clone
, but the
code might be slower or have to use clone
in places.
Hash
for Mapping a Value to a Value of Fixed Size
The Hash
trait allows you to take an instance of a type of arbitrary size and
map that instance to a value of fixed size using a hash function. Deriving
Hash
implements the hash
method. The derived implementation of the hash
method combines the result of calling hash
on each of the parts of the type,
meaning all fields or values must also implement Hash
to derive Hash
.
An example of when Hash
is required is in storing keys in a HashMap<K, V>
to store data efficiently.
Default
for Default Values
The Default
trait allows you to create a default value for a type. Deriving
Default
implements the default
function. The derived implementation of the
default
function calls the default
function on each part of the type,
meaning all fields or values in the type must also implement Default
to
derive Default
.
The Default::default
function is commonly used in combination with the struct
update syntax discussed in the “Creating Instances From Other Instances With
Struct Update
Syntax”
section in Chapter 5. You can customize a few fields of a struct and then
set and use a default value for the rest of the fields by using
..Default::default()
.
The Default
trait is required when you use the method unwrap_or_default
on
Option<T>
instances, for example. If the Option<T>
is None
, the method
unwrap_or_default
will return the result of Default::default
for the type
T
stored in the Option<T>
.
Appendix D - Useful Development Tools
In this appendix, we talk about some useful development tools that the Rust project provides. We’ll look at automatic formatting, quick ways to apply warning fixes, a linter, and integrating with IDEs.
Automatic Formatting with rustfmt
The rustfmt
tool reformats your code according to the community code style.
Many collaborative projects use rustfmt
to prevent arguments about which
style to use when writing Rust: everyone formats their code using the tool.
To install rustfmt
, enter the following:
$ rustup component add rustfmt
This command gives you rustfmt
and cargo-fmt
, similar to how Rust gives you
both rustc
and cargo
. To format any Cargo project, enter the following:
$ cargo fmt
Running this command reformats all the Rust code in the current crate. This
should only change the code style, not the code semantics. For more information
on rustfmt
, see its documentation.
Fix Your Code with rustfix
The rustfix tool is included with Rust installations and can automatically fix compiler warnings that have a clear way to correct the problem that’s likely what you want. It’s likely you’ve seen compiler warnings before. For example, consider this code:
Filename: src/main.rs
fn do_something() {} fn main() { for i in 0..100 { do_something(); } }
Here, we’re calling the do_something
function 100 times, but we never use the
variable i
in the body of the for
loop. Rust warns us about that:
$ 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
The warning suggests that we use _i
as a name instead: the underscore
indicates that we intend for this variable to be unused. We can automatically
apply that suggestion using the rustfix
tool by running the command cargo fix
:
$ 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
When we look at src/main.rs again, we’ll see that cargo fix
has changed the
code:
Filename: src/main.rs
fn do_something() {} fn main() { for _i in 0..100 { do_something(); } }
The for
loop variable is now named _i
, and the warning no longer appears.
You can also use the cargo fix
command to transition your code between
different Rust editions. Editions are covered in Appendix E.
More Lints with Clippy
The Clippy tool is a collection of lints to analyze your code so you can catch common mistakes and improve your Rust code.
To install Clippy, enter the following:
$ rustup component add clippy
To run Clippy’s lints on any Cargo project, enter the following:
$ cargo clippy
For example, say you write a program that uses an approximation of a mathematical constant, such as pi, as this program does:
Filename: src/main.rs
fn main() { let x = 3.1415; let r = 8.0; println!("the area of the circle is {}", x * r * r); }
Running cargo clippy
on this project results in this error:
error: approximate value of `f{32, 64}::consts::PI` found
--> src/main.rs:2:13
|
2 | let x = 3.1415;
| ^^^^^^
|
= note: `#[deny(clippy::approx_constant)]` on by default
= help: consider using the constant directly
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
This error lets you know that Rust already has a more precise PI
constant
defined, and that your program would be more correct if you used the constant
instead. You would then change your code to use the PI
constant. The
following code doesn’t result in any errors or warnings from Clippy:
Filename: 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); }
For more information on Clippy, see its documentation.
IDE Integration Using rust-analyzer
To help IDE integration, the Rust community recommends using
rust-analyzer
. This tool is a set of
compiler-centric utilities that speaks the Language Server Protocol, which is a specification for IDEs and programming languages to
communicate with each other. Different clients can use rust-analyzer
, such as
the Rust analyzer plug-in for Visual Studio Code.
Visit the rust-analyzer
project’s home page
for installation instructions, then install the language server support in your
particular IDE. Your IDE will gain abilities such as autocompletion, jump to
definition, and inline errors.
Appendix E - Editions
In Chapter 1, you saw that cargo new
adds a bit of metadata to your
Cargo.toml file about an edition. This appendix talks about what that means!
The Rust language and compiler have a six-week release cycle, meaning users get a constant stream of new features. Other programming languages release larger changes less often; Rust releases smaller updates more frequently. After a while, all of these tiny changes add up. But from release to release, it can be difficult to look back and say, “Wow, between Rust 1.10 and Rust 1.31, Rust has changed a lot!”
Every two or three years, the Rust team produces a new Rust edition. Each edition brings together the features that have landed into a clear package with fully updated documentation and tooling. New editions ship as part of the usual six-week release process.
Editions serve different purposes for different people:
- For active Rust users, a new edition brings together incremental changes into an easy-to-understand package.
- For non-users, a new edition signals that some major advancements have landed, which might make Rust worth another look.
- For those developing Rust, a new edition provides a rallying point for the project as a whole.
At the time of this writing, three Rust editions are available: Rust 2015, Rust 2018, and Rust 2021. This book is written using Rust 2021 edition idioms.
The edition
key in Cargo.toml indicates which edition the compiler should
use for your code. If the key doesn’t exist, Rust uses 2015
as the edition
value for backward compatibility reasons.
Each project can opt in to an edition other than the default 2015 edition. Editions can contain incompatible changes, such as including a new keyword that conflicts with identifiers in code. However, unless you opt in to those changes, your code will continue to compile even as you upgrade the Rust compiler version you use.
All Rust compiler versions support any edition that existed prior to that compiler’s release, and they can link crates of any supported editions together. Edition changes only affect the way the compiler initially parses code. Therefore, if you’re using Rust 2015 and one of your dependencies uses Rust 2018, your project will compile and be able to use that dependency. The opposite situation, where your project uses Rust 2018 and a dependency uses Rust 2015, works as well.
To be clear: most features will be available on all editions. Developers using any Rust edition will continue to see improvements as new stable releases are made. However, in some cases, mainly when new keywords are added, some new features might only be available in later editions. You will need to switch editions if you want to take advantage of such features.
For more details, the Edition
Guide is a complete book
about editions that enumerates the differences between editions and explains
how to automatically upgrade your code to a new edition via cargo fix
.
Ilova F: Kitobning tarjimalari
Ingliz tilidan boshqa tillardagi manbalar uchun.Ko'pchilik hali ham davom etmoqda; yordam berish yoki yangi tarjima haqida bizga xabar berish uchun Tarjimalar yorlig‘iga qarang!
- Português (BR)
- Português (PT)
- 简体中文
- 正體中文
- Українська
- Español, alternate
- Italiano
- Русский
- 한국어
- 日本語
- Français
- Polski
- Cebuano
- Tagalog
- Esperanto
- ελληνική
- Svenska
- Farsi
- Deutsch
- हिंदी
- ไทย
- Danske
- O'zbek
Appendix G - How Rust is Made and “Nightly Rust”
This appendix is about how Rust is made and how that affects you as a Rust developer.
Stability Without Stagnation
As a language, Rust cares a lot about the stability of your code. We want Rust to be a rock-solid foundation you can build on, and if things were constantly changing, that would be impossible. At the same time, if we can’t experiment with new features, we may not find out important flaws until after their release, when we can no longer change things.
Our solution to this problem is what we call “stability without stagnation”, and our guiding principle is this: you should never have to fear upgrading to a new version of stable Rust. Each upgrade should be painless, but should also bring you new features, fewer bugs, and faster compile times.
Choo, Choo! Release Channels and Riding the Trains
Rust development operates on a train schedule. That is, all development is
done on the master
branch of the Rust repository. Releases follow a software
release train model, which has been used by Cisco IOS and other software
projects. There are three release channels for Rust:
- Nightly
- Beta
- Stable
Most Rust developers primarily use the stable channel, but those who want to try out experimental new features may use nightly or beta.
Here’s an example of how the development and release process works: let’s
assume that the Rust team is working on the release of Rust 1.5. That release
happened in December of 2015, but it will provide us with realistic version
numbers. A new feature is added to Rust: a new commit lands on the master
branch. Each night, a new nightly version of Rust is produced. Every day is a
release day, and these releases are created by our release infrastructure
automatically. So as time passes, our releases look like this, once a night:
nightly: * - - * - - *
Every six weeks, it’s time to prepare a new release! The beta
branch of the
Rust repository branches off from the master
branch used by nightly. Now,
there are two releases:
nightly: * - - * - - *
|
beta: *
Most Rust users do not use beta releases actively, but test against beta in their CI system to help Rust discover possible regressions. In the meantime, there’s still a nightly release every night:
nightly: * - - * - - * - - * - - *
|
beta: *
Let’s say a regression is found. Good thing we had some time to test the beta
release before the regression snuck into a stable release! The fix is applied
to master
, so that nightly is fixed, and then the fix is backported to the
beta
branch, and a new release of beta is produced:
nightly: * - - * - - * - - * - - * - - *
|
beta: * - - - - - - - - *
Six weeks after the first beta was created, it’s time for a stable release! The
stable
branch is produced from the beta
branch:
nightly: * - - * - - * - - * - - * - - * - * - *
|
beta: * - - - - - - - - *
|
stable: *
Hooray! Rust 1.5 is done! However, we’ve forgotten one thing: because the six
weeks have gone by, we also need a new beta of the next version of Rust, 1.6.
So after stable
branches off of beta
, the next version of beta
branches
off of nightly
again:
nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: * - - - - - - - - * *
|
stable: *
This is called the “train model” because every six weeks, a release “leaves the station”, but still has to take a journey through the beta channel before it arrives as a stable release.
Rust releases every six weeks, like clockwork. If you know the date of one Rust release, you can know the date of the next one: it’s six weeks later. A nice aspect of having releases scheduled every six weeks is that the next train is coming soon. If a feature happens to miss a particular release, there’s no need to worry: another one is happening in a short time! This helps reduce pressure to sneak possibly unpolished features in close to the release deadline.
Thanks to this process, you can always check out the next build of Rust and
verify for yourself that it’s easy to upgrade to: if a beta release doesn’t
work as expected, you can report it to the team and get it fixed before the
next stable release happens! Breakage in a beta release is relatively rare, but
rustc
is still a piece of software, and bugs do exist.
Unstable Features
There’s one more catch with this release model: unstable features. Rust uses a
technique called “feature flags” to determine what features are enabled in a
given release. If a new feature is under active development, it lands on
master
, and therefore, in nightly, but behind a feature flag. If you, as a
user, wish to try out the work-in-progress feature, you can, but you must be
using a nightly release of Rust and annotate your source code with the
appropriate flag to opt in.
If you’re using a beta or stable release of Rust, you can’t use any feature flags. This is the key that allows us to get practical use with new features before we declare them stable forever. Those who wish to opt into the bleeding edge can do so, and those who want a rock-solid experience can stick with stable and know that their code won’t break. Stability without stagnation.
This book only contains information about stable features, as in-progress features are still changing, and surely they’ll be different between when this book was written and when they get enabled in stable builds. You can find documentation for nightly-only features online.
Rustup and the Role of Rust Nightly
Rustup makes it easy to change between different release channels of Rust, on a global or per-project basis. By default, you’ll have stable Rust installed. To install nightly, for example:
$ rustup toolchain install nightly
You can see all of the toolchains (releases of Rust and associated
components) you have installed with rustup
as well. Here’s an example on one
of your authors’ Windows computer:
> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc
As you can see, the stable toolchain is the default. Most Rust users use stable
most of the time. You might want to use stable most of the time, but use
nightly on a specific project, because you care about a cutting-edge feature.
To do so, you can use rustup override
in that project’s directory to set the
nightly toolchain as the one rustup
should use when you’re in that directory:
$ cd ~/projects/needs-nightly
$ rustup override set nightly
Now, every time you call rustc
or cargo
inside of
~/projects/needs-nightly, rustup
will make sure that you are using nightly
Rust, rather than your default of stable Rust. This comes in handy when you
have a lot of Rust projects!
The RFC Process and Teams
So how do you learn about these new features? Rust’s development model follows a Request For Comments (RFC) process. If you’d like an improvement in Rust, you can write up a proposal, called an RFC.
Anyone can write RFCs to improve Rust, and the proposals are reviewed and discussed by the Rust team, which is comprised of many topic subteams. There’s a full list of the teams on Rust’s website, which includes teams for each area of the project: language design, compiler implementation, infrastructure, documentation, and more. The appropriate team reads the proposal and the comments, writes some comments of their own, and eventually, there’s consensus to accept or reject the feature.
If the feature is accepted, an issue is opened on the Rust repository, and
someone can implement it. The person who implements it very well may not be the
person who proposed the feature in the first place! When the implementation is
ready, it lands on the master
branch behind a feature gate, as we discussed
in the “Unstable Features” section.
After some time, once Rust developers who use nightly releases have been able to try out the new feature, team members will discuss the feature, how it’s worked out on nightly, and decide if it should make it into stable Rust or not. If the decision is to move forward, the feature gate is removed, and the feature is now considered stable! It rides the trains into a new stable release of Rust.