آموزش برنامه نویسی سوئیفت (Swift): اسامی مستعار نوع (Type Aliases) —‌ بخش دوازدهم

۸۰ بازدید
آخرین به‌روزرسانی: ۰۹ مهر ۱۴۰۲
زمان مطالعه: ۱۱ دقیقه
آموزش برنامه نویسی سوئیفت (Swift): اسامی مستعار نوع (Type Aliases) —‌ بخش دوازدهم

در بخش قبلی این سری مقالات آموزش سوئیفت با مفاهیم بستار و Grand Central Dispatch آشنا شدیم. با این که این مفاهیم تا حدودی دشوار بودند، اما نکته خوب ماجرا این است که اکنون بخش دشوار را پشت سر گذاشته‌ایم. در این مقاله به معرفی مفاهیم جدیدی مانند اسامی مستعار نوع می‌پردازیم که به خواناتر ساختن کد و کاهش اندازه کد کمک می‌کنند. همچنین با تفاوت Self و self به جز کوچک/بزرگ بودن حرف اول آشنا می‌شویم.

اسامی مستعار نوع

اسامی مستعار نوع کمک می‌کنند که کدهای خود را خواناتر بنویسیم و وظایف روزمره‌مان به عنوان یک برنامه‌نویس ساده‌تر شود. در زبان‌های برنامه‌نویسی دیگر این مفهوم ممکن است به صورت alias یا typedef باشد؛ اما در سوئیفت از typealias استفاده می‌کنیم.

یک typealias در واقع تغییر نامی برای یک نوع است به طوری که درک یا استفاده از آن ساده‌تر باشد. شما در کد خود می‌توانید از typealias برای دریافت یک نوع خام و تغییر دادن نام آن به چیز دیگر استفاده کنید تا معنی بهتر انتقال یابد. به مثال زیر توجه کنید:

1struct Book {
2    let pageContents: [Int: String]
3    var currentPage: Int
4  
5    func getContents(on page: Int) -> String {
6         return pageContents[page]
7    }
8}

خواندن این کد چندان دشوار نیست؛ پیگیری مواردی که در این کد ارائه شده‌اند کاملاً آسان است؛ اما در نگاه نخست ممکن است تعجب کنید که pageContents چیست و چرا نوع آن به صورت [Int:String] است. چرا نوع آن صرفاً String نیست؟ چون ما صرفاً به محتوای صفحه اشاره می‌کنیم؟ پاسخ این است که کتاب‌ها چندین صفحه دارند و باید متوجه شویم که آیا در صفحه صحیحی قرار داریم یا نه. به همین دلیل است که یک دیکشنری به نام pageContents و با نوع [Int: String] می‌سازیم.

اما نکته مهم‌تر این است که آیا ما در مورد استفاده از [Int: String] پس از این نتیجه‌گیری تأمل می‌کنیم یا این که ممکن است به تازگی با این پروژه آشنا شده باشیم و برای اولین بار آن را می‌بینیم. این همان جایی است که اسامی مستعار نوع به کار می‌آیند. کد فوق را با استفاده از اسامی مستعار نوع به صورت زیر بازنویسی می‌کنیم:

1typealias index = Int
2typealias content = String
3typealias page = [index: content]
4
5struct Book {
6    let pageContents: page
7    var currentPage: index
8  
9    func getContents(on page: index) -> content {
10         return pageContents[index]
11    }
12}

بدین ترتیب همه نوع‌های استاندارد را که در این چارچوب معنی‌دار هستند جایگزین می‌کنیم. در پس‌زمینه طرز کار کد دقیقاً همانند کد قبلی است؛ اما از منظر ما اینک همه آن‌ها در یک چارچوب قرار گرفته‌اند.

JSON

چیزی که باید در مورد اسامی مستعار نوع دقیقاً همانند متغیرها بدانیم، این است که آن‌ها نیز تحت سلطه «دامنه» (Scope) قرار دارند. اگر یک نام مستعار نوع را در یک کلاس یا struct تعریف کنید نمی‌توانید خارج از آن کلاس یا struct از آن استفاده کنید، چون با خطایی مواجه می‌شوید که Xcode اعلام می‌کند در مورد typealiasName اطلاع ندارد.

بنابراین مثالی دیگری را با استفاده از مفهومی نسبتاً فنی‌تر ارائه می‌کنیم و به توضیح JSON می‌پردازیم. JSON اختصاری برای عبارت «نمادگذاری شیء جاوا اسکریپت» (JavaScript Object Notation) است. البته نباید نگران شوید، زیرا قصد نداریم در مورد جاوا اسکریپت صحبت کنیم و صرفاً مقدمه‌ای در مورد JSON ارائه خواهیم کرد.

JSON به طور مقدماتی در وب‌سرویس‌ها برای ارسال داده‌ها بین کلاینت‌ها و سرورها مورد استفاده قرار می‌گیرد و هدف اصلی آن ذخیره‌سازی داده‌ها روی یک دستگاه محلی به روشی ساده برای تحلیل و نگه‌داری است. JSON به طور معمول مانند زیر است:

1{
2    "breakfast": {
3        "item1": "eggs",
4        "item2": "bacon",
5        "item3": "toast"
6    }
7}
8
9// all one line
10{"breakfast":{"item1":"eggs","item2":"bacon","item3":"toast"}}

اگر آن را به زبان سوئیفت ترجمه کنیم به شکل زیر در می‌آید:

1["breakfast":["item1":"eggs","item2":"bacon","item3":"toast"]]
2
3//or
4[String: [String: String]]

دقت کنید که ما صرفاً آکولادها را با براکت عوض کرده‌ایم. ما این نوع جدید را به نام [String: Any] می‌شناسیم و هر سطح از آن را به هر شیئی که نیاز باشد تبدیل می‌کنیم. برای درک بهتر به مثال زیر توجه کنید:

1let booksJSON = data as! [String: Any]
2
3let book = booksJSON["Dr. No"] as! [String: Any]
4
5let characters = book["Characters"] as [String: Any]
6
7let jamesBond = characters["MainCharacter"] as! String
8
9let sideKicks = characters["SideKick"] as! [String]
10
11let honey = sideKicks[0] as! String
12
13let quarrel = book["Characters"]["SideKick"][1] as! String

برای نمونه خود JSON چیزی مانند زیر است:

1{
2    "Dr. No": {
3        "Characters": {
4            "MainCharacter": "James Bond",
5            "SideKick": ["Quarrel", "Honeychile Rider"],
6            "Villian": "Dr. No",
7            "Henchmen": ["Professor R.J. Dent", ... ]
8        }
9    },
10    "Goldfinger": {
11        ...
12    }
13}

اینک اگر به مثال خود بازگردیم، می‌توانیم کد فوق را طوری بازسازی کنیم که دیگر چندان نیازی به [String: Any] نداشته باشیم.

1typealias jsonData = [String:Any]
2typealias book = [String: Any]
3typealias sideKickList = [String]
4typealias bookCharacter = String
5
6let bookJSON = data as! jsonData
7
8let characters = bookJSON["Characters"] as! jsonData
9
10let book = bookJSON["Dr. No"] as! book
11
12let jamesBond = characters["MainCharacter"] as! bookCharacter
13
14let sideKicks = characters["SideKick"] as! sideKickList
15
16let quarrel = sideKicks[0] as! bookCharacter
17
18let honey = sideKicks[1] as! bookCharacter

JSON و String

همان طور که می‌بینید این روش معنی بسیار بیشتر به کد بخشیده است. حتی زمانی که تلاش می‌کنیم خودمان JSON را تحلیل کنیم، می‌توانیم این نوع را بر مبنای آن اسم مستعار نوع که دارد استنباط کنیم. اگر به شما گفته شود که قرار است داده‌های JSON در اختیار شما قرار گیرد، شما قطعاً انتظار دارید که [String: Any] یا دست‌کم چیزی که با یک رشته آغاز می‌شود و می‌تواند چندین نوع داده نگه‌داری کند، دریافت کنید. زمانی که یک حرف از یک کتاب را می‌خواهیم، باید انتظار دریافت یک String را داشته باشیم. اگر به شما گفته شود که قرار ست یک لیست دریافت کنید باید انتظار یک آرایه یا چیز مشابهی را داشته باشید. در این مورد لیستی از موارد در اختیار شما قرار می‌گیرد و لذا باید انتشار یک آرایه از رشته‌ها را به صورت [String] داشته باشید.

اسامی مستعار نوع امکان جالبی هستند و قطعاً به افزایش خوانایی کمک می‌کنند، اما باید مواظب باشید در استفاده از آن‌ها زیاده‌روی نکنید. مثلاً در مثال اخیر ممکن است کمی زیاده‌روی کرده باشیم، اما به طور معمول از آن در دیکشنری‌هایی مانند [String: Any] یا انواع بستارهایی مانند زیر استفاده می‌شود:

 typealias completion = () -> ()

که در آن‌ها می‌توانید completion را به یک تابع بستار بفرستید و دیگر نیازی به استفاده مکرر از () <- () وجود ندارد.

اسامی مستعار نوع

Property Observers

«مشاهده‌گرهای مشخصه» (Property Observers) نیز ویژگی جالبی هستند، زیرا به خودکارسازی برخی اموری که باید در زمان به‌روز شدن متغیرها مداوماً رخ دهند کمک می‌کنند.

کار Property Observer اساساً این است که تغییرات یک مشخصه را به نظاره می‌نشیند و دو شکل دارد:

  • willSet – این نوع پیش از تنظیم تغییرات در متغیر اجرا می‌شود.
  • didSet  – اقداماتی را پس از تغییر یافتن متغیر انجام می‌دهد.

شما به ندرت ممکن است بخواهید از willSet استفاده کنید و همواره از didSet استفاده خواهید کرد. مشاهده‌گرهای مشخصه به صورت زیر در کد جای می‌گیرند:

1class StockMarket {
2    let wallet = 0 {
3       willSet {
4           // do stuff before score is set
5       }
6       didSet {
7           // do stuff after score is set
8       }
9    }
10}

همان طور که می‌بینید این ویژگی بسیار جالبی محسوب می‌شود؛ اما شاید بپرسید کاربردهای آن چیست؟ ما کلاس خود را StockMarket نامگذاری کرده‌ایم، زیرا برای استفاده از هر دو نوع ویژگی مشاهده‌گرهای مشخصه برای ما معنی‌دار خواهد بود. همچنین این وضعیت را فرض گرفته‌ایم که متدهایی در ادامه وجود دارند، زیرا در حال حاضر برای ما حائز اهمیت نیستند.

1class StockMarket {
2    typealias name = String
3    typealias amount = Int
4  
5    var stocksBought = [name: amount]
6    var wallet = 100.0 {
7        // value is about to change
8        willSet {
9            updateBuyingPower(to: newValue)
10        }
11        // value did change
12        didSet {
13            getStock()
14            print("You spent $\(oldValue - wallet), you now have
15                    $\(wallet) left.")
16        }
17    }
18      
19    func buy(_ stock: Stock) {
20       wallet -= stock.price
21    }
22}

با این که کد ممکن است پیچیده به نظر بیاید؛ اما با توضیحی که در ادامه می‌دهیم برای ما روشن‌تر خواهد شد. ابتدا چند اسامی مستعار نوع داریم زیرا می‌خواهیم نشان دهیم که مثال قبلی [String: Int] به چه معنی است؛ اما [name: amount] بر روشنی آن چه که برای آن استفاده شده افزوده است.

مثال عملی

سپس عملاً از یک مشاهده‌گر مشخصه به نام wallet استفاده می‌کنیم. در ابتدا مقدار آن را برابر با 1000 دلار تعیین می‌کنیم. سپس به willSet می‌رسیم.

willSet متغیری را در اختیار ما قرار می‌دهد که newValue نام دارد. این همان مقداری است که برای wallet تعیین می‌کنیم. در این مورد از آن برای updateBuyingPower استفاده می‌کنیم. این روش همانند هر فروشگاهی است که ابتدا پول را دریافت می‌کند و سپس کالا را تحویل می‌دهد.

در ادامه نوبت به مشاهده‌گر مشخصه didSet می‌رسد. مانند مشاهده‌گر willSet متغیری به نام oldValue داریم. این همان مقداری است که wallet قبلاً برای خود داشته است. بدین ترتیب اگر چیزی در getStock در مشاهده‌گر مشخصه اشتباه شود می‌توانیم wallet را دوباره به مقدار قبلی تعیین کنیم.

در نهایت از چیزی به نام «میان‌یابی رشته» (string interpolation) استفاده کرده‌ایم که امکان استفاده از رشته‌ای را به ما می‌دهد که متغیرهایی درونش جای داده شده‌اند. شما می‌توانید از میان‌یابی رشته‌ای در یک رشته با استفاده از ()\ و جای دادن کد خود درون رشته استفاده کنید. این روش خلاصه‌ای برای جایگزینی firstName + " " + lastName است. به جای آن می‌توانیم از کد زیر استفاده کنیم.

"\(firstName) \(lastName)"

با این که در این مثال روش جدید کمی طولانی‌تر به نظر می‌رسد؛ اما در مثال فوق کاربرد مناسبی دارد. در آن مثال، ما حتی اندکی محاسبات روی مقدار قدیمی کیف پول نیز انجام داده‌ایم و مقدار کنونی کیف پول را محاسبه کرده‌ایم تا تفاضل را حساب کرده و آن را به کاربر گزارش کنیم.

چالش جدی

بنابراین در اینجا یک چالش برای شما وجود دارد، کدی که در بخش فوق نوشتیم، یک خطا دارد.

کاربران ما تنها می‌توانند سهام بخرند و هرگز نمی‌توانند پول خود را باز پس بگیرند. بررسی کنید که آیا می‌توانید آن را به روش خود حل کنید و به کاربران امکان بدهید که بتوانند سهام خود را به فروش نیز برسانند؟

دقت کنید که پیش‌تر اشاره کردیم که شما می‌توانید به مقدار قبلی بازگردید. تنها مشکل این است که اگر بخواهیم دقیقاً به مقداری که قبلاً داشتیم بازگردیم وارد حلقه نامتناهی خواهیم شد. بنابراین بررسی کنید آیا می‌توانید به مقدار قبلی بازگردید و در صورتی که اشکالی در فرایند خرید رخ دهد، وارد حلقه نامتناهی نشوید.

توجه داشته باشید که در سوئیفت زمانی که وارد حلقه نامتناهی شوید، در صورتی که امکان توقف برنامه وجود داشته باشد هیچ آسیبی وارد نمی‌شود و از این رو این مثال بهترین مکان برای تمرین این وضعیت است.

هشدار: هر چه حلقه نامتناهی زمان بیشتری اجرا شود، متوقف کردن آن دشوارتر می‌شود، زیرا رایانه به سختی می‌تواند دستور توقف برنامه را دریافت کند.

لزومی نیست که مشاهده‌گرهای مشخصه در مورد هر دو نوع willSet و didSet استفاده شوند. اگر تنها به یکی از آن‌ها نیاز دارید کافی است یکی را بنویسید. در ادامه مثالی از روش پیاده‌سازی یک برچسب امتیاز را در یک بازی می‌بینید:

1class myGame {
2     var scoreLabel = UILabel()
3     var score: Int = 0 {
4          didSet {
5               scoreLabel.text = "Score: \(score)"
6          }
7     }
8  
9     init() {
10         scoreLabel.text = "Score: 0"
11     }
12}

Self یا self

بین دو مورد Self و self تفاوت وجود دارد و تفاوت بزرگی هم هست، گرچه در ظاهر این طور به نظر نمی‌رسد.

self

در پاره‌ای موارد در کلاس‌ها و struct-ها مجبور هستیم به یک مشخصه آن نهاد اشاره کنیم و این کار از درون یک متد تحت مالکیت آن نهاد صورت می‌گیرد. در مواردی که این حالت وجود دارد باید از self کوچک استفاده کنیم. به مثال زیر توجه کنید:

1struct Kid {
2    var hasPillow = false
3  
4    func pickUpPillow() {
5        self.hasPillow = true
6    }
7  
8    func dropPillow() {
9        self.hasPillow = false
10    }
11}

اما در مورد مثال ما این حالت ضرورت ندارد. ما می‌توانیم به سادگی self را در این مثال حذف کنیم و همچنان کار می‌کند. با این وجود:

1class Kid {
2    var hasPillow: Bool
3  
4    init(hasPillow: hasPillow) {
5        self.hasPillow = hasPillow
6    }
7  
8    func pickUpPillow() {
9        self.hasPillow = true
10    }
11  
12    func dropPillow() {
13        self.hasPillow = false
14    }
15}

initializer

زمانی که مجبور هستیم یک initializer ایجاد کنیم، ساختار نرمالی که باید استفاده شود همان نام مشخصه است. این روش به حفظ ساختار در مواردی که یک کلاس kid جدید در کد خود ایجاد می‌کنید کمک می‌کند. اما زمانی که کد مقداردهی اولیه را نگاه می‌کنید، در صورتی که کدی مانند زیر بنویسید چندان معنادار نخواهد بود:

1hasPillow = hasPillow

این چنین به نظر می‌رسد که گویا چیزی را به خودش انتساب می‌دهیم. این وضعیت ابهام‌برانگیز است. ابهام زمانی رخ می‌دهد که چیزی بدون هیچ زمینه‌ای بتواند چندین معنی مختلف داشته باشد. با افزودن self این ابهام رفع می‌شود.

1self.hasPillow = hasPillow

اینک می‌بینیم که می‌خواهیم متغیر hasPillow را که در طی مقداردهی اولیه به مشخصه hasPillow مربوط به self ارسال شده است انتساب دهیم. self در این حالت به این وهله از کلاس اشاره می‌کند. همه بچه‌ها یک بالش دریافت نمی‌کنند؛ اما این kid خاص آن را دریافت خواهد کرد.

در موارد دیگر وقتی مشغول کار با کنترلرهای ویو هستید ممکن است بخواهید یک ویوی متفاوت را نمایش دهید در این حالت از کد زیر استفاده می‌کنیم:

1self.navigationController?.present(newViewController, animated: true, completion: nil)

در این کد ما اعلام می‌کنیم هر کنترلر ویویی که اینک فعال است، اگر یک navigationController (با علامت ? مشخص می‌شود) این را ارائه می‌کند می‌خواهیم آن را animated کنیم و در زمان completion: متد کنونی، هیچ کاری انجام نیابد (nil).

بنابراین استفاده از self می‌تواند شما را از زحمت وارد کردن عبارت AReallyLongViewControllerName معاف کند. ولی با وجود ویژگی autocompletion باز هم self ظاهر بسیار خواناتری دارد.

با این حال توجه داشته باشید که self چندان شبیه به همتای خود Self نیست.

Self

همان طور که self به کلاسی که در آن واقع شده اشاره می‌کند، Self نیز ارجاعی به آن ایجاد می‌کند؛ اما نوع شیئی که می‌خواهیم به آن اشاره کنیم متفاوت است. Self به سه روش استفاده می‌شود، از آنجا که ما تنها در مورد یکی از آن‌ها آموزش دیده‌ایم آن را با استفاده از پروتکل‌ها آموزش می‌دهیم.

پروتکل‌ها بسیار قدرتمند هستند و یا آشنایی با این خصوصیت آن‌ها قطعاً نظرتان در موردشان به کلی عوض می‌شود. پروتکل‌ها امکان تعریف کارکردهایی را به ما می‌دهند که می‌توانند از سوی هر چیزی که پروتکل را به کارمی گیرد مورد استفاده قرار گیرند؛ اما نکته مهم‌تر این است که می‌توانند در پیاده‌سازی خود دارای انعطاف‌پذیری زیادی باشند.

بنابراین می‌توانید از این اکستنشن پروتکل استفاده کنید:

1extension BinaryInteger {
2    func doubled() -> Self {
3       return self + self
4    }
5}

پروتکل BinaryInteger

BinaryInteger پروتکلی است که در سوئیفت عرضه شده است.

در کد فوق یک اکستنشن به وسیله تابع doubled() -> Self ارائه شده است. در اینجا برای ما مهم نیست که Self چیست و صرفاً می‌خواهیم بدانیم که تابع ما هر کاری که می‌کند، باید با انواع BinaryInteger سازگار باشد. در ادامه از کد زیر استفاده می‌کنیم:

1let number = 8
2let doubleNumber = number.doubled()
3print(doubleNumber)     //prints 16

نکته جالب در این خصوص آن است که می‌توانیم به صورت زیر عمل کنیم:

1let number = 8.doubled()
2print(number)//prints 16

آیا دلیل این وضعیت را می‌دانید؟ دلیل آن این است که نوع پیش‌فرض برای هر نوع عدد صحیح بدون هیچ 0. یا 1…9 از نوع Int است. Int با پروتکل BinaryInteger سازگاری دارد و برحسب ارتباط اکستنشن ما شامل تابع ()doubled نیز است.

همه این موارد به وسیله کلیدواژه Self ممکن شده‌اند. البته ما می‌توانیم از انواع دیگری Int مانند Int32 ،Int8 یا UInt64 و غیره نیز استفاده کنیم، چون همه آن‌ها BinaryIntegers هستند و همگی‌شان از این تابع جدید استفاده می‌کنند. زمانی که این تابع را فراخوانی می‌کنید، نوع آن استنباط می‌شود و هرجایی که Self استفاده شده باشد به وسیله نوعی که برای فراخوانی این تابع استفاده شده جایگزین می‌شود. بدین ترتیب self با مقدار ارسال شده جایگزین می‌شود.

اگر معنی همه این‌ها را متوجه نمی‌شوید جای نگرانی نیست، چون در مطلب بعدی در مورد Generic-ها صحبت خواهیم کرد و همه این‌ها معنی بیشتری برای شما خواهند یافت.

جمع‌بندی

در این مقاله در مورد اسامی مستعار نوع و شیوه‌ای که منجر به خواناتر شدن کد می‌شوند صحبت کردیم. در مورد مشاهده‌گرهای مشخصه و روشی که به مدیریت اجرای برخی وظایف می‌پردازند نیز صحبت کردیم. در نهایت به بحث تفاوت‌های Self و self پرداختیم. اطلاعات کمی در مورد self ارائه شده است؛ اما نباید ناامید شوید زیرا Self همان طور که در ادامه در بحث Generic-ها خواهید دید، مهم‌تر است.

به دلیل روش‌های مختلفی که می‌توان از Generic-ها استفاده کرد، ما مجبور هستیم محتوای زیادی را در مقاله بعدی خود داشته باشیم، با این حال نهایت تلاش خود را می‌کنیم که یک مقاله فشرده باشد. در هر صورت توصیه همیشگی ما به شما به عنوان یک فرد مبتدی در زمینه یادگیری زبان سوئیفت این است که هرگز تمرین کردن را فراموش نکنید.

برای مطالعه بخش بعدی این مجموعه مطلب آموزشی می‌توانید روی لینک زیر کلیک کنید:

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
swift2go
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *