Anophel-آنوفل ژنریک ها (Generics) در Go : بررسی عمیق

ژنریک ها (Generics) در Go : بررسی عمیق

تاریخ انتشار:
Go
زمان مطالعه: 19 دقیقه

در Go 1.18، این زبان ویژگی جدیدی به نام انواع generic (که معمولاً، ژنریک شناخته می‌شود) معرفی کرد که مدتی در لیست آرزوهای توسعه‌دهندگان Go قرار داشت. در برنامه نویسی، نوع generic، نوعی است که می تواند همراه با چندین نوع دیگر استفاده شود. معمولاً در Go، اگر می‌خواهید بتوانید از دو نوع مختلف برای یک متغیر استفاده کنید، باید از یک رابط خاص مانند io.Reader یا استفاده از {}interface استفاده کنید که اجازه می‌دهد از هر مقدار استفاده شود. . استفاده از {}interface می‌تواند کار با آن انواع را چالش‌برانگیز کند، زیرا برای تعامل با آنها باید بین چندین نوع بالقوه دیگر ترجمه کنید. استفاده از انواع generic به شما این امکان را می دهد که مستقیماً با انواع خود تعامل داشته باشید که منجر به کدهای تمیزتر و خواناتر می شود.


در این مقاله از آنوفل، برنامه ای ایجاد می کنید که با یک دسته کارت تعامل دارد. شما با ایجاد یک دسته که از {}interface برای تعامل با کارت ها استفاده می کند، شروع می کنید و سپس آن را برای استفاده از انواع ژنریک به روز می کنید. پس از این به‌روزرسانی‌ها، با استفاده از ابزارهای ژنریک، نوع دوم کارت را به دسته خود اضافه می‌کنید، و سپس دسته خود را به‌روزرسانی می‌کنید.

 

ژنریک در Go چیست؟

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


قبل از معرفی ژنریک ها، برای دستیابی به عملکردهای مشابه، مجبور بودید از رابط ها و type assertions استفاده کنید. با این حال، این روش دارای معایبی بود، مانند عدم ایمنی نوع و افزایش کد boilerplate.

 

پارامترهای نوع و محدودیت‌های نوع در Go

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

 

بیایید یک تابع ژنریک ساده برای درک سینتکس آن تعریف کنیم:

 

package main

import "fmt"

func PrintSlice[T any](s []T) {
    for _, v := range s {
        fmt.Println(v)
    }
}

func main() {
    intSlice := []int{1, 2, 3}
    stringSlice := []string{"hello", "world"}

    PrintSlice[int](intSlice)
    PrintSlice[string](stringSlice)
}

 

در تابع PrintSlice، T یک پارامتر نوع و any یک محدودیت نوع است. هر محدودیتی در زبان تعبیه شده است و امکان استفاده از هر نوع را فراهم می کند. تابع را می توان با هر نوع اسلایس، مانند []int یا []string فراخوانی کرد.

 

پیاده سازی توابع ژنریک

اکنون که درک اولیه ای از سینتکس داریم، اجازه دهید یک تابع Min ژنریک را پیاده سازی کنیم که حداقل عنصر را از یک اسلایس برمی گرداند.

 

package main

import (
    "fmt"
    "constraints"
)

func Min[T constraints.Ordered](s []T) T {
    if len(s) == 0 {
        panic("empty slice")
    }

    min := s[0]
    for _, v := range s {
        if v < min {
            min = v
        }
    }
    return min
}

func main() {
    intSlice := []int{3, 1, 4, 2}
    floatSlice := []float64{3.14, 1.61, 4.67, 2.71}

    fmt.Println(Min[int](intSlice))         // Output: 1
    fmt.Println(Min[float64](floatSlice))   // Output: 1.61
}

 

در اینجا، از محدودیت‌ها استفاده کردیم. برای اطمینان از اینکه نوع T از عملیات سفارشی پشتیبانی می‌کند، از محدودیت نوع Ordered استفاده کردیم.

 

پیاده سازی ساختارهای داده ژنریک

ژنریک ها همچنین می توانند برای ایجاد ساختارهای داده قابل استفاده مجدد استفاده شوند. در اینجا مثالی از یک استک ژنریک ساده آورده شده است:

 

package main

import "fmt"

type Stack[T any] struct {
    data []T
}

func (s *Stack[T]) Push(v T) {
    s.data = append(s.data, v)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.data) == 0 {
        var zero T
        return zero, false
    }
    lastIndex := len(s.data) - 1
    value := s.data[lastIndex]
    s.data = s.data[:lastIndex]
    return value, true
}

func (s *Stack[T]) Size() int {
    return len(s.data)
}

func main() {
    intStack := Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    intStack.Push(3)

    fmt.Println(intStack.Pop()) // Output: 3, true
    fmt.Println(intStack.Size()) // Output: 2

    stringStack := Stack[string]{}
    stringStack.Push("hello")
    stringStack.Push("world")

    fmt.Println(stringStack.Pop()) // Output: world, true
    fmt.Println(stringStack.Size()) // Output: 1
}

 

در این مثال، ما یک نوع Stack ژنریک را با پارامتر نوع T و یک محدودیت any تعریف می کنیم. سپس متد های Push، Pop و Size را روی این نوع ژنریک پیاده‌سازی می‌کنیم. تابع اصلی نحوه ایجاد و استفاده از استک ها از انواع مختلف را نشان می دهد: int و string.


موارد استفاده از ژنریک در Go

ژنریک ها را می توان در طیف گسترده ای از موارد استفاده در دنیای واقعی اعمال کرد، مانند:


نوشتن الگوریتم‌های ژنریک، مانند مرتب‌سازی، جستجو یا فیلتر کردن
ایجاد ساختارهای داده ژنریک، مانند لیست‌های لینکی، trees یا صف‌ها
توسعه توابع utility قابل استفاده مجدد برای رسیدگی به خطا، ثبت یا ذخیره سازی


مشکلات و بهترین شیوه ها در ژنریک

در حالی که ژنریک ها می توانند کد شما را مختصرتر و قابل استفاده مجدد کنند، اما باید با احتیاط از آنها استفاده کرد. استفاده بیش از حد از ژنریک ها می تواند منجر به کدهایی شود که درک و نگهداری آن سخت تر است. در اینجا برخی از بهترین روش ها برای دنبال کردن آمده است:


زمانی که کدهای شما را ساده می کنند و قابلیت استفاده مجدد را بدون به خطر انداختن خوانایی، بهبود می بخشند، از کلیات استفاده کنید.
از استفاده از محدودیت‌های نوع بیش از حد پیچیده خودداری کنید، زیرا می‌توانند درک کد شما را سخت‌تر کنند.
هنگام طراحی APIهای ژنریک، مبادلات بین انعطاف پذیری و پیچیدگی را در نظر بگیرید.

 

Go مجموعه ها بدون ژنریک

یکی از ویژگی‌های قدرتمند Go توانایی آن در نمایش انعطاف‌پذیر بسیاری از انواع با استفاده از رابط‌ها است. بسیاری از کدهای نوشته شده در Go می توانند به خوبی با استفاده از functionality interfaces ارائه شده به خوبی کار کنند. این یکی از دلایلی است که Go برای مدت طولانی بدون پشتیبانی از ژنریک وجود داشته است.


در این بخش، شما یک برنامه Go ایجاد می کنید که شبیه سازی گرفتن یک PlayingCard تصادفی از یک دسته کارت است. در این بخش، از یک {}interface استفاده می‌کنید تا به Deck اجازه دهید با هر نوع کارتی تعامل داشته باشد.


سیستم های نوع در زبان های برنامه نویسی را می توان به طور کلی به دو دسته تقسیم کرد: typing و بررسی نوع. یک زبان می تواند از تایپ قوی یا ضعیف و بررسی نوع ایستا یا پویا استفاده کند. برخی از زبان‌ها از ترکیبی از این موارد استفاده می‌کنند، اما Go به خوبی با زبان‌هایی که به‌شدت typing و به‌صورت ایستا بررسی می‌شوند، مطابقت دارد. تایپ قوی به این معنی است که Go اطمینان حاصل می کند که یک مقدار در یک متغیر با نوع متغیر مطابقت دارد، بنابراین نمی توانید مقدار int را برای مثال در یک متغیر رشته ذخیره کنید. به عنوان یک سیستم نوع چک شده استاتیک، کامپایلر Go این قوانین نوع را زمانی که برنامه را کامپایل می کند به جای زمانی که برنامه در حال اجرا است بررسی می کند.


مزیت استفاده از یک زبان با تایپ قوی و بررسی استاتیکی مانند Go این است که کامپایلر قبل از انتشار برنامه به شما امکان می دهد از اشتباهات احتمالی مطلع شوید و از برخی از خطاهای زمان اجرا "نوع نامعتبر" اجتناب کنید. البته این محدودیتی به برنامه های Go اضافه می کند، زیرا قبل از کامپایل کردن برنامه باید انواعی را که قصد استفاده از آنها را دارید بشناسید. یکی از راه های رسیدگی به این موضوع استفاده از نوع {}interface است. دلیل اینکه یک نوع {}interface برای هر مقداری کار می کند این است که هیچ روش مورد نیازی را برای رابط تعریف نمی کند (که با علامت خالی {} مشخص می شود)، بنابراین هر نوع با رابط مطابقت دارد.


برای شروع ایجاد برنامه خود با استفاده از یک {}interface برای نشان دادن کارت های خود، به یک دایرکتوری نیاز دارید تا دایرکتوری برنامه را در آن نگه دارید. در این آموزش، از دایرکتوری به نام پروژه ها استفاده خواهید کرد.


ابتدا دایرکتوری پروژه ها را بسازید و به آن بروید:

 

mkdir projects
cd projects

 

بعد، دایرکتوری پروژه خود را بسازید و به آن بروید. در این مورد، از ژنریک دایرکتوری استفاده کنید:

 

mkdir generics
cd generics

 

در فایل main.go، با اضافه کردن package شروع کنید و package های مورد نیاز خود را وارد کنید:

 

package main

import (
	"fmt"
	"math/rand"
	"os"
	"time"
)

 

اکنون نوع PlayingCard خود و توابع و متد های مرتبط با آن را تعریف کنید:

 

...

type PlayingCard struct {
	Suit string
	Rank string
}

func NewPlayingCard(suit string, card string) *PlayingCard {
	return &PlayingCard{Suit: suit, Rank: card}
}

func (pc *PlayingCard) String() string {
	return fmt.Sprintf("%s of %s", pc.Rank, pc.Suit)
}

 

در این قطعه، ساختاری به نام PlayingCard با ویژگی‌های Suit و Rank تعریف می‌کنید تا کارت‌ها را از دسته 52 کارتی نمایش دهد. Suit یکی از Diamonds، Hearts، Clubs یا Spades خواهد بود و رتبه A، 2، 3 و غیره از طریق K خواهد بود.


شما همچنین یک تابع NewPlayingCard برای عمل به عنوان سازنده ساختار PlayingCard و یک متد String تعریف کرده‌اید که رتبه و مطابقت کارت را با استفاده از fmt.Sprintf برمی‌گرداند.

 

سپس، نوع Deck خود را با متدهای AddCard و RandomCard و همچنین یک تابع NewPlayingCardDeck ایجاد کنید تا یک Deck* پر از تمام 52 کارت بازی ایجاد کنید:

 

...

type Deck struct {
	cards []interface{}
}

func NewPlayingCardDeck() *Deck {
	suits := []string{"Diamonds", "Hearts", "Clubs", "Spades"}
	ranks := []string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}

	deck := &Deck{}
	for _, suit := range suits {
		for _, rank := range ranks {
			deck.AddCard(NewPlayingCard(suit, rank))
		}
	}
	return deck
}

func (d *Deck) AddCard(card interface{}) {
	d.cards = append(d.cards, card)
}

func (d *Deck) RandomCard() interface{} {
	r := rand.New(rand.NewSource(time.Now().UnixNano()))

	cardIdx := r.Intn(len(d.cards))
	return d.cards[cardIdx]
}

 

در Deck که در بالا تعریف کردید، یک فیلد به نام کارت ایجاد می کنید تا اسلایس ای از cards را در خود جای دهد. از آنجایی که می خواهید دسته بتواند چندین نوع کارت مختلف را در خود جای دهد، نمی توانید آن را فقط به عنوان PlayingCard*[] تعریف کنید. شما آن را به عنوان {}interface[] تعریف می کنید تا بتواند هر نوع کارتی را که ممکن است در آینده ایجاد کنید، در خود جای دهد. علاوه بر فیلد {}interface[] در Deck، شما همچنین یک متد AddCard ایجاد می‌کنید که همان نوع {}interface را می‌پذیرد تا یک کارت به فیلد کارت‌های Deck اضافه شود.


شما همچنین یک متد RandomCard ایجاد می کنید که یک کارت تصادفی را از اسلایس کارت های Deck برمی گرداند. این متد از پکیج  math/rand برای تولید یک عدد تصادفی بین 0 و تعداد کارت‌های موجود در slice کارت استفاده می‌کند. خط rand.New یک مولد اعداد تصادفی جدید با استفاده از زمان فعلی به عنوان منبع تصادفی ایجاد می کند. در غیر این صورت، عدد تصادفی می تواند هر بار یکسان باشد. خط r.Intn(len(d.cards)) از مولد اعداد تصادفی برای تولید یک مقدار int بین 0 و عدد ارائه شده استفاده می کند. از آنجایی که متد Intn مقدار پارامتر را در محدوده اعداد شامل نمی شود، نیازی به کم کردن 1 از طول برای شروع از 0 نیست. در نهایت، RandomCard مقدار کارت را در شاخص عدد تصادفی برمی گرداند.

 

مراقب باشید که از کدام مولد اعداد تصادفی در برنامه های خود استفاده می کنید. پکیج  math/rand از نظر رمزنگاری ایمن نیست و نباید برای برنامه‌های حساس به امنیت استفاده شود. پکیج crypto.rand، با این حال، یک تولید کننده اعداد تصادفی را ارائه می دهد که می تواند برای این اهداف استفاده شود.

 

در نهایت، تابع NewPlayingCardDeck یک مقدار Deck* را برمی‌گرداند که با تمام کارت‌های یک دسته کارت بازی پر شده است. شما از دو slice استفاده می‌کنید، یکی با تمام مقدارهای موجود و دیگری با تمام ranks موجود، و سپس روی هر مقدار حلقه بزنید تا قبل از افزودن آن به Deck با استفاده از AddCard، یک PlayingCard* جدید برای هر ترکیب ایجاد کنید. هنگامی که کارت های Deck تولید می شوند، مقدار برگردانده می شود.

 

اکنون که Deck و PlayingCard خود را تنظیم کرده اید، می توانید توابع اصلی خود را ایجاد کنید تا از آنها برای کشیدن کارت استفاده کنید:

 

...

func main() {
	deck := NewPlayingCardDeck()

	fmt.Printf("--- drawing playing card ---\n")
	card := deck.RandomCard()
	fmt.Printf("drew card: %s\n", card)

	playingCard, ok := card.(*PlayingCard)
	if !ok {
		fmt.Printf("card received wasn't a playing card!")
		os.Exit(1)
	}
	fmt.Printf("card suit: %s\n", playingCard.Suit)
	fmt.Printf("card rank: %s\n", playingCard.Rank)
}

 

در تابع اصلی، ابتدا یک دسته کارت بازی جدید با استفاده از تابع NewPlayingCardDeck ایجاد کرده و آن را به متغیر Deck اختصاص می دهید. سپس، از fmt.Printf برای چاپ کارتی که در حال کشیدن آن هستید استفاده می‌کنید و از متد Deck’s RandomCard برای دریافت کارت تصادفی از Deck استفاده می‌کنید. پس از آن، دوباره از fmt.Printf برای چاپ کارتی که از روی Deck کشیده اید استفاده می کنید.


در مرحله بعد، از آنجایی که نوع متغیر کارت {}interface است، باید از یک  type assertion برای دریافت ارجاع به کارت به عنوان نوع PlayingCard* اصلی آن استفاده کنید. اگر نوع متغیر کارت از نوع PlayingCard* نباشد، که باید به آن نحوه نوشتن برنامه شما در حال حاضر داده شود، مقدار ok false خواهد بود و برنامه شما با استفاده از fmt.Printf پیغام خطا را چاپ می کند و با یک خروجی از آن خارج می شود. کد خطای 1 با استفاده از os.Exit. اگر از نوع PlayingCard* است، سپس مقادیر Suit و Rank playingCard را با استفاده از fmt.Printf چاپ کنید.


هنگامی که تمام تغییرات خود را ذخیره کردید، می توانید برنامه خود را با استفاده از go run با main.go، نام فایل مورد نظر اجرا کنید:

 

go run main.go

 

در خروجی برنامه خود، باید کارتی را که به طور تصادفی از روی Deck انتخاب شده است، و همچنین suit و rank کارت را مشاهده کنید:

 

Output
--- drawing playing card ---
drew card: Q of Diamonds
card suit: Diamonds
card rank: Q

 

از آنجایی که کارت به صورت تصادفی از روی Deck کشیده می شود، خروجی شما احتمالاً با خروجی نشان داده شده در بالا متفاوت خواهد بود، اما باید خروجی مشابهی مشاهده کنید. خط اول درست قبل از کشیدن کارت تصادفی از روی Deck چاپ می شود و سپس خط دوم پس از کشیدن کارت چاپ می شود. می توانید ببینید که خروجی کارت از مقدار بازگشتی به روش PlayingCard’s String استفاده می کند. در نهایت، می‌توانید دو خط خروجی suit و rank را ببینید که پس از وارد کردن مقدار کارت {}interface خود در یک مقدار PlayingCard* چاپ شده‌اند.

 

در این بخش، یک Deck ایجاد کردید که از مقادیر {}interface برای ذخیره و تعامل با هر مقداری استفاده می‌کند و از نوع PlayingCard برای عمل به عنوان کارت در آن Deck استفاده می‌کند. سپس از Deck و PlayingCards برای انتخاب یک کارت تصادفی از Deck و چاپ اطلاعات مربوط به آن کارت استفاده کردید.

 

برای دسترسی به اطلاعات خاصی در مورد مقدار PlayingCard* که ترسیم کردید، باید کارهای بیشتری را انجام دهید تا نوع {}interface را به نوع PlayingCard* با دسترسی به فیلدهای Suit و Rank تبدیل کنید. استفاده از Deck به این روش کارساز خواهد بود، اما اگر مقداری غیر از PlayingCard* به Deck اضافه شود، می تواند منجر به خطا شود. با به‌روزرسانی Deck خود برای استفاده از ابزارهای ژنریک، می‌توانید از انواع قوی Go و بررسی نوع استاتیک بهره ببرید و در عین حال از مقادیر پذیرش انعطاف‌پذیری {}interface برخوردار باشید.

 

Go مجموعه ها با Generics

در بخش قبل، یک مجموعه با استفاده از اسلایس از انواع {}interface ایجاد کردید. اما برای استفاده از این مقادیر، باید کارهای بیشتری انجام دهید تا مقادیر را از یک {}interface به نوع واقعی آن مقادیر ترجمه کنید. با این حال، با استفاده از ژنریک، می‌توانید یک یا چند پارامتر نوع ایجاد کنید، که تقریباً مانند پارامترهای تابع عمل می‌کنند، اما می‌توانند انواع را به‌جای داده، به‌عنوان مقادیر نگهداری کنند. به این ترتیب، ژنریک راهی برای جایگزینی نوع متفاوتی برای پارامترهای نوع هر بار که از نوع ژنریک استفاده می‌شود، فراهم می‌کند. اینجا جایی است که انواع ژنریک نام خود را می گیرند. از آنجایی که نوع ژنریک را می توان با چندین نوع استفاده کرد، و نه فقط یک نوع خاص مانند io.Reader یا {}interface، به اندازه کافی ژنریک است که بتواند چندین مورد استفاده را در خود جای دهد.


در این بخش، نوع Deck خود را به‌روزرسانی می‌کنید تا یک نوع ژنریک باشد که می‌تواند از هر نوع کارت خاصی در هنگام ایجاد یک نمونه از Deck به جای استفاده از رابط استفاده کند.


برای انجام اولین به‌روزرسانی، فایل main.go خود را باز کرده و وارد کردن پکیج os را حذف کنید:

 

package main

import (
	"fmt"
	"math/rand"
	// "os" package import is removed
	"time"
)

 

همانطور که در به‌روزرسانی‌های بعدی خواهید دید، دیگر نیازی به استفاده از تابع os.Exit نخواهید داشت، بنابراین حذف این بی‌خطر است.

 

سپس، ساختار Deck خود را به‌روزرسانی کنید تا یک نوع ژنریک باشد:

 

...

type Deck[C any] struct {
	cards []C
}

 

این به‌روزرسانی، سینتکس جدیدی را معرفی می‌کند که توسط ژنریک‌ها برای ایجاد انواع متغیرها یا پارامترهای نوع در اعلان ساختار شما استفاده می‌شود. تقریباً می توانید این نوع پارامترها را شبیه به پارامترهایی که در یک تابع قرار می دهید در نظر بگیرید. هنگام فراخوانی یک تابع، مقادیری را برای هر پارامتر تابع ارائه می کنید. به همین ترتیب، هنگام ایجاد یک مقدار نوع ژنریک، انواع پارامترهای نوع را ارائه می دهید.


بعد از نام ساختار خود، Deck، خواهید دید که عبارتی را در داخل پرانتز ([]) اضافه کرده اید. این براکت‌های مربع به شما امکان می‌دهند یک یا چند مورد از این پارامترها را برای ساختار خود تعریف کنید.


در مورد نوع Deck خود، شما فقط به یک پارامتر نوع به نام C نیاز دارید تا نوع کارت های موجود در Deck خود را نشان دهد. با اعلام C در داخل پارامترهای نوع خود، کد شما می گوید: "یک پارامتر نوع ژنریک به نام C ایجاد کنید که بتوانم در ساختار خود از آن استفاده کنم و اجازه دهید هر نوع (any) باشد". در پشت صحنه، any در واقع یک نام مستعار برای نوع {}interface است. این کار خوانش کلیات را آسان‌تر می‌کند و نیازی به استفاده از {}C interface ندارید. Deck شما فقط به یک نوع ژنریک برای نمایش کارت ها نیاز دارد، اما اگر به انواع ژنریک بیشتری نیاز دارید، می توانید آنها را با استفاده از سینتکس جدا شده با کاما، مانند C any، F any اضافه کنید. نامی که برای پارامترهای نوع خود استفاده می‌کنید، اگر رزرو نشده باشد، می‌تواند هر چیزی باشد که می‌خواهید، اما معمولاً کوتاه و بزرگ هستند.


در نهایت، در به روز رسانی خود به اعلان Deck، نوع slice کارت ها را در ساختار به روز کردید تا از پارامتر نوع C استفاده کنید. هنگام استفاده از ژنریک، می توانید از پارامترهای نوع خود در هر جایی که معمولاً نوع خاصی را قرار می دهید استفاده کنید. در این مورد، شما می خواهید پارامتر C شما نشان دهنده هر کارت در slice باشد، بنابراین اعلان نوع slice [] را به دنبال اعلام پارامتر C قرار می دهید.


سپس، متد AddCard نوع Deck خود را به‌روزرسانی کنید تا از نوع ژنریک که تعریف کرده‌اید استفاده کنید. در حال حاضر، از به روز رسانی تابع NewPlayingCardDeck صرف نظر خواهید کرد، اما به زودی به آن باز خواهید گشت:

 

...

func (d *Deck[C]) AddCard(card C) {
	d.cards = append(d.cards, card)
}

 

در به روز رسانی به متد Deck ها  AddCard، ابتدا پارامتر نوع ژنریک [C] را به گیرنده متد اضافه کردید. این به Go اجازه می‌دهد تا نام پارامتر نوع را که در جای دیگری در اعلان متد استفاده می‌کنید، بداند و از سینتکس مشابه براکت مربع مانند اعلان ساختار پیروی کند. با این حال، در این مورد، نیازی به ارائه هیچ محدودیتی ندارید، زیرا قبلاً در Deck ارائه شده است. سپس، پارامتر تابع کارت را به‌روزرسانی کردید تا از نوع placeholder type C به جای اصلی {}interface استفاده کنید. این اجازه می دهد تا متد استفاده از نوع خاص C در نهایت تبدیل شود.


پس از به روز رسانی متد AddCard، متد RandomCard را به روز کنید تا از نوع ژنریک C نیز استفاده کنید:

 

...

func (d *Deck[C]) RandomCard() C {
	r := rand.New(rand.NewSource(time.Now().UnixNano()))

	cardIdx := r.Intn(len(d.cards))
	return d.cards[cardIdx]
}

 

این بار، به جای استفاده از نوع ژنریک C به عنوان پارامتر تابع، متد را به روز کرده اید تا مقدار C را به جای {}interface برگردانید. جدا از به‌روزرسانی گیرنده برای گنجاندن [C]، این تنها به‌روزرسانی است که باید برای تابع انجام دهید. از آنجایی که فیلد کارت‌ها در Deck قبلاً در اعلان ساختار به‌روزرسانی شده بود، وقتی این متد مقداری را از کارت‌ها برمی‌گرداند، مقداری از نوع C را برمی‌گرداند.


اکنون که نوع Deck شما برای استفاده از ژنریک ها به روز شده است، به تابع NewPlayingCardDeck خود برگردید و آن را به روز کنید تا از نوع Deck ژنریک برای انواع PlayingCard* استفاده کنید:

 

...

func NewPlayingCardDeck() *Deck[*PlayingCard] {
	suits := []string{"Diamonds", "Hearts", "Clubs", "Spades"}
	ranks := []string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}

	deck := &Deck[*PlayingCard]{}
	for _, suit := range suits {
		for _, rank := range ranks {
			deck.AddCard(NewPlayingCard(suit, rank))
		}
	}
	return deck
}

...

 

بیشتر کد موجود در NewPlayingCardDeck ثابت می ماند، اما اکنون که از نسخه ژنریک Deck استفاده می کنید، باید نوع مورد نظر برای C را هنگام استفاده از Deck مشخص کنید. شما این کار را با ارجاع به نوع Deck خود به طور معمول انجام می دهید، خواه Deck باشد یا مرجعی مانند Deck*، و سپس با استفاده از همان براکت های مربعی که هنگام اعلام پارامترهای نوع اولیه استفاده می کردید، نوعی را ارائه می دهید که باید جایگزین C شود.


برای نوع برگشتی NewPlayingCardDeck، همچنان مانند قبل از Deck* استفاده می کنید، اما این بار، براکت ها و PlayingCard* را نیز در نظر می گیرید. با ارائه [PlayingCard*] برای پارامتر type، می‌گویید که می‌خواهید نوع PlayingCard* در اعلان و متد های Deck شما جایگزین مقدار C شود. این بدان معناست که نوع فیلد کارت‌ها در Deck اساساً از C[] به PlayingCard*[] تغییر می‌کند.


به طور مشابه، هنگام ایجاد یک نمونه جدید از Deck، باید نوع جایگزین C را نیز ارائه کنید. در جایی که معمولاً از {}Deck& برای ارجاع جدید به Deck استفاده می‌کنید، در عوض نوع را در داخل براکت‌های مربع قرار دهید تا به 

 

&Deck[*PlayingCard]{}


اکنون که انواع شما برای استفاده از ژنریک ها به روز شده است، می توانید تابع main خود را برای استفاده از آنها به روز کنید:

 

...

func main() {
	deck := NewPlayingCardDeck()

	fmt.Printf("--- drawing playing card ---\n")
	playingCard := deck.RandomCard()
	fmt.Printf("drew card: %s\n", playingCard)
	// Code removed
	fmt.Printf("card suit: %s\n", playingCard.Suit)
	fmt.Printf("card rank: %s\n", playingCard.Rank)
}

 

این بار به روز رسانی شما برای حذف کد است زیرا دیگر نیازی به وارد کردن مقدار {}interface در مقدار PlayingCard* ندارید. هنگامی که متد RandomCard در Deck را برای برگرداندن C و NewPlayingCardDeck را برای برگرداندن Deck[*PlayingCard]* به روز کردید، RandomCard را تغییر دادید تا مقدار PlayingCard* را به جای {}interface برگردانید. هنگامی که RandomCard *PlayingCard را برمی گرداند، به این معنی است که نوع کارت بازی نیز به جای رابط، {}PlayingCard* است و می توانید فوراً به فیلدهای Suit یا Rank دسترسی داشته باشید.


برای مشاهده اجرای برنامه خود پس از ذخیره تغییرات خود در main.go، دوباره از دستور go run استفاده کنید:

 

go run main.go

Output
--- drawing playing card ---
drew card: 8 of Hearts
card suit: Hearts
card rank: 8

 

حتی اگر خروجی همان نسخه قبلی برنامه شما با استفاده از {}interface است، خود کد کمی تمیزتر است و از خطاهای احتمالی جلوگیری می کند. شما دیگر نیازی به انجام assertion نوع PlayingCard* ندارید تا از رسیدگی به خطاهای اضافی جلوگیری کنید. علاوه بر این، با گفتن اینکه نمونه Deck شما فقط می‌تواند حاوی PlayingCard* باشد، هیچ امکانی برای اضافه شدن مقداری غیر از PlayingCard* به slice کارت وجود ندارد.


در این بخش، ساختار Deck خود را به‌روزرسانی کرده‌اید تا یک نوع ژنریک باشد و کنترل بیشتری بر روی انواع کارت‌هایی که هر نمونه از Deck شما می‌تواند داشته باشد، فراهم می‌کند. همچنین متد های AddCard و RandomCard را به‌روزرسانی کردید تا یک آرگومان ژنریک را بپذیرید یا یک مقدار ژنریک را برگردانید. سپس، NewPlayingCardDeck را به‌روزرسانی کردید تا یک Deck* حاوی کارت‌های PlayingCard* را برگردانید. در نهایت، مدیریت خطا را در تابع mainخود حذف کردید زیرا دیگر به آن نیاز نداشتید.

 

اکنون که Deck شما به‌صورت ژنریک به‌روزرسانی شده است، می‌توانید از آن برای نگهداری هر نوع کارتی که می‌خواهید استفاده کنید.

 

نتیجه

Generics در Go یک راه قدرتمند برای نوشتن کد قابل استفاده مجدد و ایمن ارائه می دهد. با درک سینتکس آنها و استفاده از قابلیت های آنها، می توانید پایگاه های کد کارآمدتر و قابل نگهداری ایجاد کنید. در این مقاله، ما اصول اولیه ژنریک، نحوه پیاده سازی توابع ژنریک و ساختارهای داده را پوشش دادیم و برخی از بهترین روش ها را برای استفاده از آنها ارائه کردیم. با در دست داشتن این دانش، اکنون آماده هستید تا از قدرت ژنریک ها در پروژه های Go خود استفاده کنید!

 

 

#گو #گولنگ #Generics #go #golang #ژنریک