سيتناول هذا الفصل العمليات التي تكون صالحة على قيم وسائط الأنواع والعمليات التي تكون غير صالحة في أجسام الدوال العامة.
داخل جسم دالة عامة، لا تكون عملية على قيمة من وسيط نوع صالحة إلا إذا كانت صالحة لقيم كل نوع في مجموعة الأنواع (type set) لقيد وسيط النوع. وفي التصميم والتنفيذ الحاليين للأجناس العامة المخصّصة (Go 1.25)، لا ينطبق العكس دائماً. فيجب استيفاء بعض المتطلبات الإضافية لتكون العملية صالحة.
حالياً، توجد قيود كثيرة من هذا القبيل. بعضها مؤقت وقد يُزال من إصدارات Go المستقبلية، وبعضها دائم. القيود المؤقتة سببها أساساً عبء التنفيذ، لذا تحتاج إلى بعض الوقت والجهد لتُزال في النهاية. أما القيود الدائمة فسببها مبادئ تصميم الأجناس العامة المخصّصة.
ستسرد المحتويات التالية من هذا الفصل هذه القيود. وستُسرد أيضاً بعض الحقائق والمفاهيم المتعلقة بها.
يجب أن يكون لكل قيمة مُنمَّطة نوع محدَّد، وكذلك القيم المستخدمة في دالة عامة
كما ذُكر في فصل سابق، منذ Go 1.18، يمكن تصنيف أنواع القيم في Go إلى فئتين:
- أنواع وسائط الأنواع: الأنواع المعرَّفة في قوائم وسائط الأنواع.
- الأنواع العادية: أنواع القيم غير المعرَّفة في قوائم وسائط الأنواع. وقبل Go 1.18، لم تكن توجد إلا أنواع عادية.
لا تُنفَّذ الأجناس العامة المخصّصة في Go كقوالب نصية بسيطة للشيفرة. وهذا فرق جوهري عن توليد الشيفرة. وهناك قاعدة مبدئية في برمجة Go: يجب أن يكون لكل تعبير مُنمَّط نوع محدَّد، وقد يكون نوعاً عادياً أو وسيط نوع.
على سبيل المثال، في مقطع الشيفرة التالي، لا تُصرَّف الدالة dot وحدها، أما الدوال الأخرى فتُصرَّف بشكل صحيح. والأسباب بسيطة:
- في الدالة foo، نوع x هو T، وهو وسيط نوع. وبالتأكيد، في استخدامات الدالة، قد يُجسَّد x على أنه int أو string، لكن هذا لا يغيّر حقيقة أن نوعه، من وجهة نظر المصرِّفات، وسيط نوع.
- في الدالة bar، نوعا x[i] وx[y] كلاهما وسيط نوع، وهو E.
- في الدالة win، نوعا x[1] وx[y] كلاهما نوع عادي محدَّد، وهو int.
- في الدالة dot، قد يكون نوعا x[1] وx[y] هو int أو string (نوعان عاديان مختلفان)، مع أنهما متطابقان دائماً.
func foo[T int | string](x T) {
var _ interface{} = x // حسن
}
func bar[T []E, E any](x T, i, j int) () {
x[i] = x[j] // حسن
}
func win[T ~[2]int | ~[8]int](x T, i, j int) {
x[i] = x[j] // حسن
}
func dot[T [2]int | [2]string](x T, i, j int) {
x[i] = x[j] // خطأ: عملية غير صالحة
var _ any = x[i] // خطأ: عملية غير صالحة
}
يُنظر إلى أنواع عناصر النصوص على أنها byte، لذا تُصرَّف الشيفرة التالية،
func ele[ByteSeq ~string|~[]byte](x ByteSeq, n int) {
_ = x[n] // حسن
}
وللسبب نفسه (القاعدة المبدئية)، في مقطع الشيفرة التالي، تُصرَّف الدالتان nop وjam بشكل صحيح، أما الدالة mud فلا.
func nop[T *Base, Base int32|int64](x T) {
*x = *x + 1 // حسن
}
func jam[T int32|int64](x *T) {
*x = *x + 1 // حسن
}
func mud[T *int32|*int64](x T) {
*x = *x + 1 // خطأ: عملية غير صالحة
}
وبالمثل، في مقطع الشيفرة التالي، تفشل الدالة box وحدها في التصريف، أما الدالتان الأخريان فتُصرَّفان بشكل صحيح.
func box[T chan int | chan byte](c T) {
_ = <-c // خطأ
}
func sed[T chan E, E int | byte](c T) {
_ = <-c // حسن
}
type Ch <-chan int
func cat[T chan int | Ch](c T) {
_ = <-c // حسن
}
يمكن إجراء تأكيد النوع على وسائط الأنواع
بما أن وسيط النوع نوع محدَّد، يمكن إجراء تأكيد النوع (type assertion) عليه. وتُصرَّف الشيفرة التالية، حتى مع وجود تعبيرات نوع case مكررة في وقت التشغيل في كتلة الشيفرة type-switch ضمن الدالة wua.
import "fmt"
func nel[T int | string](x any) {
if v, ok := x.(T); ok {
fmt.Printf("x is a %T\n", v)
} else {
fmt.Printf("x is not a %T\n", v)
}
}
func wua[T int | string](x any) {
switch v := x.(type) {
case T:
fmt.Println(v)
case int:
fmt.Println("int")
case string:
fmt.Println("string")
}
}
لا يجوز استخدام وسائط الأنواع كأنواع للثوابت المسمّاة (المحلية)
وهذا يعني أن قيم وسائط الأنواع كلها غير ثوابت.
على سبيل المثال، تفشل الدالة التالية في التصريف.
func f[P int]() {
const y P = 5 // خطأ: نوع الثابت P غير صالح
}
هذه الحقيقة لن تتغيّر أبداً.
بسبب هذه الحقيقة، ينتج عن تحويل ثابت إلى وسيط نوع قيمة غير ثابتة من المعطى الممرَّر إلى وسيط النوع. على سبيل المثال، في الشيفرة التالية، تُصرَّف الدالة h، أما الدالة g فلا.
const N = 5
func g[P int]() {
const _ = P(N) // خطأ: P(N) ليست ثابتة
}
func h[P int]() {
var _ = P(N) // حسن
}
بسبب قاعدة التحويل، تختلف نتيجتا الإرجاع للدالتين mud وtex.
package main
const S = "Go"
func mud() byte {
return 64 << len(string(S)) >> len(string(S))
}
func tex[T string]() byte {
return 64 << len(T(S)) >> len(T(S))
}
func main() {
println(mud()) // 64
println(tex()) // 0
}
يُرجى قراءة مقالة النصوص في Go وهذه المسألة لمعرفة سبب إرجاع الدالتين نتيجتين مختلفتين.
لا يمكن استدعاء دالة من وسيط نوع إلا إذا شاركت جميع الأنواع في مجموعة الأنواع لوسيط النوع النوع الكامن نفسه
على سبيل المثال، حالياً (Go 1.25)، في الشيفرة التالية، لا تُصرَّف الدالتان foo وbar، أما الدالة tag فتُصرَّف.
func foo[F func(int) | func(any)] (f F, x int) {
f(x) // خطأ: عملية غير صالحة: لا يمكن استدعاء f لأنه ليس دالة
}
func bar[F func(int) | func(int)int] (f F, x int) {
f(x) // خطأ: عملية غير صالحة: لا يمكن استدعاء f لأنه ليس دالة
}
type Fun func(int)
func tag[F func(int) | Fun] (f F, x int) {
f(x) // حسن
}
من غير الواضح ما إذا كانت القاعدة ستُخفَّف في إصدارات Go المستقبلية أم لا.
إذا كان النوع T في قيمة حرفية مركّبة وسيط نوع، فيجب أن تتشارك جميع الأنواع في مجموعة الأنواع لوسيط النوع النوع الكامن نفسه
على سبيل المثال، حالياً (Go 1.25)، في مقطع الشيفرة التالي، تُصرَّف الدالتان foo وbar بشكل صحيح، أما الدوال الأخرى فلا.
func foo[T ~[]int] () {
_ = T{}
}
type Ints []int
func bar[T []int | Ints] () {
_ = T{} // حسن
}
func ken[T []int | []string] () {
_ = T{} // خطأ: نوع القيمة الحرفية المركّبة T غير صالح
}
func jup[T [2]int | map[int]int] () {
_ = T{} // خطأ: نوع القيمة الحرفية المركّبة T غير صالح
}
تتطلب عملية فهرسة عنصر ألا تتضمن مجموعة الأنواع للمعامَل به الحاوي خرائط وغير خرائط في الوقت نفسه
وإذا كانت جميع الأنواع في مجموعة الأنواع خرائط، فيجب أن تتطابق أنواعها الكامنة. وإلا، فيجب أن تتطابق أنواع عناصرها. ويُنظر إلى عناصر النصوص على أنها قيم byte.
على سبيل المثال، حالياً (Go 1.25)، في مقطع الشيفرة التالي، لا تُصرَّف بشكل صحيح إلا الدالتان foo وbar.
func foo[T []byte | [2]byte | string](c T) {
_ = c[0] // حسن
}
type Map map[int]string
func bar[T map[int]string | Map](c T) {
_ = c[0] // حسن
}
func lag[T []int | map[int]int](c T) {
_ = c[0] // عملية غير صالحة: لا يمكن فهرسة c
}
func vet[T map[string]int | map[int]int](c T) {
_ = c[0] // عملية غير صالحة: لا يمكن فهرسة c
}
قد يُزال هذا القيد في إصدارات Go المستقبلية (هذا مجرد أمل لي، في الواقع لست متأكداً من ذلك).
إذا كان نوع تعبير الفهرس وسيط نوع، فيجب أن تكون جميع الأنواع في مجموعة الأنواع أعداداً صحيحة. والدالة التالية تُصرَّف بشكل صحيح.
func ind[K byte | int | int16](s []int, i K) {
_ = s[i] // حسن
}
يتطلب اشتقاق شريحة من قيمة من وسيط نوع أن تتشارك جميع الأنواع في مجموعة الأنواع لوسيط النوع النوع الكامن نفسه
على سبيل المثال، حالياً (Go 1.25)، تفشل الدالتان التاليتان في التصريف، حتى مع أن عمليات الشريحة الفرعية صالحة لجميع الأنواع في مجموعات الأنواع المقابلة.
func foo[T []int | [2]int](c T) {
_ = c[:] // لا يمكن أخذ شريحة من c
}
func bar[T [8]int | [2]int](c T) {
_ = c[:] // لا يمكن أخذ شريحة من c
}
قد يُزال هذا القيد في إصدارات Go المستقبلية (مرة أخرى، هذا مجرد أمل لي، في الواقع لست متأكداً من ذلك).
يُرجى ملاحظة أنه يُسمح بالاشتقاق من قيمة من وسيط نوع تحتوي مجموعة الأنواع الخاصة به على أنواع النصوص وشرائح البايتات فقط. على سبيل المثال، تُصرَّف الدالة التالية بشكل صحيح.
func lol[T string | []byte](c T) {
_ = c[:] // حسن
}
إذا كان الحاوي المُكرَّر عليه في حلقة for-range وسيط نوع، فيجب أن تتشارك جميع الأنواع في مجموعة الأنواع لوسيط النوع النوع الكامن نفسه
على سبيل المثال، حالياً (Go 1.25)، في الشيفرة التالية، لا تُصرَّف بشكل صحيح إلا الدالتان الأخيرتان، dot1 وdot2.
func values[T []E | map[int]E, E any](kvs T) []E {
r := make([]E, 0, len(kvs))
// خطأ: لا يمكن التكرار على kvs
for _, v := range kvs {
r = append(r, v)
}
return r
}
func keys[T map[int]string | map[int]int](kvs T) []int {
r := make([]int, 0, len(kvs))
for k := range kvs { // خطأ: لا يمكن التكرار على kvs
r = append(r, k)
}
return r
}
func sum[M map[int]int | map[string]int](m M) (sum int) {
for _, v := range m { // خطأ: لا يمكن التكرار على m
sum += v
}
return
}
func foo[T []int | []string] (v T) {
for range v {} // خطأ: لا يمكن التكرار على v
}
func bar[T [3]int | [6]int] (v T) {
for range v {} // خطأ: لا يمكن التكرار على v
}
type MyInt int
func cat[T []int | []MyInt] (v T) {
for range v {} // خطأ: لا يمكن التكرار على v
}
type Slice []int
func dot1[T []int | Slice] (v T) {
for range v {} // حسن
}
func dot2[T ~[]int] (v T) {
for range v {} // حسن
}
هذا القيد مقصود. وأعتقد أن الغاية منه ضمان أن يكون لمتغيرَي التكرار دائماً نوع محدَّد (إما نوع عادي وإما نوع وسيط نوع). لكن هذا القيد متشدد أكثر من اللازم بالنسبة لهذه الغاية. لأنه في الممارسة العملية، تتطابق أنواع المفاتيح أو أنواع العناصر لبعض الحاويات، حتى إذا اختلف النوع الكامن للحاويات. وفي حالات استخدام كثيرة، يُتجاهل أحد متغيرَي التكرار.
لست متأكداً مما إذا كان هذا القيد سيُزال في إصدارات Go المستقبلية أم لا. وفي رأيي، يقلّل هذا القيد من فائدة الأجناس العامة المخصّصة في Go إلى حد ما.
إذا كانت جميع الأنواع الممكنة شرائح ومصفوفات، وكانت أنواع عناصرها متطابقة، فيمكننا استخدام حلقات for عادية للالتفاف حول هذا القيد.
func cat[T [3]int | [6]int | []int] (v T) {
for i := 0; i < len(v); i++ { // حسن
_ = v[i] // حسن
}
}
استدعاء الدالة len المُعرَّفة مسبقاً صالح هنا. وسيتناول قسم لاحق هذا الأمر.
الشيفرة التالية أيضاً لا تُصرَّف، لكن هذا معقول. لأن العناصر المُكرَّر عليها في string هي قيم rune، بينما العناصر المُكرَّر عليها في []byte هي قيم byte.
func mud[T string | []byte] (v T) {
for range v {} // خطأ: لا يمكن التكرار على v
}
إذا كان المقصود هو التكرار على البايتات في شرائح البايتات والنصوص على حد سواء، فيمكننا استخدام الشيفرة التالية لتحقيق الهدف.
func mud[T string | []byte] (v T) {
for range []byte(v) {} // حسن
}
التحويل []byte(v) (إذا تبع الكلمة المفتاحية range) يُحسَّنه المصرِّف القياسي الرسمي لـ Go تحديداً بحيث لا ينسخ البايتات الكامنة.
لا تُصرَّف الدالة التالية الآن (Go 1.25)، حتى مع أن نوعي متغيرَي التكرار هما دائماً int وrune. ومن غير الواضح ما إذا كانت ستُصرَّف في إصدارات Go المستقبلية أم لا.
func aka[T string | []rune](runes T) {
for i, r := range runes { // لا يمكن التكرار على runes
_ = i
_ = r
}
}
التحويلات التي تتضمن وسائط أنواع
أولاً، ينبغي أن نعرف قواعد التحويل للأنواع/القيم العادية.
بحسب المواصفة الحالية (Go 1.25)، بافتراض نوعين From وTo، افترض أن أحدهما على الأقل وسيط نوع، فإن قيمة من From يمكن تحويلها إلى To إذا كان يمكن تحويل قيمة من كل نوع في مجموعة الأنواع الخاصة بـ From إلى كل نوع في مجموعة الأنواع الخاصة بـ T (لاحظ أن مجموعة الأنواع لنوع عادي لا تحتوي إلا على النوع العادي نفسه).
على سبيل المثال، تُصرَّف جميع الدوال التالية بشكل صحيح.
func pet[A ~int32 | ~int64, B ~float32 | ~float64](x A, y B){
x = A(y)
y = B(x)
}
func dig[From ~byte | ~rune, To ~string | ~int](x From) To {
return To(x)
}
func cov[V ~[]byte | ~[]rune](x V) string {
return string(x)
}
func voc[V ~[]byte | ~[]rune](x string) V {
return V(x)
}
لكن الدالة التالية تفشل في التصريف، لأن قيم string لا يمكن تحويلها إلى int.
func eve[X, Y int | string](x X) Y {
return Y(x) // خطأ
}
لا تُصرَّف الدالة التالية، حتى مع أن التحويل فيها صالح لجميع معطيات الأنواع الممكنة. والسبب أن []T نوع عادي، وليس وسيط نوع، ونوعه الكامن هو نفسه. ولا توجد قاعدة تسمح بتحويل القيم من []T إلى string.
func jon[T byte](x string) []T {
return []T(x) // خطأ
}
قد تُخفِّف إصدارات Go المستقبلية القواعد لجعل التحويل في المثال أعلاه صالحاً.
باستخدام المصرِّف القياسي الرسمي لـ Go، في البرنامج التالي،
- لا تُصرَّف الدالتان tup وpad. والسبب أن قيم النوع AgePtr لا يمكن تحويلها مباشرةً إلى *int.
- وتُصرَّف الدوال العامة الثلاث الأخرى كلها بشكل صحيح. لكن الدالة dot يجب ألا تُصرَّف وفق القاعدة الموصوفة أعلاه. وقد يكون هذا خللاً في المصرِّف القياسي، أو أن القاعدة الموصوفة في مواصفة Go الحالية تحتاج إلى تعديل صغير.
package main
type Age int
type AgePtr *Age
func dot[T ~*Age](x T) *int {
return (*int)(x) // حسن
}
func tup(x AgePtr) *int {
// خطأ: لا يمكن تحويل x (متغير من النوع AgePtr)
// إلى النوع *int
return (*int)(x)
}
func tup2(x AgePtr) *int {
return (*int)((*Age)(x))
}
func pad[T AgePtr](x T) *int {
// خطأ: لا يمكن تحويل x إلى النوع *int
return (*int)(x)
}
func pad2[T AgePtr](x T) *int {
return (*int)((*Age)(x))
}
func main() {
var x AgePtr
var _ = dot[AgePtr](x)
var _ = tup2(x)
var _ = pad2[AgePtr](x)
}
الإسنادات التي تتضمن وسائط أنواع
أولاً، ينبغي أن نعرف قواعد الإسناد للأنواع/القيم العادية.
في الأوصاف التالية، يُسمّى نوع القيمة الوجهة النوع الوجهة، ويُسمّى نوع القيمة المصدر النوع المصدر.
بحسب المواصفة الحالية (Go 1.25)، في الإسناد الذي يتضمن وسيط نوع،
- إذا كان النوع الوجهة وسيط نوع وكانت القيمة المصدر قيمة غير مُنمَّطة، فلا يكون الإسناد صالحاً إلا إذا كانت القيمة غير المُنمَّطة قابلة للإسناد إلى كل نوع في مجموعة الأنواع لوسيط النوع الوجهة.
- وإذا كان النوع الوجهة وسيط نوع لكن النوع المصدر نوع عادي، فلا يكون الإسناد صالحاً إلا إذا كان النوع العادي المصدر غير مسمّى وقيمه قابلة للإسناد إلى كل نوع في مجموعة الأنواع لوسيط النوع الوجهة.
- وإذا كان النوع المصدر وسيط نوع لكن النوع الوجهة نوع عادي، فلا يكون الإسناد صالحاً إلا إذا كان النوع العادي الوجهة غير مسمّى وقيم كل نوع في مجموعة الأنواع لوسيط النوع المصدر قابلة للإسناد إلى النوع العادي الوجهة.
- وإذا كان النوع الوجهة والنوع المصدر كليهما وسيطَي نوع، فالإسناد غير صالح.
من هذه القواعد، نستنتج أنه لا يمكن إسناد قيمة من نوع مسمّى إلى نوع مسمّى آخر.
في مقطع الشيفرة التالي، توجد أربعة إسنادات غير صالحة.
func dat[T ~int | ~float64, S *int | []bool]() {
var _ T = 123 // حسن
var _ S = nil // حسن
}
func zum[T []byte](x []byte) {
var t T = x // حسن
type Bytes []byte
var y Bytes = x // حسن (كلاهما نوعان عاديان)
x = t // حسن
x = y // حسن
// كلاهما نوعان مسمّيان.
t = y // خطأ
y = t // خطأ
// لجعل الإسنادين أعلاه صالحين،
// يجب تحويل المصدرين فيهما.
t = T(y) // حسن
y = Bytes(t) // حسن
}
func pet[A, B []byte](x A, y B){
// كلاهما وسيطا نوع.
x = y // خطأ: لا يمكن استخدام y كنوع A في الإسناد
y = x // خطأ: لا يمكن استخدام x كنوع B في الإسناد
}
من غير الواضح ما إذا كانت قواعد الإسناد ستُخفَّف في إصدارات Go المستقبلية أم لا. ويبدو أن الاحتمال ضعيف.
استدعاءات الدوال المُعرَّفة مسبقاً
في ما يلي بعض القواعد والتفاصيل لاستدعاءات بعض الدوال المُعرَّفة مسبقاً عندما تكون وسائط الأنواع متضمنة.
يكون استدعاء الدالتين len أو cap المُعرَّفتين مسبقاً صالحاً إذا كان صالحاً لجميع الأنواع في مجموعة الأنواع الخاصة بالمعطى
في مقطع الشيفرة التالي، تفشل الدالة capacity في التصريف، أما الدالتان الأخريان فتُصرَّفان بشكل صحيح.
type Container[T any] interface {
~chan T | ~[]T | ~[8]T | ~*[8]T | ~map[int]T | ~string
}
func size[T Container[int]](x T) int {
return len(x) // حسن
}
func capacity[T Container[int]](x T) int {
return cap(x) // خطأ: معطى غير صالح x للدالة cap
}
func capacity2[T ~chan int | ~[]int](x T) int {
return cap(x) // حسن
}
يُرجى ملاحظة أن استدعاء len أو cap يُرجع دائماً قيمة غير ثابتة إذا كان نوع معطى الاستدعاء وسيط نوع، حتى إذا كانت مجموعة الأنواع للمعطى تحتوي فقط على مصفوفات ومؤشرات إلى مصفوفات. على سبيل المثال، في الشيفرة التالية، تفشل جميع استدعاءات cap وlen الأولى ضمن الدالتين الأوليين في التصريف.
func f[T [2]int](x T) {
const _ = cap(x) // خطأ: cap(x) ليست ثابتة
const _ = len(x) // خطأ: len(x) ليست ثابتة
var _ = cap(x) // حسن
var _ = len(x) // حسن
}
func g[P *[2]int](x P) {
const _ = cap(x) // خطأ: cap(x) ليست ثابتة
const _ = len(x) // خطأ: len(x) ليست ثابتة
var _ = cap(x) // حسن
var _ = len(x) // حسن
}
func h(x [2]int) {
const _ = cap(x) // حسن
const _ = len(x) // حسن
const _ = cap(&x) // حسن
const _ = len(&x) // حسن
}
قد تُغيَّر القاعدة. لكن بصراحة، الاحتمال ضعيف جداً. وشخصياً، أرى أن السلوك الحالي أكثر منطقية.
بسبب هذه القاعدة، تُرجع الدالتان التاليتان نتيجتين مختلفتين.
package main
const S = "Go"
func ord(x [8]int) byte {
return 1 << len(x) >> len(x)
}
func gen[T [8]int](x T) byte {
return 1 << len(x) >> len(x)
}
func main() {
var x [8]int
println(ord(x), gen(x)) // 1 0
}
مرة أخرى، يُرجى قراءة مقالة النصوص في Go وهذه المسألة لمعرفة سبب إرجاع الدالتين نتيجتين مختلفتين.
يُرجى ملاحظة أن الدالة التالية لا تُصرَّف، لأن نوع &x هو *T، وهو مؤشر إلى وسيط نوع، وليس مؤشراً إلى مصفوفة.
func e[T [2]int]() {
var x T
var _ = len(&x) // معطى غير صالح: &x للدالة len
var _ = cap(&x) // معطى غير صالح: &x للدالة cap
}
وبعبارة أخرى، وسيط نوع تحتوي مجموعة أنواعه على نوع واحد فقط لا يكافئ ذلك النوع الوحيد. فوسيط النوع له ازدواجية موجة-جسيم. ففي بعض الحالات، يتصرف كالأنواع في مجموعة أنواعه. وفي حالات أخرى، يتصرف كنوع متميّز. وبشكل أدق، يتصرف وسيط النوع كنوع متميّز (لا يتشارك النوع الكامن مع أي أنواع أخرى) عندما يُستخدم كمكوّن من نوع مركّب. وفي المثال أعلاه، *T و*[2]int نوعان (عاديان) مختلفان.
لا توجد متطلبات إضافية لمعطى استدعاء الدالة new المُعرَّفة مسبقاً
الدالة التالية تُصرَّف بشكل صحيح.
func MyNew[T any]() *T {
return new(T)
}
وهي مكافئة لـ
func MyNew[T any]() *T {
var t T
return &t
}
متطلبات استدعاء الدالة make المُعرَّفة مسبقاً عندما يكون المعطى الأول وسيط نوع
المتطلبات الخاصة بمجموعة الأنواع لوسيط النوع:
- يجب أن تتشارك جميع الأنواع في مجموعة الأنواع النوع الكامن نفسه، أو
- أن تكون جميع الأنواع في مجموعة الأنواع أنواع قنوات تتشارك نوع العنصر نفسه، ويجب ألا تتعارض اتجاهات القنوات (يجب ألا تحتوي مجموعة الأنواع على أنواع قنوات استقبال فقط وإرسال فقط معاً).
حالياً (Go 1.25)، في مقطع الشيفرة التالي، تفشل الدالتان voc وted في التصريف، أما الدالتان الأخريان فتُصرَّفان بشكل صحيح.
func voc[T chan bool | chan int]() {
_ = make(T) // خطأ: معطى غير صالح: لا يمكن إنشاء T
}
func ted[T chan<- int | <-chan int]() {
_ = make(T) // خطأ: معطى غير صالح: لا يمكن إنشاء T
}
type Stream chan int
type Queue Stream
func fat[T Stream | chan int | Queue | chan<- int]() {
_ = make(T) // حسن
}
func nub[T Stream | chan int | Queue | <-chan int]() {
_ = make(T) // حسن
}
بحسب فهمي، هذا المتطلب وُضع لجعل العمليات اللاحقة على الحاويات المُنشأة (وهي قنوات في المثال أعلاه) قانونية دائماً. على سبيل المثال، لضمان أن تكون قيمة مستلمة من القناة المُنشأة ذات نوع محدَّد (إما وسيط نوع وإما نوع عادي).
شخصياً، أرى أن المتطلب متشدد أكثر من اللازم. فبعد كل شيء، في بعض الحالات، لا تحدث العمليات اللاحقة المفترضة.
بسبب المتطلب نفسه، لا تُصرَّف أي من الدالتين التاليتين.
func zig[T ~[]int | map[int]int](c T) {
_ = make(T) // خطأ: معطى غير صالح: لا يمكن إنشاء T
}
func rat[T ~[]int | ~[]bool](c T) {
_ = make(T) // خطأ: معطى غير صالح: لا يمكن إنشاء T
}
استدعاءات الدالة new المُعرَّفة مسبقاً ليس عليها هذا المتطلب.
يتطلب استدعاء الدالة delete المُعرَّفة مسبقاً أن يكون لجميع الأنواع في مجموعة الأنواع لمعطاه الأول نوع المفتاح نفسه
لاحظ هنا أن نوع المفتاح المتماثل قد يكون نوعاً عادياً أو نوع وسيط نوع.
الدالتان التاليتان تُصرَّفان بشكل صحيح.
func zuk[M ~map[int]string | ~map[int]bool](x M, k int) {
delete(x, k)
}
func pod[M map[K]int | map[K]bool, K ~int | ~string](x M, k K) {
delete(x, k)
}
يتطلب استدعاء الدالة close المُعرَّفة مسبقاً أن تكون جميع الأنواع في مجموعة الأنواع لمعطاه أنواع قنوات
الدالة التالية تُصرَّف بشكل صحيح.
func dig[T ~chan int | ~chan bool | ~chan<- string](x T) {
close(x)
}
لا تقبل استدعاءات الدوال complex وreal وimag المُعرَّفة مسبقاً معطيات من وسائط الأنواع الآن
قد يخالف استدعاء الدوال الثلاث بمعطيات من وسائط الأنواع القاعدة المبدئية المذكورة في القسم الأول من هذا الفصل.
هذه مشكلة لا يستطيع التصميم الحالي للأجناس العامة المخصّصة حلّها. وتوجد مسألة بهذا الشأن.
حول القيود ذات مجموعات الأنواع الفارغة
قد تكون مجموعات الأنواع لبعض أنواع الواجهات فارغة. ونوع الواجهة ذو مجموعة الأنواع الفارغة يطبّق أي نوع واجهة، بما في ذلك نفسه.
أنواع الواجهات ذات مجموعات الأنواع الفارغة عديمة الفائدة تماماً في الممارسة العملية، لكنها قد تؤثر في كمال التنفيذ من وجهة النظر النظرية.
توجد في الواقع عدة نقائص في تنفيذ المصرِّف القياسي الرسمي الحالي لـ Go (v1.25.n).
في ما يلي مثال، حيث يجب أن تُصرَّف جميع استدعاءات الدوال في الدالة g بشكل صحيح. لكن اثنين منها يفشلان في التصريف مع أحدث مصرِّف قياسي رسمي لـ Go (v1.25.n).
func f1[T any](x T) {}
func f2[T comparable](x T) {}
func f3[T []int](x T) {}
func f4[T int](x T) {}
// هذا نوع واجهة ذو مجموعة أنواع فارغة.
type C interface {
[]int
m()
}
func g[V C](v V) {
f1(v) // حسن
f2(v) // خطأ: V لا يطبّق comparable
f3(v) // حسن
f4(v) // خطأ: V لا يطبّق int
}
تنص مواصفة Go الحالية تحديداً على ما يلي:
قيد تنفيذي: لا يلزم أن يُبلّغ المصرِّف عن خطأ إذا كان نوع معامَل به وسيط نوع بمجموعة أنواع فارغة. فالدوال التي تحتوي على وسائط أنواع كهذه لا يمكن تجسيدها؛ وأي محاولة ستؤدي إلى خطأ في موضع التجسيد.
إذن، النقائص المعروضة أعلاه ليست أخطاءً في المصرِّف القياسي الرسمي لـ Go.