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


در بخش قبلی این سری مطالب آموزش سوئیفت به بررسی struct، کلاس، مشخصات و متدها پرداختیم. بدین ترتیب دریافتیم که چگونه میتوانیم اشیا را ایجاد و مشخصات و متدهای شبیه به هم را گروهبندی کنیم. ما در این سری مطالب آموزشی در مورد کلاسهای خاصی که اپل در اختیار برنامهنویسان قرار میدهد مانند UIButton یا URLSession صحبت نخواهیم کرد. قصد ما این است که شما آن دانشی را کسب کنید که وقتی کلاسها را در برنامههای خودتان میبینید، ایدهای از چگونگی آغاز به کار با آنها داشته باشید. در این نوشته به بررسی مفاهیم Initialization و De-initialization ،Override و Reference Counting میپردازیم.
Initialization
Initialization برای کلاسها و struct-ها در ابتداییترین معنی خود، یک مقدار برای struct و کلاس ارائه میکند. در برخی موارد ما قصد داریم که کلاس یا struct ما مقادیر پیشفرضی در زمان ایجاد شدن داشته باشد؛ در حالی که میخواهیم به کلاس یا struct بگوییم که مقادیر پیشفرض چه هستند.
اختلاف بین مقداردهی یک کلاس و مقداردهی struct چنین است: struct-ها در سوئیفت متدهای خاص خود را دارند اما کلاسها چنین امکانی ندارند. معنی این گفته آن است که ما وقتی یک struct ایجاد میکنیم نیازی به داشتن متد ()init در struct خود نخواهیم داشت، چون به طور خودکار برای شما ایجاد شدهاند. کلاسها تنها باید در مواردی یک متد ()init داشته باشند که یک خصوصیت کلاس در زمان ایجاد شدن آن مقداردهی اولیه نشده باشد. برای توضیح بیشتر به مثال زیر توجه کنید:
در کلاس myFullyInitializedClass میتوانیم آن را با کد زیر ایجاد کنیم:
اگر لازم باشد که تابعی را با استفاده از ()myFirstClass.myFunction فراخوانی کنیم مقدار بازگشتی آن عدد صحیح 1 خواهد بود، چون یک مقدار اولیه برای کلاس تعیین شده است.
برای ایجاد کلاس myNonInitializedClass باید یک مقدار givenNumber ارسال کنیم، بنابراین همه مشخصات کلاس را مقداردهی اولیه میکنیم. در حالتی که تنها یک مشخصه داشته باشیم؛ اما به موارد بیشتری نیاز داشته باشیم، میتوانیم همه مقادیر پیشفرض را ارسال کنیم. در واقع متد ()init را میتوان مانند تابعی بدون کلیدواژه func در نظر گرفت. تنها مقصود این متد انتساب مقادیر به مشخصات درون کلاس است و زمانی که متد myFunction فراخوانی شود مقدار بازگشتی 2 خواهد بود.
در مثالهای myStruct ما یک initializer نداریم، زیرا initializer پیشفرض در پسزمینه برای ما ایجاد شده است. زمانی که یک struct جدید ایجاد میکنیم، باید از مقدار زیر استفاده کنیم:
var myThirdStruct = myStruct(firstNumber: 3)
بدیهی است که ما 3 struct ایجاد نکردهایم؛ اما به این دلیل آن را این چنین نامگذاری کردهایم که بتوانید ترتیب کلی کلاسها و struct-ها را مشاهده کنید. ما میتوانیم myThirdStruct.myFunction را فراخوانی کنیم و در این صورت متد مقدار 3 را بازگشت میدهد.
اگر قرار بود که وهله جدیدی از myStruct ایجاد کنیم و مقدار پیشفرض را برای عدد نخست ارسال نکنیم، قطعاً با خطا مواجه میشدیم. بنابراین هنگامی که در مورد struct-ها صحبت میکنیم، باید یا مقدار پیشفرض را تعیین کرده باشیم و یا در زمان فراخوانی آن را ارسال کنیم.
Optional-ها میتوانند در کلاسها و struct-ها برای تأمین الزامات یک initializer مورد استفاده قرار گیرند. تصور کنید یک کلاس ایجاد میکنید که همواره مشخصاتش را بازگشت نمیدهد. ما میتوانیم مشخصاتی را که بیدرنگ قرار نیست در اختیار optional قرار گیرد را تعیین کنیم.
با بهرهگیری از مقادیر optional دیگر لازم نیست یک initializer داشته باشیم؛ اما چنان که در ادامه خواهید دید، میبایست به طور اجباری optional را باز کنیم تا پوشش (<Optional(<value را از پیرامون مقادیری که باید تعیین شود کنار بزنیم. البته این یک اصل کلی است که نباید از عملگر (!) یعنی unwrap اجباری استفاده کرد؛ اما در حال حاضر که ابزارهای زیادی در اختیار نداریم، مجبور به این کار هستیم. البته در ادامه ابزارهای دیگری برای این کار معرفی خواهیم کرد. اگر شما یک optional «تهی» (nil) را به صورت اجباری unwrap کنید، برنامه شما از کار میافتد و یک خطای بد به صورت زیر دریافت میکنید:
Found nil while unwrapping optional <type>
چند نوع initializer به صورت initializer پیشفرض (که توضیح دادیم)، required initializer ،convenience initializer و failable initializer وجود دارند.
Required initializer بدین منظور طراحی شده که اگر زیرکلاسی از یک کلاس در حال ایجاد باشد، زیرکلاس بتواند initializer والد خود یا همان سوپرکلاس را فراخوانی کند.
همان طور که در کد فوق میبینید، با تعیین initializer در Vehicle به صورت required، ما خودرو (car) را الزام کردیم که یک برای سوپرکلاس تعیین کند. ما از super.init برای دسترسی به initializer سوپرکلاس استفاده کردهایم. اگر معنی این حرف را متوجه نشدید، جای نگرانی نیست چون در ادامه آن را به بیان ساده بازگو میکنیم. با فراخوانی متد init برای car، ما باید مشخصه fuelType را برای Vehicle تعیین کنیم، بنابراین وقتی super.init را از درون متد init فراخوانی میکنیم، میتوانیم مقدار fuelType را به کلاس Vehicle ارسال کنیم. در ادامه طرز کار آن را نمایش دادهایم:
بدین ترتیب این وضعیت در واقع همانند آن است که کلاس Vehicle هرگز وجود نداشته است. شاید از خود بپرسید چرا ما باید از آن استفاده کنیم؟ پاسخ ساده است، چون در این روش امکان توسعهپذیری وجود دارد. اگر به مثال نخست ایجاد زیرکلاس برویم، میتوانیم بحث توسعه را بررسی کنیم:
در مثال فوق، ما یک کلاس Truck ارائه کردهایم که دارای یک مشخصه bedSize است. سپس یک کلاس SemiTruck ایجاد میکنیم که آن نیز یک initializer دارد؛ اما تنها مقدار hasSleeper را میگیرد. ما مقدار fuelType را به صورت تعیین میکنیم زیرا در این مثال، همه Semi-truck-ها دارای سوخت دیزل هستند. این وضعیت مجاز است.
بدین ترتیب به مفهوم Convenience Initializer میرسیم. Convenience Initializer به همان ترتیبی عمل میکند که در مثال آخر دیدیم؛ اما در مواردی که به خود مقادیر اهمیت نمیدهیم، برای تعیین مقادیر پیشفرض مورد استفاده قرار میگیرد.
بدین ترتیب میتوانیم ()Person را بدون تعیین یک مقدار فراخوانی کنیم و یک مقدار پیشفرض name به صورت "unknown person" خواهیم داشت. این میتواند یک جایگزین مناسب به جای استفاده از optional در سراسر یک کلاس باشد.
Failable initializer میتواند از وضعیتی که در آن یک مشخصه کلاس به مقدار نادرستی تعیین میشود جلوگیری کند و بدین ترتیب میتوانیم در مواردی که کلاس نتواند initialize شود، مقدار nil بازگشت دهیم.
در کد فوق ما یک failable initializer را با استفاده از init? تنظیم کردهایم. بدین ترتیب میتوانیم فرایند initialization را لغو کرده و یک مقدار تهی برای متغیر بازگشت دهیم. با استفاده از failable initializer همچنین میتوانیم متغیری بسازیم که optional کلاس را در خود ذخیره میکند. در این حالت اگر یک نام ارسال نکنیم، مقدار تهی بازگشت مییابد و در غیر این صورت یک وهله myFailableClass به صورت optional بازگشت مییابد.
De-initialization
De-initialization به سادگی به معنی پاکسازی مقادیر تعیین شده پس از اتمام کار با کلاس است. در ادامه در مورد «شمارش ارجاع» (Reference Counting) صحبت خواهیم کد؛ اما فعلاً باید بدانید که هر زمان یک کلاس ایجاد میشود، در واقع ارجاعی به آن ایجاد شده است. در صورتی که شیء دیگری داشته باشید که از این کلاس استفاده میکند، با استفاده از De-initialization میتوانید به اشیای دیگر کمک کنید که این کلاس را از حافظه خود حذف کنند. در این مثال قصد داریم در مورد کلاس Timer در زبان برنامه نویسی سوئیفت صحبت کنیم. کلاس Timer یک ارجاع رشتهای به کلاسی که مورد استفاده قرار میدهد ایجاد میکند و زمانی که کارش با این کلاس پایان یافت، باید timer را از کلاس خود حذف کنیم. برای توضیح بیشتر به کد زیر توجه کنید:
deinit درست پیش از آن که کلاس از حافظه حذف شود، فراخوانی میشود. زمانی که از myCounter = nil استفاده میکنیم، کلاس آماده حذف خود از حافظه میشود. در این مرحله deinit فراخوانی میشود و هر کدی که در آن قرار داشته باشد اجرا میشود. این کد میتواند مربوط به کپی کردن دادهها و ذخیرهسازی در جایی بیرون از کلاس یا مانند مثال فوق، بررسی معتبر بودن timer و اعتبار زدایی از آن باشد.
فرایند اعتبار زدایی یک timer به آن اعلام میکند که ارجاع خود به کلاس را حذف کند و هر وظیفه پاکسازی دیگری که جهت حذف خود از حافظه نیاز دارد را به اجرا درآورد. زمانی که timer تخریب شد، کلاسِ مالک deinit را به پایان میبرد و خود را از حافظه حذف میکند.
Override
Override امکان ایجاد زیرکلاس و تغییر initializer پیشفرض سوپرکلاس را میدهد.
در اینجا ما یک سوپرکلاس به نام Ball داریم که دارای یک initializer پیشفرض به نام ()init است، چون یک initializer از Basketball را فراخوانی میکنیم که دارای همان نام است باید از کلیدواژه override استفاده کنیم. ما همچنان باید super.init را فراخوانی کنیم. بنابراین سوپرکلاس کار خود را انجام میدهد؛ اما در نهایت مقدار پیشفرض 3 برای کلاس Basketball تعیین میشود.
از کلیدواژه override روی متدهایی استفاده میکنیم که نام یکسانی دارند و در کلاس والد وجود دارند. در اینجا متد bouceHeight را override میکنیم، چون همان امضای متد کلاس والد را دارد و امضای متد صرفاً روش دیگری برای بیان این است که شبیه آن است و باید به همان طریق فراخوانی شود. تنها تفاوت این است که به جای بازگشت دادن مقدار Double(size) * 0.5 باید مقدار Double(size) * 0.75 را بازگشت دهیم. فرایند تبدیل از یک نوع به نوع دیگر به نام cast کردن شناخته میشود. در مورد این مفهوم در بخش بعدی این سری مطالب آموزشی بیشتر صحبت خواهیم کرد.
در مورد زمانی که باید از override استفاده کنید، نباید نگرانی چندانی داشته باشید، چون Xcode زمانی که نیاز به استفاده از کلیدواژه override باشد، یک پیام خطا ایجاد میکند و حتی زمانی که روی خطا کلیک کنید آن را به طور خودکار برای شما ایجاد میکند.
شمارش ارجاع (Reference Counting)
هر زمان که یک کلاس در سوئیفت ایجاد میکنید، در واقع یک ارجاع به شیئی تولید میشود. زمانی که ارجاعی ایجاد شود، سیستم یک شمارنده همراه با آن ایجاد میکند. این شمارنده به طور مستقیم تعداد اشیایی که به آن اشاره میکند را در خود ذخیره ساخته است.
در این بخش ما دو ارجاع داریم که هر دوی A و B به داده اشاره میکند و از این رو میتوانیم شمارنده را بهروزرسانی کنیم تا اعلام کنیم که دو ارجاع وجود دارد. اگر A قرار بود به صورت nil تعیین شود در این صورت ما تنها یک ارجاع به دادهها داشتیم.
اینک سؤال این است که اگر هر دوی A و B دیگر به دادهها ارجاع نکنند چه میشود؟ بر اساس قوانین حافظه، دادهها در هر صورت در آن قرار میگیرند و تا زمانی که رایانه خاموش نشود یا به طور اتفاقی به بلوکی از حافظه که دادهها در آن قرار دارند دسترسی یافته و یا آن را به صورت nil تنظیم کنیم، باقی میمانند. این موارد معمولاً در زمانهایی رخ میدهند که مقادیر جدید روی مقادیر موجود نوشته شوند.
Garbage Collection
به لطف سیستمهای عامل، لازم نیست در مورد این موقعیت زیاد نگران باشیم. در سوئیفت و به بیان دقیقتر در کامپایلر clang ما یک ویژگی به نام ARC یعنی شمارش ارجاع خودکار داریم که در زمانهای متناوب اجرا شده و بررسی میکند که چه دادههایی در حافظه هستند که دیگر مورد ارجاع نیستند و آنها را از حافظه حذف میکند. این فرایند از زمانهای قدیم که به Objective-C مربوط میشود، به نام garbage collection شناخته میشود و همچنان از سوی زبانهای دیگر نیز از این اصطلاح استفاده میشود. این به آن معنی نیست که یک اصطلاح بهتر یا بدتر از دیگری است؛ بلکه صرفاً به روش آزادسازی دادهها از حافظه گفته میشود.
مشکل ARC زمانی است که با ارجاعهای قوی سر و کار داریم. با استفاده از کلاس شمارنده فوق میتوان این وضعیت را به صورت زیر ترسیم کرد:
از آنجا که Counter مالک Timer است و Timer ارجاعی به Counter ایجاد کرده است، هر کدام از آنها شمارنده ارجاع 1 را دارند. اگر Counter را بدون حذف ارجاع قوی تخصیص زدایی کنیم، Timer به طور مستقل و با شمارنده ارجاع 1 تا زمانی که رایانه خاموش نشده است، باقی میماند. ARC نمیتواند این موقعیت را مدیریت کند. این همان نکتهای است که به نام «نشت حافظه» نامیده میشود. این وضعیتی است که شیء قرار گرفته در حافظه به طور مستقل است و دیگر حذف نخواهد شد.
نشت حافظه
نشت حافظه به هر اندازهای که باشد خوب نیست و باید همه تلاش خود را بکنیم که چنین واقعهای رخ ندهد؛ اما در ابتدای کار با مواردی مواجه خواهید شد که این موقعیت رخ میدهد. این بدان معنی نیست که شما بیدرنگ حافظه را از دست میدهید، بلکه صرفاً به این معنی است که در موارد بروز نشت حافظه، نمیتوانید از آن حافظه برای کار دیگری استفاده کنید. خاموش کردن رایانه به مدت 15 ثانیه، سادهترین روش برای اصلاح آن است. شاید تاکنون با این موقعیت مواجه شده باشید که شرکتهای ارائهدهنده خدمات اینترنتی از شما میخواهند مودم خود را به مدت 15 ثانیه خاموش کنید. در واقع اگر حافظه دستگاه پر شده باشد، با خاموش کردن آن به مدت 15 ثانیه، این حافظه از کار افتاده پاک میشود و پس از بازنویسی دادههای صحیح در حافظه، مجدداً کارکرد صحیح خود را باز مییابد.
بنابراین اگر Timer را تخصیص زدایی کنیم، همچنان روی تخصیص زدایی از کلاس کنترل داریم و هر کاری که بخواهیم را میتوانیم انجام دهیم.
ارجاع ضعیف
روش دیگر که میتوان برای کمک به چرخههای ارجاع قوی مانند این استفاده کرد، نشانهگذاری کلاسهایی که موجب ایجاد یک ارجاع قوی میشوند با استفاده از کلیدواژه weak یا unowned است.
یک ارجاع ضعیف (weak) امکان تهی شدن را ایجاد میکند و این فرایند به طور خودکار در زمانی که شیء مورد ارجاع تخصیص زدایی شود رخ میدهد. از این رو نوع مشخصه شما باید optional باشد. شما به عنوان یک برنامهنویس مجبور هستید که پیش از استفاده، این موضوع را بررسی کنید. در واقع کامپایلر تا جایی که میتواند شما را وادار به این کار میکند تا کد امنی بنویسید.
یک ارجاع unowned فرض میگیرد که در طی چرخه عمر خود هرگز nil نخواهد شد. یک ارجاع unowned باید در طی initialization تعیین شود؛ این بدان معنی است که ارجاع به صورت یک نوع غیر optional تعریف میشود که میتواند به طور مطمئنی بدون بررسیها مورد استفاده قرار گیرد. اگر به شیوهای شیء مورد ارجاع تخصیص زدایی شود، در این صوت اپلیکیشن در زمانی که ارجاع unowned مورد استفاده قرار گیرد از کار خواهد افتاد.
با استفاده از توضیحات فوق میتوانید مفاهیم ارجاع unowned و weak را بهتر درک کنید.
جمعبندی
ما در این نوشته در مورد Initialization و چگونگی اجرای آن مطالبی آموختیم. همچنین با دلیل اهمیت آن در کلاسها آشنا شدم. در اغلب موارد در سوئیفت مشغول نوشتن struct هستیم؛ اما در برخی موارد هم باید از کلاس استفاده کنیم. قاعده سرانگشتی این است که باید کار خود را با struct آغاز کنیم و سپس در صورتی که با محدودیت مواجه شدیم از کلاس استفاده کنیم.
در ادامه در مورد deinitialization مواردی آموختیم و مواردی که لازم است از آن استفاده کرد را دانستیم. آموختیم که لازم نیست آن را در هر کلاس بگنجانیم؛ اما باید مطمئن شویم که از آن در مواردی که ممکن است به چرخه ارجاع قوی منتهی شود استفاده کردهایم.
همچنین در مورد override و دلیل نیاز به آن مواردی آموختیم و در انتها یکی از مهمترین موضوعاتی که در هنگام نوشتن برنامهها باید در مورد انواع ارجاع بدانیم، یعنی شمارش ارجاع را مورد بررسی قرار دادیم. دانستن این که مواردی ممکن است منجر به بروز نشت حافظه شوند و جلوگیری از آن، چیزی است که در زمان بروز باگ در اپلیکیشن باید کنترل کاملی روی آن داشته باشید. این مسئله منجر به از کار افتادن اپلیکیشن نمیشود؛ اما میتواند به همان بدی باشد زیرا موجب کمبود حافظه روی سیستم کاربر میشود.
در بخش بعدی این سری مطالب آموزشی در مورد تبدیل نوع، باز کردن امن Optional-ها و کنترل دسترسی صحبت خواهیم کرد. این موارد را به سرعت خواهیم آموخت و اطلاعاتی که در این مسیر کسب میکنیم، قدرتمندی زیادی برحسب انعطافپذیری که در مجموعه ابزارهای ما ایجاد میکنند، در اختیارمان قرار میدهند.
برای مطالعه قسمت بعدی این مطلب روی لینک زیر کلیک کنید:
آموزش برنامهنویسی سوئیفت (Swift): تبدیل نوع – بخش هشتم
اگر این مطلب برای شما مفید بوده است، آموزشها زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزش های برنامه نویسی
- آموزش برنامه نویسی Swift (سوئیفت) برای برنامه نویسی iOS
- مجموعه آموزش های مهندسی نرم افزار
- آموزش آرایه در برنامه نویسی Swift (سوئیفت)
- شروع برنامهنویسی با زبان سوئیفت در اوبونتو
==