باستثناء عمليات المعاملات التي قُدّمت في المقالة السابقة، تُعدّ عمليات الدوال نوعًا آخر من العمليات الشائعة في البرمجة. وكثيرًا ما تُسمّى عمليات الدوال استدعاءات الدوال (function calls). ستقدّم هذه المقالة كيفية تعريف الدوال واستدعائها.
تعريفات الدوال
لننظر إلى تعريف دالة.
func SquaresOfSumAndDiff(a int64, b int64) (s int64, d int64) {
x, y := a + b, a - b
s = x * x
d = y * y
return // <=> return s, d
}
يمكننا أن نجد أن تعريف الدالة يتكوّن من عدة أجزاء. ومن اليسار إلى اليمين:
- الجزء الأول هو الكلمة المفتاحية
func. - الجزء التالي هو اسم الدالة، ويجب أن يكون معرّفًا. اسم الدالة هنا هو
SquareOfSumAndDiff. - الجزء الثالث هو قائمة تعريف وسائط الإدخال، وهي محصورة بين زوج من
(). - الجزء الرابع هو قائمة تعريف النتائج المُخرَجة (أو المُرجَعة). يمكن لدوال Go إرجاع عدة نتائج. في هذا المثال المحدد، قائمة تعريف النتائج محصورة أيضًا بين زوج من
(). لكن إذا كانت القائمة فارغة أو كانت مكوّنة من تعريف نتيجة مجهولة واحد فقط، فإن زوج()في قائمة تعريف النتائج يكون اختياريًا (انظر أدناه للتفاصيل). - الجزء الأخير هو جسم الدالة، وهو محصور بين زوج من
{}. وداخل جسم الدالة، تُستخدم الكلمة المفتاحيةreturnلإنهاء تدفق التنفيذ الأمامي الطبيعي والدخول في مرحلة الخروج (انظر القسم الذي يلي القسم التالي) لاستدعاء الدالة.
في المثال أعلاه، يتكوّن تعريف كل وسيط ونتيجة من اسم ونوع (يأتي النوع بعد الاسم). ويمكننا النظر إلى تعريفات الوسائط والنتائج على أنها تعريفات متغيرات قياسية بدون الكلمة المفتاحية var. للدالة المعرَّفة أعلاه وسيطان، a وb، ونتيجتان، s وd. وجميع أنواع الوسائط والنتائج هي int64. وتُعامَل الوسائط والنتائج كمتغيرات محلية داخل أجسام دوالها المقابلة.
يمكن أن تكون الأسماء في قائمة تعريف النتائج في تعريف الدالة موجودة أو غائبة كلها معًا. وكلا الحالتين شائعة الاستخدام عمليًا. وإذا عُرّفت نتيجة باسم، فتُسمّى نتيجة مسمّاة، وإلا فتُسمّى نتيجة مجهولة (anonymous).
عندما تكون جميع النتائج في تعريف الدالة مجهولة، يجب أن تتبع الكلمة المفتاحية return داخل جسم الدالة المقابل سلسلة من قيم الإرجاع، يقابل كل منها تعريف نتيجة في تعريف الدالة. على سبيل المثال، تعريف الدالة التالي مكافئ للتعريف أعلاه.
func SquaresOfSumAndDiff(a int64, b int64) (int64, int64) {
return (a+b) * (a+b), (a-b) * (a-b)
}
في الواقع، إذا لم تُستخدم جميع الوسائط إطلاقًا داخل جسم الدالة المقابل، فيمكن أيضًا حذف الأسماء في قائمة تعريف الوسائط في تعريف الدالة كلها معًا. ومع ذلك، نادرًا ما تُستخدم الوسائط المجهولة عمليًا.
رغم أنه يبدو أن متغيرات الوسائط والنتائج معرَّفة خارج جسم تعريف الدالة، فإنها تُعتبر متغيرات محلية عامة داخل جسم الدالة. والفرق هو أن المتغيرات المحلية ذات الأسماء غير الفارغة المعرَّفة داخل جسم الدالة يجب أن تُستخدم دائمًا في جسم الدالة. ولا يمكن تكرار الأسماء غير الفارغة للمتغيرات المحلية في المستوى الأعلى والوسائط والنتائج في تعريف الدالة.
لا تدعم Go قيم الوسائط الافتراضية. والقيمة الأولية لكل نتيجة هي القيمة الصفرية لنوعها. على سبيل المثال، ستطبع الدالة التالية (وتُرجع) دائمًا 0 false.
func f() (x int, y bool) {
println(x, y) // 0 false
return
}
إذا كانت أجزاء النوع لبعض الوسائط المتتالية في قائمة تعريف الوسائط متطابقة، فيمكن لهذه الوسائط أن تشترك في جزء النوع نفسه في قائمة تعريف الوسائط. وينطبق الأمر نفسه على قوائم تعريف النتائج. على سبيل المثال، تعريفا الدالة أعلاه باسم SquaresOfSumAndDiff مكافئان لـ
func SquaresOfSumAndDiff(a, b int64) (s, d int64) {
return (a+b) * (a+b), (a-b) * (a-b)
// السطر أعلاه مكافئ
// للسطر التالي.
/*
s = (a+b) * (a+b); d = (a-b) * (a-b); return
*/
}
لاحظ أنه حتى لو كانت النتيجتان مسمّاتين، يمكن أن تتبع الكلمة المفتاحية return قيم إرجاع.
إذا كانت قائمة تعريف النتائج في تعريف الدالة تحتوي على تعريف نتيجة مجهولة واحد فقط، فلا حاجة إلى حصر قائمة تعريف النتائج بين (). وإذا لم يكن لتعريف الدالة أي نتائج مُرجَعة، فيمكن حذف جزء قائمة تعريف النتائج كليًا. أما جزء قائمة تعريف الوسائط فلا يمكن حذفه أبدًا، حتى لو كان عدد وسائط الدالة المعرَّفة صفرًا.
إليك المزيد من أمثلة تعريف الدوال.
func CompareLower4bits(m, n uint32) (r bool) {
r = m&0xF > n&0xF
return
// السطران أعلاهما مكافئان
// للسطر التالي.
/*
return m&0xF > n&0xF
*/
}
// ليس لهذه الدالة وسائط. تتكوّن
// قائمة تعريف النتائج من تعريف
// نتيجة مجهولة واحد فقط، لذا لا
// يُشترط حصرها بين ().
func VersionString() string {
return "go1.0"
}
// ليس لهذه الدالة نتائج. وجميع
// وسائطها مجهولة، لأننا لا
// نهتم بها. قائمة تعريف النتائج
// فارغة (لذا حُذفت).
func doNothing(string, int) {
}
من الحقائق التي تعلمناها من المقالات السابقة في Go 101 أن دالة الدخول main في كل برنامج Go معرَّفة بدون وسائط ونتائج.
لاحظ أن الدوال يجب أن تُعرَّف مباشرة على مستوى الحزمة. بعبارة أخرى، لا يمكن تعريف دالة داخل جسم دالة أخرى. وفي قسم لاحق، سنتعلم أنه يمكننا تعريف دوال مجهولة داخل أجسام دوال أخرى. لكن الدوال المجهولة ليست تعريفات دوال.
استدعاءات الدوال
يمكن استدعاء دالة معرَّفة عبر اسمها مضافًا إليه قائمة معطيات. ويجب أن تكون قائمة المعطيات محصورة بين (). وكثيرًا ما نسمّي هذا تمرير المعطيات (argument passing) (أو تمرير الوسائط، parameter passing). ويقابل كل معطى أحادي القيمة وسيطًا واحدًا (ويُمرَّر إليه).
ملاحظة: تمرير المعطيات هو أيضًا إسنادات قيم.
لا يُشترط أن يكون نوع المعطى مطابقًا لنوع الوسيط المقابل. والشرط الوحيد في المعطى هو أن يكون قابلًا للإسناد (أي قابلًا للتحويل ضمنيًا) إلى نوع الوسيط المقابل.
إذا كان للدالة نتائج مُرجَعة، فإن كل استدعاء لها يُعتبر تعبيرًا. وإذا كانت تُرجع عدة نتائج، فإن كل استدعاء لها يُعتبر تعبيرًا متعدد القيم. ويمكن إسناد التعبير متعدد القيم إلى قائمة قيم وجهة بالعدد نفسه.
فيما يلي مثال كامل يوضّح كيفية استدعاء بعض الدوال المعرَّفة.
package main
func SquaresOfSumAndDiff(a int64, b int64) (int64, int64) {
return (a+b) * (a+b), (a-b) * (a-b)
}
func CompareLower4bits(m, n uint32) (r bool) {
r = m&0xF > n&0xF
return
}
// تهيئة متغير على مستوى الحزمة
// باستدعاء دالة.
var v = VersionString()
func main() {
println(v) // v1.0
x, y := SquaresOfSumAndDiff(3, 6)
println(x, y) // 81 9
b := CompareLower4bits(uint32(x), uint32(y))
println(b) // false
// يُستنتج "Go" كنص،
// ويُستنتج 1 كـ int32.
doNothing("Go", 1)
}
func VersionString() string {
return "v1.0"
}
func doNothing(string, int32) {
}
من المثال أعلاه، نتعلم أنه يمكن تعريف الدالة إما قبل أي من استدعاءاتها أو بعده.
يمكن تأجيل استدعاءات الدوال أو تشغيلها في goroutines جديدة (خيوط خضراء) في Go. اقرأ مقالة لاحقة للتفاصيل.
مرحلة خروج (أو إرجاع) استدعاء الدالة
في Go، بالإضافة إلى مرحلة التنفيذ الأمامي الطبيعية، قد يمرّ استدعاء الدالة بمرحلة خروج (تُسمّى أيضًا مرحلة الإرجاع). وتبدأ مرحلة خروج استدعاء الدالة عند إرجاع الدالة المستدعاة. بعبارة أخرى، عند إرجاع استدعاء دالة، من الممكن أنه لم يخرج بعد.
يمكن العثور على شروح أكثر تفصيلًا لمراحل خروج استدعاءات الدوال في هذه المقالة.
الدوال المجهولة
تدعم Go الدوال المجهولة. وتعريف الدالة المجهولة يكاد يكون مطابقًا لتعريف الدالة، باستثناء عدم وجود جزء اسم الدالة في تعريف الدالة المجهولة.
يمكن استدعاء الدالة المجهولة مباشرة بعد تعريفها. مثال:
package main
func main() {
// ليس لهذه الدالة المجهولة وسائط
// لكن لها نتيجتان.
x, y := func() (int, int) {
println("This function has no parameters.")
return 3, 4
}() // استدعها. لا حاجة إلى معطيات.
// الدالة المجهولة التالية ليس لها نتائج.
func(a, b int) {
// السطر التالي يطبع: a*a + b*b = 25
println("a*a + b*b =", a*a + b*b)
}(x, y) // مرّر المعطيين x وy إلى الوسيطين a وb.
func(x int) {
// الوسيط x يحجب x الخارجي.
// السطر التالي يطبع: x*x + y*y = 32
println("x*x + y*y =", x*x + y*y)
}(y) // مرّر المعطى y إلى الوسيط x.
func() {
// السطر التالي يطبع: x*x + y*y = 25
println("x*x + y*y =", x*x + y*y)
}() // لا حاجة إلى معطيات.
}
لاحظ أن الدالة المجهولة الأخيرة تقع في نطاق المتغيرين x وy المعرَّفين أعلاه، ويمكنها استخدام المتغيرين مباشرة. وتُسمّى هذه الدوال إغلاقات (closures). في الواقع، يمكن اعتبار جميع الدوال المخصّصة في Go إغلاقات. ولهذا تكون دوال Go مرنة بقدر دوال كثير من اللغات الديناميكية.
لاحقًا، سنتعلم أنه يمكن إسناد دالة مجهولة إلى قيمة دالة، ويمكن استدعاؤها في أي وقت لاحق.
الدوال المدمجة
هناك بعض الدوال المدمجة في Go، مثل الدالتين println وprint. ويمكننا استدعاء هذه الدوال دون استيراد أي حزم.
يمكننا استخدام الدالتين المدمجتين real وimag للحصول على الجزأين الحقيقي والتخيلي لقيمة مركّبة. ويمكننا استخدام الدالة المدمجة complex لإنتاج قيمة مركّبة. لاحظ أنه إذا كانت معطيات استدعاء إحدى الدالتين كلها ثوابت، فسيُقيَّم الاستدعاء في وقت التصريف، وتكون قيمة نتيجة الاستدعاء ثابتًا أيضًا. وعلى وجه الخصوص، إذا كان أي من المعطيات ثابتًا غير مُنمَّط (untyped)، فإن قيمة النتيجة تكون أيضًا ثابتًا غير مُنمَّط. ويُعتبر الاستدعاء تعبيرًا ثابتًا.
مثال:
// c ثابت مركّب غير مُنمَّط.
const c = complex(1.6, 3.3)
// نتيجتا real(c) وimag(c) كلتاهما
// قيمتان عشريتان غير مُنمَّطتين. ويُستنتج
// كلتاهما أدناه كقيمتين من النوع float32.
var a, b float32 = real(c), imag(c)
// يُستنتج d كقيمة مُنمَّطة من النوع complex64.
// ونتيجتا real(d) وimag(d) كلتاهما
// قيمتان مُنمَّطتان من النوع float32.
var d = complex(a, b)
// يُستنتج e كقيمة مُنمَّطة من النوع complex128.
// ونتيجتا real(e) وimag(e) كلتاهما
// قيمتان مُنمَّطتان من النوع float64.
var e = c
ستُقدَّم المزيد من الدوال المدمجة في مقالات أخرى من Go 101 لاحقًا.
المزيد عن الدوال
هناك المزيد من المفاهيم والتفاصيل المتعلقة بالدوال التي لم تتناولها هذه المقالة. ويمكننا تعلّم تلك المفاهيم والتفاصيل في مقالة أنواع الدوال وقيمها لاحقًا.