مقدمة: ما هي Generics ولماذا ظهرت في Go؟
\nلفترة طويلة، ظلت لغة Go (Golang) واحدة من اللغات الحديثة القليلة التي لا تدعم البرمجة العامة (generics). كان على المطورين إما تكرار الكود لأنواع بيانات مختلفة، أو استخدام الواجهات (interfaces) و interface{} الفارغة، مما أدى إلى فقدان أمان الأنواع والحاجة إلى إجراء تحويل الأنواع في وقت التشغيل.
مع إصدار Go 1.18 (فبراير 2022)، ظهرت الأدوية الجنيسة التي طال انتظارها في اللغة. Generics هي آلية تسمح بكتابة دوال وأنواع وهياكل بيانات تعمل مع أنواع عشوائية، مع الحفاظ على الكتابة الصارمة للأنواع في مرحلة الترجمة.
\nفي هذه المقالة، سنشرح:
\n- \n
- لماذا نحتاج إلى الأدوية الجنيسة في Go؛ \n
- كيفية تعريف الدوال والأنواع العامة؛ \n
- ما هي القيود (constraints) وكيفية استخدامها؛ \n
- أمثلة عملية للتطبيق. \n
لماذا نحتاج إلى Generics في Go؟
\nقبل ظهور الأدوية الجنيسة، كانت المشكلة النموذجية تبدو كالتالي: الحاجة إلى كتابة دالة تعمل مع شرائح (slices) من أنواع مختلفة (مثل int، string، float64).
بدون الأدوية الجنيسة، كان لا بد إما من كتابة ثلاث دوال منفصلة:
\nfunc SumInts(nums []int) int {\n total := 0\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nfunc SumFloats(nums []float64) float64 {\n total := 0.0\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nأو استخدام interface{} مع تحويل الأنواع - وهو أمر غير آمن وغير مريح.
مع الأدوية الجنيسة، تكفي دالة واحدة:
\nfunc Sum[T int | float64](nums []T) T {\n var total T\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nالمزايا واضحة:
\n- \n
- أمان الأنواع - يتم اكتشاف أخطاء الأنواع في مرحلة الترجمة؛ \n
- تقليل تكرار الكود - دالة واحدة لأنواع متعددة؛ \n
- قابلية القراءة - يصبح الكود أكثر إيجازاً ووضوحاً. \n
أساسيات بناء جملة Generics في Go
\nتعريف دالة عامة
\nيستخدم بناء الجملة الأساسي الأقواس المربعة [] لتحديد معاملات النوع:
func اسمالدالة[اسمالنوع constraint](المعاملات) النتيجة {\n // جسم الدالة\n}\n\nمثال لدالة بسيطة تعيد القيمة التي تم تمريرها:
\nfunc Identity[T any](value T) T {\n return value\n}\n\nfunc main() {\n fmt.Println(Identity[int](42)) // 42\n fmt.Println(Identity[string]("Go")) // Go\n}\n\nيمكن لـ Go استنتاج النوع تلقائياً، لذا فإن التحديد الصريح [int] ليس ضرورياً:
fmt.Println(Identity(42)) // Go سيفهم أن T = int\nfmt.Println(Identity("Go")) // T = string\n\n\nالأنواع العامة (الهياكل)
\nتعمل الأدوية الجنيسة ليس فقط مع الدوال، ولكن أيضاً مع الأنواع المخصصة:
\ntype Stack[T any] struct {\n elements []T\n}\n\nfunc (s *Stack[T]) Push(value T) {\n s.elements = append(s.elements, value)\n}\n\nfunc (s *Stack[T]) Pop() (T, bool) {\n if len(s.elements) == 0 {\n var zero T\n return zero, false\n }\n value := s.elements[len(s.elements)-1]\n s.elements = s.elements[:len(s.elements)-1]\n return value, true\n}\n\nfunc main() {\n intStack := Stack[int]{}\n intStack.Push(10)\n intStack.Push(20)\n val, ok := intStack.Pop()\n fmt.Println(val, ok) // 20 true\n}\n\n\nالقيود (Constraints): تحديد أنواع محددة
\nفي الأمثلة أعلاه، استخدمنا any - وهو قيد مدمج يعني "أي نوع". ومع ذلك، غالباً ما يكون من الضروري فرض قيود: على سبيل المثال، أن يدعم النوع المقارنة أو العمليات الحسابية.