طرح‌چه

جنریک ها (generics) | گولنگ به زبان ساده

۳ خرداد ۱۴۰۴

جنریک ها (generics) | گولنگ به زبان ساده
توی این قسمت با جنریک ها و نحوه استفاده از آنها آشنا میشیم.

پشتیبانی از Generics در نسخه ۱.۱۸ به گولنگ اضافه شد. Generics یا نوع‌های پارامتریک (Parametric Polymorphism) یکی از ویژگی‌هایی است که امکان نوشتن توابع، ساختارهایی را فراهم می‌کند که می‌توانند با انواع مختلف داده کار کنند بدون اینکه مجبور باشیم برای هر نوع داده، کد جداگانه‌ای بنویسیم.

دنیای بدون جنریک ها

تا قبل از معرفی جنریک ها, اگر نیاز داشتیم تابعی داشته باشیم که با نوع های مختلفی از داده ها کار کند باید به ازای هر نوع داده, کدهای تابع را چندین بار تکرار می‌کردیم.

برای مثال فرض کنید می‌خواهید تابعی داشته باشید که دو عدد را به عنوان پارامتر ورودی میگیرد و از بین این دو عدد بزرگترین مقدار را برمی‌گرداند. این تابع رو می‌توانیم به صورت زیر برای نوع های int و float64 داشته باشیم:

package main

import "fmt"

func main() {
    fmt.Println(maxInt(3, 5)) // 5
    fmt.Println(maxFloat64(3.5, 1.3)) // 3.5
}

func maxInt(a, b int) int {
    if a > b {
        return a
    }
	
    return b
}

func maxFloat64(a, b float64) float64 {
    if a > b {
        return a
    }
	
    return b
}

در مثال بالا به دلیل اینکه هرکدام از تابع های ما نمی‌توانند هر دو نوع float64 و int رو عنوان پارامتر ورودی یا خروجی بپذیرند, مجبور شدیم دوبار یک کد کاملا یکسان رو تکرار کنیم.

برای اینکه در مثال قبل از تکرار پرهیز کنیم  راه حل های دیگری نیز وجود دارد:

  • استفاده از reflection
  • استفاده از type assertion/interface assertion
  • استفاده از code generation 

هرکدوم از روش های بالا مشکلاتی دارند و راه حل تمیزی نیستن. برای مثال اگر از type assertion / interface assertion استفاده کنیم کد ما به صورت زیر خواهد بود:

package main

import "fmt"

func main() {
    var result int
	
    result = max(3, 5).(int)
	
    fmt.Println(result) // 5
}

func max(a, b any) any {
    switch a.(type) {
        case int:
            x := a.(int)
            y := b.(int)
			
            if x > y {
                return x
            }
			
            return y
		case float64:
            x := a.(float64)
            y := b.(float64)
			
            if x > y {
                return x
            }
			
            return y
		default:
            panic("'a' and 'b' both should be int or float64")
	}
}

کد بالا به درستی کار میکند اما مشکلاتی دارد:

  • استفاده از any باعث میشود که خوانایی برنامه پایین بیاید: در هنگام استفاده از تابع max باید ابتدا پیاده سازی آن را چک کنیم تا متوجه شویم چه دیتا تایپ هایی را پشتیبانی می‌کند.

  • دشوار شدن نگهداری و توسعه ی کد: در هنگام کامپایل نمیتوان خطاهای احتمالی را کشف کرد. در صورتی که اشتباها تابع max را با نوعی از داده کال کنیم که آن را پیاده سازی نمیکند با panic در زمان اجرای برنامه (runtime) مواجهه خواهیم شد.

  • تکرار شدن کد: همچنان میتونیم ببینیم کدی که برای مقایسه ی دو مقدار عددی نوشتیم به ازای هر دیتا تایپ int و float64 تکرار شده است.

تابع های جنریک (Generic functions)

جنریک در Go به کمک type parameters تعریف می‌شود. یعنی می‌توان برای تابع، نوعی کلی تعریف کرد و بعداً هنگام استفاده، نوع واقعی را مشخص کرد.

با استفاده از Generics میتوانیم تابع max را به صورت زیر بازنویسی کنیم:

package main

import "fmt"

func main() {
    fmt.Println(max(3, 5)) // 5
    fmt.Println(max(6.5, 5)) // 6.5
}

func max[T int | float64](a, b T) T {
    if a > b {
        return a
    }
	
    return b
}

در مثال بالا [int |float] لیستِ نوعِ آرگومان ها است, همچنین int و float هرکدام یک نوعِ آرگومان محسوب می‌شوند. بنابراین نوعِ T میتواند هرکدام از نوع های float یا int باشد. تابع max یک نوعِ پارامتریک یا جنریک محسوب می‌شود.

در هنگام استفاده از تابع max میتوانیم نوعِ پارامترهای ورودی و خروجی را با مشخص کردن نوع T کنترل کنیم. در مثال زیر نوع پارامتر T را int در نظر گرفتیم بنابراین هردو پارامتر ورودی تابع باید از نوع int باشند, در غیر اینصورت خطای زمان کامپایل رخ می‌دهد.

package main

import "fmt"

func main() {
	result := max[int](6, 8)
    fmt.Println(result) // 8
}

func max[T int | float64](a, b T) T {
    if a > b {
        return a
    }

    return b
}

تایپ های جنریک (Generic types)

با استفاده از پارامتر لیست میتوان تایپ های جنریک ایجاد کرد. فرض کنید میخواهیم یک slice جنریک به نام List[T] داشته باشم, میتوانیم آن را به صورت زیر پیاده سازی کنیم:

package main

import "fmt"

type List[T int|float64][]T

func main() {
    numbers1 := List[int]{1, 2, 3, 4, 5}
    fmt.Println(numbers1)

    numbers2 := List[float64]{1.5, 2.5, 3.5}
    fmt.Println(numbers2)
}

متد های جنریک (Generic methods)

اگر نوع داده‌, جنریک باشد (مثل List[T]), میتوان متدهایی نوشت که از پارامتر تایپ‌ های آن استفاده کنند.

package main

import "fmt"

type List[T int|float64][]T

func (l List[T]) First() T {
	return l[0]
}

func main() {
	l := List[int]{2, 4, 6, 8, 10}
	fmt.Println(l.First()) // 2
}

بر خلافِ تابع ها, متدها نمی‌توانند دارای لیست پارامتر تایپ باشند اما میتوانند از پارامتر تایپِ نوعِ داده‌ی خود استفاده کنند, به عنوان مثال کد زیر با خطای زمان کامپایل مواجه می‌شود:

package main

import "fmt"

type List[T int|float64][]T

func (l List[T]) Index[V int|float64](i V) int {
	for index := range l {
		if l[index] == i {
			return index
		}
	}
	
	return -1 // index not found
}

func main() {
	l := List[int]{2, 4, 6, 8, 10}
	fmt.Println(l.Index(4))
}

قسمت قبل: اینترفیس (interface) |‌ گولنگ به زبان ساده

قسمت بعد: به زودی…


دیدگاه ها