Go Programming

تطبيق Generics في Go: بناء مجموعات وهياكل بيانات مخصصة آمنة النوع

منذ إصدار Go 1.18، خضعت اللغة لأهم تطور في أنظمة الأنواع. سمحت Generics للمطورين بكتابة كود قابل لإعادة الاستخدام وآمن النوع، مما يقلل الحاجة إلى الأكواد المتكررة أو التحويلات غير الآمنة. بالنسبة للمطورين من المستوى المتوسط والمتقدم، لم يعد إتقان Generics خياراً بل أصبح ضرورياً لبناء تطبيقات قوية وقابلة للتوسع.

في هذا المنشور، سنتجاوز الدوال البسيطة ونغوص في بناء مجموعات وهياكل بيانات مخصصة وآمنة النوع. سنستكشف كيفية الاستفادة من قيود الأنواع (Type Constraints) لإنشاء هياكل بيانات تعمل بسلاسة عبر أنواع مختلفة مع الحفاظ على ضمانات الأمان الصارمة التي تشتهر بها Go.

فهم قيود الأنواع

قبل بناء هياكل معقدة، من الضروري فهم قيود الأنواع. في Go، يحدد قيد النوع مجموعة الأنواع التي يمكن استخدامها مع نوع عام أو دالة عامة. يمكن أن تكون القيود واجهات بسيطة أو أنواع مركبة معقدة باستخدام الاتحادات (|).

لهياكل البيانات الخاصة بنا، غالباً ما نحتاج إلى دعم الأنواع القابلة للمقارنة (للمستودعات والمجموعات) أو الأنواع المرتبة (للترتيب). توفر Go قيوداً مدمجة مثل comparable و ordered في حزمة constraints، ولكن في كثير من الأحيان، نعرف واجهات خاصة بنا للحصول على أقصى قدر من المرونة.

package main

import (
    "fmt"
)

// Orderable يحدد الأنواع التي تدعم عوامل المقارنة
type Orderable interface {
    ~int | ~float64 | ~string
}

بناء مكدس عام (Generic Stack)

لنبدأ بهيكل بيانات أساسي: المكدس (Stack). تقليدياً، كان يتطلب تنفيذ المكدس واحداً لكل نوع بيانات (على سبيل المثال، IntStack، StringStack). مع Generics، يمكننا إنشاء تنفيذ واحد يتعامل مع أي نوع.

إليك تنفيذ آمن للنوع لمكدس باستخدام Generics في Go:

package main

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

func NewStack[T any]() *Stack[T] {
    return &Stack[T]{}
}

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

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

func (s *Stack[T]) Peek() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    return s.items[len(s.items)-1], true
}

func (s *Stack[T]) IsEmpty() bool {
    return len(s.items) == 0
}

هذا التنفيذ أنيق لأن معلمة النوع T لنوع Stack يمكن أن تكون أي نوع. ومع ذلك، هذا يؤدي إلى فخ أداء محتمل عند التعامل مع مجموعات بيانات كبيرة.

التحسين باستخدام قيود الواجهات

بينما يعتبر T any مرناً، فإن تخزين المؤشرات أو الهياكل (Structs) يمكن أن يؤدي إلى تخصيص ذاكرة غير ضروري. بالنسبة للمجموعات مثل المكدسات، الطوابير، أو القوائم، غالباً ما يكون من المفيد تقييد T للأنواع التي تطبق interface{} أو سلوكيات محددة للسماح بدلالات القيمة حيث يناسب ذلك.

فكر في سيناريو نريد فيه بناء طابور عام (Generic Queue). على عكس المكدس، يحتاج الطابور إلى التعامل مع الإزالة من المقدمة، وهو ما يمكن أن يكون غير فعال مع الشرائح (Slices). قد يكون نهج القائمة المرتبطة أفضل، ولكن دعنا نلتزم بتنفيذ قائم على الشرائح للبساطة، مع التركيز على أمان النوع.

type Queue[T comparable] struct {
    items []T
}

func NewQueue[T comparable]() *Queue[T] {
    return &Queue[T]{
        items: make([]T, 0),
    }
}

func (q *Queue[T]) Enqueue(item T) {
    q.items = append(q.items, item)
}

func (q *Queue[T]) Dequeue() (T, bool) {
    if len(q.items) == 0 {
        var zero T
        return zero, false
    }
    item := q.items[0]
    q.items = q.items[1:]
    return item, true
}

من خلال استخدام comparable، نقيّد الطابور للأنواع التي يمكن مقارنتها، وهو أمر مفيد إذا كنا نخطط لإضافة عمليات مثل التحقق من التكرارات لاحقاً.

تطبيق عملي: البحث العام في مجموعة

أحد أقوى استخدامات Generics هو إنشاء طرق مساعدة عامة لهياكل البيانات الخاصة بك. على سبيل المثال، إضافة دالة بحث عامة إلى Stack الخاص بنا تتطلب أن يكون النوع comparable.

func (s *Stack[T]) Contains(item T) bool {
    for _, v := range s.items {
        if v == item {
            return true
        }
    }
    return false
}

إذا حاولنا استخدام هذه الطريقة مع هيكل (Struct) لا يدعم عمليات التساوي (أي يحتوي على شرائح، مستودعات، أو دوال)، فسيمنع مترجم Go تجميع الكود، مما يضمن أمان النوع وقت البناء.

أفضل الممارسات والخاتمة

عند العمل مع Generics في Go، ضع في اعتبارك أفضل الممارسات التالية:

  • تجنب الإفراط في التصميم: لا تستخدم Generics لكل شيء. استخدمها عندما يكون لديك كود متكرر يختلف فقط بالنوع.
  • تفضيل أنواع القيم: حيثما أمكن، استخدم أنواع القيم في القيود لتجنب عبء عدم الإحالة إلى المؤشرات، ما لم تكن بحاجة تحديداً إلى دلالات المرجع.
  • تسمية واضحة: استخدم معاملات أنواع أحرفية (T، E، K) ما لم يضيف الاسم وضوحاً كبيراً.

تُعد Generics في Go أداة قوية لبناء مجموعات وهياكل بيانات آمنة النوع. من خلال الاستفادة من قيود الأنواع، يمكنك كتابة كود مرن وفعال وقابل للصيانة يتوسع مع احتياجات تطبيقك. مع استمرار نضج نظام Go البيئي، توقع ظهور المزيد من المكتبات المعيارية والعادات البرمجية حول هذه الأنماط.

ابدأ في إعادة كتابة الكود المتكرر (Boilerplate) اليوم واطلع على مكاسب الإنتاجية التي تقدمها Generics في Go.

Share: