nil معرّف مُعرَّف مسبقًا مهم وكثير الاستخدام في Go. وهو التمثيل الحرفي للقيم الصفرية لكثير من أصناف الأنواع. وقد ينظر كثير من مبرمجي Go الجدد ممن لديهم خبرات في بعض اللغات الشائعة الأخرى إلى nil على أنه نظير null (أو NULL) في اللغات الأخرى. هذا صحيح جزئيًا، لكن توجد فروق كثيرة بين nil في Go وnull (أو NULL) في اللغات الأخرى.
سيسرد ما تبقّى من هذه المقالة جميع أصناف الحقائق والتفاصيل المتعلقة بـ nil.
nil معرّف مُعرَّف مسبقًا في Go
يمكننا استخدام nil دون تعريفه.
يستطيع nil تمثيل القيم الصفرية لكثير من الأنواع
في Go، يستطيع nil تمثيل القيم الصفرية لأصناف الأنواع التالية:
- أنواع المؤشرات (بما فيها المؤشرات غير الآمنة نوعيًا).
- أنواع الخرائط.
- أنواع الشرائح.
- أنواع الدوال.
- أنواع القنوات.
- أنواع الواجهات.
المعرّف nil المُعرَّف مسبقًا ليس له نوع افتراضي
لكل معرّف آخر من المعرّفات المُعرَّفة مسبقًا في Go نوع افتراضي. مثلًا،
- النوع الافتراضي لكل من
trueوfalseهو النوعbool. - النوع الافتراضي لـ
iotaهوint.
لكن nil المُعرَّف مسبقًا ليس له نوع افتراضي، مع أن له أنواعًا ممكنة كثيرة. في الواقع، nil المُعرَّف مسبقًا هو القيمة غير المُنمَّطة (untyped) الوحيدة التي ليس لها نوع افتراضي في Go. ويجب أن تتوفر معلومات كافية ليستنتج المصرِّف نوع قيمة nil من السياق.
مثال:
package main
func main() {
// يجب أن تتوفر معلومات كافية
// ليستنتج المصرِّف نوع قيمة nil.
_ = (*struct{})(nil)
_ = []int(nil)
_ = map[int]bool(nil)
_ = chan string(nil)
_ = (func())(nil)
_ = interface{}(nil)
// هذه الأسطر مكافئة للأسطر أعلاه.
var _ *struct{} = nil
var _ []int = nil
var _ map[int]bool = nil
var _ chan string = nil
var _ func() = nil
var _ interface{} = nil
// السطر التالي لا يُصرَّف.
var _ = nil
}
nil المُعرَّف مسبقًا ليس كلمة مفتاحية في Go
يمكن حجب nil المُعرَّف مسبقًا.
مثال:
package main
import "fmt"
func main() {
nil := 123
fmt.Println(nil) // 123
// السطر التالي يفشل في التصريف،
// لأن nil يمثّل الآن قيمة int
// في هذا النطاق.
var _ map[string]int = nil
}
(بالمناسبة، null وNULL في كثير من اللغات الأخرى ليسا كلمتين مفتاحيتين أيضًا.)
قد تختلف أحجام قيم nil ذات الأنواع من أصناف مختلفة
تخطيطات الذاكرة لجميع قيم النوع الواحد متطابقة دائمًا. وقيم nil من ذلك النوع ليست استثناءً (بافتراض أن القيم الصفرية للنوع يمكن تمثيلها بـ nil). وحجم قيمة nil يساوي دائمًا أحجام قيم غير nil التي أنواعها هي نفس نوع قيمة nil. لكن قيم nil التي أنواعها من أصناف مختلفة قد تكون لها أحجام مختلفة.
مثال:
package main
import (
"fmt"
"unsafe"
)
func main() {
var p *struct{} = nil
fmt.Println( unsafe.Sizeof( p ) ) // 8
var s []int = nil
fmt.Println( unsafe.Sizeof( s ) ) // 24
var m map[int]bool = nil
fmt.Println( unsafe.Sizeof( m ) ) // 8
var c chan string = nil
fmt.Println( unsafe.Sizeof( c ) ) // 8
var f func() = nil
fmt.Println( unsafe.Sizeof( f ) ) // 8
var i interface{} = nil
fmt.Println( unsafe.Sizeof( i ) ) // 16
}
تعتمد الأحجام على المصرِّف والمعمارية. والنتائج المطبوعة أعلاه خاصة بمعماريات 64 بت وبمصرِّف Go القياسي. أما على معماريات 32 بت، فستكون الأحجام المطبوعة نصف ذلك.
بالنسبة إلى مصرِّف Go القياسي، فإن حجمَي قيمتين من نوعين مختلفين من الصنف نفسه، ويمكن تمثيل قيمتيهما الصفريتين بـ nil المُعرَّف مسبقًا، متساويان دائمًا. مثلًا، أحجام جميع قيم جميع أنواع الشرائح المختلفة متساوية.
قد لا تكون قيمتا nil من نوعين مختلفين قابلتين للمقارنة
مثلًا، تفشل المقارنتان في المثال التالي في التصريف. والسبب أنه في كل مقارنة، لا يمكن تحويل أيٍّ من المعامَلين به ضمنيًا إلى نوع الآخر.
// سبب فشل التصريف: أنواع غير متطابقة.
var _ = (*int)(nil) == (*bool)(nil) // خطأ
var _ = (chan int)(nil) == (chan bool)(nil) // خطأ
اقرأ قواعد المقارنة في Go لتعرف أي قيمتين يمكن مقارنتهما معًا. وقيم nil ذات الأنواع ليست استثناءً من قواعد المقارنة.
أسطر الشيفرة في المثال التالي تُصرَّف كلها دون مشاكل.
type IntPtr *int
// النوع الكامن للنوع IntPtr هو *int.
var _ = IntPtr(nil) == (*int)(nil)
// كل نوع في Go ينفّذ النوع interface{}.
var _ = (interface{})(nil) == (*int)(nil)
// يمكن تحويل قيم نوع قناة أحادية الاتجاه
// إلى نوع القناة ثنائية الاتجاه
// الذي له نوع العناصر نفسه.
var _ = (chan int)(nil) == (chan<- int)(nil)
var _ = (chan int)(nil) == (<-chan int)(nil)
قد لا تكون قيمتا nil من النوع نفسه قابلتين للمقارنة
في Go، لا تدعم أنواع الخرائط والشرائح والدوال المقارنة. ومقارنة قيمتين، بما في ذلك قيمتا nil، من نوع غير قابل للمقارنة غير قانونية. والمقارنات التالية تفشل في التصريف.
var _ = ([]int)(nil) == ([]int)(nil)
var _ = (map[string]int)(nil) == (map[string]int)(nil)
var _ = (func())(nil) == (func())(nil)
لكن أي قيم من الأنواع غير القابلة للمقارنة المذكورة أعلاه يمكن مقارنتها بمعرّف nil المجرّد.
// الأسطر التالية تُصرَّف دون مشاكل.
var _ = ([]int)(nil) == nil
var _ = (map[string]int)(nil) == nil
var _ = (func())(nil) == nil
قد لا تكون قيمتا nil متساويتين
إذا كانت إحدى قيمتي nil المقارنتين قيمة واجهة والأخرى ليست كذلك، بافتراض أنهما قابلتان للمقارنة، فإن نتيجة المقارنة تكون دائمًا false. والسبب أن القيمة غير الواجهية ستُحوَّل إلى نوع قيمة الواجهة قبل إجراء المقارنة. فالقيمة الواجهية المحوَّلة لها نوع ديناميكي، بينما القيمة الواجهية الأخرى ليس لها نوع ديناميكي. ولهذا تكون نتيجة المقارنة دائمًا false.
مثال:
fmt.Println( (interface{})(nil) == (*int)(nil) ) // false
استرجاع العناصر من خرائط nil لن يسبب panic
استرجاع عنصر من قيمة خريطة nil سيعيد دائمًا قيمة العنصر الصفرية.
مثلًا:
fmt.Println( (map[string]int)(nil)["key"] ) // 0
fmt.Println( (map[int]bool)(nil)[123] ) // false
fmt.Println( (map[int]*int64)(nil)[123] ) // <nil>
من القانوني التكرار على قنوات وخرائط وشرائح ومؤشرات مصفوفات nil
عدد خطوات الحلقة عند التكرار على خرائط وشرائح nil هو صفر.
عدد خطوات الحلقة عند التكرار على مؤشر مصفوفة nil هو طول نوع المصفوفة المقابل له. (لكن إذا لم يكن طول نوع المصفوفة المقابل صفرًا، ولم يكن متغير التكرار الثاني مُهمَلًا ولا محذوفًا، فسيسبب التكرار panic في وقت التشغيل.)
التكرار على قناة nil سيحجب الـgoroutine الحالية إلى الأبد.
مثلًا، ستطبع الشيفرة التالية 0 و1 و2 و3 و4، ثم تُحجب إلى الأبد. ولن تُطبع Hello وworld وBye أبدًا.
for range []int(nil) {
fmt.Println("Hello")
}
for range map[string]string(nil) {
fmt.Println("world")
}
for i := range (*[5]int)(nil) {
fmt.Println(i)
}
for range chan bool(nil) { // الحجب هنا
fmt.Println("Bye")
}
استدعاء الطرق عبر مستقبِلات nil غير الواجهية لن يسبب panic
مثال:
package main
type Slice []bool
func (s Slice) Length() int {
return len(s)
}
func (s Slice) Modify(i int, x bool) {
s[i] = x // panic إذا كانت s تساوي nil
}
func (p *Slice) DoNothing() {
}
func (p *Slice) Append(x bool) {
*p = append(*p, x) // panic إذا كانت p تساوي nil
}
func main() {
// المُحدِّدات التالية لن تسبب حالات panic.
_ = ((Slice)(nil)).Length
_ = ((Slice)(nil)).Modify
_ = ((*Slice)(nil)).DoNothing
_ = ((*Slice)(nil)).Append
// السطران التاليان لن يسببا panic أيضًا.
_ = ((Slice)(nil)).Length()
((*Slice)(nil)).DoNothing()
// السطران التاليان سيسببان panic. لكن حالات
// panic لن تُطلق في وقت استدعاء
// الطرق. بل ستُطلق عند
// إلغاء الإشارة إلى مؤشرات nil في أجسام الطرق.
/*
((Slice)(nil)).Modify(0, true)
((*Slice)(nil)).Append(true)
*/
}
في الواقع، تنفيذ الطريقة Append أعلاه ليس مثاليًا، وينبغي تعديله ليصبح كما في المثال التالي.
func (p *Slice) Append(x bool) {
if p == nil {
*p = []bool{x}
return
}
*p = append(*p, x)
}
*new(T) ينتج قيمة T تساوي nil إذا كانت القيمة الصفرية للنوع T مُمثَّلة بالمعرّف nil المُعرَّف مسبقًا
مثال:
package main
import "fmt"
func main() {
fmt.Println(*new(*int) == nil) // true
fmt.Println(*new([]int) == nil) // true
fmt.Println(*new(map[int]bool) == nil) // true
fmt.Println(*new(chan string) == nil) // true
fmt.Println(*new(func()) == nil) // true
fmt.Println(*new(interface{}) == nil) // true
}
الخلاصة
في Go، من أجل البساطة والملاءمة، صُمِّم nil كمعرّف يمكن استخدامه لتمثيل القيم الصفرية لبعض أصناف الأنواع. وهو ليس قيمة واحدة، بل يمكنه تمثيل قيم كثيرة بتخطيطات ذاكرة مختلفة.