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

كتل الشيفرة ونطاقات المعرّفات

ستشرح هذه المقالة كتل الشيفرة ونطاقات المعرّفات في Go.

(يُرجى ملاحظة أن تعريفات التسلسل الهرمي لكتل الشيفرة في هذه المقالة تختلف قليلاً عن وجهة نظر حزم go/* القياسية.)

كتل الشيفرة

في مشروع Go، توجد أربعة أنواع من كتل الشيفرة (وتُسمى الكتل فيما بعد):

  • كتلة الكون (universe block) تحتوي على كل شيفرة المشروع المصدرية.
  • لكل حزمة كتلة حزمة (package block) تحتوي على كل الشيفرة المصدرية، باستثناء تعريفات استيراد الحزمة في تلك الحزمة.
  • ولكل ملف كتلة ملف (file block) تحتوي على كل الشيفرة المصدرية في ذلك الملف، بما في ذلك تعريفات استيراد الحزمة.
  • عموماً، يحيط زوج من الأقواس {} بكتلة محلية (local block). لكن بعض الكتل المحلية لا تكون محاطة بـ {}، وتُسمى هذه الكتل كتلاً محلية ضمنية. أما الكتل المحلية المحاطة بـ {} فتُسمى كتلاً محلية صريحة. لا تشكّل {} في القيم الحرفية المركّبة وتعريفات الأنواع كتلاً محلية.

تتبع بعض الكلمات المفتاحية في جميع أنواع تدفقات التحكم بعض كتل الشيفرة الضمنية.

  • تتبع الكلمة المفتاحية if أو switch أو for كتلتان محليتان متداخلتان. إحداهما ضمنية والأخرى صريحة. الكتلة الصريحة متداخلة في الكتلة الضمنية. وإذا تبع الكلمة المفتاحية تعريف متغير قصير، فإن المتغيرات تُعرَّف في الكتلة الضمنية.
  • تتبع الكلمة المفتاحية else كتلة واحدة صريحة أو ضمنية، وهي متداخلة في الكتلة الضمنية التي تتبع الكلمة المفتاحية المقابلة لها if. وإذا تبع الكلمة المفتاحية else كلمة مفتاحية أخرى if، فيمكن أن تكون كتلة الشيفرة التي تتبع else ضمنية، وإلا وجب أن تكون الكتلة صريحة.
  • تتبع الكلمة المفتاحية select كتلة صريحة واحدة.
  • تتبع كل كلمة مفتاحية case و default كتلة ضمنية واحدة، وهي متداخلة في الكتلة الصريحة التي تتبع الكلمة المفتاحية المقابلة لها switch أو select.

الكتل المحلية غير المتداخلة في أي كتل محلية أخرى تُسمى كتلاً محلية عليا (أو على مستوى الحزمة). والكتل المحلية العليا كلها أجسام دوال.

لاحظ أن وسائط الإدخال ونتائج الإخراج للدالة تُعتبر معرَّفة في كتلة شيفرة الجسم الصريحة للدالة، حتى لو بقيت تعريفاتها خارج زوج الأقواس المحيط بكتلة جسم الدالة.

التسلسل الهرمي لكتل الشيفرة:

  • كتل الحزم متداخلة في كتلة الكون.
  • كتل الملفات أيضاً متداخلة مباشرة في كتلة الكون، وليس في كتل الحزم. (هذا الشرح يختلف عن حزم go/* القياسية.)
  • كل كتلة محلية عليا متداخلة في كل من كتلة الحزمة وكتلة الملف. (هذا الشرح يختلف أيضاً عن حزم go/* القياسية.)
  • الكتلة المحلية غير العليا يجب أن تكون متداخلة في كتلة محلية أخرى.

(الاختلافات عن مواصفات Go هدفها تبسيط الشروحات التالية حول حجب المعرّفات.)

هذه صورة تُظهر التسلسل الهرمي للكتل في برنامج:

تُستخدم كتل الشيفرة أساساً لشرح مواضع التعريف المسموح بها ونطاقات معرّفات عناصر الشيفرة المصدرية.

مواضع تعريف عناصر الشيفرة المصدرية

توجد ستة أنواع من عناصر الشيفرة المصدرية التي يمكن تعريفها:

  • استيرادات الحزم.
  • الأنواع المعرَّفة وأسماء الأنواع البديلة (type alias).
  • الثوابت المسمّاة.
  • المتغيرات.
  • الدوال.
  • التسميات (labels).

تُستخدم التسميات في جمل break و continue و goto.

يربط التعريف معرّفاً غير فارغ بعنصر من عناصر الشيفرة المصدرية (ثابت أو نوع أو متغير أو دالة أو تسمية أو حزمة). بعبارة أخرى، في التعريف يُسمّى عنصر الشيفرة المصدرية المعرَّف بالمعرّف غير الفارغ. وبعد التعريف، يمكننا استخدام المعرّف غير الفارغ لتمثيل عنصر الشيفرة المصدرية المعرَّف.

يوضّح الجدول التالي كتل الشيفرة التي يمكن أن تُعرَّف فيها جميع أنواع عناصر الشيفرة المصدرية مباشرةً:

| | كتلة الكون | كتل الحزم | كتل الملفات | الكتل المحلية | | --- | --- | --- | --- | | المعرَّفة مسبقاً (العناصر المدمجة) (1) | نعم | | | | | استيرادات الحزم | | | نعم | | | الأنواع المعرَّفة وأسماء الأنواع البديلة (غير المدمجة) | | نعم | نعم | نعم | | الثوابت المسمّاة (غير المدمجة) | | نعم | نعم | نعم | | المتغيرات (غير المدمجة) (2) | | نعم | نعم | نعم | | الدوال (غير المدمجة) | | نعم | نعم | | | المتغيرات (غير المدمجة) (2) | | | | نعم |

(1) العناصر المعرَّفة مسبقاً موثّقة في حزمة builtin القياسية. (2) باستثناء متغيرات حقول البنى.

لذا،

  • لا يمكن أبداً تعريف استيرادات الحزم في كتل الحزم والكتل المحلية.
  • لا يمكن أبداً تعريف الدوال في الكتل المحلية. (يمكن إحاطة الدوال المجهولة داخل الكتل المحلية لكنها ليست تعريفات.)
  • لا يمكن تعريف التسميات إلا في الكتل المحلية.

يُرجى ملاحظة ما يلي:

  • إذا كانت الكتلة الحاوية الأعمق لتعريفَي عنصرَي شيفرة هي الكتلة نفسها، فلا يمكن أن يكون اسمَا (معرّفا) العنصرين متطابقين.
  • يجب ألا يكون اسم (معرّف) عنصر شيفرة على مستوى الحزمة معرَّف في حزمة ما مطابقاً لأي اسم استيراد حزمة معرَّف في أي ملف مصدري في الحزمة (أي يجب ألا يكون اسم استيراد حزمة في حزمة ما مطابقاً لأي عنصر شيفرة على مستوى الحزمة معرَّف في الحزمة). قد تُخفَّف هذه القاعدة لاحقاً.
  • إذا كانت كتلة جسم الدالة الحاوية الأعمق لتعريفَي تسميتين هي الكتلة نفسها، فلا يمكن أن يكون اسمَا (معرّفا) التسميتين متطابقين.
  • يجب أن تكون إشارات التسمية ضمن كتلة جسم الدالة الأعمق التي تحتوي على تعريف التسمية.
  • لبعض الأجزاء الخاصة في الكتل المحلية الضمنية في جميع أنواع تدفقات التحكم متطلبات خاصة. عموماً، لا يُسمح بتعريف أي عناصر شيفرة مباشرةً في مثل هذه الكتل المحلية الضمنية، باستثناء بعض تعريفات المتغيرات القصيرة. - يمكن أن تتبع كل كلمة مفتاحية if أو switch أو for مباشرةً تعريف متغير قصير.
  • يمكن أن تتبع كل كلمة مفتاحية case في تدفق تحكم select مباشرةً تعريف متغير قصير.

(بالمناسبة، ترى حزم go/* القياسية أن كتل شيفرة الملفات لا يمكن أن تحتوي إلا على تعريفات استيراد الحزم.)

عناصر الشيفرة المصدرية المعرَّفة في كتل الحزم لكن خارج أي كتل محلية تُسمى عناصر شيفرة مصدرية على مستوى الحزمة. ويمكن أن تكون عناصر الشيفرة المصدرية على مستوى الحزمة ثوابت مسمّاة أو متغيرات أو دوال أو أنواعاً معرَّفة أو أسماء أنواع بديلة.

نطاقات عناصر الشيفرة المصدرية المعرَّفة

نطاق المعرّف المعرَّف يعني المدى الذي يكون فيه المعرّف قابلاً للتعرّف عليه (أو المدى المرئي).

دون النظر في حجب المعرّفات الذي سيُشرح في القسم الأخير من هذه المقالة، تعريفات النطاق لمعرّفات جميع أنواع عناصر الشيفرة المصدرية مذكورة أدناه.

  • نطاق معرّف معرَّف مسبقاً/مدمج هو كتلة الكون.
  • نطاق معرّف استيراد حزمة هو كتلة الملف التي تحتوي على تعريف استيراد الحزمة.
  • نطاق معرّف يدل على ثابت أو نوع أو متغير أو دالة (وليس طريقة) معرَّف على مستوى الحزمة هو كتلة الحزمة.
  • نطاق معرّف يدل على مستقبِل طريقة أو وسيط دالة أو متغير نتيجة هو جسم الدالة المقابل (كتلة محلية).
  • يبدأ نطاق معرّف ثابت أو متغير معرَّف داخل جسم دالة من نهاية مواصفة الثابت أو المتغير (أو نهاية التعريف بالنسبة لمتغير معرَّف تعريفاً قصيراً) وينتهي عند نهاية الكتلة الحاوية الأعمق.
  • يبدأ نطاق معرّف نوع أو اسم نوع بديل معرَّف داخل جسم دالة من نهاية المعرّف في مواصفة النوع المقابلة وينتهي عند نهاية الكتلة الحاوية الأعمق.
  • نطاق التسمية هو جسم كتلة الدالة الأعمق التي تحتوي على تعريف التسمية، لكنه يستثني جميع أجسام الدوال المجهولة المتداخلة في الدالة الحاوية.
  • حول نطاقات وسائط الأنواع، يُرجى قراءة كتاب Go generics 101.

لا نطاقات للمعرّفات الفارغة.

(لاحظ أن iota المعرَّف مسبقاً مرئي فقط في تعريفات الثوابت.)

ربما لاحظت الفرق الطفيف في تعريفات نطاق المعرّفات بين تعريفات الأنواع المحلية والمتغيرات المحلية، والثوابت المحلية وأسماء الأنواع البديلة المحلية. يعني هذا الفرق أن النوع المعرَّف قد يكون قادراً على الإشارة إلى نفسه في تعريفه. إليك مثالاً يُظهر الفرق.


package main

func main() {
	// var v int = v   // خطأ: v غير معرَّف
	// const C int = C // خطأ: C غير معرَّف
	/*
	type T = struct {
		*T    // خطأ: T يستخدم <T>
		x []T // خطأ: T يستخدم <T>
	}
	*/

	// تعريفات الأنواع التالية كلها صالحة.
	type T struct {
		*T
		x []T
	}
	type A [5]*A
	type S []S
	type M map[int]M
	type F func(F) F
	type Ch chan Ch
	type P *P

	// ...
	var p P
	p = &p
	p = ***********************p
	***********************p = p

	var s = make(S, 3)
	s[0] = s
	s = s[0][0][0][0][0][0][0][0]

	var m = M{}
	m[1] = m
	m = m[1][1][1][1][1][1][1][1]
}

لاحظ أن استدعاء fmt.Print(s) واستدعاء fmt.Print(m) يؤديان كلاهما إلى panic (بسبب طفح المكدس).

والفرق في النطاق بين التعريفات على مستوى الحزمة والتعريفات المحلية:


package main

// هنا المعرّفان في كل سطر هما
// المعرّف نفسه. المعرّفان الأيمنان ليسا
// المعرّفين المعرَّفين مسبقاً. بل هما
// نفس المعرّف الأيسر في كل سطر. لذا
// يفشل السطران معاً في التصريف.
/*
const iota = iota // خطأ: حلقة تعريف ثابت
var true = true   // خطأ: حلقة فحص الأنواع
*/

var a = b // يمكن الإشارة إلى متغيرات معرَّفة لاحقاً
var b = 123

func main() {
	// المعرّفان في الجانب الأيمن في
	// السطرين التاليين هما المعرَّفان مسبقاً.
	const iota = iota // صحيح
	var true = true   // صحيح
	_ = true

	// تفشل الأسطر التالية في التصريف، لأن
	// c يشير إلى متغير d معرَّف لاحقاً.
	/*
	var c = d
	var d = 123
	_ = c
	*/
}

حجب المعرّفات

باستثناء التسميات، يمكن حجب معرّف معرَّف في كتلة شيفرة خارجية بالمعرّف نفسه المعرَّف في كتل شيفرة متداخلة (مباشرةً أو غير مباشرة) في الكتلة الخارجية.

لا يمكن حجب التسميات.

إذا حُجب معرّف، فإن نطاقه سيستثني نطاقات المعرّفات الحاجبة له.

فيما يلي مثال مثير للاهتمام. تحتوي الشيفرة على 6 متغيرات معرَّفة باسم x. ويحجب كل x معرَّف في كتلة أعمق كل متغيرات x المعرَّفة في كتل أقل عمقاً.


package main

import "fmt"

var p0, p1, p2, p3, p4, p5 *int
var x = 9999 // x#0

func main() {
	p0 = &x
	var x = 888  // x#1
	p1 = &x
	for x := 70; x < 77; x++ {  // x#2
		p2 = &x
		x := x - 70 //  // x#3
		p3 = &x
		if x := x - 3; x > 0 { // x#4
			p4 = &x
			x := -x // x#5
			p5 = &x
		}
	}

	// 9999 888 77 6 3 -3
	fmt.Println(*p0, *p1, *p2, *p3, *p4, *p5)
}

مثال آخر: يطبع البرنامج التالي Sheep Goat بدلاً من Sheep Sheep. يُرجى قراءة التعليقات للتفسيرات.


package main

import "fmt"

var f = func(b bool) {
	fmt.Print("Goat")
}

func main() {
	var f = func(b bool) {
		fmt.Print("Sheep")
		if b {
			fmt.Print(" ")
			f(!b) // الـ f هو f على مستوى الحزمة.
		}
	}
	f(true) // الـ f هو f المحلي.
}

إذا عدّلنا البرنامج أعلاه كما هو موضّح فيما يلي، فسيطبع Sheep Sheep.


func main() {
	var f func(b bool)
	f = func(b bool) {
		fmt.Print("Sheep")
		if b {
			fmt.Print(" ")
			f(!b) // الـ f هو أيضاً f المحلي الآن.
		}
	}
	f(true)
}

في بعض الحالات، عندما تُحجب المعرّفات بمتغيرات معرَّفة بتعريفات متغيرات قصيرة، قد يلتبس على بعض الـ gophers الجدد ما إذا كان المتغير في تعريف متغير قصير مُعاداً تعريفه أم معرَّفاً من جديد. يُظهر المثال التالي (الذي يحتوي على أخطاء) الفخ الشهير في Go. لقد وقع فيه كل gopher تقريباً في أيامه الأولى من استخدام Go.


package main

import "fmt"
import "strconv"

func parseInt(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		// قد يعتقد بعض الـ gophers الجدد أن err متغير
		// معرَّف بالفعل في تعريف المتغير القصير التالي.
		// لكن في الحقيقة، كل من b و err معرَّفان
		// تعريفاً جديداً هنا. ومتغير err المعرَّف
		// تعريفاً جديداً يحجب متغير err المعرَّف
		// أعلاه.
		b, err := strconv.ParseBool(s)
		if err != nil {
			return 0, err
		}

		// إذا وصل التنفيذ إلى هنا، فقد يتوقع بعض
		// الـ gophers الجدد إرجاع خطأ nil.
		// لكن في الواقع، سيُرجَع الخطأ الخارجي
		// غير الـ nil بدلاً من ذلك، لأن نطاق متغير
		// err الداخلي ينتهي عند نهاية شرط
		// if الخارجي.
		if b {
			n = 1
		}
	}
	return n, err
}

func main() {
	fmt.Println(parseInt("TRUE"))
}

الناتج:


1 strconv.Atoi: parsing "TRUE": invalid syntax

لدى Go 25 كلمة مفتاحية فقط. لا يمكن استخدام الكلمات المفتاحية كمعرّفات. وكثير من الكلمات المألوفة في Go ليست كلمات مفتاحية، مثل int و bool و string و len و cap و nil وغيرها. إنها مجرد معرّفات معرَّفة مسبقاً (مدمجة). وهذه المعرّفات المعرَّفة مسبقاً معرَّفة في كتلة الكون، لذا يمكن للمعرّفات المعرَّفة حسب الطلب أن تحجبها. إليك مثالاً غريباً تُحجب فيه معرّفات كثيرة معرَّفة مسبقاً. وهو يُصرَّف ويعمل بشكل سليم.


package main

import (
	"fmt"
)

// يحجب معرّف الدالة المدمجة "len".
const len = 3
// يحجب معرّف الثابت المدمج "true".
var true = 0
// يحجب معرّف المتغير المدمج "nil".
type nil struct {}
// يحجب معرّف النوع المدمج "int".
func int(){}

func main() {
	fmt.Println("a weird program")
	var output = fmt.Println

	// يحجب استيراد الحزمة "fmt".
	var fmt = [len]nil{{}, {}, {}}
	// عذراً، "len" ثابت.
	// var n = len(fmt)
	// استخدم دالة cap المدمجة بدلاً منها، :(
	var n = cap(fmt)

	// تتبع الكلمة المفتاحية "for" كتلة شيفرة
	// محلية ضمنية واحدة وكتلة شيفرة محلية
	// صريحة واحدة. ويحجب متغير التكرار
	// "true" المتغير "true" على مستوى الحزمة
	// المعرَّف أعلاه.
	for true := 0; true < n; true++ {
		// يحجب الثابت المدمج "false".
		var false = fmt[true]
		// المتغير "true" المعرَّف تعريفاً
		// جديداً يحجب متغير التكرار "true".
		var true = true+1
		// عذراً، "fmt" مصفوفة وليست حزمة.
		// fmt.Println(true, false)
		output(true, false)
	}
}

الناتج:


a weird program
1 {}
2 {}
3 {}

نعم، هذا المثال متطرف. فهو يحتوي على كثير من الممارسات السيئة. حجب المعرّفات مفيد، لكن يُرجى عدم الإفراط في استخدامه.