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

قيم nil في Go

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 كمعرّف يمكن استخدامه لتمثيل القيم الصفرية لبعض أصناف الأنواع. وهو ليس قيمة واحدة، بل يمكنه تمثيل قيم كثيرة بتخطيطات ذاكرة مختلفة.