ساخت الگوریتم های ژنریک (Generic) در سوئيفت — به زبان ساده
با معرفی Swift یک سری از ابزارهای جدید نیز همراه با آن عرضه شدند که باعث شدهاند تجربه کدنویسی سادهتر و گویاتر شود. Swift علاوه بر دارا بودن ساختار ساده، همه جنبههای موفق زبانهای دیگر برنامهنویسی را نیز در خود جمع کرده است تا از خطاهای رایج برنامهنویسی مانند «استثنای اشارهگر تهی» (null pointer exceptions) و نشت حافظه (memory leaks) جلوگیری کند.
به طور عکس زبان Objective-C غالباً به خاطر عدم وجود نظم و ترتیب در کدنویسی یاد میشود. با این که Objective-C زبانی قوی و پایدار است؛ اما در زمان اجرا خطاهای زیادی در آن رخ میدهد. این تأخیر در کشف خطا، معمولاً ناشی از اشتباههای برنامهنویسی در ارتباط با مدیریت حافظه و عملیات تبدیل نوع است. ما در این مقاله به بررسی تکنیک جدید طراحی به نام ژنریک (Generic) میپردازیم و توضیح میدهیم که چگونه این تکنیک باعث میشود کد مفیدتر و امنتر شود.
ساخت فریمورکها
همان طور که در ادامه ملاحظه خواهیم کرد، ساختمانهای دادهای مانند لیستهای پیوندی، درختهای دودویی و جدولهای هَش (Hash) یک نقشه اولیه برای پردازش و تحلیل داده ارائه میکنند. ساختمانهای داده نیز همچون هر برنامه با طراحی مناسب باید با در نظر گرفتن قابلیت توسعه و استفاده مجدد طراحی شوند.
برای روشنتر شدن موضوع فرض کنید مشغول ساخت یک سرویس ساده هستید که گروهی از دانشجویان را فهرست میکند. این دادهها میتوانند به سادگی با استفاده از ساختمان داده از نوع لیست پیوندی سازماندهی شده و به روش زیر نمایش یابند:
//basic structure class StudentNode { var key: Student? var next: StudentNode? }
چالش
با این که این ساختار از نوع توصیفی است؛ اما قابلیت استفاده مجدد ندارد. به بیان دیگر این ساختار برای فهرستبندی دانشآموزان کاربرد دارد؛ اما با استفاده از آن نمیتوان هیچ نوع داده دیگری مثلاً معلمان را مدیریت کرد. خصوصیت دانشآموز (Student) یک کلاس است که میتواند شامل مشخصات خاص دیگری مانند نام، زمانبندی و مقطع تحصیلی نیز باشد. اگر تلاش شود از این کلاس برای سازماندهی معلمان استفاده شود، این وضعیت موجب عدم مطابقت نوع خواهد شد.
این مشکل از طریق وراثت قابل حل است؛ اما همچنان هدف اصلی ما که قابلیت استفاده مجدد از کد است اجابت نمیشود. Generic-ها امکان ایجاد ساختارهای generic را میدهند به طوری که میتوان از آنها به روشهای متفاوتی استفاده کرد.
کلاسهای Generic
علاوه بر ساختمانهای داده و الگوریتمها، تابعهای اصلی Swift مانند آرایهها و دیکشنریها نیز از ژنریکها بهره میگیرند. برای مثال با استفاده از ژنریکها میتوان StudentNode را طوری بازنویسی کرد که قابلیت استفاده مجدد نیز پیدا کند.
//refactored structure class Person<T> { var key: T? var next: Person<T>? }
در اینجا چندین تغییر مهم با ساختار معکوس را شاهد هستیم. نام کلاس StudentNode به چیزی تغییر یافته است که عمومیتر محسوب میشود (مانند Person). ساختار <T> که پس از نام کلاس دیده میشود به صورت یک placeholder نامیده شده است. در ژنریک ها، مقادیری که درون <> مشاهده میشوند، به صورت متغیر اعلان شدهاند. زمانی که Placeholder به نام T ایجاد میشود، میتوان در هر جای دیگری که یک ارجاع به کلاس را قبول میکند از آن استفاده کرد. در این مثال ما نوع کلاس Student را با placeholder ژنریک T عوض کردهایم.
قدرت ژنریکها را میتوان در پیادهسازی آنها مشاهده کرد. زمانی که کلاس بازنویسی میشود، Person میتواند لیستهای دانشآموزان، معلمان و هر نوع دیگری که دوست داشته باشیم را مدیریت کند.
//a new student var studentNode = Person<Student>() //a new teacher var teacherNode = Person<Teacher>()
تابعهای Generic
علاوه بر کلاسها، تابعهای ژنریک نیز میتوان ایجاد کرد. میدانیم که الگوریتمهایی مانند insertionSort و bubbleSort به مرتبسازی مجموعههایی از اعداد تصادفی میپردازند. با استفاده از عناصر آرایه ژنریک، این الگورتیمها میتوانند از هر نوعی که با پروتکل Comparable مطابقت داشته باشد استفاده کنند. این موارد میتوانند شامل کاراکترهای مبتنی بر Swift و همچنین اعداد باشد:
extension Array where Element: Comparable { func insertionSort() -> Array<Element> { //check for trivial case guard self.count > 1 else { return self } var output: Array<Element> = self for primaryindex in 0..<output.count { let key = output[primaryindex] var secondaryindex = primaryindex while secondaryindex > -1 { if key < output[secondaryindex] { //move to correct position output.remove(at: secondaryindex + 1) output.insert(key, at: secondaryindex) } secondaryindex -= 1 } } return output } } //execute sort let results: Array<Int> = numberList.insertionSort()
افزونههای Generic
ژنریکها میتوانند روی پروتکلها نیز اعمال شوند. پیادهسازی آنها همانند افزونههای از نوع معمولی در اغلب موارد همراه با افزونههای پروتکل مشاهده میشود. پروتکلها همانند زبانهای دیگر به تعریف قواعدی برای شیءهای خاص کمک میکنند. در Swift این قواعد میتوانند با پیادهسازیشان به صورت زیر بسط یابند:
protocol Sortable { func isSorted<T: Comparable>(_ sequence: Array<T>) -> Bool } //protocol extension extension Sortable { func isSorted<T: Comparable>(_ sequence: Array<T>) -> Bool { //check trivial cases guard sequence.count <= 1 else { return true } var index = sequence.startIndex //compare sequence values while index < sequence.endIndex - 1 { if sequence[index] > sequence[sequence.index(after: index)] { return false } index = sequence.index(after: index) } return true } }
برنامهنویسی مبتنی بر پروتکل
این پیادهسازی نشان میدهد که تابعهای ژنریک (مانند isSorted) چگونه میتوانند برای تست انواع مختلفی از توالی آرایه استفاده شوند. به جای این که isSorted به یک کلاس یا نوع منفرد بچسبد، میتوان در هر شیئی که با پروتکل Sortable مطابقت دارد از آن استفاده کرد:
class QuickTest: XCTestCase, Sortable { func testDecendingQSort() { var sequence: Array<Int> = [8, 7, 6, 5, 4, 3, 2, 1] let results = sequence.quickSort() //evaluate results processQuickResults(with: results) } //test using protocol method func processQuickResults<T: Comparable>(with sequence: Array<T>) { XCTAssertTrue(isSorted(sequence), "test failed”) } }
اگر این مطلب برایتان مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش برنامه نویسی Swift (سوئیفت) برای برنامه نویسی iOS
- مجموعه آموزشهای مهندسی نرم افزار
- آرایه ها در زبان برنامه نویسی سوئیفت (Swift) — به زبان ساده
- آموزش آرایه در برنامه نویسی Swift (سوئیفت)
- وراثت کلاس و ترکیب بندی در زبان برنامه نویسی سوئیفت — به زبان ساده
==