في الفصلين الأخيرين، استخدمنا عدة تجسيدات لأنواع ودوال عامة. وهنا، يقدّم هذا الفصل مقدمة رسمية للأنواع والدوال المجسَّدة.
تجسيدات الأنواع/الدوال العامة
يجب تجسيد الأنواع العامة (generic types) لتُستخدم كأنواع للقيم، ويجب تجسيد الدوال العامة لتُستدعى أو تُستخدم كقيم دوال.
يُجسَّد النوع (أو الدالة) العامة بإحلال قائمة معطيات الأنواع (type argument list) محل قائمة وسائط الأنواع (type parameter list) في تعريفه (مواصفته). ويكون طول قائمة معطيات الأنواع (الكاملة) هو نفس طول قائمة وسائط الأنواع. ويقابل كل معطى نوع وسيط نوع ويُمرَّر إلى ذلك الوسيط. بافتراض أن قيد وسيط النوع المقابل لمعطى نوع ما هو C، فيجب أن يكون معطى النوع
- إما نوعاً عادياً (نوعاً غير واجهي أو نوع واجهة أساسياً) يحقق C (لكن ليس مطلوباً منه تطبيقه)؛
- أو وسيط نوع يطبّق C إذا كان التجسيد داخل تعريف دالة عامة أو مواصفة نوع حاويتين. ويكون وسيط النوع معرَّفاً في تعريف الدالة العامة أو مواصفة النوع الحاويتين.
الدوال المجسَّدة هي دوال غير عامة. والأنواع المجسَّدة هي أنواع قيم عادية مسمّاة.
مثل قوائم وسائط الأنواع، تُحاط قائمة معطيات الأنواع أيضاً بأقواس مربعة، وتُفصل معطيات الأنواع بفواصل في قائمة معطيات الأنواع. وقاعدة إدراج الفواصل في قوائم معطيات الأنواع هي أيضاً نفسها في قوائم وسائط الأنواع.
تتطابق قائمتا معطيات أنواع إذا تساوى طولا صيغتيهما الكاملتين وتطابقت جميع أنواع المعطيات المقابلة فيهما. ويتطابق نوعان مجسَّدان إذا جُسِّدا من النوع العام نفسه وبقائمة معطيات الأنواع الكاملة نفسها.
كما أُشير عدة مرات في الأوصاف أعلاه، قد تكون قائمة معطيات الأنواع جزئية. يُرجى قراءة قسم «استنتاجات معطيات الأنواع» التالي للتفاصيل.
في البرنامج التالي، يُجسَّد النوع العام Data أربع مرات. ولثلاثة من التجسيدات الأربعة قائمة معطيات الأنواع نفسها (يُرجى ملاحظة أن byte المُعرَّف مسبقاً هو اسم مستعار للنوع uint8 المُعرَّف مسبقاً). لذا فإن نوع المتغير x، والنوع الذي يشير إليه الاسم المستعار Z، والنوع الكامن للنوع المعرَّف W هي النوع نفسه.
package main
import (
"fmt"
"reflect"
)
type Data[A int64 | int32, B byte | bool, C comparable] struct {
a A
b B
c C
}
var x = Data[int64, byte, [8]byte]{1<<62, 255, [8]byte{}}
type Y = Data[int32, bool, string]
type Z = Data[int64, uint8, [8]uint8]
type W Data[int64, byte, [8]byte]
// السطر التالي يفشل في التصريف لأن
// []uint8 لا يحقق القيد comparable.
// type T = Data[int64, uint8, []uint8] // خطأ
func main() {
println(reflect.TypeOf(x) == reflect.TypeOf(Z{})) // true
println(reflect.TypeOf(x) == reflect.TypeOf(Y{})) // false
fmt.Printf("%T\n", x) // main.Data[int64,uint8,[8]uint8]
fmt.Printf("%T\n", Z{}) // main.Data[int64,uint8,[8]uint8]
}
في ما يلي مثال يستخدم بعض الدوال المجسَّدة لدالة عامة.
package main
type Ordered interface {
~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 |
~int32 | ~uint32 | ~int64 | ~uint64 | ~uintptr |
~float32 | ~float64 | ~string
}
func Max[S ~[]E, E Ordered](vs S) E {
if len(vs) == 0 {
panic("no elements")
}
var r = vs[0]
for i := range vs[1:] {
if vs[i] > r {
r = vs[i]
}
}
return r
}
type Age int
var ages = []Age{99, 12, 55, 67, 32, 3}
var langs = []string {"C", "Go", "C++"}
func main() {
var maxAge = Max[[]Age, Age]
println(maxAge(ages)) // 99
var maxStr = Max[[]string, string]
println(maxStr(langs)) // Go
}
في المثال أعلاه، تُجسَّد الدالة العامة Max مرتين.
- التجسيد الأول Max[[]Age, Age] ينتج قيمة دالة من النوع func([]Age] Age.
- والتجسيد الثاني، Max[[]string, string]، ينتج قيمة دالة من النوع func([]string) string.
استنتاجات معطيات الأنواع لتجسيدات الدوال العامة
في مثال الدالة العامة المعروض في القسم الأخير، يُسمّى تجسيدا الدالة تجسيدَي الصيغة الكاملة، حيث تُعرض جميع معطيات الأنواع في قوائم معطيات الأنواع الحاوية لها. تدعم Go استنتاجات الأنواع (type inferences) لتجسيدات الدوال العامة، بحيث يمكن أن تكون قائمة معطيات الأنواع لدالة مجسَّدة جزئية أو حتى محذوفة كلياً، ما دام يمكن استنتاج معطيات الأنواع المفقودة من وسائط القيم ومعطيات الأنواع المعروضة.
على سبيل المثال، يمكن إعادة كتابة الدالة main في المثال الأخير من القسم السابق كما يلي
func main() {
var maxAge = Max[[]Age] // قائمة معطيات جزئية
println(maxAge(ages)) // 99
var maxStr = Max[[]string] // قائمة معطيات جزئية
println(maxStr(langs)) // Go
}
يجب أن تكون قائمة معطيات الأنواع الجزئية بادئةً لقائمة المعطيات الكاملة. وفي الشيفرة أعلاه، حُذف المعطى الثاني في الحالتين، لأنه يمكن استنتاجه من المعطى الأول.
إذا استُدعيت دالة مجسَّدة مباشرةً وأمكن استنتاج بعض معطيات الأنواع اللاحقة من أنواع معطيات القيم، فيمكن أن تكون قائمة معطيات الأنواع أيضاً جزئية أو حتى محذوفة كلياً. على سبيل المثال، يمكن أيضاً إعادة كتابة الدالة main كما يلي
func main() {
println(Max(ages)) // 99
println(Max(langs)) // Go
}
تُظهر الدالة main المعاد كتابتها أن استدعاءات الدوال العامة يمكن أن تكون ببساطة الدوال العادية (أحياناً على الأقل)، حتى وإن كانت تعريفات الدوال العامة أكثر إسهاباً.
يُرجى ملاحظة أن قوائم معطيات الأنواع قد تُحذف كلياً لكن لا يجوز أن تكون فارغة. والشيفرة التالية غير قانونية.
func main() {
println(Max[](ages)) // خطأ صياغي
println(Max[](langs)) // خطأ صياغي
}
ذُكر أعلاه أن قائمة معطيات الأنواع الجزئية يجب أن تكون بادئةً لقائمة المعطيات الكاملة. وهذا يعني أيضاً أن معطيات الأنواع المستنتجة في قائمة معطيات الأنواع تنتمي إلى لاحقة قائمة معطيات الأنواع. على سبيل المثال، تفشل الشيفرة التالية في التصريف.
package main
func foo[A, B, C any](v B) {}
func main() {
// خطأ: لا يمكن استخدام _ كقيمة أو كنوع
foo[int, _, bool]("Go")
}
يمكن استنتاج معطيات الأنواع من أنواع العناصر وأنواع الحقول وأنواع الوسائط وأنواع النتائج لمعطيات القيم. على سبيل المثال،
package main
func luk[E any](v struct{x E}) {}
func kit[E any](v []E) {}
func wet[E any](v func() E) {}
func main() {
luk(struct{x int}{123}) // حسن
kit([]string{"go", "c"}) // حسن
wet(func() bool {return true}) // حسن
}
إذا كانت مجموعة الأنواع لقيد وسيط نوع ما تحتوي على نوع واحد فقط، وكان وسيط النوع مستخدماً كنوع وسيط قيمة في دالة عامة، فسيحاول المصرِّف استنتاج نوع معطى قيمة غير مُنمَّطة يُمرَّر إلى وسيط القيمة ليكون ذلك النوع الوحيد. وإذا فشلت المحاولة، فيُنظر إلى معطى القيمة غير المُنمَّطة هذا على أنه غير صالح.
على سبيل المثال، في البرنامج التالي، لا يُصرَّف إلا استدعاء الدالة الأول.
package main
func foo[T int](x T) {}
func bar[T ~int](x T) {}
func main() {
// النوع الافتراضي للقيمة 1.0 هو float64، لكن
// 1.0 قابلة للتمثيل أيضاً كقيم صحيحة.
foo(1.0) // حسن
foo(1.23) // خطأ: لا يمكن استخدام 1.23 كـ int
bar(1.0) // خطأ: float64 لا يطبّق ~int
bar(1.23) // خطأ: float64 لا يطبّق ~int
}
لا تزال قواعد استنتاج معطيات الأنواع وتنفيذه في عملية تحسين مستمرة من إصدار إلى آخر. على سبيل المثال، فشلت الشيفرة التالية في التصريف قبل Go 1.21 لكنها تُصرَّف بشكل صحيح منذ Go 1.21 (يُستنتج T على أنه float64).
func Less[T ~int | ~float64](x, y T) bool {
return x < y
}
var _ = Less(1, 0.0) // لا يُصرَّف إلا منذ 1.21
قبل Go 1.21، عند استخدام دالة عامة كقيمة دالة، وجب أن تكون قائمة معطيات الأنواع للدالة المجسَّدة كاملة. ومنذ Go 1.21، يُسمح بأن تكون قائمة معطيات الأنواع جزئية إذا أمكن استنتاج بعض معطيات الأنواع اللاحقة. على سبيل المثال
func Sum[T ~float32 | ~float64](x, y T) T {
return x + y
}
// لا يُصرَّف السطران التاليان إلا منذ Go 1.21.
var _ func(float32, float32) float32 = Sum
var _ func(float64, float64) float64 = Sum
// قبل Go 1.21، وجب كتابة السطرين أعلاه كما يلي:
var _ func(float32, float32) float32 = Sum[float32]
var _ func(float64, float64) float64 = Sum[float64]
مثال كامل:
package main
import "fmt"
func Convert[S ~[]E, E, T any](s S, conv func(E) T) []T {
r := make([]T, len(s))
for i := range s {
r[i] = conv(s[i])
}
return r
}
func isEven(n int) bool {
return n&1 == 0
}
func intsToBools(s []int) []bool {
// لا يُصرَّف السطر التالي إلا منذ Go 1.21.
var conv func([]int, func(int) bool) []bool = Convert
// قبل Go 1.21، لجعل السطر أعلاه يُصرَّف، وجب كتابة
// طرفه الأيمن كما يلي: Convert[[]int, int bool]
return conv(s, isEven)
}
func main() {
var s = []int{2, 3, 5, 8}
fmt.Println(intsToBools(s)) // [true false false true]
}
وبالمثل، منذ Go 1.21، يمكن استنتاج معطيات الأنواع عبر تحليل أنواع وسائط الطرق وأنواع نتائجها. على سبيل المثال، فشلت الشيفرة التالية في التصريف قبل Go 1.21 لكنها تُصرَّف بشكل صحيح منذ Go 1.21.
package main
type C[I, O any] interface {
M(I) O
}
type Foo struct {
v int
}
func (f Foo) M(n int) bool {
return n == f.v
}
func bar[T C[I, O], I, O any](t T) {}
func main() {
var foo Foo
// 1.20: خطأ: لا يمكن استنتاج I. ويجب كتابتها
// كـ bar[Foo, int, bool](foo) ليتم تصريفها.
// 1.21+: تُصرَّف بشكل صحيح.
bar(foo)
}
مثال أكثر تعقيداً (يُصرَّف بشكل صحيح منذ Go 1.21):
type Getter[T any] interface {
Get() T
}
type Age[T uint8 | int16] struct {
n T
}
func (a Age[T]) Get() T {
return a.n
}
func doSomething[T any](g Getter[T]) T {
return g.Get()
}
// لا يُصرَّف السطران إلا منذ Go 1.21.
var _ = doSomething(Age[uint8]{255}) // خطأ
var _ = doSomething(Age[int16]{256}) // خطأ
// قبل Go 1.21، لجعل السطرين أعلاه
// يُصرَّفان، وجب كتابتهما كما يلي:
var _ = doSomething[uint8](Age[uint8]{255})
var _ = doSomething[int16](Age[int16]{256})
لا يشرح الكتاب الآليات التفصيلية المتضمنة في عملية الاستنتاج. يُرجى قراءة مواصفة Go وهذه التدوينة للاطلاع على تفاصيل استنتاجات معطيات الأنواع.
لا تعمل استنتاجات معطيات الأنواع مع تجسيدات الأنواع العامة
حالياً (Go 1.25)، لا يُدعَم استنتاج معطيات الأنواع للأنواع المجسَّدة من القيم الحرفية. وهذا يعني أن قائمة معطيات الأنواع لتجسيد نوع عام يجب أن تكون دائماً بالصيغة الكاملة.
على سبيل المثال، في مقطع الشيفرة التالي، سطر تعريف المتغير y غير صالح، حتى وإن كان ممكناً استنتاج معطى النوع على أنه int16.
type Set[E comparable] map[E]bool
// يُصرَّف بشكل صحيح
var x = Set[int16]{123: false, 789: true}
// خطأ: لا يمكن استخدام نوع عام دون تجسيد.
var y = Set{int16(123): false, int16(789): true}
مثال آخر:
import "sync"
type Lockable[T any] struct {
sync.Mutex
Data T
}
// يُصرَّف بشكل صحيح
var a = Lockable[float64]{Data: 1.23}
// خطأ: لا يمكن استخدام نوع عام دون تجسيد
var b = Lockable{Data: float64(1.23)} // خطأ
من غير الواضح ما إذا كان سيُدعَم استنتاج معطيات الأنواع لتجسيدات الأنواع العامة في إصدارات Go المستقبلية أم لا.
الفروق بين تمرير وسائط الأنواع والأنواع العادية كمعطيات أنواع
ذُكر أعلاه أن
- عندما يُستخدم نوع عادي كمعطى نوع، لا يُشترط في النوع العادي إلا أن يحقق قيد وسيط النوع المقابل. وهذا تغيير أُجري في Go 1.20.
- وعندما يُستخدم وسيط نوع كمعطى نوع، يُشترط في وسيط النوع أن يطبّق قيد وسيط النوع المقابل.
على سبيل المثال، منذ Go 1.20، جميع التجسيدات في البرنامج التالي صالحة (وكانت غير صالحة قبل Go 1.20). ولا يطبّق أي من معطيات الأنواع، وهي كلها أنواع عادية، القيود comparable، لكنها كلها تحقق القيود.
package main
type Base[T comparable] []T
type _ Base[any]
type _ Base[error]
type _ Base[[2]any]
type _ Base[struct{x any}]
func Gf[T comparable](x T) {}
var _ = Gf[any]
var _ = Gf[error]
var _ = Gf[[2]any]
var _ = Gf[struct{x any}]
func main() {
Gf[any](123)
Gf[error](nil)
Gf[[2]any]([2]any{})
Gf[struct{x any}](struct{x any}{})
}
من ناحية أخرى، جميع التجسيدات في البرنامج التالي غير صالحة. فجميع معطيات الأنواع هي وسائط أنواع، ولا يطبّق أي منها القيود comparable، ولهذا فهي كلها معطيات أنواع غير صالحة.
package main
type Base[T comparable] []T
func Gf[T comparable](x T) {}
func vut[T any]() {
_ = Gf[T] // T لا يطبّق comparable
var _ Base[T] // T لا يطبّق comparable
}
func law[T struct{x any}]() {
_ = Gf[T] // T لا يطبّق comparable
var _ Base[T] // T لا يطبّق comparable
}
func pod[T [2]any]() {
_ = Gf[T] // T لا يطبّق comparable
var _ Base[T] // T لا يطبّق comparable
}
func main() {}
جميع معطيات الأنواع المستخدمة في البرنامج التالي صالحة. فكلها وسائط أنواع وتطبّق القيود comparable.
package main
type Base[T comparable] []T
func Gf[T comparable](x T) {}
func vut[T comparable]() {
_ = Gf[T]
var _ Base[T]
}
func law[T struct{x int}]() {
_ = Gf[T]
var _ Base[T]
}
func pod[T [2]string]() {
_ = Gf[T]
var _ Base[T]
}
func main() {}
المزيد عن الأنواع المجسَّدة
بما أن النوع المجسَّد نوع عادي،
- فيمكن استخدامه كحدود اتحاد (union terms) إذا كان نوعاً غير واجهي.
- ويمكن استخدامه كحدود اتحاد إذا كان نوع واجهة بلا طرق.
- ويمكن استخدامه كقيد نوع إذا كان نوع واجهة.
- ويمكن تضمينه في أنواع البنى إذا حقق متطلبات الحقول المضمَّنة.
تعريفا النوعين العامين C1 وC2 في الشيفرة التالية صالحان.
package main
type Slice[T any] []T
type C1[E any] interface {
Slice[E] // اسم نوع عادي
}
type C2[E any] interface {
[]E | Slice[E] // حسن
}
func foo[E any, T C2[E]](x T) {}
func main() {
var x Slice[bool]
var y Slice[string]
foo(x)
foo(y)
}
تعرض الشيفرة التالية تعريف نوع بنية عادي يضمّن تجسيدين للنوع العام Set. ولتجنّب تكرار أسماء الحقول، يستخدم أحد الحقلين المضمَّنين اسماً مستعاراً لتجسيد من النوع العام.
package main
type Set[T comparable] map[T]struct{}
type Strings = Set[string]
type MyData struct {
Set[int]
Strings
}
func main() {
var md = MyData {
Set: Set[int]{},
Strings: Strings{},
}
md.Set[123] = struct{}{}
md.Strings["Go"] = struct{}{}
}
حول عدم اتساق صياغة التجسيد بين الأجناس العامة المخصّصة والأجناس العامة المدمجة
من المحتويات السابقة، يمكننا أن نجد أن صياغة التجسيد في الأجناس العامة المخصّصة في Go لا تتسق مع الأجناس العامة المدمجة في Go.
type TreeMap[K comparable, V any] struct {
// ... // التنفيذ الداخلي
}
func MyNew[T any]() *T {
return new(T)
}
// تختلف طريقة التجسيدين.
var _ map[string]int
var _ TreeMap[string, int]
// تختلف طريقة التجسيدين.
var _ = new(bool)
var _ = MyNew[bool]()
شخصياً، أرى أن عدم الاتساق مؤسف وأنه يزيد العبء المعرفي في برمجة Go. ومن ناحية أخرى، أعترف أنه من الصعب (أو حتى المستحيل) جعل الصياغة متسقة. ومن المؤسف أن Go لم تدعم الأجناس العامة المخصّصة منذ البداية.