المحتويات:
- مقدمة بسيطة عن مكرِّرات Go
- لا تستدعِ دالة رد النداء yield مرة أخرى إذا أعاد أي استدعاء للدالة القيمة false من قبل
- لا تستدعِ دالة رد النداء yield لدالة مكرِّر إذا كانت دالة المكرِّر قد أرجعت
- استدعاء دالة رد النداء yield بالتزامن يحتاج إلى مزامنة دقيقة
- ستُنفَّذ استدعاءات الدوال العليا في مكدس الاستدعاءات لـ goroutine بالتزامن ولكن ليس في الوقت نفسه أبداً عند المرور على دوال المكرِّرات
- قد يكون استدعاء دالة المكرِّر صراحةً مكافئاً للمرور عليها أو غير مكافئ
- في تصميم المكرِّرات عيب
- لا يزال في تطبيق المكرِّرات بعض الأخطاء
- لم تُثبَت أهمية المكرِّرات بعد
مقدمة بسيطة عن مكرِّرات Go
قدّمت Go 1.23 المكرِّرات (iterators). المكرِّرات في Go دوال يمكن المرور عليها بحلقة range. وتحديداً، يمكن المرور على تعبيرات الدوال التي تكون أنواعها الكامنة بالصيغ التالية.
func(func() bool)
func(func(V) bool)
func(func(K, V) bool)
// يمثل K و V كليهما نوعين.
تأخذ جميع هذه الأنواع وسيط دالة رد نداء يُسمّى yield بالاصطلاح. وقد تأخذ دوال رد النداء هذه صفراً أو واحداً أو اثنين من الوسائط، وتُرجع دائماً نتيجة من النوع bool.
على سبيل المثال، يطبع برنامج Go التالي abc (منذ Go 1.23).
package main
import "fmt"
func main() {
for v := range abc {
fmt.Print(v)
}
}
func abc(yield func(string) bool) {
if yield("a") {
if yield("b") {
yield("c")
}
}
}
تُستخدم الدالة abc هنا كمكرِّر. ووسيطها yield دالة رد نداء تأخذ وسيطاً من النوع string وتُرجع نتيجة من النوع bool.
- تُمرَّر المعطيات الثلاثة
"a"و"b"و"c"بالترتيب إلى الوسيط من النوعstringوتُستخدم كقيم (لنسخ) متغير الحلقةv. - يمكن النظر إلى جسم الحلقة أساساً باعتباره جسم دالة
yield. مع ملاحظة أنه إذا أنهى تنفيذ خطوة من خطوات الحلقة أثناء المرور على دالة مكرِّر الحلقة بأكملها (مثلbreakللخروج من الحلقة)، فإن استدعاءyieldالمقابل يُرجع false (إلى دالة المكرِّر)؛ وإلا فإن استدعاءyieldالمقابل يُرجع true.
يمكن أيضاً كتابة دالة المكرِّر في المثال أعلاه على النحو التالي
func abc(yield func(string) bool) {
_ = yield("a") && yield("b") && yield("c")
}
لا تستدعِ دالة رد النداء yield مرة أخرى إذا أعاد أي استدعاء للدالة القيمة false من قبل
يجب عدم استدعاء دالة رد النداء الخاصة بدالة مكرِّر مرة أخرى إذا أعاد أي استدعاء لها القيمة false من قبل. على سبيل المثال، ينهار البرنامج التالي عند تشغيله.
package main
import "fmt"
func main() {
for v := range ab {
fmt.Println(v)
break // احذف هذا السطر لتجنّب الانهيار.
}
}
func ab(yield func(string) bool) {
var done = yield("a")
fmt.Println(done) // false
yield("b") // خطأ وقت التشغيل: واصلت دالة range التكرار
// بعد أن أرجعت دالة جسم حلقة for
// القيمة false.
}
لا يفحص المصرِّف Go الرسمي مثل هذه الاستخدامات غير الصالحة. لذا ينتج عن هذا الاستخدام غير الصالح خطأ وقت تشغيل (قابل للاستعادة).
لاحظ أن go vet أيضاً لا يفحص مثل هذه الاستخدامات غير الصالحة (حتى الإصدار v1.24.x).
لا تستدعِ دالة رد النداء yield لدالة مكرِّر إذا كانت دالة المكرِّر قد أرجعت
يجب عدم استدعاء دالة رد النداء الخاصة بدالة مكرِّر مرة أخرى إذا كانت دالة المكرِّر قد أرجعت بالفعل. على سبيل المثال، ينهار البرنامج التالي عند تشغيله.
package main
import "fmt"
var f func(string) bool
func main() {
for v := range abc {
fmt.Print(v)
}
f("d") // خطأ وقت التشغيل: واصلت دالة range
// التكرار بعد خروج الحلقة
// بأكملها
}
func abc(yield func(string) bool) {
_ = yield("a") && yield("b") && yield("c")
f = yield
}
استدعاء دالة رد النداء yield بالتزامن يحتاج إلى مزامنة دقيقة
الاستدعاءات المتوازية لدالة رد النداء yield ليست متزامنة افتراضياً. وعليك القيام بالمزامنة بنفسك.
على سبيل المثال، ينهار البرنامج التالي بسبب panic مُربك.
package main
import "fmt"
import "sync"
var c = make(chan int)
func main() {
// خطأ وقت التشغيل: واصلت دالة range التكرار بعد حدوث panic في جسم الحلقة
for v := range oneHundred {
<-c
fmt.Println(v)
}
}
func oneHundred(yield func(int) bool) {
var wg sync.WaitGroup
for v := range 100 {
wg.Add(1)
go func() {
defer wg.Done()
yield(v)
}()
}
close(c)
wg.Wait()
}
لجعله يعمل، ينبغي مزامنته، هكذا
func abc(yield func(int) bool) {
var wg sync.WaitGroup
var mu sync.Mutex // استخدم mutex للمزامنة
for v := range 100 {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock() // امنع استدعاء yield
defer mu.Unlock() // على التوازي.
yield(v)
}()
}
close(c)
wg.Wait()
}
لكن يُوصى بشدة بعدم استخدام دوال رد النداء yield في goroutines أخرى.
ستُنفَّذ استدعاءات الدوال العليا في مكدس الاستدعاءات لـ goroutine بالتزامن ولكن ليس في الوقت نفسه أبداً عند المرور على دوال المكرِّرات
عند المرور على دالة مكرِّر، تُستدعى ضمنياً. وأثناء المرور على دالة المكرِّر، تتنقل عملية تنفيذ الـ goroutine ذهاباً وإياباً بين استدعاء دالة المكرِّر وجسم الحلقة (جسم الحلقة جملة داخل مستدعي استدعاء دالة المكرِّر).
على سبيل المثال، يطبع البرنامج التالي 0a1b2c.
package main
import "fmt"
func main() {
for v := range abc {
fmt.Print(v)
}
}
func abc(yield func(string) bool) {
for i, v := range "abc" {
fmt.Print(i)
if (!yield(string(v))) {
break
}
}
}
فيما يلي مثال آخر، حيث تتنقل عملية تنفيذ الـ goroutine ذهاباً وإياباً بين 222 استدعاء لدالة مكرِّر وجسم الحلقة في الدالة main. وأخيراً، يطبع البرنامج 4194304 و4194305 و4194306 بالترتيب.
package main
func wrap(it func(yield func(int) bool)) func(yield func(int) bool) {
return func(yield func(int) bool) {
for v := range it {
yield(v+1)
}
}
}
func main() {
iterator := func(yield func(int) bool) {
_ = yield(0) && yield(1) && yield (2)
}
for range 1 << 22 { // استخدام 23 يُسقط البرنامج
iterator = wrap(iterator)
}
for v := range iterator {
println(v)
}
}
قد يكون استدعاء دالة المكرِّر صراحةً مكافئاً للمرور عليها أو غير مكافئ
بالنسبة إلى دالة المكرِّر المعروضة أعلاه:
func abc(yield func(string) bool) {
_ = yield("a") && yield("b") && yield("c")
}
فإن الطريقتين التاليتين لاستخدامها متكافئتان.
الطريقة 1
for v := range abc {
fmt.Print(v)
}
الطريقة 2
abc(func(v string) bool {
fmt.Print(v)
return true
})
لكن بالنسبة إلى دالة المكرِّر التالية:
func abcThenPanic(yield func(string) bool) {
_ = yield("a") && yield("b") && yield("c")
panic("bye")
}
فإن الطريقتين التاليتين لاستخدامها غير متكافئتين.
الطريقة 1
for v := range abcThenPanic {
defer func() {
recover()
}()
fmt.Print(v)
}
الطريقة 2
abcThenPanic(func(v string) bool {
defer func() {
recover()
}()
fmt.Print(v)
return true
})
وعندما تحتوي أجسام الحلقات على جملتي goto أو return، يصبح التكافؤ بين الطريقتين أقل احتمالاً.
في تصميم المكرِّرات عيب
ينبغي أن يتصرف البرنامجان التاليان بالطريقة نفسها، لكنهما لا يفعلان ذلك الآن (حتى سلسلة أدوات Go v1.24.1):
البرنامج 1
package main
func main() {
defer func() {
var v = recover()
println(v == 123)
}()
for range iter {
panic(123)
}
}
func iter(yield func() bool) {
defer func() {
recover()
}()
yield()
}
يطبع البرنامج false.
البرنامج 2
package main
func main() {
defer func() {
var v = recover()
println(v == 123)
}()
for range iter {}
panic(123)
}
func iter(yield func() bool) {
defer func() {
recover()
}()
yield()
}
يطبع البرنامج true.
سبب اختلاف السلوك، وفق التصميم الحالي، هو ما يلي:
- حالة panic الناشئة في جسم الحلقة عند المرور على دالة مكرِّر ستنتقل إلى دالة المكرِّر.
- ينبغي ألا تستعيد دالة المكرِّر حالة panic الناشئة في جسم الحلقة. فإذا استعادت دالة المكرِّر حالة panic المنتقلة من جسم الحلقة، فسينشئ وقت التشغيل حالة panic جديدة ضمنياً.
إذا كان استدعاء yield يُرجع false فقط عندما يُنشئ جسم الحلقة حالة panic، فلن تكون هناك فروق في السلوك بين البرنامجين.
لا يزال في تطبيق المكرِّرات بعض الأخطاء
على سبيل المثال، ينبغي أن يكون للبرنامجين التاليين السلوك نفسه، لكنهما لا يفعلان ذلك الآن (حتى سلسلة أدوات Go v1.24.1).
البرنامج 1
package main
import "fmt"
func main() {
defer foo()
panic(123)
}
func foo() {
for range iter {
fmt.Println(recover())
}
}
func iter(yield func() bool) {
yield()
}
يطبع البرنامج <nil> ثم ينهار.
البرنامج 2
package main
import "fmt"
func main() {
defer foo()
panic(123)
}
func foo() {
for range 1 {
fmt.Println(recover())
}
}
يطبع البرنامج 123 ثم ينتهي طبيعياً.
لم تُثبَت أهمية المكرِّرات بعد
للمكرِّرات بالتأكيد فائدة معينة. لكن لم يثبت بعد أن الأمر يستحق إضافة كل هذا التعقيد إلى اللغة.