سيشرح هذا المقال ترتيبات تقييم التعبيرات في جميع أنواع السيناريوهات.
يُقيَّم التعبير بعد التعبيرات التي يعتمد عليها
هذا أمر سهل الفهم. ومن الأمثلة الواضحة أن التعبير يُقيَّم بعد تقييم تعبيراته الفرعية. مثلاً، في نداء الدالة f(x, y[n])،
- يُقيَّم
f()بعد التعبيرات التي يعتمد عليها، ومنهاfوxوy[n]. - تقييم التعبير
y[n]يأتي بعد تقييمnوy.
يرجى قراءة ترتيب تهيئة عناصر شيفرة البرنامج للاطلاع على مثال آخر حول ترتيبات تهيئة المتغيرات على مستوى الحزمة.
ترتيب تهيئة المتغيرات على مستوى الحزمة
عند تحميل حزمة في وقت التشغيل، يحاول وقت تشغيل Go تهيئة المتغيرات غير المهيَّأة على مستوى الحزمة التي لا تعتمد على متغيرات غير مهيَّأة، وفق ترتيب تعريفها، حتى لا تُهيَّأ أي متغيرات في هذه العملية. في برنامج Go صُرِّف بنجاح، لا ينبغي أن تبقى متغيرات غير مهيَّأة بعد انتهاء كل هذه العمليات.
تُعامَل المتغيرات على مستوى الحزمة التي تظهر كمعرّفات فارغة (blank identifiers) مثل أي متغيرات أخرى في عملية التهيئة.
مثلاً، في البرنامج التالي، يعتمد المتغير a على b، ويعتمد المتغيران c و_ على a. لذا
- أول متغير يُهيَّأ هو
b، وهو أول متغير على مستوى الحزمة لا يعتمد على متغيرات أخرى على مستوى الحزمة. - ثاني متغير يُهيَّأ هو
a. بعد تهيئةb، يصبحaأول متغير على مستوى الحزمة لا يعتمد على متغيرات غير مهيَّأة على مستوى الحزمة. - ثالث ورابع متغير يُهيَّأ هما
_وc. بعد تهيئةaوb، لم يعد أيٌّ من_وcيعتمد على متغيرات غير مهيَّأة على مستوى الحزمة.
package main
import "fmt"
var (
_ = f()
a = b / 2
b = 6
c = f()
)
func f() int {
a++
return a
}
func main() {
fmt.Println(a, b, c) // 5 6 5
}
يطبع البرنامج أعلاه 5 6 5.
المتغيرات المتعددة في الطرف الأيسر من مواصفة متغير مهيَّأة بتعبير واحد متعدد القيم في الطرف الأيمن تُهيَّأ معاً. مثلاً، في تعريف متغير على مستوى الحزمة var x, y = f()، يُهيَّأ المتغيران x وy معاً. بعبارة أخرى، لن يُهيَّأ أي متغير آخر بينهما.
يُفكَّك تعريف المتغير على مستوى الحزمة الذي يحتوي على عدة تعبيرات قيم مصدرية إلى عدة تعريفات متغيرات ذات قيمة مصدرية واحدة قبل تهيئة جميع المتغيرات على مستوى الحزمة. مثلاً،
var m, n = expr1, expr2
مكافئ لـ
var m = expr1
var n = expr2
إذا وُجدت تبعيات خفية بين المتغيرات، فإن ترتيب التهيئة بين تلك المتغيرات غير محدد. في المثال التالي (منقول من مواصفة Go)،
- سيُهيَّأ المتغير
aبعدbبالتأكيد، - لكن ما إذا كان
xيُهيَّأ قبلb، أو بينbوa، أو بعدa، فهذا غير محدد. - كما أن اللحظة التي تُنادى فيها الدالة
sideEffect()(قبل تهيئةxأو بعدها) غير محددة أيضاً.
// x له تبعية خفية على a و b
var x = I(T{}).ab()
// افترض أن sideEffect غير مرتبطة بـ x و a و b.
var _ = sideEffect()
var a = b
var b = 42
type I interface { ab() []int }
type T struct{}
func (T) ab() []int { return []int{a, b} }
يرجى ملاحظة أن مواصفة Go لا تفرض تحديد ترتيب تصريف ملفات المصدر داخل الحزمة، لذا حاول ألا تضع بعض المتغيرات على مستوى الحزمة في ملفات مصدر مختلفة داخل الحزمة إذا كانت هناك علاقات تبعية معقدة بين تلك المتغيرات؛ وإلا فقد يُهيَّأ متغير بقيم مختلفة من قِبَل مصرِّفات Go مختلفة.
واعلم أن بعض إصدارات سلسلة أدوات Go لا تطبّق بشكل صحيح القواعد المذكورة أعلاه في هذا القسم. مثلاً:
ترتيبات تقييم المعامَلات به في العمليات المنطقية
في تعبير منطقي a && b، لا يُقيَّم تعبير المعامَل به الأيمن b إلا إذا قُيِّم تعبير المعامَل به الأيسر a بالقيمة true. لذا فإن b، إذا احتاج إلى تقييم، يُقيَّم بعد تقييم a.
في تعبير منطقي a || b، لا يُقيَّم تعبير المعامَل به الأيمن b إلا إذا قُيِّم تعبير المعامَل به الأيسر a بالقيمة false. لذا فإن b، إذا احتاج إلى تقييم، يُقيَّم بعد تقييم a.
الترتيب المعتاد
بخصوص عمليات التقييم داخل جسم دالة، تقول مواصفة Go
... عند تقييم المعامَلات به لتعبير أو إسناد أو جملة إرجاع، تُقيَّم جميع نداءات الدوال ونداءات الطرائق وعمليات الاتصال (عبر القنوات) بترتيب معجمي من اليسار إلى اليمين.
يُسمّى الترتيب الموصوف للتو الترتيب المعتاد.
يرجى ملاحظة أن تحويل القيمة الصريح T(v) ليس نداء دالة.
مثلاً، في التعبير []int{x, fa(), fb(), y}، بافتراض أن x وy متغيران وأن fa وfb دالتان، فمن المضمون أن يُقيَّم (يُنفَّذ) النداء fa() قبل fb(). غير أن ترتيبات التقييم التالية غير محددة في مواصفة Go:
- ترتيب تقييم
x(أوy) وfa()(أوfb()). - ترتيب تقييم
xوyوfaوfb.
مثال آخر، وهو الإسناد التالي، معروض في مواصفة Go.
y[z.f()], ok = g(h(a, b), i()+x[j()], <-c), k()
حيث
cتعبير قناة وسيُقيَّم إلى قيمة قناة.gوhوiوjوkتعبيرات دوال.fطريقة للتعبيرz.
وبمراعاة القاعدة المذكورة في القسم السابق أيضاً، ينبغي أن تضمن المصرِّفات ترتيبات التقييم التالية في وقت التشغيل.
- تحدث نداءات الدوال ونداءات الطرائق وعمليات الاتصال عبر القنوات بالترتيب
z.f()→h()→i()→j()→<-c→g()→k(). - يُقيَّم
h()بعد تقييم التعبيراتhوaوb. - يُقيَّم
y[]بعد تقييمz.f(). - يُقيَّم
z.f()بعد تقييم التعبيرz. - يُقيَّم
x[]بعد تقييمj().
مع ذلك، فإن الترتيبات التالية (وغيرها الكثير) غير محددة.
- ترتيب تقييم
yوzوgوhوaوbوxوiوjوcوk. - ترتيب تقييم
y[]وx[]و<-c.
بحسب الترتيب المعتاد، نعلم أن المتغيرات المعرَّفة التالية x وm وn (المعروضة أيضاً في مواصفة Go) ستُهيَّأ بقيم غامضة.
a := 1
f := func() int { a++; return a }
// قد تكون x هي [1, 2] أو [2, 2]: ترتيب التقييم
// بين a و f() غير محدد.
x := []int{a, f()}
// قد تكون m هي {2: 1} أو {2: 2}: ترتيب التقييم
// بين إسنادي عنصري الخريطة
// غير محدد.
m := map[int]int{a: 1, a: 2}
// قد تكون n هي {2: 3} أو {3: 3}: ترتيب التقييم
// بين المفتاح والقيمة غير محدد.
n := map[int]int{a: f()}
ترتيبات التقييم والإسناد في جمل الإسناد
إلى جانب القواعد المقدَّمة أعلاه، تحدد مواصفة Go مزيداً بشأن ترتيب تقييم التعبيرات وترتيب الإسنادات الفردية في جملة الإسناد:
يجري الإسناد على مرحلتين. أولاً، تُقيَّم جميع معامَلات به تعبيرات الفهرسة وإزاحة المؤشر (بما في ذلك إزاحة المؤشر الضمنية في المحدِّدات) في الطرف الأيسر، والتعبيرات في الطرف الأيمن، بالترتيب المعتاد. ثانياً، تُنفَّذ الإسنادات بترتيب من اليسار إلى اليمين.
لاحقاً، قد نسمي المرحلة الأولى مرحلة التقييم والمرحلة الثانية مرحلة التنفيذ.
لا تحدد مواصفة Go بوضوح ما إذا كانت الإسنادات المنفَّذة خلال المرحلة الثانية قد تؤثر على نتائج تقييم التعبيرات المحصَّلة في المرحلة الأولى، وهو ما أثار بعض الخلافات من قبل. لذا سيقدّم هذا المقال هنا شرحاً إضافياً لترتيبات التقييم في إسنادات القيم.
أولاً، لنوضّح أن الإسنادات المنفَّذة خلال المرحلة الثانية لا تؤثر على نتائج تقييم التعبيرات المحصَّلة في نهاية المرحلة الأولى.
لتسهيل الشروح التالية، نفترض أن قيمة الحاوية (شريحة أو خريطة) في تعبير فهرسة كهدف للإسناد قابلة دائماً لأخذ العنوان. وإن لم تكن كذلك، فيمكننا اعتبار أن قيمة الحاوية قد حُفظت مسبقاً في قيمة حاوية مؤقتة قابلة لأخذ العنوان واستُبدلت بها قبل تنفيذ المرحلة الثانية.
عند انتهاء مرحلة التقييم وقبيل بدء مرحلة التنفيذ مباشرة، يكون كل تعبير هدف في الطرف الأيسر للإسناد قد قُيِّم إلى صيغته الأولية. وللتعبيرات الهدف المختلفة صيغ أولية مختلفة.
- إذا كان تعبير الهدف معرّفاً فارغاً، فإن صيغته الأولية تبقى معرّفاً فارغاً.
- إذا كان تعبير الهدف تعبير فهرسة حاوية (مصفوفة أو شريحة أو خريطة)
c[k]، فإن صيغته الأولية هي(*cAddr)[k]، حيثcAddrمؤشر يشير إلىc. - في الحالات الأخرى، يجب أن ينتج تعبير الهدف قيمة قابلة لأخذ العنوان، فتكون صيغته الأولية إزاحة (dereference) للعنوان الناتج عن تقييم تعبير الهدف.
بافتراض أن a وb متغيران قابلان لأخذ العنوان من النوع نفسه، فإن الإسناد التالي
a, b = b, a
سيُنفَّذ على الخطوات التالية:
// مرحلة التقييم:
P0 := &a; P1 := &b
R0 := b; R1 := a
// الصيغة الأولية: *P0, *P1 = R0, R1
// مرحلة التنفيذ:
*P0 = R0
*P1 = R1
إليك مثالاً آخر، حيث يُعدَّل x[0] بدلاً من x[1].
x := []int{0, 0}
i := 0
i, x[i] = 1, 2
fmt.Println(x) // [2 0]
خطوات التنفيذ المفكَّكة للسطر الثالث الموضَّح أدناه تكون كالتالي:
// مرحلة التقييم:
P0 := &i; P1 := &x; T2 := i
R0 := 1; R1 := 2
// الآن، T2 == 0
// الصيغة الأولية: *P0, (*P1)[T2] = R0, R1
// مرحلة التنفيذ:
*P0 = R0
(*P1)[T2] = R1
مثال أكثر تعقيداً بقليل.
package main
import "fmt"
func main() {
m := map[string]int{"Go": 0}
s := []int{1, 1, 1}; olds := s
n := 2
p := &n
s, m["Go"], *p, s[n] = []int{2, 2, 2}, s[1], m["Go"], 5
fmt.Println(m, s, n) // map[Go:1] [2 2 2] 0
fmt.Println(olds) // [1 1 5]
}
خطوات التنفيذ المفكَّكة للسطر العاشر الموضَّح أدناه تكون كالتالي:
// مرحلة التقييم:
P0 := &s; PM1 := &m; K1 := "Go"; P2 := p; PS3 := &s; T3 := 2
R0 := []int{2, 2, 2}; R1 := s[1]; R2 := m["Go"]; R3 := 5
// الآن، R1 == 1، R2 == 0
// الصيغة الأولية:
// *P0, (*PM1)[K1], *P2, (*PS3)[T3] = R0, R1, R2, R3
// مرحلة التنفيذ:
*P0 = R0
(*PM1)[K1] = R1
*P2 = R2
(*PS3)[T3] = R3
المثال التالي يُدوِّر جميع عناصر شريحة موضعاً واحداً.
x := []int{2, 3, 5, 7, 11}
t := x[0]
var i int
for i, x[i] = range x {}
x[i] = t
fmt.Println(x) // [3 5 7 11 2]
مثال آخر:
x := []int{123}
x, x[0] = nil, 456 // لن يحدث panic
x, x[0] = []int{123}, 789 // سيحدث panic
على الرغم من أنه قانوني، لا يُنصح باستخدام إسنادات متعددة القيم معقدة في Go، لأن قراءتها ليست جيدة ولها آثار سلبية على سرعة التصريف وأداء التنفيذ معاً.
كما ذُكر أعلاه، ليست كل الترتيبات محددة في مواصفة Go لإسنادات القيم، لذا قد تنتج بعض الشيفرات المكتوبة بشكل سيئ نتائج مختلفة. في المثال التالي، ترتيب تقييم التعبيرين x+1 وf(&x) غير محدد. لذا قد يطبع المثال 100 99 أو 1 99.
package main
import "fmt"
func main() {
f := func (p *int) int {
*p = 99
return *p
}
x := 0
y, z := x+1, f(&x)
fmt.Println(y, z)
}
فيما يلي مثال آخر سيطبع نتائج غامضة. قد يطبع 1 7 2 أو 1 8 2 أو 1 9 2، حسب اختلاف تطبيقات المصرِّفات.
package main
import "fmt"
func main() {
x, y := 0, 7
f := func() int {
x++
y++
return x
}
fmt.Println(f(), y, f())
}
ترتيبات تقييم التعبيرات في كتل شيفرة switch-case
سبق وصف ترتيب تقييم التعبيرات في كتلة شيفرة switch-case. وهنا نعرض مثالاً فحسب. ببساطة، قبل الدخول إلى كتلة شيفرة فرع ما، تُقيَّم تعبيرات case وتُقارن بتعبير switch واحداً تلو الآخر، حتى تُسفر مقارنة عن القيمة true.
package main
import "fmt"
func main() {
f := func(n int) int {
fmt.Printf("f(%v) is called.\n", n)
return n
}
switch x := f(3); x + f(4) {
default:
case f(5):
case f(6), f(7), f(8):
case f(9), f(10):
}
}
في وقت التشغيل، تُقيَّم نداءات f() بالترتيب من الأعلى إلى الأسفل ومن اليسار إلى اليمين، حتى تُسفر مقارنة عن القيمة true. لذا لن تُقيَّم f(8) وf(9) وf(10) في هذا المثال.
المخرج:
f(3) is called.
f(4) is called.
f(5) is called.
f(6) is called.
f(7) is called.
ترتيبات تقييم التعبيرات في كتل شيفرة select-case
عند تنفيذ كتلة شيفرة select-case، وقبل الدخول إلى كتلة شيفرة فرع ما، تُقيَّم جميع معامَلات به القنوات في عمليات الاستقبال ومعامَلات به جمل الإرسال المتضمنة في كتلة شيفرة select-case مرة واحدة بالضبط، بترتيب المصدر (من الأعلى إلى الأسفل، ومن اليسار إلى اليمين).
لاحظ أن تعبير الهدف الذي تُسنَد إليه عملية استقبال في case لن يُقيَّم إلا إذا اختيرت عملية الاستقبال تلك لاحقاً.
في المثال التالي، لن يُقيَّم التعبير *fptr("aaa") أبداً، لأن عملية الاستقبال المقابلة له <-fchan("bbb", nil) لن تُختار.
package main
import "fmt"
func main() {
c := make(chan int, 1)
c <- 0
fchan := func(info string, c chan int) chan int {
fmt.Println(info)
return c
}
fptr := func(info string) *int {
fmt.Println(info)
return new(int)
}
select {
case *fptr("aaa") = <-fchan("bbb", nil): // حاجب
case *fptr("ccc") = <-fchan("ddd", c): // غير حاجب
case fchan("eee", nil) <- *fptr("fff"): // حاجب
case fchan("ggg", nil) <- *fptr("hhh"): // حاجب
}
}
مخرج البرنامج أعلاه:
bbb
ddd
eee
fff
ggg
hhh
ccc
لاحظ أن التعبير *fptr("ccc") هو آخر تعبير يُقيَّم في المثال أعلاه. إذ يُقيَّم بعد اختيار عملية الاستقبال المقابلة له <-fchan("ddd", c).