ساخت الگوریتم های ژنریک (Generic) در سوئيفت — به زبان ساده

۵۸ بازدید
آخرین به‌روزرسانی: ۲۷ شهریور ۱۴۰۲
زمان مطالعه: ۴ دقیقه
ساخت الگوریتم های ژنریک (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-algorithms-data-structures
نظر شما چیست؟

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