ستسرد هذه المقالة جميع قواعد مقارنة القيم وتحويلها ومقارنتها في Go. يُرجى ملاحظة أن أنواع وسائط النوع (type parameter types) (المستخدمة بكثرة في الأجناس العامة المخصّصة) متجاهَلة عمداً في أوصاف قواعد التحويل وقابلية الإسناد (assignability) والمقارنة. بعبارة أخرى، لا يتناول هذا الكتاب الحالات التي تتضمّن أجناساً عامة مخصّصة.
قواعد تحويل القيم
في Go، إذا أمكن تحويل قيمة v صراحةً إلى النوع T، فيمكن تمثيل التحويل بالصيغة (T)(v). وفي معظم الحالات، ولا سيما عندما يكون T اسم نوع (معرّفاً)، يمكن تبسيط الصيغة إلى T(v).
من الحقائق التي ينبغي معرفتها أنه عندما يُقال إن قيمة x يمكن تحويلها ضمنياً إلى النوع T، فهذا يعني أنه يمكن أيضاً تحويل x صراحةً إلى النوع T.
1. قاعدة التحويل الظاهري
إذا كان نوعان يدلّان على النوع نفسه، فيمكن تحويل قيم أحدهما ضمنياً إلى أي من النوعين.
على سبيل المثال،
- يمكن تحويل قيم النوع
byteوالنوعuint8إلى بعضها بعضاً. - يمكن تحويل قيم النوع
runeوالنوعint32إلى بعضها بعضاً. - يمكن تحويل قيم النوع
[]byteوالنوع[]uint8إلى بعضها بعضاً.
لا شيء أكثر لشرحه بشأن هذه القاعدة، سواء اعتبرت أن هذه الحالة تتضمّن تحويلات أم لا.
2. قواعد التحويل المتعلقة بالنوع الأساسي
بافتراض قيمة غير واجهية x ونوع غير واجهي T، ولنفترض أن نوع x هو Tx،
- إذا اشترك
TxوTفي النوع الأساسي نفسه (بإهمال وسوم البنى struct tags)، فيمكن تحويلxصراحةً إلىT. - إذا كان أي من
TxأوTنوعاً غير مسمّى وكان نوعاهما الأساسيان متطابقين (مع مراعاة وسوم البنى)، فيمكن تحويلxضمنياً إلىT. - إذا كان لـ
TxوTنوعان أساسيان مختلفان، لكن كان كل منTxوTنوع مؤشر غير مسمّى، وكان نوعاهما القاعديان يشتركان في النوع الأساسي نفسه (بإهمال وسوم البنى)، فيمكن تحويلxصراحةً إلىT.
(ملاحظة: موضعا إهمال وسوم البنى قد أصبحا ساريين منذ Go 1.8.)
مثال:
package main
func main() {
// []int وIntSlice وMySlice تشترك
// في النوع الأساسي نفسه: []int
type IntSlice []int
type MySlice []int
type Foo = struct{n int `foo`}
type Bar = struct{n int `bar`}
var s = []int{}
var is = IntSlice{}
var ms = MySlice{}
var x map[Bar]Foo
var y map[Foo]Bar
// لا يعمل أي من التحويلين الضمنيين.
/*
is = ms // خطأ
ms = is // خطأ
*/
// يجب استخدام تحويلات صريحة هنا.
is = IntSlice(ms)
ms = MySlice(is)
x = map[Bar]Foo(y)
y = map[Foo]Bar(x)
// التحويلات الضمنية مقبولة هنا.
s = is
is = s
s = ms
ms = s
}
مثال على التحويل المتعلق بالمؤشرات:
package main
func main() {
type MyInt int
type IntPtr *int
type MyIntPtr *MyInt
var pi = new(int) // نوع pi هو *int
// لـ ip وpi النوع الأساسي نفسه،
// ونوع pi غير مسمّى، لذا
// يعمل التحويل الضمني.
var ip IntPtr = pi
// var _ *MyInt = pi // لا يمكن التحويل ضمنياً
var _ = (*MyInt)(pi) // موافق، يجب التحويل صراحةً
// لا يمكن تحويل قيم *int إلى MyIntPtr
// مباشرةً، بل يمكن ذلك بشكل غير مباشر.
/*
var _ MyIntPtr = pi // لا يمكن التحويل ضمنياً
var _ = MyIntPtr(pi) // لا يمكن التحويل صراحةً
*/
var _ MyIntPtr = (*MyInt)(pi) // موافق
var _ = MyIntPtr((*MyInt)(pi)) // موافق
// لا يمكن تحويل قيم IntPtr إلى
// MyIntPtr مباشرةً، بل يمكن ذلك بشكل غير مباشر.
/*
var _ MyIntPtr = ip // لا يمكن التحويل ضمنياً
var _ = MyIntPtr(ip) // لا يمكن التحويل صراحةً
*/
var _ MyIntPtr = (*MyInt)((*int)(ip)) // موافق
var _ = MyIntPtr((*MyInt)((*int)(ip))) // موافق
}
3. قاعدة التحويل الخاصة بالقنوات
بافتراض قيمة قناة x، ولنفترض أن نوعها Tx نوع قناة ثنائية الاتجاه، وأن T نوع قناة أيضاً (ثنائية الاتجاه أو لا). إذا كان لـ Tx وT نوع العنصر نفسه، وكان أي من Tx أو T نوعاً غير مسمّى، فيمكن تحويل x ضمنياً إلى T.
مثال:
package main
func main() {
type C chan string
type C1 chan<- string
type C2 <-chan string
var ca C
var cb chan string
cb = ca // موافق، النوع الأساسي نفسه
ca = cb // موافق، النوع الأساسي نفسه
// تُصرَّف الأسطر الأربعة بنجاح وفقاً لهذه القاعدة الثالثة.
var _, _ chan<- string = ca, cb // موافق
var _, _ <-chan string = ca, cb // موافق
var _ C1 = cb // موافق
var _ C2 = cb // موافق
// لا يمكن تحويل قيم C
// إلى C1 وC2 مباشرةً.
/*
var _ = C1(ca) // خطأ في التصريف
var _ = C2(ca) // خطأ في التصريف
*/
// يمكن تحويل قيم C
// إلى C1 وC2 بشكل غير مباشر.
var _ = C1((chan<- string)(ca)) // موافق
var _ = C2((<-chan string)(ca)) // موافق
var _ C1 = (chan<- string)(ca) // موافق
var _ C2 = (<-chan string)(ca) // موافق
}
4. قواعد التحويل المتعلقة بتطبيق الواجهات
بافتراض قيمة x ونوع واجهة I، إذا كان نوع x (أو نوعه الافتراضي) هو Tx وكان Tx يطبّق I، فيمكن تحويل x ضمنياً إلى النوع I. ونتيجة التحويل قيمة واجهية (من النوع I) تغلّف
- نسخة من
x، إذا كانTxنوعاً غير واجهي؛ - نسخة من القيمة الديناميكية لـ
x، إذا كانTxنوع واجهة.
يُرجى قراءة الواجهات في Go للتفاصيل والأمثلة.
5. قاعدة تحويل القيم غير المنمَّطة (untyped values)
يمكن تحويل قيمة غير منمَّطة ضمنياً إلى النوع T، إذا أمكن تمثيل القيمة غير المنمَّطة كقيم من النوع T.
مثال:
package main
func main() {
var _ []int = nil
var _ map[string]int = nil
var _ chan string = nil
var _ func()() = nil
var _ *bool = nil
var _ interface{} = nil
var _ int = 123.0
var _ float64 = 123
var _ int32 = 1.23e2
var _ int8 = 1 + 0i
}
6. قاعدة تحويل الثوابت
(تتداخل هذه القاعدة جزئياً مع القاعدة السابقة.)
بوجه عام، لا يزال تحويل ثابت ما يُنتج ثابتاً كنتيجة (إلا إذا لم يكن النوع الهدف نوعاً أساسياً).
بافتراض قيمة ثابتة x ونوع أساسي T، إذا كانت x قابلة للتمثيل كقيمة من النوع T، فيمكن تحويل x صراحةً إلى T. وبوجه خاص، إذا كانت x قيمة غير منمَّطة، فيمكن تحويل x ضمنياً إلى T.
مثال:
package main
func main() {
// جميع التحويلات الضمنية قانونية.
const I = 123
const I1, I2 int8 = 0x7F, -0x80
const I3, I4 int8 = I, 0.0
const F = 0.123456789
const F32 float32 = F
const F32b float32 = I
const F64 float64 = F
const C1, C2 complex64 = F, I
// const F64b float64 = I3 // لا تُصرَّف
const F64b = float64(I3) // تُصرَّف بنجاح
// const I5 int = C2 // لا تُصرَّف
const I5 = int(C2) // تُصرَّف بنجاح
}
7. قواعد تحويل الأعداد غير الثابتة
يمكن تحويل قيم الأعداد العشرية والصحيحة غير الثابتة صراحةً إلى أي من الأنواع العشرية والصحيحة.
يمكن تحويل قيم الأعداد المركّبة غير الثابتة صراحةً إلى أي من الأنواع المركّبة.
لاحظ أنه
- لا يمكن تحويل قيم الأعداد المركّبة غير الثابتة إلى الأنواع العشرية والصحيحة.
- لا يمكن تحويل قيم الأعداد العشرية والصحيحة غير الثابتة إلى الأنواع المركّبة.
- يُسمح بفيض البيانات والتقريب في تحويلات الأعداد غير الثابتة. وعند تحويل عدد عشري غير ثابت إلى عدد صحيح، يُهمَل الكسر (اقتطاع نحو الصفر).
مثال:
package main
import "fmt"
func main() {
var a, b = 1.6, -1.6 // كلاهما من النوع float64
fmt.Println(int(a), int(b)) // 1 -1
var i, j int16 = 0x7FFF, -0x8000
fmt.Println(int8(i), uint16(j)) // -1 32768
var c1 complex64 = 1 + 2i
var _ = complex128(c1)
}
8. قواعد التحويل المتعلقة بالنصوص
إذا كان نوع قيمة ما (أو نوعها الافتراضي) نوعاً صحيحاً، فيمكن تحويل القيمة صراحةً إلى أنواع النصوص.
يمكن تحويل قيمة نص صراحةً إلى نوع شريحة نوعه الأساسي []byte (المعروف أيضاً بـ []uint8)، والعكس صحيح.
يمكن تحويل قيمة نص صراحةً إلى نوع شريحة نوعه الأساسي []rune (المعروف أيضاً بـ []int32)، والعكس صحيح.
يُرجى قراءة النصوص في Go للتفاصيل والأمثلة.
9. التحويلات المتعلقة بالشرائح
منذ Go 1.17، يمكن تحويل شريحة إلى مؤشر مصفوفة. وفي هذا التحويل، إذا كان طول نوع المصفوفة القاعدي لنوع المؤشر أكبر من طول الشريحة، يحدث panic.
إليك مثالاً.
منذ Go 1.20، يمكن تحويل شريحة إلى مصفوفة. وفي هذا التحويل، إذا كان طول نوع المصفوفة أكبر من طول الشريحة، يحدث panic.
إليك مثالاً.
10. قواعد التحويل المتعلقة بمؤشرات unsafe
يمكن تحويل قيمة مؤشر من أي نوع صراحةً إلى نوع نوعه الأساسي unsafe.Pointer، والعكس صحيح.
يمكن تحويل قيمة uintptr صراحةً إلى نوع نوعه الأساسي unsafe.Pointer، والعكس صحيح.
يُرجى قراءة المؤشرات غير الآمنة من حيث النوع في Go للتفاصيل والأمثلة.
قواعد إسناد القيم
يمكن النظر إلى الإسنادات على أنها تحويلات ضمنية. وقواعد التحويل الضمني مُدرجة ضمن جميع قواعد التحويل في القسم السابق.
إضافةً إلى هذه القواعد، يجب أن تكون قيم الهدف في الإسنادات قيماً قابلة لعنونة، أو تعبيرات فهرس خريطة، أو المعرّف الفارغ.
في الإسناد، تُنسخ قيمة المصدر إلى القيمة الهدف. وبالدقة، يُنسخ الجزء المباشر من قيمة المصدر إلى القيمة الهدف.
لاحظ أن تمرير الوسائط وإرجاع النتائج هما في الواقع إسنادان للقيم.
قواعد مقارنة القيم
تنص مواصفة Go:
في أي مقارنة، يجب أن يكون المعامَل به الأول قابلاً للإسناد إلى نوع المعامَل به الثاني، أو العكس.
إذن، قاعدة المقارنة تشبه قاعدة الإسناد كثيراً. بعبارة أخرى، تكون قيمتان قابلتين للمقارنة إذا أمكن تحويل إحداهما ضمنياً إلى نوع الأخرى. أليس كذلك؟ تقريباً، لأن هناك قاعدة أخرى ذات أولوية أعلى من قاعدة المقارنة الأساسية المذكورة أعلاه.
إذا كان كلا المعامَلين بهما في مقارنة ما منمَّطين، فيجب أن يكون نوعاهما كليهما نوعين قابلين للمقارنة.
بحسب القاعدة أعلاه، إذا طبّق نوع غير قابل للمقارنة (وهو بالضرورة نوع غير واجهي) نوع واجهة ما، فمن غير القانوني مقارنة قيم النوعين، حتى لو أمكن تحويل قيم النوع الأول (غير الواجهي) ضمنياً إلى النوع الثاني (الواجهي).
لاحظ أنه على الرغم من أن قيم أنواع الشرائح/الخرائط/الدوال لا تدعم المقارنات، فيمكن مقارنتها بقيم nil غير المنمَّطة (المعروفة أيضاً بمعرّفات nil المجرّدة).
لا تغطي القواعد الأساسية الموصوفة أعلاه جميع الحالات. فماذا لو كان كلا المعامَلين بهما في مقارنة ما قيمتين غير منمَّطتين (ثابتتين)؟ القواعد الإضافية بسيطة:
- يمكن مقارنة القيم المنطقية غير المنمَّطة بقيم منطقية غير منمَّطة.
- يمكن مقارنة القيم العددية غير المنمَّطة بقيم عددية غير منمَّطة.
- يمكن مقارنة قيم النصوص غير المنمَّطة بقيم نصوص غير منمَّطة.
تتبع نتائج مقارنة قيمتين عدديتين غير منمَّطتين الحدس.
لاحظ أنه لا يمكن مقارنة قيمة nil غير منمَّطة بقيمة nil غير منمَّطة أخرى.
أي مقارنة ينتج عنها قيمة منطقية غير منمَّطة.
يعرض المثال التالي بعض المقارنات المتعلقة بأنواع غير قابلة للمقارنة.
package main
// بعض المتغيرات من أنواع غير قابلة للمقارنة.
var s []int
var m map[int]int
var f func()()
var t struct {x []int}
var a [5]map[int]int
func main() {
// الأسطر التالية تفشل في التصريف.
/*
_ = s == s
_ = m == m
_ = f == f
_ = t == t
_ = a == a
_ = nil == nil
_ = s == interface{}(nil)
_ = m == interface{}(nil)
_ = f == interface{}(nil)
*/
// الأسطر التالية تُصرَّف بنجاح.
_ = s == nil
_ = m == nil
_ = f == nil
_ = 123 == interface{}(nil)
_ = true == interface{}(nil)
_ = "abc" == interface{}(nil)
}
كيف تُقارَن قيمتان؟
لنفترض أن قيمتين قابلتين للمقارنة ولهما النوع نفسه T. (إذا كان لهما نوعان مختلفان، فيجب أن تكون إحداهما قابلة للتحويل ضمنياً إلى نوع الأخرى. وهنا لا نأخذ في الاعتبار الحالات التي تكون فيها كلتا القيمتين غير منمَّطتين.)
- إذا كان
Tنوعاً منطقياً، تكون القيمتان متساويتين فقط إذا كانتا كلتاهماtrueأو كلتاهماfalse. - إذا كان
Tنوعاً صحيحاً، تكون القيمتان متساويتين فقط إذا كان لهما التمثيل نفسه في الذاكرة. - إذا كان
Tنوعاً عشرياً، تكون القيمتان متساويتين فقط إذا تحقق أي من الشروط التالية: - كلتاهما+Inf.
- كلتاهما
-Inf. - كل منهما إما
-0.0أو+0.0. - كلتاهما ليست
NaNولهما تمثيل البايتات نفسه في الذاكرة.
- إذا كان
Tنوعاً مركّباً، تكون القيمتان متساويتين فقط إذا كان جزآهما الحقيقيان (كقيم عشرية) وجزآهما التخيليان (كقيم عشرية) متساويين كليهما. - إذا كان
Tنوع مؤشر (آمناً كان أو غير آمن)، تكون القيمتان متساويتين فقط إذا كانت عناوين الذاكرة المخزّنة فيهما متساوية. - إذا كان
Tنوع قناة، تكون قيمتا القناتين متساويتين إذا كانتا تشيران معاً إلى قيمة بنية القناة الداخلية الأساسية نفسها أو كانتا كلتاهما قناتين nil. - إذا كان
Tنوع بنية، فستُقارَن كل زوج من الحقول المتناظرة في قيمتي البنية. - إذا كان
Tنوع مصفوفة، فستُقارَن كل زوج من العناصر المتناظرة في قيمتي المصفوفة. - إذا كان
Tنوع واجهة، فيُرجى قراءة كيف تُقارَن قيمتا واجهة. - إذا كان
Tنوع نص، فيُرجى قراءة كيف تُقارَن قيمتا نص.
يُرجى ملاحظة أن مقارنة واجهتين لهما النوع الديناميكي غير القابل للمقارنة نفسه تُنتج panic. وفيما يلي مثال ستحدث فيه بعض حالات panic أثناء المقارنات.
package main
func main() {
type T struct {
a interface{}
b int
}
var x interface{} = []int{}
var y = T{a: x}
var z = [3]T{{a: y}}
// كل سطر من الأسطر التالية قد يُنتج panic.
_ = x == x
_ = y == y
_ = z == z
}