في عالم الأجناس العامة المخصّصة (custom generics)، يمكن تعريف نوع كنوع عام، ويمكن تعريف دالة كدالة عامة. بالإضافة إلى ذلك، الأنواع العامة هي أنواع معرّفة، لذا قد تملك طرقاً.
يحتوي تعريف نوع عام أو دالة عامة أو طريقة لنوع عام على جزء قائمة وسائط النوع (type parameter list)، وهو الفرق الرئيسي عن تعريف نوع أو دالة أو طريقة عادية.
مثال على نوع عام
أولاً، لنطّلع على مثال يوضّح شكل الأنواع العامة. قد لا يكون مثالاً مثالياً، لكنه يُظهر فائدة الأنواع العامة المخصّصة.
package main
import "sync"
type Lockable[T any] struct {
sync.Mutex
Data T
}
func main() {
var n Lockable[uint32]
n.Lock()
n.Data++
n.Unlock()
var f Lockable[float64]
f.Lock()
f.Data += 1.23
f.Unlock()
var b Lockable[bool]
b.Lock()
b.Data = !b.Data
b.Unlock()
var bs Lockable[[]byte]
bs.Lock()
bs.Data = append(bs.Data, "Go"...)
bs.Unlock()
}
في المثال أعلاه، Lockable نوع عام. مقارنةً بالأنواع غير العامة، يوجد جزء إضافي، هو قائمة وسائط النوع، في تعريف النوع العام (أو مواصفته، بتعبير أدق). هنا، قائمة وسائط النوع للنوع العام Lockable هي [T any].
قد تحتوي قائمة وسائط النوع على تعريف واحد أو أكثر لوسائط النوع، محاطة بأقواس مربعة ومفصولة بفواصل. يتكوّن كل تعريف وسيط من اسم وسيط نوع وقيد (نوع). في المثال أعلاه، T هو اسم وسيط النوع وany هو قيد T.
يرجى ملاحظة أن any معرّف مُعلن مسبقاً جديد أُدخل في Go 1.18. وهو اسم مستعار لنوع الواجهة الفارغ interface{}. ينبغي أن نعلم أن جميع الأنواع تطبّق نوع الواجهة الفارغ.
(ملاحظة: عموماً، لا تقول كتب Go 101 إن الاسم المستعار للنوع نوع. إنها تقول فقط إن الاسم المستعار للنوع يدل على نوع. لكن للتبسيط، كثيراً ما يقول كتاب Go Generics 101 إن any نوع.)
يمكننا النظر إلى القيود على أنها أنواع للأنواع (وسائط النوع). جميع قيود الأنواع هي في الواقع أنواع واجهات. القيود هي جوهر الأجناس العامة المخصّصة وستُشرح بالتفصيل في الفصل التالي.
T يدل على نوع وسيط نوع. يبدأ نطاقه بعد اسم النوع العام المعرّف وينتهي عند نهاية مواصفة النوع العام. وهنا يُستخدم كنوع للحقل Data.
منذ Go 1.18، تنقسم أنواع القيم إلى فئتين:
- نوع وسيط النوع؛
- الأنواع العادية.
قبل Go 1.18، كانت جميع أنواع القيم أنواعاً عادية.
النوع العام هو نوع معرّف. يجب تجسيده لاستخدامه كنوع قيمة. يُسمّى الترميز Lockable[uint32] نوعاً مُجسَّداً (للنوع العام Lockable). في هذا الترميز، يُسمّى [uint32] قائمة معطيات النوع ويُسمّى uint32 معطى نوع، ويُمرَّر إلى وسيط النوع T المقابل. هذا يعني أن نوع الحقل Data للنوع المُجسَّد Lockable[uint32] هو uint32.
يجب أن يطبّق معطى النوع قيد وسيط النوع المقابل له. القيد any هو أكثر القيود تسامحاً، ويمكن تمرير أي نوع قيمة إلى وسيط النوع T. معطيات النوع الأخرى المستخدمة في المثال أعلاه هي: float64 وbool و[]byte.
كل نوع مُجسَّد هو نوع مسمّى ونوع عادي. مثلاً، Lockable[uint32] وLockable[[]byte] كلاهما نوعان مسمّيان.
يُظهر المثال أعلاه كيف تتجنّب الأجناس العامة المخصّصة تكرار الشيفرة في تعريفات الأنواع. بدون الأجناس العامة المخصّصة، يلزم تعريف عدة أنواع بنى، كما تُظهر الشيفرة التالية.
package main
import "sync"
type LockableUint32 struct {
sync.Mutex
Data uint32
}
type LockableFloat64 struct {
sync.Mutex
Data float64
}
type LockableBool struct {
sync.Mutex
Data bool
}
type LockableBytes struct {
sync.Mutex
Data []byte
}
func main() {
var n LockableUint32
n.Lock()
n.Data++
n.Unlock()
var f LockableFloat64
f.Lock()
f.Data += 1.23
f.Unlock()
var b LockableBool
b.Lock()
b.Data = !b.Data
b.Unlock()
var bs LockableBytes
bs.Lock()
bs.Data = append(bs.Data, "Go"...)
bs.Unlock()
}
تحتوي الشيفرة غير العامة على تكرارات كثيرة، يمكن تجنّبها باستخدام النوع العام الموضّح أعلاه.
مثال على طريقة لنوع عام
قد لا يُعجب بعض الناس بتطبيق النوع العام أعلاه. بل يفضّلون استخدام تطبيق مختلف كما تُظهر الشيفرة التالية. مقارنةً بتطبيق Lockable في القسم السابق، يُخفي التطبيق الجديد حقول البنية عن مستخدمي الحزمة الخارجيين للنوع العام.
package main
import "sync"
type Lockable[T any] struct {
mu sync.Mutex
data T
}
func (l *Lockable[T]) Do(f func(*T)) {
l.mu.Lock()
defer l.mu.Unlock()
f(&l.data)
}
func main() {
var n Lockable[uint32]
n.Do(func(v *uint32) {
*v++
})
var f Lockable[float64]
f.Do(func(v *float64) {
*v += 1.23
})
var b Lockable[bool]
b.Do(func(v *bool) {
*v = !*v
})
var bs Lockable[[]byte]
bs.Do(func(v *[]byte) {
*v = append(*v, "Go"...)
})
}
في الشيفرة أعلاه، عُرّفت طريقة Do للنوع الأساسي العام Lockable. هنا، نوع المستقبِل هو نوع مؤشر، ونوعه الأساسي هو النوع العام Lockable. على خلاف تعريفات الطرق للأنواع الأساسية العادية، يوجد جزء قائمة وسائط نوع يلي اسم النوع العام للمستقبِل Lockable في جزء المستقبِل. هنا، قائمة وسائط النوع هي [T].
قائمة وسائط النوع في تعريف طريقة لنوع أساسي عام هي في الواقع تكرار لقائمة وسائط النوع المحدّدة في مواصفة النوع الأساسي العام للمستقبِل. ولجعل الشيفرة نظيفة، تُحذف قيود وسائط النوع (ويجب حذفها) من القائمة. لهذا السبب قائمة وسائط النوع هنا هي [T] بدلاً من [T any].
هنا، يُستخدم T في نوع معطى قيمة، func(*T).
- نوع طريقتها Do للنوع المُجسَّد Lockable[uint32] هو func(f func(*uint32)) .
- نوع طريقتها Do للنوع المُجسَّد Lockable[float64] هو func(f func(*float64)) .
- نوع طريقتها Do للنوع المُجسَّد Lockable[bool] هو func(f func(*bool)) .
- نوع طريقتها Do للنوع المُجسَّد Lockable[[]byte] هو func(f func(*[]byte)) .
يرجى ملاحظة أن أسماء وسائط النوع المستخدمة في تعريف طريقة لنوع أساسي عام ليس مطلوباً أن تكون مطابقة للأسماء المقابلة المستخدمة في مواصفة النوع العام. مثلاً، تعريف الطريقة أعلاه مكافئ للتعريف المُعاد كتابته التالي.
func (l *Lockable[Foo]) Do(f func(*Foo)) {
...
}
مع ذلك، من الممارسات السيئة عدم إبقاء أسماء وسائط النوع المقابلة متطابقة.
بالمناسبة، قد يكون اسم وسيط النوع حتى المعرّف الفارغ _ إذا لم يُستخدم (وهذا صحيح أيضاً بالنسبة لوسائط النوع في تعريفات الأنواع والدوال العامة). مثلاً،
func (l *Lockable[_]) DoNothing() {
}
الأسماء المستعارة للأنواع العامة
الأسماء المستعارة للأنواع العامة مدعومة منذ Go 1.24.
type TreeMap[K comparable, E any] struct {
// ... حقول غير مُصدَّرة
}
// عدة أسماء مستعارة لأنواع عامة:
type TreeSet[K comparable] = TreeMap[K, struct{}]
type TreeMapWithIntKey[E any] = TreeMap[int, E]
type TreeMapWithStringKey[E any] = TreeMap[string, E]
مثال على دالة عامة
الآن، لنطّلع على مثال لكيفية تعريف الدوال (غير الطرقية) العامة واستخدامها.
package main
// تتحقق NoDiff مما إذا كانت مجموعة
// من القيم متطابقة كلها أم لا.
func NoDiff[V comparable](vs ...V) bool {
if len(vs) == 0 {
return true
}
v := vs[0]
for _, x := range vs[1:] {
if v != x {
return false
}
}
return true
}
func main() {
var NoDiffString = NoDiff[string]
println(NoDiff("Go", "Go", "Go")) // true
println(NoDiffString("Go", "go")) // false
println(NoDiff(123, 123, 123, 123)) // true
println(NoDiff[int](123, 123, 789)) // false
type A = [2]int
println(NoDiff(A{}, A{}, A{})) // true
println(NoDiff(A{}, A{}, A{1, 2})) // false
println(NoDiff(new(int))) // true
println(NoDiff(new(int), new(int))) // false
println(NoDiff[bool]()) // true
// _ = NoDiff() // خطأ: لا يمكن استنتاج V
// خطأ: *** لا تطبّق comparable
// _ = NoDiff([]int{}, []int{})
// _ = NoDiff(map[string]int{})
// _ = NoDiff(any(1), any(1))
}
في المثال أعلاه، NoDiff دالة عامة. على خلاف الدوال غير العامة، وبشكل مشابه للأنواع العامة، يوجد جزء إضافي، هو قائمة وسائط النوع، في تعريف الدالة العامة. هنا، قائمة وسائط النوع للدالة العامة NoDiff هي [V comparable]، حيث V اسم وسيط النوع وcomparable قيد V.
comparable معرّف مُعلن مسبقاً جديد أُدخل في Go 1.18. وهو يدل على واجهة تطبّقها جميع الأنواع القابلة للمقارنة. وسيُشرح بمزيد من التفاصيل في الفصل التالي.
هنا، يُستخدم وسيط النوع V كنوع للوسيط (القيمة) المتغير.
الترميزات NoDiff[string] وNoDiff[int] وNoDiff[bool] تُسمّى دوالاً مُجسَّدة (للدالة العامة NoDiff). وبشكل مشابه للأنواع المُجسَّدة، تُسمّى [string] و[int] و[bool] في هذه الترميزات قوائم معطيات النوع. وفي هذه القوائم، تُسمّى string وint وbool معطيات نوع، وكلها تُمرَّر إلى وسيط النوع V.
يمكن حذف قائمة معطيات النوع بالكامل من تعبير دالة مُجسَّدة إذا أمكن استنتاج جميع معطيات النوع من معطيات القيم. لهذا السبب لا تحتوي بعض الاستدعاءات للدالة العامة NoDiff على قوائم معطيات نوع في المثال أعلاه.
- في الاستدعاء NoDiff("Go", "Go", "Go") ، يُستنتج معطى النوع على أنه string ، وهو النوع الافتراضي لمعطيات القيم.
- في الاستدعاء NoDiff(123, 123, 123, 123) ، يُستنتج معطى النوع على أنه int ، وهو النوع الافتراضي لمعطيات القيم.
- في الاستدعاءين اللذين معطيات قيمهما A{} و A{1, 2} ، يُستنتج معطى النوع على أنه [2]int ، وهو نوع معطيات القيم.
- في الاستدعاءين اللذين معطى قيمتهما new(int) ، يُستنتج معطى النوع على أنه *int ، وهو نوع معطيات القيم.
يرجى ملاحظة أن جميع معطيات النوع هذه تطبّق الواجهة comparable. لا يمكن تمرير الأنواع غير القابلة للمقارنة، مثل []int وmap[string]int، كمعطيات نوع في استدعاءات الدالة العامة NoDiff. ويرجى ملاحظة أنه على الرغم من أن any نوع (قيمة) قابل للمقارنة، فإنه لا يطبّق comparable، لذا فهو أيضاً ليس معطى نوع مؤهلاً. سيُتحدَّث عن هذا بالتفصيل في الفصل التالي.
يُظهر المثال أعلاه كيف تتجنّب الأجناس العامة تكرار الشيفرة في تعريفات الدوال. بدون الأجناس العامة المخصّصة، نحتاج إلى تعريف دالة لكل معطى نوع مستخدم في المثال. وستكون أجسام تعريفات الدوال هذه متطابقة تقريباً.
يمكن النظر إلى الدوال العامة على أنها صيغ مبسّطة لطرق الأنواع العامة
يمكن النظر إلى الدالة العامة المعروضة في القسم أعلاه على أنها صيغة مبسّطة لطريقة لنوع عام، كما تُظهر الشيفرة التالية.
package main
type NoDiff[V comparable] struct{}
func (nd NoDiff[V]) Do(vs ...V) bool {
... // نفس جسم الدالة العامة أعلاه
}
func main() {
var NoDiffString = NoDiff[string]{}.Do
println(NoDiffString("Go", "go")) // false
println(NoDiff[int]{}.Do(123, 123, 789)) // false
println(NoDiff[*int]{}.Do(new(int))) // true
}
في الشيفرة أعلاه، NoDiff[string]{}.Do وNoDiff[int]{}.Do وNoDiff[*int]{}.Do ثلاث قيم طرق لأنواع مُجسَّدة مختلفة.
يمكننا النظر إلى النوع العام على أنه فضاء وسائط نوع، وإلى جميع طرقه على أنها دوال تشترك في فضاء وسائط النوع نفسه.
لتبسيط الأوصاف، تُسمّى طرق الأنواع العامة أحياناً دوال عامة أيضاً في هذا الكتاب.