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.