دالة عامة
توضع معاملات النوع بين قوسين مربّعين قبل المعاملات العادية. لكل منها قيد يحدّد الأنواع المسموحة.
يُستنتج T وU من الوسائط، فنادرًا ما تكتب Map[int, string](...). يعمل الاستنتاج من وسائط الدالة؛ ولا يستطيع استنتاج نوع يظهر في النتيجة فقط. في func Zero[T any]() T يجب أن تكتب Zero[int]().
قبل Go 1.18 كان يجب كتابة هذه الدالة مرة لكل نوع، أو جعلها تأخذ []interface{} وتفقد أمان الأنواع.
القيود
القيد واجهة. يحدّد الأنواع الممكن استخدامها، ويخبر المترجم في المقابل بالعمليات المسموحة على قيم ذلك النوع.
| القيد | يسمح بـ | يتيح لك استخدام |
|---|---|---|
any | كل نوع | الإسناد والتمرير والتخزين |
comparable | الأنواع التي تدعم == | == و!= ومفاتيح الخرائط |
cmp.Ordered (Go 1.21) | الأعداد الصحيحة والعشرية والنصوص | < و<= و> و>=، إضافة إلى == |
| واجهة ذات توابع | الأنواع التي لها تلك التوابع | استدعاء التوابع |
اتحاد أنواع مثل ~int | ~float64 | تلك الأنواع بالضبط | المعاملات التي تدعمها جميعها |
مع any بدل comparable تفشل v == target في الترجمة: invalid operation: v == target (incomparable types in type set). القيد هو ما يجعل المعامل مشروعًا.
هاتان الدالتان المساعدتان موجودتان أصلًا: slices.Index وslices.Max. تغطّي الحزم القياسية slices وmaps وcmp معظم الحاجات العامة اليومية، فتفقّدها قبل كتابة دوالك.
قيود مخصّصة باتحادات الأنواع و~
يمكن للقيد أن يسرد أنواعًا بـ |. المعاملات المسموحة هي التي يدعمها كل نوع مسرود.
العلامة ~ مهمّة. تعني ~int64 "أي نوع نوعه الأساسي int64"، فيُقبل النوع المسمّى Cents. ولو كُتب int64 وحده لفشلت Sum([]Cents{...}) برسالة Cents does not satisfy Number (possibly missing ~ for int64 in Number).
الواجهات التي تحتوي اتحادات أنواع لا تُستخدم إلا كقيود، ولا تُستخدم أبدًا كأنواع متغيّرات عادية. var n Number لا تُترجم.
في الحزمة golang.org/x/exp/constraints قيود جاهزة Integer وFloat وSigned. لكنها خارج المكتبة القياسية، فلا يستطيع المشغّل هنا استيرادها؛ والقيد القياسي هو cmp.Ordered.
الأنواع العامة
يمكن أن تملك البنى والشرائح والخرائط وغيرها معاملات نوع أيضًا. توابع النوع العام تستخدم معاملات المستقبِل.
تفصيلتان في هذا المثال تتكرّران باستمرار:
- القيمة الصفرية لـ T.
var zero Tهي طريقة إعادة "لا شيء" من شيفرة عامة. لا يوجدT{}أوnilيعمل مع كل نوع. - التجسيد. يجب تجسيد النوع العام قبل استخدامه:
Stack[string]لاStack. وداخل توابعه يُكتب المستقبِل بالشكلStack[T].
أضافت Go 1.24 الأسماء البديلة العامة للأنواع: type Set[T comparable] = map[T]struct{} تُترجم الآن.
حدود ستصطدم بها
- لا معاملات نوع في التوابع.
func (s *Stack[T]) Map[U any](...)غير مسموحة. استخدم دالة على المستوى الأعلى. - لا تخصيص. لا يمكنك كتابة تطبيق منفصل لـ
T = string. الحل البديل type switch علىany(v)داخل الدالة، وهو عادة إشارة إلى أن الأنواع العامة ليست الأداة المناسبة. - لا قيود معاملات بالاسم. لا يمكنك قول "أي نوع له تابع
+"؛ بل تسرد الأنواع في اتحاد. - الوصول إلى الحقول عبر قيد لا يعمل. حتى لو كان لكل نوع في الاتحاد حقل
ID، فإنv.IDغير مسموحة. استخدم تابعًا في القيد بدلًا من ذلك.
متى لا تستخدم الأنواع العامة
تناسب الأنواع العامة أنواع الحاويات (المكدّسات، المجموعات، الذاكرات المؤقّتة، الأشجار) والخوارزميات على الشرائح والخرائط التي تفعل الشيء نفسه لكل نوع عنصر. ولا تناسب كل مكان.
- نوع محدّد واحد. إذا كانت الدالة لا تُستدعى إلا مع
[]User، فالنسخة العامة تضيف كلفة قراءة ولا شيء غيرها. - سلوك يختلف حسب النوع. هذا ما وُجدت الواجهات من أجله. الدالة التي تأخذ
io.Writerأبسط من التي تأخذ[T io.Writer]، وتعمل بالطريقة نفسها. - استبدال معاملات الواجهات.
func Print[T fmt.Stringer](v T)ليست أفضل منfunc Print(v fmt.Stringer).
إرشاد فريق Go نفسه: اكتب الشيفرة لنوع محدّد أولًا، ولجأ إلى معاملات النوع عندما تلاحظ أنك تكتب الشيفرة نفسها مرة ثانية ولم يتغيّر إلا الأنواع.
أخطاء شائعة
- استخدام
anyحيث يلزمcomparableأوcmp.Ordered. يرفض المترجم==أو<على معامل نوعany. - نسيان
~. الأنواع التي يعرّفها المستخدم مثلtype ID intتفشل مع قيدintعادي. - إعادة
nilلمعامل نوع. غير مسموح ما لم يحصر القيد T في أنواع شبيهة بالمؤشرات. أعد قيمة صفرية. - كتابة دالة مساعدة عامة موجودة أصلًا. تفقّد
slicesوmapsوcmpأولًا.
الأسئلة الشائعة
هل تملك Go أنواعًا عامة (generics)؟
نعم، منذ Go 1.18 (مارس 2022). يمكن للدوال والأنواع تعريف معاملات نوع بين قوسين مربّعين: func Map[T, U any](s []T, f func(T) U) []U. لكل معامل نوع قيد، وهو واجهة تصف ما يجب أن يدعمه النوع.
ما الفرق بين any وcomparable في generics في Go؟
يسمح any بكل نوع لكنه لا يتيح فعل شيء تقريبًا بالقيم سوى إسنادها وتمريرها وتخزينها. أما comparable فيسمح فقط بالأنواع التي تدعم == و!=، وهذا ما تحتاجه لمفاتيح الخرائط أو فحوص المساواة. ولـ < و> استخدم cmp.Ordered.
ماذا تعني العلامة ~ في قيد في Go؟
~T تعني "أي نوع نوعه الأساسي T". تطابق ~int النوع int وأيضًا type Celsius int. دون العلامة، يطابق int في القيد int نفسه فقط، فتُرفض الأنواع التي يعرّفها المستخدم.
هل يمكن أن تملك توابع Go معاملات نوع؟
لا. يمكن للتابع استخدام معاملات النوع الخاصة بنوع مستقبِله (func (s *Stack[T]) Push(v T))، لكنه لا يستطيع تعريف معاملات جديدة. اكتب دالة عامة على المستوى الأعلى بدلًا من ذلك: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].