Hashable در سوئیفت چیست؟ — از صفر تا صد

۱۵۹ بازدید
آخرین به‌روزرسانی: ۰۳ مهر ۱۴۰۲
زمان مطالعه: ۱۳ دقیقه
Hashable در سوئیفت چیست؟ — از صفر تا صد

زمانی که با انواع داده مقدماتی در سوئیفت سر و کار می‌یابیم، بارها با پروتکل Hashable مواجه می‌شویم. برای نمونه یک کلید در نوع Dictionary باید از نوع Hashable باشد. یا یک عنصر در نوع داده set نیز باید Hashable باشد. در این مقاله به توضیح مفهوم Hashable در سوئیفت خواهیم پرداخت.

997696

در مثال زیر اعلان‌هایی برای انواع داده دیکشنری و مجموعه می‌بینید که کاربرد Hashable در آن‌ها روشن شده است:

@frozen struct Dictionary<Key, Value> where Key: Hashable
@frozen struct Set<Element> where Element: Hashable

مستندات سوئیفت چندین نوع داده Hashable لیست می‌کند که شامل انواع بولی، صحیح و رشته‌ها می‌شود. این موارد می‌توانند به عنوان کلید برای دیکشنری و همچنین عناصری برای مجموعه (Set) مورد استفاده قرار گیرند.

با این حال روشن نیست که چه چیزی باعث می‌شود این انواع داده Hashable باشند و چگونه می‌توانیم انواع داده Hashable خودمان را بسازیم. شاید هم اساساً از خود بپرسیم که چه نیازی به داشتن انواع داده Hashable وجود دارد؟ آیا این نوع داده‌ها مزیت خاصی دارند؟ سؤال‌های زیادی از این دست می‌توان مطرح نمود.

در ادامه این مقاله یک مرور جامع در مورد پروتکل Hashable در سوئیفت به عمل می‌آوریم و موارد استفاده رایج آن را نشان می‌دهیم. به طور خاص، قصد داریم سؤالات/موضوعات زیر را توضیح دهیم.

  1. هش کردن کلاً به چه معنا است؟
  2. پروتکل Hashable در سوئیفت به چه معنا است؟
  3. چگونه می‌توانیم نوع سفارشی خودمان را بسازیم که با پروتکل Hashable سازگار باشد؟

هش کردن چیست و چرا به آن نیاز داریم؟

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

مفاهیم مقدماتی

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

الگوریتم اصطلاحاً به نام «تابع هش» یا «هش کننده» (Hasher) گفته می‌شود. مقدار تبدیل شده به نام «مقدار هش»، «کد هش» یا صرفاً «هش» نامیده می‌شود.

نمودار فوق یک مثال ساده‌شده از فرایند هش کردن نشان می‌دهد. ما اساساً کار خود را با چهار نوع میوه (آیتم داده‌ای) در سمت چپ آغاز می‌کنیم.

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

چند نکته وجود دارد که باید توجه کنیم:

  1. فرایند هش کردن باید تکرارپذیر باشد. تابع هش در صورت ارائه ورودی یکسان باید همواره همان مقدار هش را تولید کند. هر بار که مقدار هش Apple را می‌خواهیم، تابع هش باید به طور پایدار مقدار 839021 را به ما بازگشت دهد.
  2. مقادیر هش باید یکتا باشند. Apple ،Orange ،Peach و Pineapple همگی به مقادیر هش متمایزی نگاشت می‌شوند. زمانی که از تابع هش می‌خواهیم مقدار هش یک میوه جدید را محاسبه کند، تابع باید مقداری مانند 5423781 تولید کند که از همه مقادیر هش موجود برای میوه‌های دیگر متفاوت است.
  3. مقادیر هش باید به ظاهر تصادفی باشند. اگر بخواهیم از اصطلاح‌های فنی استفاده کنیم، مقادیر هش نباید به سادگی معکوس شوند تا بتوان آیتم‌های داده اصلی را از روی آن‌ها بازتولید کرد. از این رو مقادیر هش باید تصادفی باشند تا احتمال معکوس سازی به کمترین مقدار برسد.
  4. مقادیر هش نباید اعداد صحیح مثبت باشند. در نمودار فوق از اعداد صحیح صرفاً برای نمایش مفهوم کلی استفاده کردیم. تابع هش خروجی (یعنی مقادیر هش) را تعیین می‌کند و از این رو بسته به خود تابع هش، مقادیر هش می‌توانند اعداد صحیح منفی نیز باشند و یا شامل اعداد، حروف، و حتی نمادها باشند.

کاربردها

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

عملیات جستجو در پایگاه داده

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

به مثال زیر توجه کنید. فرض کنید لیستی از کسب‌وکارهای کوچک محلی در یک جدول پایگاه دارید. یک روز می‌خواهید تا خودروی خود را سرویس کنید و به یاد می‌آورید که یکی از دوستانتان تعمیرگاهی به نام John’s Mechanic را به شما پیشنهاد کرده است:

Hashable در سوئیفت

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

زمانی که با استفاده از نام تعمیرگاه در جدول جستجو می‌کنید، پایگاه داده باید رشته 15 کاراکتری را با فیلد نام کسب‌وکار برای همه رکوردها مقایسه کند.

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

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

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

اکنون زمانی که درخواست جستجو با استفاده از نام John’s Mechanic ارسال می‌شود، پایگاه داده ابتدا از تابع هش برای محاسبه مقدار هش آیتم مورد جستجو استفاده می‌کند.

چنان که پیش‌تر اشاره کردیم، انتظار می‌رود که تابع هش برای ورودی‌های یکسان همواره مقادیر هش یکسانی تولید کند. به این ترتیب می‌توانیم اندیس آیتم مورد نظر را در پایگاه داده یافته و رکورد آن را پیدا کنیم.

به طور میانگین پیچیدگی زمانی تابع جستجو با استفاده از هش کردن برابر با O(1) است، یعنی مقدار زمان ثابت است و به اندازه داده‌ها بستگی ندارد.

Hashable در سوئیفت

کاربرد رمزنگاری رمز عبور

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

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

بدون استفاده از هش کردن

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

با استفاده از هش کردن

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

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

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

Hashable در سوئیفت

ساختمان داده دیکشنری در برنامه‌نویسی

تقریباً همه زبان‌های برنامه‌نویسی مدرن از ساختمان داده دیکشنری استفاده می‌کنند، اما ممکن است نام آن متفاوت باشد، مثلاً ممکن است از نام‌هایی مانند «آرایه انجمنی» (associative array) «نقشه» (Map) ،hashmap ،hash یا object استفاده کنند.

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

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

1// instantiate a dictionary
2var personalInfo = ["firstName": "John", "address": "1100 Main St."]
3// data retrieval
4let firstName = personalInfo["firstName"]
5let middleName = personalInfo["middleName"]
6// data insertion
7personalInfo["lastName"] = "Smith"
8personalInfo["firstName"] = "Mike"
9// data deletion
10let deletedAddress = personalInfo.removeValue(forKey: "address")
11let deletedMiddleName = personalInfo.removeValue(forKey: "middleName")

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

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

اگر چنین باشد، آیتم داده‌ای متناظر بازگشت می‌یابد. اگر چنین نباشد، مقدار تهی (nil) بازمی‌گردد. به همین دلیل است که نوع داده بازگشتی دیکشنری به صورت optional است، چون موجود بودن مقدار بازگشتی تضمین‌شده نیست.

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

پروتکل Hashable

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

هنگامی که به صفحه مستندات پروتکل Hashable مراجعه می‌کنیم (+)، متوجه می‌شویم که پروتکل Hashable به صورت زیر تعریف شده است:

Hashable نوعی است که می‌تواند به صورت یک Hasher هش شود تا یک مقدار هش صحیح تولید کند.

تعریف آن کاملاً روشن است و می‌توانیم کلیدواژه‌های مورد اشاره یعنی نوع، Hashable و عدد صحیح را به روشنی تشخیص دهیم و در ادامه هر کدام از آن‌ها را بیشتر توضیح می‌دهیم.

نوع

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

با این حال معنی عمیق‌تر گفته فوق این است که نوع concrete این نوع داده ژنریک Set باید با پروتکل Hashable سازگار باشد. به بیان دیگر مانند همه پروتکل‌های دیگر، پروتکل Hashable باید در سطح نوع (یعنی class ،struct) پیاده‌سازی شده باشد به طوری که همه وهله‌ها قابل هش باشند.

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

Hasher

Hasher در مستندات سوئیفت (+) به صورت یک تابع هش سراسری تعریف شده است که از سوی انواع Set و Dictionary مورد استفاده قرار می‌گیرد. Hasher در پشت صحنه عملاً به صورت یک struct پیاده‌سازی شده است و از این رو می‌توانیم به سادگی یک وهله از Hasher را با حالت خالی با فراخوانی ()var hasher = Hasher بسازیم. تابع هش استاندارد سراسری از یک مقدار seed 125 بیتی تصادفی استفاده می‌کند و بدین ترتیب میزان پیش‌بینی پذیری مقادیر هش کاهش می‌یابد. به بیان دقیق‌تر، تابع هش سراسری مورد استفاده در کتابخانه استاندارد سوئیفت SipHash (+) است که در ابتدا از سوی «جین-فیلیپ آئوماسون» (Jean-Philippe Aumasson) و «دنیل جی. برنشتاین» (Daniel J. Bernstein) در سال 2012 توسعه یافته است.

هم‌اکنون SipHash-1-3 با 320 بیت حالت در کتابخانه جاری پیاده‌سازی شده است. لازم به اشاره است که اپل هشدار داده است تابع هش سراسری و پیاده‌سازی آن ممکن است در هر نسخه جدید تغییر یابد. در این وهله از Hasher می‌توانیم داده‌ها (یعنی اعداد صحیح، رشته‌ها) را با تغییر دادن متد combine(bytes:) یا (:_)combine به دفعات مختلف به میزان دلخواه وارد کنیم. در متد combine(bytes:) اقدام به ارائه بایت‌های جدیدی از داده‌ها به Hahser می‌کنیم که در منطقه مجاور منفردی از حافظه حضور دارند و بیت‌های آن را با حالت Hahser ترکیب می‌کنیم.

متد (:_)combine صرفاً یک عملیات آسان اجرا می‌کند که یک مقدار Hashable مانند عدد صحیح یا رشته می‌گیرد. این متد در پشت صحنه متد hash(into:) را فراخوانی می‌کند تا مقدار آن را با حالت Hashet ترکیب کند. زمانی که کار با متدهای combine برای به‌روزرسانی حالت Hasher پایان یافت، متد finalize فراخوانی خواهد شد که حالت Hasher را نهایی می‌کند و مقدار هش را بازگشت می‌دهد. فراخوانی متد فوق موجب می‌شود که Hasher مصرف شود. از این رو توصیه شده است که یک Hasher که مالک آن نیستید را نهایی نکنید و یا روی یک Hasher نهایی شده عملیاتی اجرا نکنید.

جنبه مهم دیگر متد finalize این است که فرایند نهایی‌سازی از نظر محاسباتی پرهزینه است. در SipHash-1-3 این هزینه سه برابر عملیات combine 64 بیتی است. از این رو باید به خاطر داشته باشیم که باید استفاده از finalize را در حد امکان در زمان پیاده‌سازی پروتکل Hashable در انواع سفارشی خود محدود کنیم تا کد بهینه‌ای داشته باشیم.

عدد صحیح

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

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

اگر دو وهله از hashValue یکسان باشند چه اتفاقی رخ می‌دهد؟ این پدیده به نام «تصادم» (collision) شناخته می‌شود. چند روش برای حل مشکل تصادم هش وجود دارند. یک رویکرد رایج «زنجیره‌سازی» (chaining) نام دارد. به این ترتیب به جای یک آیتم منفرد در هر Bucket، جدول هش می‌تواند شامل چندین آیتم زنجیرشده با هم باشد به طوری که هر کلید هش یک ارجاع به یک لیست پیوندی خواهد داشت.

رویکرد رایج دیگر «کاوش خطی» (linear probing) یا «آدرس‌دهی باز» (open addressing) است که البته به صورت داخلی از سوی نوع Dictionary در سوئیفت برای مدیریت تصادم پیاده‌سازی شده است. ابن متد اساس مشکل تصادم را با استفاده از اسلات (یا Bucket) بعدی موجود حل می‌کند. بدین ترتیب اگر به انتها برسد به آغاز بازمی‌گردد تا این که یک Bucket برای آیتم پیدا کند.

سازگاری با پروتکل Hashable

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

ساختمان داده ساده

ابتدا یک struct ساده به نام Student با دو مشخصه رشته‌ای به نام‌های firstName و lastName می‌سازیم. فرض کنید می‌خواهیم از یک دیکشنری برای ردگیری نمرات امتحان نهایی دانش آموزان استفاده کنیم. بدین ترتیب یک دیکشنری به صورت زیر وهله‌سازی می‌کنیم:

1var scores = [Student: Int]()

با این حال Xcode خطای زیر را به ما نشان می‌دهد:

Type ‘Student’ does not conform to protocol ‘Hashable’

توجه داشته باشید که در زمان نگارش این مقاله از Xcode نسخه 11.3 و سوئیفت 5.1 استفاده می‌کنیم:

1struct Student {
2    var firstName: String
3    var lastName: String
4}

برای حذف این خطا باید متد hash(into:) را که از سوی پروتکل Hashable الزام شده است به صورت زیر پیاده‌سازی کنیم. تابع هش سراسری hasher از متد combine برای هش کردن مقادیر رشته‌ای نام و نام خانوادگی دانش‌آموزان استفاده می‌کند.

1extension Student : Hashable {
2    func hash(into hasher: inout Hasher) {
3        hasher.combine(firstName)
4        hasher.combine(lastName)
5    }
6}

باید توجه کنید که ما متد finalize را در متد hash(into:) فراخوانی نکردیم، زیرا کامپایلر به طور خودکار محاسبه را با فراخوانی متد finalize با استفاده از کد زیر در زمان نیاز به مقدار هش تکمیل خواهد کرد.

1var hashValue: Int {
2    var hasher = Hasher()
3    self.hash(into: &hasher)
4    return hasher.finalize()
5}

پس از این که نوع داده سفارشی Student را با پروتکل Hashable سازگار کردیم، خطای فوق‌الذکر از بین می‌رود. از این لحظه به بعد هر وهله از Student می‌تواند یک کلید Dictionary یا یک عنصر در Set باشد.

ساختمان داده ترکیبی

فرض کنید در مدرسه معلم تصمیم می‌گیرد دانش آموزان را دوبه‌دو با هم گروه‌بندی کند تا روی پروژه‌های درسی کار کنند. برای کمک به مدیریت تعیین گروه‌ها و ردگیری اعضای گروه می‌توانیم یک Struct جدید به نام Dyad به صورت زیر بسازیم:

1struct Dyad {
2    var leader: Student
3    var teammate: Student
4}

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

1var dyadScores = [Dyad: Int]()

در این مورد نیز Xcode به ما اعلام می‌کند که Dyad با پروتکل Hashable سازگاری ندارد. برخی توسعه‌دهندگان که تجربه قبلی در پیاده‌سازی پروتکل Hashable دارند، ممکن است به روش زیر عمل کنند:

1extension Dyad: Hashable {
2    func hash(into hasher: inout Hasher) {
3        hasher.combine(leader.hashValue)
4        hasher.combine(teammate.hashValue)
5    }
6}

البته رویکرد فوق قطعاً نادرست نیست، اما چنان که پیش‌تر اشاره کردیم، فرایند نهایی سازی کاملاً پرهزینه است. می‌توانیم تخمین بزنیم که چه مقدار زمان برای محاسبه hashValue برای یک Dyad مورد نیاز است. فرض کنید که زمان مورد نیاز برای اجرای یک متد combine برابر با t واحد زمانی باشد، بنابراین زمان مورد نیاز برای اجرای متد finalize برابر با 3t خواهد بود. برای محاسبه hashValue برای یک Student به 5t زمان نیاز داریم، چون دو فراخوانی به combine و یک فراخوانی به finalize صورت می‌گیرد.

بنابراین در کد فوق زمان مورد نیاز برای محاسبه hashValue مربوط به Dyas در مجموع برابر با 15t خواهد بود. 5t برای هر رهبر، 5t برای هر عضو گروه و 5t برای خود Dyad. آیا می‌توانیم این کد را بهینه‌تر سازیم؟ پاسخ مثبت است. در کد زیر استفاده از متد finalize به کمترین مقدار ممکن رسیده است و به این جهت زمان مورد نیاز برای محاسبه hashValue مربوط به Dyad به مقدار زیادی کاهش یافته است. در این نسخه بهبودیافته، زمان مورد نیاز تنها برابر با 7t است که از نیمی از زمان مورد نیاز در نسخه قبلی نیز کمتر است.

بنابراین یکی از پیام‌های مهمی که از این مسئله می‌گیریم این است که وقتی نوع سفارشی را با پروتکل Hashable سازگار می‌سازیم، باید تابع hash(into:) را با کمترین مقدار استفاده از فراخوانی متد finalize طراحی کنیم.

1extension Dyad: Hashable {
2    func hash(into hasher: inout Hasher) {
3        hasher.combine(leader.firstName)
4        hasher.combine(leader.lastName)
5        hasher.combine(teammate.firstName)
6        hasher.combine(teammate.lastName)
7    }
8}

سنتز خودکار

از نسخه سوئیفت 4.1 به بعد عملاً سنتز خودکار سازگاری Hashable از سوی کامپایلر برای انواع سفارشی شامل struct پشتیبانی می‌شود. از این رو اگر صرفاً Student را برای سازگاری با پروتکل Hashable و بدون استفاده از یک بسط اعلان کنیم، از آنجا که هر دو مشخصه ذخیره شده در آن یعنی firstName و lastName به صورت Hashable هستند، خود struct نیز به صوت خودکار Hashable می‌شود.

بنابراین نیازی به نوشتن پیاده‌سازی‌های hash(into:) یا hashValue وجود ندارد. به خصوص در مورد گزینه دوم سوئیفت 5 اقدام به منسوخ ساختن hashValue به نفع hash(into:) کرده است. با این حال همچنان کدی که در آن hashValue در انواع سفارشی برای سازگاری با پروتکل Hashable پیاده‌سازی شده باشد، کامپایل خواهد شد.

1struct Student : Hashable {
2    var firstName: String
3    var lastName: String
4}

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

1struct Dyad : Hashable {
2    var leader: Student
3    var teammate: Student
4}

سخن پایانی

امیدواریم با مطالعه این راهنما درک بهتری از پروتکل Hashable در سوئیفت و پس‌زمینه وسیع‌تر آن به دست آورده باشید. با پیاده‌سازی تابع هش سراسری و سنتز خودکار در نسخه کنونی سوئیفت، امکان پیاده‌سازی پروتکل Hashable هر چه بیشتر تسهیل شده است.

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

==

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

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