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

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

در بخش قبلی از این سری مقالات آموزش سوئیفت در مورد انواع مختلف متغیرها صحبت کردیم؛ اما نوید دادیم که در بخش بعدی، یعنی مقاله حاضر به بررسی عمیق‌تر انواع داده که شامل: «انواع مقداری» (Value Types)، «انواع ارجاعی» (Reference Types) و همچنین اشاره‌گرها (Pointers) می‌شود خواهیم پرداخت. توجه کنید که اشاره‌گرها احتمالاً یکی از دشوارترین مفاهیم برنامه‌نویسی محسوب می‌شوند؛ اما جای نگرانی نیست؛ چون ما سعی می‌کنیم آن را به ساده‌ترین زبان ممکن بیان کنیم.

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

ابتدا رشته‌ای ایجاد می‌کنیم که مقدار hello را در خود ذخیره سازد. این کار دشواری چندانی ندارد و قبلاً آن را انجام داده‌ایم.

1var myString = "Hello"

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

memory

شاید از خود بپرسید منظور از 0\ در انتهای رشته چیست. این کاراکتر به نام «خاتمه دهنده تهی» (Null Terminator) نامیده می‌شود. علامت ممیز به برنامه اعلام می‌کند که آماده یک دستور است و علامت صفر نیز به معنی هیچ است. مقادیر رشته‌ای به این صورت در حافظه ذخیره می‌شوند و به روش فوق خاتمه یک رشته معین می‌شود.

البته شیوه ذخیره‌سازی اعداد کمی متفاوت است.

binary numbers

اعداد به صورتی عددهایی دودویی ذخیره می‌شوند. درک مفهوم دودویی آسان است، اگر ابتدای اعداد را از 1 تصور کنیم، این بلوک سمت راست حافظه محسوب می‌شود و ارقام پس از آن هر یک با اضافه شدن 2 رقم اشغال می‌شوند. اگر معلوماتی در مورد کارت‌های گرافیکی یا مموری استیک‌ها داشته باشید، می‌دانید که بازی‌های آتاری و NES از حافظه 8 بیتی، بازی‌های سگا، SNES از حافظه 16 بیتی، بازی‌های پلی‌استیشن از حافظه 32 بیتی و بازی‌های نینتندو 64 از حافظه 64 بیتی استفاده می‌کنند.

در زبان دودویی اگر یک موقعیت 0 باشد، برابر با خاموش یا نادرست (false) است. اگر موقعیتی برابر با 1 باشد، به معنی روشن یا درست (True) بودن آن موقعیت است. بر اساس این منطق کافی است موقعیت‌هایی که در آن‌ها 1 قرار دارد را بشماریم. آیا می‌توانید عددی که در تصویر فوق وجود دارد را محاسبه کنید؟

همان طور که می‌بینید، در تصویر فوق مجموعه‌ای از 8 بیت حافظه وجود دارد که با همدیگر یک بایت را تشکیل می‌دهند. در بخش پیشین این مطالب اشاره کردیم که هر کاراکتر در یک رشته، 2 بایت از حافظه را اشغال می‌کند. یعنی هر کاراکتر برای ذخیره شدن به 16 بیت نیاز دارد. 16 بیت در سیستم دودویی، در بیشینه وضعیت، 256 حالت مختلف ارائه می‌کند. با این وجود وقتی در مورد موقعیت‌های حافظه صحبت می‌کنیم، همواره باید از 0 آغاز کنیم. بنابراین 16 بیت، بازه کامل 0 تا 255 را شامل می‌شود. شاید از خود بپرسید چگونه می‌توانیم کاراکترهایی این بازه را به دست آوریم. در این مورد باید به جدول ASCII (مراجع کنید و ببینید که هر کاراکتر چه مقداری دارد. در این جدول حتی می‌توان کاراکترهای پنهانی مانند کاراکتر «خاتمه دهنده تهی» (0\) را مشاهده کرد.

 ASCII table
برای مشاهده تصویر در ابعاد اصلی روی این لینک کلیک کنید (+).

مقادیر مختلف در حافظه در بخش حافظه استاتیک ذخیره می‌شوند. این بخش به نام «پشته» (stack) و «هیپ» (heap) نامیده می‌شود.

static memory

حافظه استاتیک و حافظه پشته دسترسی سریعی دارند؛ اما دسترسی به مقادیر درون حافظه هیپ کُند است. البته منظور از کند بودن بر اساس مقیاس‌های زمانی رایانه‌ها است، و ممکن است این مقادیر از نظر شما کند محسوب نشوند. ما برای بارگذاری مقادیر از حافظه پشته به یک میلی‌ثانیه یا کمتر نیاز داریم. در حالی که بارگذاری از هیپ به 10 میلی‌ثانیه زمان نیاز دارد. در ادامه این نوشته همراه با معرفی مفاهیم مختلف، پیاده‌سازی این نوع حافظه‌ها را نیز ارائه می‌کنیم و به این ترتیب متوجه می‌شوید که هر چیزی را کجا باید قرار دهید.

انواع مقداری

«انواع مقداری» (Value Types) مفهوم ساده‌ای دارند. در واقع روش تفکر طبیعی ما در مورد اشیا نیز این گونه است. زمانی که چیزی مانند یک چوب گلف را توصیف می‌کنیم، ممکن است بگوییم چوب گلف کوتاه است یا چوب بیس‌بال نارنجی است. انواع مقداری بخش‌هایی از حافظه هستند که شامل مقدار مورد نظر ما است. اگر مقدار 8 را در حافظه ذخیره بکنیم و بعدتر بخواهیم به این مقدار دسترسی داشته باشیم، با مراجعه به این نوع حافظه، مقدار 8 را دریافت خواهیم. همان طور که اشاره کردیم، نوع مقداری مفهوم بسیار ساده‌ای دارد.

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

Reference Types

انواع ارجاعی و اشاره‌گرها

«انواع ارجاعی» (Reference Types) از جهاتی مانند انواع مقداری هستند، چون با فراخوانی‌شان همان مقداری که از انواع مقداری به دست می‌آمد را به دست می‌آورید؛ اما این انواع داده امکانات بیشتری نیز دارند. انواع ارجاعی از «اشاره‌گرها» (Pointers) استفاده می‌کنند تا مقداری که به دنبالش هستید را در اختیار شما قرار دهند.

اگر به برنامه‌های C یا Objective-C نگاه کنید می‌بینید که در همه جا از اشاره‌گرها استفاده شده است. ساختار آن چنین است:

1// same as var age = 21 except this is a pointer in C
2char *name[]= "Bob";
3// In Objective-C it would look like this
4NSString *name = @"Bob";

به این دلیل از مثال زبان C استفاده کردیم که درک آن آسان‌تر است. در قطعه کد فوق در خط 2 یک اشاره‌گر را با استفاده از * ایجاد می‌کنیم. این * به سیستم اعلام می‌کند: «یک آدرس به من بده که بتوانم این مقدار را در حافظه ذخیره کنم.» سپس سیستم در پاسخ فرضاً می‌گوید: «شما می‌توانید از بلوک 3 برای ذخیره‌سازی مقدار مورد نظر استفاده کنید». در نهایت شما عبارت Bob را در بلوک 3 حافظه ذخیره می‌کنید.

اگر با استفاده از دستور ;(printf(name بخواهیم به مقدار مورد نظر دسترسی پیدا بکنیم، این آدرس را مشاهده می‌کنیم که احتمالاً چیزی شبیه 0x03 و شاید کمی طولانی‌تر خواهد بود. این همان بازنمایی هگزادسیمال از آن بلوک حافظه است. اگر بخواهیم مقدار Bob را داشته باشیم باید از دستور زیر استفاده کنیم.

1;printf(*name)

در زبان C اگر بخواهیم مقداری را از هر کجا تغییر بدهیم باید از name& استفاده کنیم که آدرس این مقدار را اعلام می‌کند و می‌توان سپس روی آن عملی انجام داد. زمانی که مقدار تغییر می‌یابد می‌توانید بلافاصله با اشاره به name* از آن استفاده کنید. این خصوصیت بسیار جالب و در عین حال کاملاً خطرناک است. در مورد آن در ادامه بیشتر صحبت خواهیم کرد.

با این که سوئیفت تکیه زیادی روی انواع مقداری دارد؛ اما در آن از انواع ارجاعی نیز استفاده می‌شود. شما در همه اپلیکیشن‌های iPhone یا Mac چه بخواهید و چه نخواهید از هر دو این نوع داده‌ها استفاده خواهید کرد.

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

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

زمانی که یک مقدار مانند Bob را در یک اشاره‌گر C ذخیره می‌سازید، در واقع هر حرف را در یک بخش جدا از حافظه قرار می‌دهید. همه ما می‌دانیم که B واقعاً کجا قرار دارد، کاراکتر «خاتمه دهنده تهی» (0\) به رایانه اعلام می‌کند که چه موقع باید کار چرخش روی مکان‌های حافظه را خاتمه داده و مقداری که به دست آورده را بازگشت دهد. در ادامه وقتی در مورد انواع کالکشن (Collection) صحبت می‌کنیم، در این مورد بیشتر توضیح خواهیم داد. فعلاً همین قدر بدانید که رشته‌ها در زبان سوئیفت از نوع ارجاعی نیستند؛ بلکه دارای نوع مقداری هستند.

همین طور که بحث را ادامه می‌دهیم، احتمالاً ایده‌های جدیدی به ذهن شما می‌رسد. اگر بخواهید زبان‌های C++ ،C یا Objective-C را یاد بگیرید، این اطلاعات برای شما بسیار ارزشمند خواهند بود؛ اما در زبان سوئیفت همه این پیچیدگی‌ها از دید برنامه‌نویس پنهان نگاه داشته شده است و از این رو اصلاً لازم نیست در مورد عملگرهای * و & نگرانی داشته باشید.

انواع ارجاعی می‌توانند در پشته ذخیره شوند؛ اما در هیپ نیز ذخیره می‌شوند. می‌دانیم که یک نوع ارجاعی را می‌توانیم با استفاده از دستور malloc به صورت تخصیص دستی در هیپ ذخیره کنیم.

هر متغیری که با استفاده از دستور malloc ایجاد شود، در زمان خاتمه برنامه به طور خودکار از حافظه خارج نخواهد شد و باید بسته به زبانی که در آن برنامه‌نویسی می‌کنید از دستورهای dealloc یا free برای حذف کردن آن مقدار از حافظه بهره بگیرید، در غیر این صورت این مقدار تا زمان راه‌اندازی مجدد رایانه در حافظه باقی خواهد ماند. این وضعیت به نام «نشت حافظه» (Memory Leak) نامیده می‌شود.

انواع کالکشن

در بخش آخر این راهنما، به بررسی نوع داده «کالکشن» یا «مجموعه‌ای» در زبان برنامه‌نویسی سوئیفت می‌پردازیم. انواع کالکشن به نوعی از داده‌ها گفته می‌شود که کلکسیون یا مجموعه‌ای از آیتم‌ها را در برمی‌گیرند. این نوع داده به طور کلی شامل دو دسته «آرایه» (Array) و «دیکشنری» (Dictionary) می‌شود.

آرایه‌ها

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

1var myArray = ["eggs", "milk", "butter", "cheese"]

در این صورت می‌توانیم با استفاده از ساختار [myArray[0 به هر یک از این آیتم‌ها دسترسی داشته باشیم. این دستور مقدار eggs را بازمی‌گرداند، زیرا مکان 0 از ابتدای آرایه محاسبه می‌شود. با در نظر گرفتن این منطق اگر خواهیم به مقدار Butter دسترسی یابیم، باید از ساختار [myArray[2 استفاده کنیم، زیرا مکان آن 2 واحد از eggs فاصله دارد.

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

1var myArray[2] = "Sugar"

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

1var myIntegerArray = [Int]()

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

1var myDoubleArray: Double = []

جای شگفتی است که این وضعیت در رشته‌های C معنی بیشتری می‌یابد. زمانی که در مثال قبلی خود واژه Bob را در یک رشته C ذخیره کردیم، در واقع یک آرایه از رشته‌ها ساخته‌ایم. اگر از ساختار [name[0 استفاده کنیم، رشته ما مقدار B بازمی‌گرداند چون 0 ابتدایی‌ترین مکان حافظه محسوب می‌شود. در این حالت تنها باید مواظب باشید که از محدوده آرایه خارج نشوید، چون در این صورت با مشکلاتی مواجه می‌شوید. در اغلب موارد، رایانه با ارائه خطای «index out of bounds» یا چیزی مشابه آن به کمک شما می‌آید. با این حال در صورتی که در زمان اجرای برنامه با این وضعیت رو به رو شوید، برنامه از کار می‌افتد.

دیکشنری‌ها

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

1var myDictionary = ["Kevin": "Lead Minion", "Bob", "Determined Minion", "Stewart": "Rocker Minion"

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

1myDictionary["Kevin"]

دقت کنید که با استفاده از همین ساختار می‌توانید یک مقدار را در دیکشنری وارد کنید، همانند آرایه می‌توان از مقدار key برای وارد کردن مقداری در دیکشنری استفاده کرد؛ تنها الزام این است که نوع مقدار وارد شده با نوع دیکشنری متناسب باشد. برای نمونه:

1myDictionary["Bob"] = "King Bob"

یک دستور مجاز است؛ اما دستور زیر یک دستور مجاز نیست زیرا دیکشنری فوق از نوع رشته‌ای یا [String: String] است.

1myDictionary["Bob"] = 2

کاراکتر دو نقطه (:) «کلید» را از «مقدار» جدا می‌کند. می‌توان با استفاده از دستور زیر دیکشنری‌ها را اعلان و وهله‌سازی کرد:

1var myStringDictionary = [String: String]()

همچنین می‌توان از دستور زیر استفاده کرد:

1var myStringIntDictionary: [String: Int] = [:]

هر دو دستور فوق معتبر هستند.

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

سخن پایانی

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

برای مطالعه قسمت بعدی این مطلب روی لینک زیر کلیک کنید:

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

==

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

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