الكتاب الأساسي

قواعد تحويل القيم وإسنادها ومقارنتها في Go

ستسرد هذه المقالة جميع قواعد مقارنة القيم وتحويلها ومقارنتها في 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. (إذا كان لهما نوعان مختلفان، فيجب أن تكون إحداهما قابلة للتحويل ضمنياً إلى نوع الأخرى. وهنا لا نأخذ في الاعتبار الحالات التي تكون فيها كلتا القيمتين غير منمَّطتين.)

  1. إذا كان T نوعاً منطقياً، تكون القيمتان متساويتين فقط إذا كانتا كلتاهما true أو كلتاهما false.
  2. إذا كان T نوعاً صحيحاً، تكون القيمتان متساويتين فقط إذا كان لهما التمثيل نفسه في الذاكرة.
  3. إذا كان T نوعاً عشرياً، تكون القيمتان متساويتين فقط إذا تحقق أي من الشروط التالية: - كلتاهما +Inf.
  • كلتاهما -Inf.
  • كل منهما إما -0.0 أو +0.0.
  • كلتاهما ليست NaN ولهما تمثيل البايتات نفسه في الذاكرة.
  1. إذا كان T نوعاً مركّباً، تكون القيمتان متساويتين فقط إذا كان جزآهما الحقيقيان (كقيم عشرية) وجزآهما التخيليان (كقيم عشرية) متساويين كليهما.
  2. إذا كان T نوع مؤشر (آمناً كان أو غير آمن)، تكون القيمتان متساويتين فقط إذا كانت عناوين الذاكرة المخزّنة فيهما متساوية.
  3. إذا كان T نوع قناة، تكون قيمتا القناتين متساويتين إذا كانتا تشيران معاً إلى قيمة بنية القناة الداخلية الأساسية نفسها أو كانتا كلتاهما قناتين nil.
  4. إذا كان T نوع بنية، فستُقارَن كل زوج من الحقول المتناظرة في قيمتي البنية.
  5. إذا كان T نوع مصفوفة، فستُقارَن كل زوج من العناصر المتناظرة في قيمتي المصفوفة.
  6. إذا كان T نوع واجهة، فيُرجى قراءة كيف تُقارَن قيمتا واجهة.
  7. إذا كان 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
}