آموزش برنامه نویسی سوئیفت (Swift): بستار و Grand Central Dispatch – بخش یازدهم

۱۰۱ بازدید
آخرین به‌روزرسانی: ۰۹ مهر ۱۴۰۲
زمان مطالعه: ۱۷ دقیقه
آموزش برنامه نویسی سوئیفت (Swift): بستار و Grand Central Dispatch – بخش یازدهم

در بخش قبلی این سری مطالب آموزش سوئیفت مجله فرادرس با ساختار کد، خوانایی و موارد دیگری آشنا شدیم. گرچه این مفاهیم چندان فنی محسوب نمی‌شوند؛ اما اگر به این مهارت‌ها مجهز باشید، پروژه‌هایتان مقیاس‌پذیر می‌شوند و می‌توانید در مورد روش‌های سازمان‌دهی کدها راحت‌تر فکر کنید. ساختار کد بین پروژه‌های مختلف متفاوت است، زیرا توسعه‌دهندگان به ندرت در مورد روش طرح‌بندی کدهای خودشان صحبت می‌کنند مگر این که شما عملاً وارد نخستین شغل برنامه‌نویسی خود شده باشید. حتی در این صورت نیز در اغلب موارد توسعه‌دهندگان توضیح زیادی نمی‌دهند و انتظار دارند که شما این مفاهیم را خودتان دریابید. در مواردی که بخواهید اپلیکیشن‌های خودتان را شخصاً بنویسید نیز آموزش‌های چندانی در این زمینه نخواهید داشت. در این نوشته قصد داریم به توضیح بستار و Grand Central Dispatch بپردازیم.

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

Grand Central Dispatch

Grand Central Dispatch روشی است که اپل برای مدیریت صف‌های dispatch مورد استفاده قرار می‌دهد. سه نوع صف وجود دارند:

صف سریال

در این روش کارهایی که به صف ارسال می‌شوند به همان ترتیبی که دریافت شده‌اند اجرا می‌شوند و آن را می‌توان نوعی رویه «ورودی اول، خروجی اول» (FIFO) انگاشت. این نوع صف به نام صف dispatch خصوصی نیز شناخته می‌شود.

صف همزمان

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

صف dispatch اصلی

این همان نخ اصلی اپلیکیشن یا همان جایی است که اپلیکیشن قرار دارد. زمانی که کد خود را درون ()viewDidLoad در یک کنترلر ویو قرار می‌دهید، این صف همه کارها را انجام می‌دهد.

اگر همه این مواردی که مطرح شد را کنار بگذاریم و بخواهیم توضیح دهیم که همه این صف‌ها در رابطه با سخت‌افزار چگونه عمل می‌کنند، می‌توانیم به تصویر زیر رجوع کنیم:

بستار

تعاریف مرتبط

در ادامه توضیحی در مورد شیوه چیدمان وظایف در زمان اضافه شدن به صف ارائه می‌کنیم. پیش از ادامه چند تعریف را ملاحظه کنید:

حلقه اجرا

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

نخ

به بیان ساده نخ به وظایف منفرد گفته می‌شود. اگر این وظیفه نیازمند روال خاص خود باشد، این وظیفه در نخ دیگری مورد پردازش قرار می‌گیرد (این مفهوم نباید با هسته CPU اشتباه گرفته شود).

CPU

مغز رایانه است و آن قطعه‌ای است که محاسبات را با یک یا چند هسته اجرا می‌کند.

هسته

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

CPU Clock Speed

سرعتی است که پردازنده اقدام به خواندن مقادیر 0 و 1 یا باینری می‌کند. اگر تاکنون کنجکاو بوده‌اید که منظور از 1 گیگاهرتز چیست، باید بگوییم که گیگا پیشوندی به معنی میلیارد است و از این رو 1 گیگاهرتز برای هر هسته به معنی این است که هر هسته CPU می‌تواند در ثانیه 1 میلیارد صفر و یک را بخواند. یک رایانه معمولی امروزه سرعت پردازنده 2.8 گیگاهرتز و عموماً 8 هسته دارد. این بدان معنی است که این رایانه می‌تواند 22.4 = 8 * 2.8 میلیارد یک و صفر را در هر ثانیه در سرعت ساعت پایه خود بخواند. اگر کنجکاو هستید که سرعت ساعت CPU چه ارتباطی با حلقه اجرا دارد، پاسخ این است که ساعت برخی تیک‌ها را اجرا می‌کند که هر تیک یک مقدار 1 یا صفر را می‌خواند.

چرخه دستورالعمل

در برخی موارد از اصطلاحی به نام چرخه استفاده می‌شود. منظور از چرخه‌های CPU مجموعه دستورالعمل‌هایی هستند که CPU آن‌ها را می‌خواند تا وظایفی را از طریق یک سری از تیک‌ها اجرا کند.

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

اگر آن را به یک صف سراسری (همزمان) ارسال کنیم، روی هسته موجود بعدی قرار می‌گیرد و به محض دریافت شدن مورد پردازش قرار می‌گیرد. در صورتیکه به یک صف خصوصی (سریال) ارسال کنیم، روی هسته بعدی موجود قرار می‌گیرد و به محض این که هسته بتواند همه تمرکز خود را روی این وظیفه قرار دهد، مورد پردازش قرار می‌گیرد. این شرایط معمولاً در طی چندین میلی‌ثانیه ایجاد می‌شود.

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

در سیستم‌های جدیدتر، این مشکل همچنان وجود دارد؛ اما با وجود 8 هسته که می‌توانند به اجرای وظایف بپردازند، مشکل چندان هویدا نیست.

با توجه به گفته‌های فوق وظیفه 1 و وظیفه 2 به طور همزمان روی دو هسته مجزا اجرا می‌شوند؛ با این حال چون ما تنها 3 هسته داریم، هر وظیفه بعدی باید منتظر بماند تا وظیفه جاری کار خود را پایان دهد.

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

وظیفه 1 و 3 را می‌توان به این صورت در نظر گرفت که به طور سریالی پشت سر هم اجرا می‌شوند. وظیفه 3 تا زمانی که وظیفه 1 پایان نیافته باشد، آغاز نمی‌شود. برای نمونه وظیفه 4 می‌تواند از سوی وظیفه 3 ایجاد شده باشد تا کاری را همزمان با پایان یافتن وظیفه 3 اجرا کند، مثلاً داده‌ها را از سرور دانلود کند.

وظیفه 5 می‌تواند یا در صورت پایان نیافتن وظیفه 3 به صورت سریالی ایجاد شود و یا از سوی هر یک از صف‌های دیگر در صورتی که وظیفه 3 پایان گرفته باشد، ایجاد شود. در اغلب موارد هنگامی که این الگو را در ابزارهای XCode ملاحظه می‌کنید، باید بدانید که می‌تواند به صورت سریالی ایجاد شود.

وظیفه 6 نیز می‌تواند با هر کدام از صف‌ها ایجاد شود، زیرا متصل است و همه هسته‌ها نیز موجود هستند.

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

روش‌های زمان‌بندی کارها

بنابراین در مورد روش اجرای زمان‌بندی وظایف به قدر کافی صحبت کردیم؛ اما در مورد روش زمان‌بندی آن‌ها روی صف‌های متفاوت هنوز توضیح نداده‌ایم. ابتدا باید در مورد اولویت‌بندی‌های صف‌های مختلف (QoS) صحبت کنیم و سپس کدهای مربوطه را ارائه می‌کنیم.

User Interactive یا (تعاملی کاربر - نخ اصلی)

این صف اصلی اپلیکیشن شما است و زمانی که منطق برنامه‌تان را می‌نویسید همان صف پیش‌فرض محسوب می‌شود که جزء نخ‌های پس‌زمینه نیست. همه آیتم‌های رابط کاربری (UI) در این صف استفاده می‌شوند، زمانی که کاربر روی یک دکمه ضربه می‌زند، این کار روی صف اصلی اجرا می‌شود. کارهایی که در صف اصلی قرار دارند، می‌بایست بی‌درنگ اجرا شوند.

User Initiated  (آغاز شده از سوی کاربر)

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

Default  (پیش‌فرض)

این صف پیش‌فرض است که وظایف پس‌زمینه به آن انتساب می‌یابند. این صف اولویت پایین‌تری نسبت به حالت User Initiated دارد. دلیل آن این است که کاربر این کارها را بی‌درنگ یا در آینده نزدیک نمی‌خواهد. این صف هنگامی مورد استفاده قرار می‌گیرد که اولویت صف تعیین نشده باشد. کارهایی که در این صف قرار می‌گیرند، می‌توانند از بازه زمانی بی‌درنگ تا در طی چند دقیقه اجرا شوند.

Utility  (کاربردی)

نام این صف کاملاً گویا است به شرط این که بدانید موارد کاربردی در دنیای برنامه‌نویسی به کدام موارد گفته می‌شود. از این صف برای ایجاد درخواست‌های شبکه جهت دانلود داده‌های غیر ضروری یا ذخیره‌سازی دائمی داده‌ها روی دیسک استفاده می‌شود. در این صف کارها در طی چند ثانیه تا چند دقیقه اجرا می‌شوند.

Background  (پس‌زمینه)

این صف برای وظایفی استفاده می‌شود که به منظور نگهداری (maintenance) اپلیکیشن مورد نیاز هستند. این امور برای شما مهم هستند؛ اما کاربر از آن‌ها اطلاعی ندارد. مثالی از آن اندیس‌گذاری است. کارهای موجود در این صف ممکن است چند دقیقه طول بکشند.

بدین ترتیب متوجه شدیم که هر وظیفه‌ای با اولویت بالاتر (1 بالاترین اولویت است) می‌تواند وظایف با اولویت پایین‌تر را متوقف کند. باید اطمینان حاصل کنید که کد شما نیز بر همین اساس عمل می‌کند. هر وظیفه همچنین یک اولویت نسبی در رابطه با صف خود دارد که بین 0 و 15- است. بنابراین شما اندکی اولویت زمان‌بندی برای هر صف پس‌زمینه نیز تعیین می‌کنید.

1// Concurrent Queue
2let concurrentQueue = DispatchQueue(label: "yourQueueLabel",
3                                    attributes: .concurrent)
4
5concurrentQueue.sync {
6    // work to perform concurrently, can add more to this queue by
7    // using this again
8}
9
10// Serial Queue
11let serialQueue = DispatchQueue(label: "yourQueueLabel")
12serialQueue.sync {
13    // work to perform serially
14}
15
16// Background threads using utility queue as an example
17DispatchQueue.global(qos: .utility).async {
18    // perform work on the utility queue
19    // replace utility with the queue you need
20    // if you forget, just use "." and autocomplete will help you
21}
22
23// Default Queue
24DispatchQueue.global().async {
25    // perform work on the default queue
26}
27
28// Getting to the main queue, these should be called from a
29// background thread
30DispatchQueue.main.sync {
31    // work to perform on the main thread (user interactive)
32    // pauses the current queue until this body completes
33}
34
35DispatchQueue.main.async {
36    // work to perform on the main thread (user interactive)
37    // continues on with background processing
38}

اگر می‌خواهید منتظر بمانید تا یک کار پس‌زمینه پایان یابد، چند روش برای انجام این کار وجود دارد که نخستین مورد آن یک گروه dispatch است.

گروه dispatch شامل وظایف پس‌زمینه متفاوتی است که باید پیش از ادامه کار اپلیکیشن پایان یابند. این وظایف باید با دستور enter وارد یک گروه dispatch شده و در زمان پایان گرفتن با دستور leave از این گروه خارج شوند. این وضعیت امکان می‌دهد که پردازش‌های پس‌زمینه پیش از ادامه منطق برنامه پایان گیرند.

1var dispatchGroup = DispatchGroup()
2
3func downloadData() {
4     dispatchGroup.enter()
5     // perform download
6     dispatchGroup.leave()
7}
8
9DispatchQueue.global(qos: .userinitiated).async {
10     downloadData()
11}
12
13DispatchQueue.global(qos: .default).async {
14     downloadData()
15}
16
17dispatchGroup.wait()

ابتدا یک dispatchGroup تعریف کرده‌ایم که بتوان از آن برای ردگیری دانلودها استفاده کرد. سپس یک دانلود در صف user-initiated آغاز می‌شود و از این رو به سرعت پایان می‌گیرد.

در ادامه دانلود دیگری در صف default آغاز می‌شود. این دانلود ممکن است کمی بیش‌تر از دانلود user-initiated طول بکشد؛ اما هر دو وارد صف می‌شوند.

سپس به ()dispatchGroup.wait می‌رسیم و برنامه صف اصلی را تا زمانی که دانلودها کامل شوند مسدود می‌کند و هر دو downloadData را فراخوانی می‌کنند تا تابع را اجرا کند. این دستور به dispatchGroup اعلام می‌کند که ما یک آیتم کمتر برای انتشار داریم.

زمانی که دانلود دوم نیز پایان گیرد، ()dispatchGroup.wait کامل می‌شود و برنامه ما به صورت نرمال به کار خود ادامه می‌دهد. روش دیگر برای استفاده از dispatch با بهره‌گیری از semaphores-ها است.

1var semaphore = DispatchSemaphore(0)
2
3func downloadData() {
4    // download data
5    semaphore.signal()
6}
7
8semaphore.wait()

Dispatch semaphores با استفاده از یک مقدار 0 برای همگام‌سازی یک وظیفه منفرد مورد بهره‌برداری قرار می‌گیرند. اگر بیش از یک مورد داشته باشید کافی است تعداد وظایفی که باید semaphore را signal کنند را تعیین کنید و یا شماره آن‌ها را تا پیش از ادامه کار برنامه کاهش دهید. در این مورد نیز متد ()wait برای مکث در اجرای برنامه استفاده می‌شود. تفاوت بین یک dispatch semaphore و یک dispatch group در این است که گروه، زمانی که برنامه وارد آن می‌شود تعیین خواهد شد؛ اما semaphore یک مقدار اولیه دارد و زمانی که وظیفه‌ای کامل شد، شماره آن یک واحد کاهش می‌یابد یا این که متد ()signal فراخوانی می‌شود.

گزینه‌ای به صورت Dispatch sources نیز وجود دارد؛ اما بررسی آن‌ها خارج از حیطه این مقاله است. اگر می‌خواهید در این مورد اطلاعات بیشتری داشته باشید، می‌توانید به مستندات اپل (+) در این زمینه مراجعه کنید.

توضیحات که تا به اینجا مطرح کردیم دید خوبی دست‌کم در سطحی که ما بررسی می‌کنیم از GCD ارائه می‌کند. برای کسب اطلاعات بیشتر در این خصوص می‌توانید به صفحه مربوطه در مستندات اپل برای توضیحات تفصیلی (+) و برای استفاده (+) مراجعه کنید.

بستار (Closure)

اگر سابقه برنامه‌نویسی به زبان دیگری دارید، با مفهوم بستارها و شیوه استفاده از آن‌ها آشنایی دارید. بستارها در زبان‌های دیگر به نام بلوک‌ها یا لامبداها شناخته می‌شوند. بستارها به طور عمده در «برنامه‌نویسی تابعی» (functional programming) استفاده می‌شوند؛ اما موارد استفاده دیگری نیز دارند.

ساده‌ترین راه برای توصیف یک بستار این است که بگوییم بستار، منطقی است که می‌تواند به صورت یک متغیر به بخش‌های مختلف برنامه ارسال شود و متعاقباً در کد اجرا شود.

  • نخستین چیزی که باید در مورد بستارها بدانید این است که از نوع ارجاعی (reference) هستند.
  • دومین نکته‌ای که باید بدانید این است که بستارها در اغلب موارد ناهمگام هستند. این بدان معنی است که باید دقت مضاعفی داشته باشید تا اطمینان حاصل کنید که هیچ چیز دیگری هنگام کار کردن با بستار آن را تغییر نمی‌دهد و ضمناً باید تضمین کنید که بستارهای با اجرای طولانی پیش از تلاش برای دسترسی به مقادیرشان پایان می‌یابند.
  • آخرین نکته‌ای که باید در مورد بستارها بدانید این است که آن‌ها هر چیزی که درونشان استفاده شود را دریافت می‌کنند. این بدان معنی است که اگر متغیری در سطح بالای یک فایل تعیین شود و درون بستار مورد استفاده قرار گیرد، بستارها یک ارجاع قوی به آن متغیر ایجاد می‌کنند. بنابراین باید مراقب باشید که شیئی را که تحت مالکیت بستار قرار دارد را به مالکیت خود درنیاورید. اگر classA بستار را ایجاد کرده باشد، بستار نباید یک ارجاع قوی به classA داشته باشد. از این بستار می‌توان با بهره گرفتن از کلیدواژه weak یا unowned استفاده کرد که در ادامه روش آن را توضیح می‌دهیم.

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

1func applyAddition() -> (Int, Int) -> Int {
2  
3   func add(num1: Int, num2: Int) -> Int {
4      return num1 + num2
5   }
6  
7   return add           // this is the add function
8}
9
10let addValues = applyAddition()   // addValues = add(num1:num2:)
11let result = addValues(num1: 3, num2: 4)
12print(result)    //prints 7

اگر بخواهیم کد فوق را به صورت گام به گام توضیح دهیم، ابتدا یک تابع ایجاد کرده‌ایم که تابع درون آن را با استفاده از متد زیر بازگشت می‌دهد:

 func applyAddition() -> (Int, Int) -> Int

شیوه خواندن این تابع شبیه به تابعی به نام است که هیچ پارامتری نمی‌گیرد () و مقدار بازگشتی آن <- تابعی است که انتظار می‌رود دو پارامتر ورودی از نوع Int به صورت (Int, Int) بگیرد. و تابعی که دو Int می‌گیرد انتظار می‌رود که یک Int به صورت Int <- بازگشت دهد.

سپس تابعی را تعریف می‌کنیم که مقدار زیر را بازگشت می‌دهد:

func add(num1: Int, num2: Int) -> Int

این تابع شبیه به هر تابع دیگری است و تنها تفاوت در این است که امضای تابع باید با امضای نوع بازگشتی مطابقت داشته باشد.

...-> (Int, Int) -> Int

func add(num1: Int,num2: Int) -> Int

سپس یک add بازگشت می‌دهیم که نام تابع (add(num1:num2 است. در ادامه یک ثابت رl برای نگهداری تابع به صورت زیر ایجاد می‌کنیم:

let addValues = applyAddition()

زمانی که این کار را انجام می‌دهیم، در واقع بیان می‌کنیم که addValues باید همان منطقی را اجرا کند که تابع بازگشتی اجرا می‌کند. از این رو addValues همان add است.

از آنجا که addValues مقداری را بازگشت می‌دهد، باید یک متغیر برای نگهداری این مقدار بازگشتی ارائه کنیم. در این مثال، ما آن را result می‌نامیم. همچنین addValues نیازمند دو پارامتر است که در addFunction تعریف می‌شوند و بنابراین هنگامی که اضافه شدن مقادیر (addFunction) را فراخوانی می‌کنیم باید دو عدد صحیح نیز ارائه کنیم. در ادامه addFunction به بلوکی از حافظه ارجاع می‌دهد که منطق تابع add در آن قرار دارد و آن بلوک کد را اجرایی کند و در نهایت مقدار مورد نظر را به صورت result بازگشت می‌دهد.

در ادامه result را نمایش می‌دهیم و می‌بینیم که این وضعیت عملاً کار کرده است. با استفاده از بستارها می‌توانیم ساختار فوق را به صورت کاملاً فشرده بنویسیم:

1let addValues = { (num1, num2) -> Int in
2   return num1 + num2
3}
4
5let result = addValues(3, 4)
6print(result)     //prints 7

کد فوق دقیقاً همان مثالی است که در بخش قبلی توضیح دادیم به جز این که تابع applyAddition اول حذف شده است، زیرا شامل هیچ پارامتری نیست. می‌توان انتظار داشت که applyAddition شامل پارامترهایی باشد که می‌توانیم مورد بررسی قرار دهیم تا یکی از تابع‌های چندگانه ممکن را بازگشت دهد؛ اما بررسی و نوشتن آن را بر عهده شما می‌گذاریم. این کار چندان دشوار نیست، کافی است به چگونگی استفاده از یک گزاره if در applyAddition فکر کنید که یک تابع متفاوت بازگشت می‌دهد.

روش خواندن این مثال جدید چنین است که ابتدا یک ثابت به نام addValues ایجاد می‌شود که یک عبارت { ... } = را اجرا خواهد کرد که دو پارامتر ورودی (num1, num2) می‌گیرد و یک عدد صحیح  Int <- بازگشت می‌دهد که در کد زیر به صورت in است.

سپس عملیات return num1 + num2 اجرا می‌شود و در ادامه می‌توانیم از addValues برای افزودن هر مقدار دلخواه، بدون نیاز به نوشتن چندباره این کد استفاده کنیم. این وضعیت در هنگامی که می‌خواهیم نسخه‌های چندگانه‌ای از این کد تهیه کنیم واقعاً به کار می‌آید.

1func madeGoal(isThreePoints: Bool) -> (Int) -> Int {
2   if isThreePoints {
3      return { [unowned num1] in return num1 + 3 }
4   } else {
5      return { [unowned num1] in return num1 + 2 }
6   }
7}
8
9let twoPoints = madeGoal(isThreePoints: false)
10let threePoints = madeGoal(isThreePoints: true)
11
12var playerScore = 0
13
14playerScore = twoPoints(playerScore)
15playerScore = threePoints(playerScore)
16
17print(playerScore)      //prints 5

در کد فوق تابعی را می‌بینیم که یک بستار را بازگشت می‌دهد. این تابع یک پارامتر بولی منفرد را به نام isThreePoints دریافت می‌کند. این بخش تصمیم می‌گیرد که کدام تابع را باید بازگشت دهیم. منطق موجود در madeGoal بررسی می‌کند که آیا باید یک هدف دو نقطه‌ای یا سه‌نقطه‌ای بازگشت دهیم. سپس می‌توانیم هر یک از این تابع‌ها را انتخاب کرده و آن‌ها را به صورت مستقل به متغیرهایشان انتساب دهیم که در ادامه در playerScore فراخوانی می‌شوند. زیرا یک مقدار بازگشت می‌دهد که باید دو برابر شود و این مقدار به playerScore بازگشت یابد. در غیر این صورت playerScore هرگز به‌روزرسانی نمی‌شود.

این یک روش برای ثبت امتیازها در یک بازی محسوب می‌شود. تنها نکته‌ای که باید به خاطر داشته باشید این است که وقتی بازی تمام شد، باید twoPoints و threePoints را به صورت nil تعیین کنید تا ارجاعی که به deinit وجود دارد حذف شود.

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

{ [unowned propertyName] in ... }

ما در بخش‌های قبلی در مورد شمارش ارجاع صحبت کردیم. در آن‌ها اشاره شد که هر نوع ارجاع یک شماره تعداد اشیایی که به آن اشاره می‌کند را ذخیره می‌سازد. یک شیء نمی‌تواند به صورت خودکار زمانی که شیء دیگر به آن ارجاع می‌کند، از حافظه آزاد شود.

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

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

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

1func dataTask(with request: URLRequest, 
2              completionHandler: 
3                 @escaping (Data?, URLResponse?, Error?) -> Void) ->        
4                    URLSessionDataTask {
5     ...
6}

اعلان تابع دو پارامتر دارد که پارامتر نخست برای URL درخواست است و پارامتر دوم نیازمند تابعی با سه پارامتر از نوع Data، URLResponse، Error است که انتظار نمی‌رود مقدار بازگرداند. در مجموع این تابع یک URLSessionDataTask بازگشت می‌دهد که می‌توان از آن برای ()resume یا ()pause کردن درخواست استفاده کرد. در مورد escaping@ نیز در ادامه بیشتر توضیح خواهیم داد.

اینک به بخش جالب ماجرا می‌رسیم. دلیل این که این بستار trailing نامیده می‌شود این است که پارامتر آخر تابع خود یک تابع می‌گیرد. البته لزومی به استفاده از این ساختار وجود ندارد:

1let task = URLSession.shared.dataTask(with: URL, completionHandler: {
2      (data, response, error) in
3       ...
4       // do stuff with data, response, or error
5)}

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

1let task = URLSession.shared.dataTask(with: URL) {
2      (data, response, error) in
3       ...
4       // do stuff with data, response, or error
5}

اینک بدیهی است که ما اساساً پارامتر :completionHandler را گرفته‌ایم و به جای آن از ساختار بستار trailing پس از بستن پرانتز پارامترهای تابع استفاده کرده‌ایم.

اگر یک گام به عقب بازگردیم می‌بینیم که چند مورد تازه در اعلان تابع وجود دارد. نخستین مورد escaping@ است.

هنگامی که یک بستار به عنوان یک آرگومان به یک تابع ارسال می‌شود و بستار پس از پایان یافتن تابع اجرا می‌شود، از خصوصیت escaping@ برای بیان این نکته استفاده می‌کنیم که بستار می‌تواند تابعی را که در آن جای گرفته است رد کند. در مورد وظایفی که به زمان اجرای طولانی نیاز دارند این خصوصیت مفیدی محسوب می‌شود. مخالف این وضعیت، استفاده از خصوصیت noescape@ است که حالت پیش‌فرض محسوب می‌شود. زمانی که از noescape@ استفاده می‌کنیم در واقع به کامپایلر اعلام می‌کنیم که این بستار پیش از پایان گرفتن تابع فراخوانی می‌شود.

اگر نمی‌دانید از کدام حالت باید استفاده کنید صرفاً از حالت non-escaping استفاده کنید و اجازه دهید Xcode با اعلام هشدار یا خطا در مواردی که به خصوصیت escaping نیاز هست به شما کمک کند.

مورد جدید بعدی Void است. Void ساده است و صرفاً نوع خاصی است که معادل بازگشتی تهی است. زمانی که void را می‌بینیم این احتمال وجود دارد که جهت شفافیت ذکر شده باشد. در مورد خاص URLSession از این نوع به منظور اطلاع‌رسانی این نکته به کامپایلر استفاده می‌شود که بداند ما به دنبال تابعی هستیم که هیچ چیز بازگشت نمی‌دهد و نه یک سه‌تایی به صورت زیر:

(Data?, URLResponse?, Error?)

جمع‌بندی

در این نوشته با نکات زیادی در مورد بستارها و روش استفاده از آن‌ها در کد آشنا شدیم. بستارها به طور مکرر در سوئیفت مورد استفاده قرار می‌گیرند. احتمالاً متوجه شده‌اید که متدهای مختلفی که به انواعی مانند map ، .filter ، .sorted ،reduced. و deinit انتساب یافته‌اند همگی بستار هستند. GCD پر از این موارد است و هر باز که می‌خواهم کد را به نخ پس‌زمینه یا نخ اصلی بفرستیم از آن‌ها استفاده می‌کنیم. همه آن‌ها دارای ساختار بستار trailing و برخی با پارامتر و برخی بدون پارامتر هستند.

GCD در آغاز کدنویسی سوئیفت یکی از پر کاربردترین موارد محسوب می‌شود و با استفاده از آن می‌توانید بار محاسباتی را در نخ‌های مختلف اپلیکیشن توزیع کنید و بدین ترتیب از نیروی هسته‌های مختلف پردازنده برای اجرای همزمان وظایف و ارتقای عملکرد بهره بگیرید.

شاید از این که متوجه شده‌اید دشوارترین بخش سوئیفت، بستارها هستند ناراحت و یا شاید هم خوشحال شده باشید. خوشحال از این جهت که می‌دانید دشوارترین بخش را پشت سر گذارده‌اید و ناراحت از این جهت که همچنان در درک بستارها دچار مشکل هستید. البته جای نگرانی نیست چون این مشکل رایجی است و با گوگل کردن عبارت swift closures متوجه می‌شوید که این حجم انبوه آموزش‌ها نشان‌دهنده تلاش بسیاری از افراد برای درک آن است.

در بخش بعدی این سری مقالات به توضیح Type Aliases ،Property Observers، و تفاوت Self با self می‌پردازیم. برای مطالعه آن به لینک زیر مراجعه کنید:

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

==

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

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