هذه هي نسخة المدونة من محادثاتي في Google Open Source Live:
https://youtu.be/nr8EpUO9jhw?si=jlWTapr6NM6isLgt&embedable=true
و GopherCon 2021:
https://youtu.be/Pae9EeCdy8?si=M-87Eisb2nU1qmJ&embedable=true
\ يضيف إصدار Go 1.18 ميزة لغة جديدة رئيسية: دعم البرمجة العامة. في هذا المقال، لن أصف ما هي البرمجة العامة ولا كيفية استخدامها. يتعلق هذا المقال بمتى يجب استخدام البرمجة العامة في كود Go، ومتى لا يجب استخدامها.
\ للتوضيح، سأقدم إرشادات عامة، وليست قواعد صارمة وثابتة. استخدم حكمك الخاص. ولكن إذا لم تكن متأكدًا، فأنا أوصي باستخدام الإرشادات المناقشة هنا.
لنبدأ بإرشادات عامة لبرمجة Go: اكتب برامج Go عن طريق كتابة الكود، وليس عن طريق تعريف الأنواع. عندما يتعلق الأمر بالبرمجة العامة، إذا بدأت في كتابة برنامجك بتحديد قيود معلمات النوع، فأنت على الأرجح في المسار الخطأ. ابدأ بكتابة الدوال. من السهل إضافة معلمات النوع لاحقًا عندما يكون واضحًا أنها ستكون مفيدة.
بعد قول ذلك، دعنا ننظر في الحالات التي يمكن أن تكون فيها معلمات النوع مفيدة.
إحدى الحالات هي عند كتابة دوال تعمل على أنواع الحاويات الخاصة المحددة باللغة: الشرائح والخرائط والقنوات. إذا كانت الدالة لها معلمات بتلك الأنواع، وكود الدالة لا يقوم بأي افتراضات معينة حول أنواع العناصر، فقد يكون من المفيد استخدام معلمة نوع.
\ على سبيل المثال، هنا دالة تعيد شريحة من جميع المفاتيح في خريطة من أي نوع:
// MapKeys returns a slice of all the keys in m. // The keys are not returned in any particular order. func MapKeys[Key comparable, Val any](m map[Key]Val) []Key { s := make([]Key, 0, len(m)) for k := range m { s = append(s, k) } return s }
\ هذا الكود لا يفترض أي شيء حول نوع مفتاح الخريطة، ولا يستخدم نوع قيمة الخريطة على الإطلاق. إنه يعمل لأي نوع من الخرائط. هذا يجعله مرشحًا جيدًا لاستخدام معلمات النوع.
\ البديل لمعلمات النوع لهذا النوع من الدوال هو عادة استخدام الانعكاس، ولكن هذا نموذج برمجة أكثر إحراجًا، ولا يتم التحقق منه بشكل ثابت في وقت البناء، وغالبًا ما يكون أبطأ في وقت التشغيل.
حالة أخرى يمكن أن تكون فيها معلمات النوع مفيدة هي لهياكل البيانات ذات الغرض العام. هيكل البيانات ذو الغرض العام هو شيء مثل الشريحة أو الخريطة، ولكنه غير مدمج في اللغة، مثل القائمة المرتبطة، أو الشجرة الثنائية.
\ اليوم، البرامج التي تحتاج إلى مثل هذه الهياكل البيانية عادة ما تفعل واحدًا من شيئين: كتابتها بنوع عنصر محدد، أو استخدام نوع واجهة. استبدال نوع عنصر محدد بمعلمة نوع يمكن أن ينتج هيكل بيانات أكثر عمومية يمكن استخدامه في أجزاء أخرى من البرنامج، أو بواسطة برامج أخرى. استبدال نوع الواجهة بمعلمة نوع يمكن أن يسمح بتخزين البيانات بشكل أكثر كفاءة، مما يوفر موارد الذاكرة؛ يمكن أيضًا أن يسمح للكود بتجنب تأكيدات النوع، وأن يتم التحقق من النوع بالكامل في وقت البناء.
\ على سبيل المثال، هذا جزء مما قد يبدو عليه هيكل بيانات شجرة ثنائية باستخدام معلمات النوع:
// Tree is a binary tree. type Tree[T any] struct { cmp func(T, T) int root *node[T] } // A node in a Tree. type node[T any] struct { left, right *node[T] val T } // find returns a pointer to the node containing val, // or, if val is not present, a pointer to where it // would be placed if added. func (bt *Tree[T]) find(val T) **node[T] { pl := &bt.root for *pl != nil { switch cmp := bt.cmp(val, (*pl).val); { case cmp < 0: pl = &(*pl).left case cmp > 0: pl = &(*pl).right default: return pl } } return pl } // Insert inserts val into bt if not already there, // and reports whether it was inserted. func (bt *Tree[T]) Insert(val T) bool { pl := bt.find(val) if *pl != nil { return false } *pl = &node[T]{val: val} return true }
\ كل عقدة في الشجرة تحتوي على قيمة من معلمة النوع T
. عندما يتم تهيئة الشجرة بوسيطة نوع معينة، سيتم تخزين قيم ذلك النوع مباشرة في العقد. لن يتم تخزينها كأنواع واجهة.
\ هذا استخدام معقول لمعلمات النوع لأن هيكل بيانات Tree
، بما في ذلك الكود في الطرق، مستقل إلى حد كبير عن نوع العنصر T
.
\ يحتاج هيكل بيانات Tree
إلى معرفة كيفية مقارنة قيم نوع العنصر T
؛ يستخدم دالة مقارنة ممررة لذلك. يمكنك رؤية هذا في السطر الرابع من طريقة find
، في الاستدعاء إلى bt.cmp
. بخلاف ذلك، فإن معلمة النوع لا تهم على الإطلاق.
يوضح مثال Tree
إرشادًا عامًا آخر: عندما تحتاج إلى شيء مثل دالة مقارنة، فضل الدالة على الطريقة.
\ كان يمكننا تعريف نوع Tree
بحيث يكون نوع العنصر مطلوبًا أن يكون له طريقة Compare
أو Less
. سيتم ذلك عن طريق كتابة قيد يتطلب الطريقة، مما يعني أن أي وسيطة نوع تستخدم لتهيئة نوع Tree
ستحتاج إلى أن يكون لها تلك الطريقة.
\ ستكون النتيجة أن أي شخص يريد استخدام Tree
مع نوع بيانات بسيط مثل int
سيضطر إلى تعريف نوع عدد صحيح خاص به وكتابة طريقة المقارنة الخاصة به. إذا قمنا بتعريف Tree
لأخذ دالة مقارنة، كما في الكود الموضح أعلاه، فمن السهل تمرير الدالة المطلوبة. من السهل كتابة دالة المقارنة تلك كما هو الحال في كتابة طريقة.
\ إذا حدث أن نوع عنصر Tree
لديه بالفعل طريقة Compare
، فيمكننا ببساطة استخدام تعبير طريقة مثل ElementType.Compare
كدالة مقارنة.
\ بعبارة أخرى، من الأسهل بكثير تحويل طريقة إلى دالة من إضافة طريقة إلى نوع. لذلك بالنسبة لأنواع البيانات ذات الغرض العام، فضل الدالة بدلاً من كتابة قيد يتطلب طريقة.
حالة أخرى يمكن أن تكون فيها معلمات النوع مفيدة هي عندما تحتاج أنواع مختلفة إلى تنفيذ بعض الطرق المشتركة، وتبدو التنفيذات للأنواع المختلفة متطابقة.
\ على سبيل المثال، ضع في اعتبارك sort.Interface
في المكتبة القياسية. يتطلب أن ينفذ النوع ثلاث طرق: Len
، وSwap
، وLess
.
\ هنا مثال على نوع عام SliceFn
الذي ينفذ sort.Interface
لأي نوع شريحة:
// SliceFn implements sort.Interface for a slice of T. type SliceFn[T any] struct { s []T less func(T, T) bool } func (s SliceFn[T]) Len() int { return len(s.s) } func (s SliceFn[T]) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] } func (s SliceFn[T]) Less(i, j int) bool { return s.less(s.s[i], s.s[j]) }
\ لأي نوع شريحة، طرق Len
وSwap
متطابقة تمامًا. تتطلب طريقة Less
مقارنة، وهي جزء Fn
من الاسم SliceFn
. كما هو الحال مع مثال Tree
السابق، سنمرر دالة عند إنشاء SliceFn
.
\ هنا كيفية استخدام SliceFn
لفرز أي شريحة باستخدام دالة مقارنة:
// SortFn sorts s in place using a comparison function. func SortFn[T any](s []T, less func(T, T) bool) { sort.Sort(SliceFn[T]{s, less}) }
\ هذا مشابه لدالة المكتبة القياسية sort.Slice
، ولكن دالة المقارنة مكتوبة باستخدام القيم بدلاً من فهارس الشريحة.
\ استخدام معلمات النوع لهذا النوع من الكود منا