آموزش سوئیفت (Swift): آشنایی با Getter و Setter — بخش شانزدهم
در بخش قبلی این سری مقالات آموزش زبان برنامهنویسی سوئیفت با کاربرد ژنریکها به همراه بستار و Enum آشنا شدیم. در این بخش قصد داریم از همه این مباحث جدا شویم و در مورد چند موضوع صحبت کنیم که موجب میشوند کد سوئیفت کارایی بیشتری پیدا کند. بدین ترتیب قصد آشنایی با Getter و Setter ،inout و lazy را داریم. برای مطالعه بخش قبلی این مجموعه مطلب آموزشی به لینک زیر رجوع کنید:
inout
Inout کلیدواژهای است که وقتی استفاده میشود که پارامترهایی به تابعها ارسال میشوند. در واقع inout زمانی مورد استفاده قرار میگیرد که بخواهیم یک متغیر را به یک تابع ارسال کنیم و مقدار آن متغیر را بدون ایجاد متغیر جدید تغییر دهیم.
در کد زیر با روش تغییر یک مقدار با و بدون inout آشنا میشویم:
1// Without inout
2var number = 10
3func multiply(number: Int, by multiplier: Int) -> Int {
4 return number * multiplier
5}
6
7number = multiply(number: number, by: 2) // number is assigned 20
8print(number) // prints 20
9
10// With inout
11var secondNumber = 5
12
13func multiply(number: inout Int, by: multiplier) {
14 number *= multiplier
15}
16
17multiply(number: &secondNumber, by: 2)
18// passed in a reference to secondNumber using &
19print(number) // prints 10
هنگامی که یک تابع استاندارد بدون استفاده inout ایجاد شود، متغیر ارسالی «تغییرناپذیر» (immutable) است و امکان اصلاح آن وجود نخواهد داشت. به بیان دیگر به صورت یک ثابت ارسال میشود. کلیدواژه inout امکان تغییر دادن متغیر ارسالی را میدهد، زیرا با ارجاع ارسال شده است و نه با مقدار، چرا که در این صورت باید یک & در ابتدای آن وجود میداشت. اگر میخواهید در این رابطه بیشتر بدانید به مطلب زیر رجوع کنید:
در مثال فوق هنگامی که از یک تابع بدون inout استفاده کنیم، از آنجا که number به (:multiply(number:by ارسال شده است، در واقع به صورت number ارسال نشده است بلکه مقدار کنونی number که 10 است ارسال شده است. آن را میتوان به صورت زیر فراخوانی کرد:
1number = multiply(number: 10، by: 2)
اگر به تابعی که از inout استفاده میکند نگاه کنیم، میبینیم که سه تغییر رخ داده است، نخستین تغییر این است که هیچ نوع بازگشتی وجود ندارد. دوم این که از کلیدواژه در کنار نوع پارامتر استفاده کردهایم (inout Int). تغییر سوم این است که هیچ گزاره return در بدنه تابع ما وجود ندارد.
زمانی که تابع inout را فراخوانی میکنیم مجبور نیستیم که یک مقدار بازگشتی انتساب دهیم، زیرا هیچ مقداری بازگشت نمییابد. به جای آن زمانی که تابع inout را فراخوانی میکنیم، در واقع مکان متغیر را در حافظه ارسال میکنیم. برای این که موضوع روشنتر شود، باید بگوییم که وقتی از &secondNumber استفاده میکنیم، secondNumber به آدرس حافظه 0x01 انتساب مییابد. این وضعیت در عمل به صورت زیر ترجمه میشود:
1multiply(number: 0x01، by: 2)
البته نباید سردرگم شوید، چون وقتی به آدرس 0x01 نگاه میکنیم تا مقدار مورد نظر را ببینیم، همچنان مقدار «عدد دوم» (secondNumber) را میبینیم که 5 است.
درون تابع inout همه چیز به طرز متفاوتی عمل میکند. ما از number *= multiplier برای تغییر مقدار ذخیره شده در آدرس secondNumber استفاده میکنیم، زیرا مقدار را مستقیماً تغییر میدهیم و مقدار تغییر یافته در هر جایی در برنامه که ارجاعی به secondNumber صورت گرفته باشد، اعمال خواهد شد. جنبه مثبت این وضعیت آن است که مصرف حافظه کمی دارد و باید صرفاً نگران این متغیر که شامل مقدار number است نگران باشید.
جنبه منفی این رویکرد برای استفاده از inout آن است که همه انواع ارجاع را ناممکن میسازد. اگر secondNumber را در جایی از برنامه که ارجاع یافته تغییر دهید، ممکن است نخواهید در همه جاهای دیگر مقدار آن تغییر پیدا کند.
به عنوان مثال عملیتر، اگر بخواهیم یک مقدار را در userData فوقالذکر ارسال کنیم که از نوع Data است و شامل برخی اطلاعات باشد که لازم باشد در جای دیگری در اپلیکیشن به آن ارجاع دهیم، آن را به صورت یک پارامتر inout به یک تابع ارسال میکنیم و بدین ترتیب دادهها تغییر مییابند و دادههای اولیه نیز همچنان در موارد نیاز در دسترس خواهند بود. اگر بعدها به userData مراجعه کنیم و انتظار داشته باشیم که همان مقدار را داشته باشد ممکن است متوجه شویم که تغییر یافته است. بهترین حالت این است که اپلیکیشن تغییر را مدیریت کند و موارد مقتضی را بر همین مبنا اجرا کند. بدترین حالت این است که اپلیکیشن به دلیل تهی بودن یک مقدار یا این که نوع داده ذخیره شده در مکان حافظه تغییر یافته است، از کار بیفتد.
استفاده کردن یا نکردن از inout تصمیم شخصی شما است. هر چند این پارامتر گزینه کاملاً امنی محسوب نمیشود، اما بدان معنی نیست که هرگز نباید از آن استفاده کرد. این پارامتر در مواردی که محاسباتی را اجرا میکنید و نمیخواهید به طور پیوسته نتیجه برخی تابعها را هر بار که یک تابع را در محاسبات خود اجرا میکنید به currentResult انتساب دهید، عالی خواهد بود.
Lazy
هنگامی که یک کلاس را ایجاد میکنیم، تقریباً همواره مشخصههایی میسازیم که از سوی آن کلاس استفاده میشود. این مشخصهها میتوانند صرفاً یک فلگ باشند که روشن یا خاموش میشوند تا حالت کنونی کلاس را تعیین کنند و یا میتوانند چیزی بزرگتر مانند یک کلاس دیگر باشند که این کلاس برای اجرای برخی کارها از آن بهره میگیرد. به مثال زیر توجه کنید:
1// CLLocationCoordinate2d is a structured way of using two Doubles
2class myClass {
3 var coordinates: CLLocationCoordinate2d
4 var mapView: MKMapView
5
6 init(coordinates: CLLocationCoordinate2d) {
7 self.coordinates = coordinates
8 self.mapView = MKMapView()
9 }
10
11 func getLocation(of coordinates: CLLocationCoordinate2d {
12 mapView.setCenter(coordinates, animated: true)
13 }
14}
این مثال چیز بزرگی به نظر نمیرسد، با استفاده از این کلاس در واقع از یک CLLocationCoordinate2d برای نمایش مکانی روی نقشه استفاده میکنیم.
اگر بخواهید بدانید CLLocationCoordinate2d چیست، باید بگوییم که یک struct شامل طول و عرض جغرافیایی مکان به همراه برخی متدهای ساده است. هم طول و هم عرض جغرافیایی به صورت CLLocationDegrees هستند که صرفاً یک «نوع مستعار» (typealias) برای این نوع Double محسوب میشود. به بیان سادهتر CLLocationCoordinate2d یک روش برای ارائه دو مقدار Double است که مکانی را روی نقشه تعیین میکنند و در مجموع بسیار سبک است.
در سوی دیگر MKMapView، حافظه زیادی اشغال میکند. فقط بارگذاری یک نقشه و بزرگنمایی به یک مختصات باعث مصرف چندین مگابایت از حافظه میشود. زمانی که از یک نقشه در برنامههای خود استفاده میکنیم، تقریباً 2000 annotation بارگذاری میشود و هنگامی که کمی در نقشه بگردیم مصرف حافظه تا 430 مگابایت افزایش پیدا میکند. پس چنان که میبینید نماهای نقشه تا حدودی پرهزینه هستند. همان طور که حدس میزنید این نمای نقشه همان نمایی است که هنگام باز کردن اپلیکیشن Maps در گوشی خود مشاهده میکنید.
خبر خوب این است که iPhone-ها و iPad-ها امروزه چندین گیگابایت حافظه دارند و لذا این مسئله چندان بزرگ به حساب نمیآید، اما با این حال همچنان میتوان این وضعیت را بهینهسازی کرد. این همان جایی است که مشخصههای با ذخیرهسازی Lazy به کار میآیند.
سناریویی را تصور کنید که یک «نما» (view) در اپلیکیشن خود داریم و این نما میتواند یک نقشه را نمایش دهد یا ندهد. اگر نقشه را نمایش ندهد قطعاً دوست نداریم صدها مگابایت داده را در حافظه بارگذاری کنیم، اما همچنان میخواهیم که بتوانیم در صورت نیاز نقشه را در اپلیکیشن خود و همچنین در تابعهایی دیگری که نما را مالکیت میکنند داشته باشیم. در مثال زیر طرز کار این رویکرد را میتوانید ملاحظه کنید:
1class myViewController: UIViewController {
2 var coordinates: CLLocationCoordinate2d?
3 lazy var mapView = MKMapView()
4 var view: UIView!
5
6 // do stuff
7
8 @IBAction func showMapTapped(_ sender: UIButton) {
9 createView()
10 view.addSubview(mapView)
11 guard let coordinates = self.coordinates else { return }
12 mapView.setCenter(coordinates, animated: true)
13 }
14}
کلیدواژه lazy در ابتدای ()var mapView = MKMapView جایی است که بخش اصلی داستان اتفاق میافتند. این کلیدواژه به برنامه اعلام میکند که آماده شود چون ممکن است mapView در این نما استفاده شود. زمانی که زمان استفاده از نمای نقشه فرا برسد، کد ایجاد آن به صورت فوق خواهد بود.
در ادامه کد میبینیم که وقتی کاربر روی یک دکمه برای نمایش نقشه ضربه بزند، ()createView را فراخوانی میکنیم. این متد شامل منطقی است که در پشت صحنه نوشتهایم تا نمایی را که نقشه را نمایش میدهد به نمای جاری اضافه کنیم. زمانی که از (view.addSubview(mapView استفاده میکنیم، کد mapView فراخوانی میشود که mapView را ایجاد میکند و در صورت نیاز میتوانیم تابعها را روی mapView فراخوانی کنیم.
اگر این نما ایجاد نشده باشد و یک تابع را روی mapView فراخوانی کنیم، mapView در آن زمان ایجاد خواهد شد. بنابراین Lazy اساساً ایجاد mapView را تا زمانی که واقعاً ضروری باشد به تعویق میاندازد. مشخصههای Lazy میتوانند به صورت «بستار» (closure) ها نیز باشند. در واقع این حالتی است که عموماً مورد استفاده قرار میگیرند و بدین ترتیب از محاسبات اضافی تا زمانی که واقعاً ضروری نباشد جلوگیری میکنند. این حالت را در پشته Core Data به طور مکرر مشاهده میکنید. با این حال اگر از چیزی سر در نیاوردید لازم نیست، نگران باشید، چون فعلاً روی lazy تمرکز داریم.
1lazy var persistentContainer: NSPersistentContainer = {
2 let container = NSPersistentContainer(name: "DataModel")
3
4 container.loadPersistentStores(completionHandler: {
5 (storeDescription, error) in
6 if let error = error as NSError? {
7 fatalError("error.localizedDescription")
8 }
9 })
10
11 return container
12}()
کانتینرهای دائمی ممکن است لازم باشند یا نباشند؛ اما لازم نیست آنها را از همان ابتدا مستقیماً ایجاد کنیم. به جای آن صبر میکنیم تا زمانی فرا رسد که قبل از ایجاد کردن آن، عملاً لازم باشد که دادهها را در پایگاه داده ذخیره یا از آن بارگذاری کنیم. بدین منظور از یک closure استفاده میکنیم که کانتینر دائمی را با کمترین مراحل مورد نیاز برای ایجاد کانتینر ایجاد کند.
استفاده از Lazy زیبا است و غالباً باید در جاهایی استفاده شود که مفید باشد. نباید نگران باشید که رویههای ساده با استفاده از Lazy پیچیده میشوند، چون در هر صورت امکان Lazy ساختن ثابتها وجود ندارد. اگر تلاش کنید یک ثابت را به صورت Lazy تعریف کنید، Xcode شما را مأیوس خواهد کرد.
Getter و Setter
Getter-ها و Setter-ها بخشی از «مشخصههای محاسبه شده» (Computed Properties) هستند. آنها خویشاوند نزدیک مشاهدهگرهای مشخصه به نام didSet و willSet محسوب میشوند. چنان که احتمالاً به خاطر دارید didSet و willSet جهت اجرای وظایف اضافی در زمان تغییر یافتن یک مشخصه محاسبه شده استفاده میشوند. Getter-ها و Setter-ها منطقی در اختیار ما قرار میدهند که میتوانیم برای تعیین یک مقدار یا بازیابی آن مورد استفاده قرار دهیم. به مثال زیر توجه کنید:
1var number: Int { get set }
2var gettableNumber: Int { get }
در مثال فوق، number پیادهسازی پیشفرض get و set را ارائه میکند که در صورت عدم اضافه شدن { get set } به انتها میتوانستیم داشته باشیم. تنها دلیل استفاده از آنها روشنتر شدن موضوع بوده است. حفظ انسجام کد همواره خوب است و اگر مشخصهای دارید که تنها get دارد، در این صورت بهتر است { get set } را روی مشخصههایی اضافه کنید که قابلیت get و set داشته باشند.
زمانی که تنها از { get } استفاده میکنیم در واقع صرفاً امکان بازیابی مقدار را داریم و نمیتوانیم مقدار را تعیین کنیم. این وضعیت مشابه یک ثابت است، گرچه عموماً محاسبه مشخصههای دیگر را نیز بازگشت میدهد. در ادامه چند مثال را میبینید که در آنها میتوان از { get } استفاده کرد.
1struct Employee {
2 var hourlyRate: Double
3 var hoursWorked: Double
4 // computed property using { get }
5 var earnings: Int {
6 get {
7 return hourlyRate * hoursWorked
8 }
9 }
10}
در مثال فوق ما یک نرخ ساعتی و همچنین تعداد ساعتهای کارکرد را داریم که با استفاده از دستورهای زیر قابل تعیین هستند:
1myEmployee.hourlyRate = 9.75
2myEmployee.hoursWorked = 40
سپس میتوانیم دریافتی کارمند را با استفاده از دستور زیر به دست آوریم:
1let wagesEarned = myEmployee.earnings
در ادامه مثالی پیچیدهتر را بررسی میکنیم.
1struct Employee {
2 var hourlyRate: Double = 0
3 var hoursWorked: Double = 0
4
5
6 // computed property with public get and private set
7 private(set) var earnings: Double {
8 get { return earnings }
9 set { hourlyRate = earnings / hoursWorked }
10 }
11 init(hourlyRate: Double, hoursWorked: Double) {
12 self.hourlyRate = hourlyRate
13 self.hoursWorked = hoursWorked
14 }
15
16
17 func updateEarnings(to amount: Double, hours: Double) {
18 guard amount >= 0 else { return }
19 guard hours > 0 else { return }
20 self.earnings = amount
21 self.hoursWorked = hours
22 }
23}
در کد فوق چند فاصله اضافی درج کردهایم تا خوانایی بهتری داشته باشد. در این مثال یک setter خصوصی در ابتدای (private(set داریم. بدین ترتیب مطمئن میشویم که مقدار صحیحی تعیین شده است. ما هرگز یک «دریافتی» (earnings) منفی یا 0 نخواهیم داشت، بنابراین میتوانیم مطمئن باشیم که earnings مقدار مثبتی دارد و این که قبل از تعیین مقدار واقعی earnings و hoursWorked مقداری کارکرد داشتهایم. سپس از setter مربوط به earnings برای بهروزرسانی مقدار hourlyRate استفاده میکنیم.