Published on

Swift-da Struct-lardan qanday foydalanish kerak

Authors

Yana xush kelibsiz, hammaga salom! Men — Nik, bu Swiftful Thinking. Va'da qilganimdek, oldingi videoda biz obyektga yo'naltirilgan dasturlash (OOP) haqida gaplashdik, va o'sha yerda tilga olgan obyektlarimizdan biri — struct edi. Struct'lar — qiymat turlari (value types) bo'lib, ular stackda saqlanadi. Ushbu videoda esa struct'lar bilan qanday ishlash mumkinligini chuqurroq ko'rib chiqamiz: avval ularni yaratishni, so'ngra ularni mutatsiya qilishni (mutate) o'rganamiz. "Mutatsiya qilish" so'zi, agar ilgari hech qachon kod yozmagan bo'lsangiz, qo'rqinchli tuyulishi mumkin, ammo aslida bu unchalik murakkab emas — bu shunchaki obyektni yangilash, degani. Bu — qiymat turlari, xususan struct'lar haqida gap ketganda ishlatiladigan maxsus atama.

Demak, biz bir qancha obyekt yaratamiz, so'ngra ular ichidagi ma'lumotlarni qanday yangilash mumkinligini o'rganamiz. Bu jarayonda ko'plab misollar ko'rib chiqamiz, va men ulardan qaysilari boshlang'ichlar uchun qulayroq, qaysilari emasligini alohida ta'kidlab o'taman. Ikkalasini ham bilish muhim, deb hisoblayman — ammo agar siz hozirgina kod yozishni o'rganayotgan bo'lsangiz, boshlang'ichlar uchun qulay variantlarda qolishni tavsiya qilardim, chunki bu kelajakda sizni ko'plab debugging muammolaridan qutqarib qoladi. Yana eslatib o'taman: ushbu videoda biz faqat struct'lar haqida gaplashamiz, keyingi bir necha videoda esa enum va class kabi boshqa turlarni o'rganamiz.


Yana hammaga xush kelibsiz! Oldingi videoda biz obyektga yo'naltirilgan dasturlashni ko'rib chiqdik — bu qiyin video edi, ishonchim komilki, sizlar uchun ham qiyin bo'lgandir. Ammo o'zingizni bosib qolmang — bu osonlashib boradi, biz shunchaki ba'zi narsalarni oldindan ko'rib chiqishimiz kerak, shunda keyinchalik kod yozayotganimizda butunlay adashib qolmaymiz. Ishonib qo'ying, bu ancha osonlashadi.

Ushbu videoda biz struct'larga chuqurroq sho'ng'iymiz. Oldingi videoda struct va class o'rtasidagi farqlar haqida bir oz gaplashgan edim, ammo buni haqiqatan tushunishning yagona yo'li — struct va class yaratib, ular bilan birozgina o'ynab ko'rishdir.


Yangi playground sahifa

Navigator-da o'ng tugmani bosib, yangi playground sahifa yaratamiz va unga Structs deb nom beramiz, va shu yerda kod yozishni boshlaymiz.

Agar oldingi video bilan birga bo'lmagan bo'lsangiz, struct'lar haqida bir nechta asosiy narsani eslatib o'taman: struct'lar — tez, juda-juda tez ishlaydi, shuning uchun Swift-da struct'lardan foydalanishni yaxshi ko'ramiz. Struct'lar stackda saqlanadi — xotirada ular aynan shu yerda joylashadi. Stack'dagi obyektlar esa qiymat turlari hisoblanadi, shuning uchun struct'lar ham qiymat turlaridir, chunki ular stack'da joylashgan. Qiymat turlari esa nusxalanadi (copied), va biz buni mutatsiya qilinadi (mutated) deb ataymiz.

"Mutatsiya" so'zi hozirgacha biz ko'rib chiqmagan atama, ammo men buni shu videoda tushuntirib beraman. Oldingi videoda struct'larga nisbatan "siz ularni nusxalab-joylashtirishingiz mumkin" deganman — bu, asosan, oddiy til bilan tushuntirilgan usul edi. Ammo aslida struct'ni tahrirlayotganimizda biz nusxalab-joylashtirmaymiz, balki struct'ni mutatsiya qilamiz. Shu haqida ushbu videoda birozroq gaplashamiz.


Birinchi struct: Quiz

Keling, birinchi struct'imizni yozishni boshlaylik. Quiz nomli struct yarataman — bu oldingi videodagi misolning davomi. Aytaylik, har bir quiz'da sarlavha (title) bo'ladi, shuning uchun String turidagi title konstantasini yozamiz. Va har bir quiz'da, ehtimol, yaratilgan sana (date created) qiymati ham bo'ladi — bu Date turida bo'ladi. Ko'pincha, agar siz biror narsani backend'ingizda saqlayotgan bo'lsangiz, yaratilgan sanani ham saqlashni xohlaysiz — shunda bu obyekt qachon yaratilganini bilib turasiz. Shuning uchun bunga production ilovalarida tobora ko'proq duch kelasiz.

struct Quiz {
    let title: String
    let dateCreated: Date
}

Hozirgacha ushbu kursda biz oddiy turlardan foydalanib kelganmiz — masalan, String turidagi konstanta yaratib, uni "hello" qiymatiga tenglashtirishimiz mumkin edi. Ammo endi men Quiz turidagi myQuiz konstantasini yaratib, uni bir quiz qiymatiga tenglashtirishim mumkin. Quiz yaratmoqchi bo'lganimda, initializer deb ataladigan narsani ko'ramiz — bu tugatish (completion) bizga quiz uchun initializer'ni ko'rsatadi. Bu bizga shuni aytadi: quiz yaratish uchun unga sarlavha va yaratilgan sana berishimiz kerak.

Bu, aslida, ushbu pleylist davomida yaratgan boshqa har qanday funksiyani chaqirishdan farq qilmaydi — bu shunchaki funksiya, va bular uning ikki parametri. Sarlavha uchun, aytaylik, "Quiz 1" deb yozaman, yaratilgan sana uchun esa bugungi sanani qo'yaman — bugungi sanani yozishning yana bir usuli — shunchaki .now:

let myQuiz: Quiz = Quiz(title: "Quiz 1", dateCreated: .now)

Endi kodimda myQuiz.titlega murojaat qilib, uni chop etishim mumkin:

print(myQuiz.title)

Buni ishga tushirsak, albatta, ishlayveradi. Ammo birinchi savol shu: bu closure qayerdan kelib chiqdi, axir biz "sarlavha bering, yaratilgan sana bering" deydigan funksiyani o'zimiz yozmagan edik-ku? Buning sababi shuki, struct'larda implicit init (o'z-o'zidan yaratiladigan initializer) deb ataladigan narsa mavjud. Bu yerga izoh qo'yib qo'yaylik: struct'larda implicit init bor, va bu shuni bildiradi — kompilyator bu init'ni biz uchun avtomatik yaratib beradi.


Init-ni qo'lda yozish

Aslida bu — shu obyekt uchun initializer bo'lgan funksiya, xolos. Va agar buni qo'limda yozmoqchi bo'lsam, init yaratib, qavslarni ochib-yopib, ichiga jingalak qavslarni qo'yishim mumkin. Shu init funksiyasi ichida title va dateCreatedni parametr sifatida qabul qilaman, va shu obyektni yaratish uchun ushbu initni chaqirib, unga uzatilgan qiymatlar bilan obyektni sozlayman. Bu yerda men buni self.title deb ataymen — self deganda aynan shu obyektning o'zidagi titleni nazarda tutaman, parametr sifatida kelgan titleni emas. Demak, men shu titleni bizga uzatilgan titlega tenglashtiraman, xuddi shunday self.dateCreatedni ham uzatilgan dateCreatedga tenglashtiraman:

struct Quiz {
    let title: String
    let dateCreated: Date

    init(title: String, dateCreated: Date) {
        self.title = title
        self.dateCreated = dateCreated
    }
}

Mana shu initializer — yuqoridagi kod bilan bir xil narsa, faqat farqi shundaki, Swift buni biz uchun avtomatik yaratib bergan, va biz buni qo'lda yozmaganmiz — bu Swift-dagi bir qulaylik, xolos. Shuning uchun, agar biz shu quiz'ni yaratganimizda va shu initializer'dan foydalanganimizda, bizga buni kodimizda yozish shart emas edi — shunchaki yuqoridagi kabi yozsak ham, baribir ishlayverardi. Albatta, agar xohlasak, buni har doim qo'lda yozib olishimiz mumkin.

Demak, keyingi savol shu bo'ladi: nega umuman o'zimiz init yaratishimiz kerak? Buning sababi — agar o'zimiz init yaratsak, uni moslashtirish (customize) imkoniyatiga ega bo'lamiz.


Standart (default) qiymatlar

Masalan, men shu kodni nusxalab, yana bir nusxasini joylashtiraman, birinchisini izohga olib qo'yaman. Aytaylik, har safar quiz yaratganimizda sarlavhani uzatishni xohlaymiz, ammo agar yaratilgan sanani uzatmasak, unga standart qiymat (default value) berishni xohlaymiz. Buni funksiya parametriga standart qiymat berish orqali amalga oshiramiz — bu yerda dateCreated parametrini .nowga tenglab qo'yamiz:

struct Quiz {
    let title: String
    let dateCreated: Date

    init(title: String, dateCreated: Date = .now) {
        self.title = title
        self.dateCreated = dateCreated
    }
}

Endi bizda xuddi avvalgi initializer bor, faqat bu safar unda standart qiymat mavjud. Buni qilishimning sababi shu: agar men endi quyidagicha yozsam, dateCreated xira (grayed out) ko'rinadi — chunki uni uzatish shart emas. Agar men uni uzatmasam, u standart qiymatga, ya'ni .nowga teng bo'ladi:

let myQuiz = Quiz(title: "Quiz 1")

Endi shu obyektni yaratganimda dateCreatedni qo'shishim shart emas.


Optional qiymatlar struct ichida

Yana bir tezkor misol ko'raylik. Aytaylik, shu quiz ichida yana bir narsa bor — masalan, Bool turidagi, faqat premium foydalanuvchilar kira oladigan "premium quiz"ligini bildiruvchi qiymat. Buni optional Boolean qilib qo'yaylik — shunday qilib, struct'lar ichida ham optional'lardan foydalanish mumkinligini ko'rsatib o'taman.

Shu kodni nusxalab, yana bir initializer (uchinchisini) yarataman, bu safar uni yanada ko'proq moslashtiramiz. isPremium uchun ham qiymat qabul qilamiz, va bizga faqat optional Boolean kerak, shuning uchun buni ham optional Boolean qilib qo'yamiz:

struct Quiz {
    let title: String
    let dateCreated: Date
    let isPremium: Bool?

    init(title: String, dateCreated: Date = .now, isPremium: Bool? = nil) {
        self.title = title
        self.dateCreated = dateCreated
        self.isPremium = isPremium
    }
}

Bu ham ishlaydi — endi quiz yaratganimda dateCreated hamon xira bo'lib turadi, chunki uni uzatish shart emas; agar uzatmoqchi bo'lsam, Option tugmasini bosib Enter bossam, sanani qo'lda kiritishim mumkin, aks holda shunchaki Enter bossam, sanani qo'shishim shart bo'lmaydi. isPremiumga esa true, false yoki nil qiymatlarini berishim mumkin — farqi yo'q, chunki bu optional qiymat ekanligini Swift tushunib turadi.


Optional parametr va nil coalescing operatori

Init'lar haqida ta'kidlab o'tmoqchi bo'lgan so'nggi bir narsa shu: ba'zan dateCreated uchun standart qiymat o'rniga, dasturchidan sanani albatta uzatishini xohlaymiz-u, ammo unga nil qiymatini ham berish imkoniyatini qoldirmoqchimiz — ya'ni nil uzatishga ruxsat berishni xohlaymiz, ammo obyektning o'zida bu qiymat hech qachon nil bo'lmasligini ta'minlashni xohlaymiz.

Shuning uchun dateCreated parametrini optional qilib qo'yaman. Endi Quiz turidagi obyekt yaratayotganimda, dateCreated talab qilinadi, va men optional sanani uzataman, so'ngra esa optional bo'lmagan xususiyatga optional qiymatni tenglashtirishga harakat qilaman — bu menga sanani unwrap qilishim kerakligi haqida xato beradi. Mana shu — optional'larni unwrap qilish qobiliyatimizdan foydalanishimiz mumkin bo'lgan holatlardan biri: biz if let ishlatishimiz mumkin, yoki nil coalescing operatoridan foydalanishimiz mumkin. Shuning uchun men shu yerda: shu quiz'dagi dateCreated — biz uzatgan qiymat bo'ladi, aks holda esa .now bo'ladi, deb yozaman:

struct Quiz {
    let title: String
    let dateCreated: Date
    let isPremium: Bool?

    init(title: String, dateCreated: Date?, isPremium: Bool? = nil) {
        self.title = title
        self.dateCreated = dateCreated ?? .now
        self.isPremium = isPremium
    }
}

Bu shuni ta'minlaydiki, biz har doim bir qiymat uzatamiz — ammo agar nil uzatsam, u .nowga teng bo'lib qoladi. Bu, ehtimol, eng ideal misol emas, ammo siz struct'lar yaratishni davom ettirar ekansiz, xuddi shunga o'xshash holatlarga duch kelasiz, va kodingizning qanday ko'rinishini o'zingiz moslashtirib borishingiz mumkin bo'ladi.


Struct'ni mutatsiya qilish muammosi

Endi boshqa bir misolga o'tamiz. Quiz'ni tahrirlashga harakat qilib ko'rmoqchiman, chunki oldingi videoda aytganimdek, quiz'lar — qiymat turlari, demak ularni tahrirlaganimizda biz, aslida, ularni nusxalab-joylashtiramiz, yoki to'g'rirog'i — mutatsiya qilamiz. Keling, struct'ni qanday mutatsiya qilishni o'rganaylik.

Yana bir struct yarataman, bu safar UserModel deb nomlayman. (Keyingi bir necha kursda buni tez-tez ko'rasiz: men ma'lumot modeli yaratganimda, ko'pincha nomining oxiriga "model" so'zini qo'shaman — shunday qilib UserModel foydalanuvchi uchun ma'lumot modeli bo'ladi. Aslida, ko'pchilik production ilovalar buni shunchaki User deb qo'yadi, chunki yozish osonroq, "model" so'zi shart emas. Ammo o'rganayotganingizda, bu — "bu mening ma'lumot modelim" ekanligini aniq ko'rsatib turadigan qulay belgi bo'ladi.) UserModelda, aytaylik, har bir foydalanuvchida String turidagi name bor, va har bir foydalanuvchida u premium ekanligi yoki yo'qligini bildiruvchi Bool turidagi isPremium qiymati bor:

struct UserModel {
    let name: String
    let isPremium: Bool
}

Bu struct'da faqat letlar bor, var yo'q — agar var va let o'rtasidagi farqni bilmasangiz, ushbu pleylistdagi oldingi videolarni tomosha qiling, chunki men hozir buni bilishingizni taxmin qilib gapiraman. Demak, struct faqat letlardan iborat bo'lsa, biz buni immutable struct (o'zgarmas struct) deb ataymiz — bu, aslida, hamma narsa konstanta ekanligini bildiradi, bu esa "immutable" — "mutable emas" ekanligini, demak buni mutatsiya qila olmasligimizni bildiradi.

Mutatsiya qilishni hali o'rganmadik, ammo immutable struct — bu men o'z ilovalarimning ko'pchiligida foydalanadigan kodlash amaliyoti. Ba'zilar har doim shundan foydalanishni tavsiya qiladi, ba'zilar esa bunga ehtiyoj yo'q deydi — ammo immutable struct, asosan, bularning barchasi konstanta ekanligini bildiradi. Bunday qilishning foydasi shunda: dasturchi sifatida, agar shu UserModelni ilovamiz bo'ylab uzatsak — masalan, u yuzlab turli fayllarga tegishi mumkin — bular konstanta bo'lgani uchun, biz bu ma'lumot hech qachon o'zgarmasligini bilamiz. Demak, biror narsa o'zgarmas bo'lganida shuning foydasi shu: kodingizning qayerida bo'lishidan qat'i nazar, u har doim aynan shu qiymatlarga ega bo'lib qoladi. Murakkabroq ilovalar yoza boshlaganingizda, bundan ko'plab foyda olasiz.

Shu foydalanuvchini yaratish uchun var yaratib, buni user1 deb ataymiz, turi UserModel bo'ladi. Endi keyinroq kodimda, aytaylik, "foydalanuvchini premium deb belgilash" degan funksiya bo'ladi. Foydalanuvchini premium deb belgilash uchun, asosan, shu mavjud UserModelni — bir xil ism bilan — olib, faqat isPremium qiymatini falsedan truega o'zgartirishni xohlayman.

Ammo bu konstanta ekanligini bilamiz, shuning uchun bu qiymatni to'g'ridan-to'g'ri tahrirlay olmaymiz. Buning o'rniga, biz xuddi shu ma'lumotlarga ega, ammo bitta qiymati o'zgargan ikkinchi struct yaratamiz. Avval user1ni chop etib ko'raylik:

var user1 = UserModel(name: "Nick", isPremium: false)
print(user1) // isPremium: false

Buni ishga tushirsak, user1 chop etiladi, va ko'ramizki, Nick uchun isPremiumfalse. Endi men shu user1 o'zgaruvchisini yangi UserModelga tenglashtirmoqchiman. Yangi UserModel yaratganimda, "Nick"ni qaytadan qo'lda yozishim ham mumkin edi, ammo men aslida shu obyektning hozirgi qiymatini olib, undan foydalanmoqchiman — shuning uchun user1.nameni uzataman, isPremiumni esa false o'rniga true qilib o'zgartiraman. Endi yana user1ni chop etaman:

user1 = UserModel(name: user1.name, isPremium: true)
print(user1) // isPremium: true

Kodni ishga tushirsak, birinchi marta false, ikkinchi marta esa true ekanligini ko'ramiz. Demak, biz nima qildik: bir UserModel yaratdik, so'ngra undagi bitta qiymatni o'zgartirish uchun, mavjud barcha ma'lumotlardan foydalanib, faqat shu bitta qiymatni o'zgartirib, butunlay yangi UserModel yaratdik.

Umid qilamanki, bu tushunarli bo'ldi — bu birozgina mantiqsizdek tuyulishi mumkin: "biz allaqachon shu obyektga egamiz-ku, nega uni shunchaki o'zgartirish o'rniga ikkinchi obyekt yaratib, keyin uning qiymatini o'zgartiramiz?" Buning sababi — bular qiymat turlari, va qiymat turini o'zgartirganingizda, siz, asosan, uni mutatsiya qilayotgan, ya'ni o'sha qiymat turining yangi versiyasini yaratayotgan bo'lasiz.

Bu — buni amalga oshirishning juda qo'lbola (manual) usuli, va biz kodda buni odatda shu tarzda qilmaymiz. Buni biroz yaxshilashning yo'li bor.


Mutable struct: UserModel2

Keling, avvalgisini izohga olib, yana bir struct yarataylik — UserModel2 deb nomlaymiz, bu UserModelning yana bir versiyasi, xolos. Bu safar String turidagi name bo'ladi, ammo bu safar isPremiumni var qilib yozamiz:

struct UserModel2 {
    let name: String
    var isPremium: Bool
}

Demak, bu xuddi avvalgisi bilan bir xil, faqat bu safar var bor — bu endi immutable struct emas, bu — mutable struct. Buning ma'nosi shuki, biz buni mutatsiya qila olamiz, va shu varni o'zgartirganimizda, biz, aslida, struct'ning o'zini mutatsiya qilayotgan bo'lamiz.

Quyida user2 nomli var yarataman, uni UserModel2ga tenglashtiraman. Avvalgisi kabi, user2ni chop etib, funksiya tugagandan keyin yana chop etamiz. Bu safar esa, yangi versiyaga tenglashtirish o'rniga, shunchaki shu qiymatga murojaat qilib, uni o'zgartiramiz:

var user2 = UserModel2(name: "Nick", isPremium: false)
print(user2)

user2.isPremium = true
print(user2)

Kodni ishga tushirsak, yana ko'ramiz: avval false, keyin true. Bu birozgina mantiqsizdek tuyulishi mumkin — bu xuddi avvalgisidan butunlay boshqacha ko'rinadi, ammo aslida unday emas.

Keling, buni tahlil qilaylik: user2 — bu, aslida, o'zgaruvchining o'zi, demak biz user2 obyektining o'zini o'zgartira olamiz. Avvalgi misolda bizda bitta obyekt bor edi, va biz yangi UserModel yaratib, eskisini unga tenglashtirib o'zgartirgan edik. Bu yerda esa, mavjud obyektni shunchaki tahrirlayotganga o'xshaymiz, ammo aslida biz nima qilayotgan bo'lsak — mavjud obyektni mutatsiya qilib, natijada uni yangi mutatsiyalangan versiyaga tenglashtirib qo'yayotganmiz.

Buni isbotlash uchun: agar men shu o'zgaruvchini konstanta qilib qo'ysam va user2ni o'zgartirishga harakat qilsam, kompilyatordan xatolik olamiz — chunki user2.isPremiumni chaqirib, uni tahrirlayotganimizda, aslida biz struct'ni "sahna ortida" mutatsiya qilayotganmiz, ya'ni bu obyektni hozirgi modeldan yangi versiyasiga o'zgartirib qo'yayotganmiz. Bu juda chalkash, chunki bularning bari "sahna ortida" sodir bo'ladi va buni payqab qolish qiyin. Demak, kamida boshlang'ich daraja uchun, struct'ni mutatsiya qilish — yuqoridagi (UserModel bilan qilgan) usulimiz bilan bir xil narsa, va bu — to'g'ri yo'nalishdagi bir qadam, ammo men buni yanada bir qadam oldinga olib bormoqchiman.


Mutatsiyani struktura ichiga olib kirish: UserModel3

Shu kodni nusxalab, yana bir struct yarataylik — UserModel3 deb ataymiz, va bu safar yana immutable structga qaytamiz — ya'ni hamma narsa let bo'ladi. Ammo bu safar, struct immutable bo'lishiga qaramay, isPremium qiymatini tahrirlay olishimni xohlayman.

Avvalroq bizda yangi UserModel yaratib, uni qaytaradigan funksiya bor edi — endi men shu funksiyani olib, uni to'g'ridan-to'g'ri struct ichiga ko'chiraman. Demak, struct ichida markUserAsPremium nomli funksiya yarataman — bu funksiya UserModel3ni qaytaradi, ammo bu safar buni struct ichidan turib amalga oshiramiz. Avvalroq biz user1ga murojaat qilib, undan nameni olishimiz kerak edi, ammo endi biz struct'ning o'zi ichidamiz, shuning uchun namega to'g'ridan-to'g'ri murojaat qilishimiz mumkin. So'ngra isPremiumni yangi qiymatga o'rnatamiz — bu shunchaki funksiya, unga Bool turidagi yangi qiymatni parametr sifatida berishimiz mumkin:

struct UserModel3 {
    let name: String
    let isPremium: Bool

    func markUserAsPremium(newValue: Bool) -> UserModel3 {
        return UserModel3(name: name, isPremium: newValue)
    }
}

Endi kodimda UserModel3 turidagi user3 o'zgaruvchisini yaratib, uni mos qiymatlar bilan o'rnataman. So'ngra user3ni shu user3.markUserAsPremiumning natijasiga tenglashtirishim mumkin:

var user3 = UserModel3(name: "Nick", isPremium: true)
user3 = user3.markUserAsPremium(newValue: false)

Bu xuddi avvalgisi kabi ishlaydi. Yana eslatib o'taman: Swift-da, agar funksiyadan shunchaki bir narsani qaytarayotgan bo'lsak, return so'zini yozish shart emas. Yagona farq shu: markUserAsPremium funksiyasi endi struct'ning o'zi ichida. Oldingi misolda struct ichida hech narsa yo'q edi, bu yerda esa u struct ichida. Bu — yaxshiroq kodlash amaliyoti, chunki endi struct o'z ma'lumotini o'zgartirish vakolatiga ega — biz bilamizki, bu struct har safar o'zgarganda, bu o'zgarish aynan struct'ning o'zidan kelib chiqadi. Yana eslatib o'tay: biz bu yerda yangi struct yaratib, so'ngra shu struct'ni qaytaramiz — shuning uchun biz user3ni shu funksiyadan qaytgan yangi struct'ga tenglashtirib qo'yamiz.


Mutating funksiyalar: UserModel4

Keling, so'nggi bir versiyani ko'rib chiqaylik — bu safar yana bir mutable struct bo'ladi, va buni UserModel4 deb ataymiz. Bu yerda yana nameni konstanta, isPremiumni esa o'zgaruvchi qilib qo'yamiz. Ammo avvalgi safar buni qilganimizda, bizda struct ichida bo'lmagan funksiya bor edi, va biz user2.isPremium = true deb yozgan edik. Endi esa biz shu mutatsiyani struct'ning o'zi ichiga olib kirmoqchimiz.

Shuning uchun struct ichida yana bir funksiya yarataman — markUserAsPremium deb ataymiz, unga Bool turidagi yangi qiymatni parametr sifatida beraman, va biz shunchaki shu qiymatni mahalliy ravishda o'zgartiramiz: isPremiumni yangi qiymatga tenglashtiramiz.

(Aytmoqchi, agar funksiya yangi qiymat qabul qilayotgan bo'lsa, "premium deb belgilash" unchalik aniq nom emas — chunki bu funksiya doim premium holatiga o'tkazishi kerakdek tuyuladi. Shuning uchun ikki yo'l bor: yoki markUserAsPremium funksiyasi isPremiumni doim truega o'rnatadi, yoki updateIsPremium(newValue:) kabi funksiya yaratib, unga istalgan qiymatni uzatamiz. Ikkisi ham to'g'ri — faqat shuni aniq qilib qo'yish kerak: agar "premium deb belgilash" deyilsa, u doim premium holatga o'tkazishi kerak.)

Ammo shu yerda kompilyator bizga "self is immutable" (self — o'zgarmas) degan xatolik beradi, ya'ni biz shu obyektni mutatsiya qilishga urinayotganimizni aytadi. Garchi bu var bo'lib, texnik jihatdan o'zgartira olsak ham, bu kod endi struct'ning o'zi ichida joylashgan.

Esingizda bo'lsa, oldingi videoda aytganimdek, biz bir obyektni mutatsiya qilganimizda, aslida biz uni yangi versiyasiga "nusxalab-joylashtirayotgan"dek bo'lamiz. Demak, muammo shu: kompilyator bizga aytayapti — biz shu o'zgaruvchini o'zgartirishga urinayotganimizda, aslida biz shu struct'ning butunlay yangi versiyasini yaratayotganmiz, ya'ni struct'ni mutatsiya qilayotganmiz. Ammo muammo shundaki, agar biz shu struct ichida funksiya ishga tushirsak-yu, struct'ning o'zi o'zgarib ketsa, bu narsa nimanidir buzib qo'yishi mumkin — chunki biz ichida turgan obyektning o'zi mutatsiyaga uchraydi. Bu kodimizda muammo keltirib chiqaradi: agar struct ichidagi funksiyani chaqirayotgan bo'lsangiz-u, lekin shu struct aslida o'zgarib ketsa, demak bu yerda muammo bor, chunki funksiya struct'ning bir qismi (child) hisoblanadi.

Swift-da buning yechimi — funksiyani mutating deb belgilashdir. Agar shu yerda "fix" (tuzatish) tugmasini bossam, bu kompilyatorga shu funksiya struct'ning o'zini mutatsiya qilishini bildiradi, va endi buni amalga oshirishimiz mumkin — shuning uchun bu yerga ham mutating deb yozaman:

struct UserModel4 {
    let name: String
    var isPremium: Bool

    mutating func markUserAsPremium() {
        isPremium = true
    }

    mutating func updateIsPremium(newValue: Bool) {
        isPremium = newValue
    }
}

Demak, bu — Swift kompilyatorining bizga yordam berishi: bizga shu struct'ni mutatsiya qilishga urinayotganimizni aytib, buni amalga oshirish uchun funksiyani mutating deb belgilashimiz kerakligini ko'rsatishi.


UserModel2 va UserModel4: farqi nimada?

UserModel4ning UserModel2ga nisbatan afzalligi shunda: UserModel2da biz isPremium qiymatini struct'dan tashqarida turib o'zgartirar edik — bunga kodimizning istalgan joyidan turib amalga oshirishimiz mumkin, ammo bu juda tartibsizlikka olib kelishi mumkin, chunki endi ilovangizda UserModel2ning qiymatini tahrirlayotgan ko'plab joylar paydo bo'lishi mumkin. UserModel4da esa, bu qiymat faqat shu model'ning o'zi ichidan yangilanadi.

Va bu qiymat faqat shu model ichidan yangilanishi sababli, biz buni private(set) var qilib belgilashimiz mumkin. (Kelajakda private va private set nimani anglatishi haqida alohida video qilaman, ammo hozircha shuni bilingki, bu shu qiymatni faqat shu yerning ichidan o'rnatishimiz mumkinligini bildiradi.)

struct UserModel4 {
    let name: String
    private(set) var isPremium: Bool

    mutating func markUserAsPremium() {
        isPremium = true
    }

    mutating func updateIsPremium(newValue: Bool) {
        isPremium = newValue
    }
}

Demak, agar user4ni yaratsam, men user4.markUserAsPremium()ni chaqirishim mumkin, user4.updateIsPremium(newValue: true)ni ham chaqirishim mumkin — ammo user4.isPremium = true deb to'g'ridan-to'g'ri yoza olmayman. UserModel2da qilganimiz usulni endi bu yerda qila olmaymiz, chunki setter (qiymat o'rnatuvchi) endi struct'ning ichiga maxfiy (private). Men shu qiymatni olishim (get) mumkin, ammo struct'dan tashqarida turib uni o'rnatishim (set) mumkin emas:

var user4 = UserModel4(name: "Nick", isPremium: false)

user4.markUserAsPremium()
user4.updateIsPremium(newValue: true)

let newValue = user4.isPremium   // qiymatni o'qish mumkin
user4.isPremium = true           // xatolik: setter struct ichiga maxfiy

Bu — qabul qilish uchun ancha ko'p narsa edi. Struct'lar bilan ishlash, albatta, birozroq murakkab, ammo struct'ni qanday mutatsiya qilishni bilishingiz kerak.


Qaysi usulni tanlash kerak?

Aytishim kerakki, agar mening kanalimni kuzatib borsangiz, boshlang'ich darajadagi kurslarimning ko'pchiligida men aynan UserModel3 uslubidan foydalanaman — ya'ni men struct'ni immutable holatda saqlayman, va uni tahrirlamoqchi bo'lganimda, xuddi shu ma'lumotlar bilan, faqat o'zgartirilayotgan qiymat o'zgargan holda, struct'ning yangi versiyasini yarataman. Men kod yozishni o'rganganimda, meni aynan shunday o'rgatishgan edi.

Bu, ehtimol, boshlang'ichlar uchun eng yaxshi yo'ldir, chunki bu juda aniq (explicit): siz bilasizki, bu shunchaki shu struct'ning yangi versiyasini qaytaradigan funksiya, xolos. Demak, mutatsiya qanday ishlashi haqida adashib qolsangiz ham, shuni bilasiz: bu funksiyani ishga tushirganingizda, sizga shunchaki yangilangan yangi ma'lumot modeli qaytadi. Boshlang'ich darajadagi ilovalar bilan ishlaganda, bu usul mutatsiya atrofidagi ko'plab tushunmovchilik va asabiylashishlarning oldini oladi, deb o'ylayman.

Ammo, ancha murakkabroq ilovada, men, ehtimol, UserModel4 uslubiga, ya'ni haqiqiy mutatsiyaga ko'proq moyil bo'lardim — buning sababi, struct'ni butunlay yangidan yaratish o'rniga mutatsiya qilishning ozgina texnik afzalliklari bor. Ammo bu afzalliklar, siz hali kod yozishni o'rganayotgan bo'lsangiz, sizni chalkashtirib yuborishga arzimaydi. Buni qilishda chinakam mohir bo'lganingizdan keyin esa, kodingizni shu uslubga tomon siljitishni boshlashingiz mumkin.


Struct — tuple'larga ajoyib muqobil

Endi siz struct'lardan qanday foydalanishni bilasiz, va biz shu struct'lar ichiga xohlagancha ma'lumot bo'lagini joylashtirib, so'ngra ularni ilovamiz bo'ylab uzatishimiz mumkin. Demak, bu, asosan, tuple'lardan foydalanishga ajoyib muqobildir. Bir-ikki video oldin sizlarga name, isPremium va isNewdan iborat tuple qanday yasalishini ko'rsatgan edim — endi esa biz shu barcha ma'lumotni o'z ichiga olgan bitta struct yaratishimiz mumkin:

struct User5 {
    let name: String
    let isPremium: Bool
    let isNew: Bool
}

Va shu tarzda struct'ga xohlagancha ko'p ma'lumot qo'shishda davom etishimiz mumkin. Endi esa, buni ilovamiz bo'ylab uzatganimizda, biz shunchaki bitta User5 qiymatini uzatamiz — bizga endi chalkash bo'lib ketadigan uzundan-uzoq tuple kerak emas.


Tomosha qilganingiz uchun rahmat, har doimgidek! Siz mohir iOS dasturchi bo'lish sari yo'l olmoqdasiz. Men — Nik, bu Swiftful Thinking, agar tayyor bo'lsangiz, keyingi videoda ko'rishamiz!

Buy mea coffee