الكلية الجامعية للعلوم التطبيقيةبرمجة 1
English
المحاضرة 12 · 100 دقيقة

تصميم الدوال

كيف نكتب دوال صغيرة واضحة يسهل فهمها وإعادة استخدامها؟

في نهاية هذه المحاضرة ستكون قادرًا على

  1. تكتب دوالًا لها الاسم نفسه، وتحدد أيها تختار Java عند الاستدعاء
  2. تشرح لماذا لا تعرف الدالة متغيرات main
  3. تثبت باستخدام المتتبّع أن الأنواع البدائية تُمرَّر بالقيمة
  4. تقسّم برنامجًا طويلًا إلى دوال صغيرة، لكل منها اسم واضح ومسؤولية واحدة
  5. تتتبّع دالة تستدعي نفسها، وتحدد حالة الأساس فيها
  6. تعيد استخدام isPrime وgcd وpower في برامج جديدة
محتويات المحاضرة
  1. دوال كثيرة قد تحمل الاسم نفسه إذا اختلفت معاملاتها
  2. تختار Java الدالة من عدد الوسائط وأنواعها
  3. يجب أن تكون معاملات كل نسخة واضحة الاختلاف
  4. المتغير يعيش داخل الدالة التي صرّحت عنه فقط
  5. تأخذ الدالة نسخة من القيمة، لا المتغير نفسه
  6. الدالة swap تفشل، والمتتبّع يكشف السبب
  7. البرنامج الذي يُكتب كله في main يصعب فهمه
  8. التقسيم يغيّر شكل البرنامج ولا يغيّر سلوكه
  9. الاسم الجيد يصف مهمة واحدة
  10. الدالة تستطيع أن تستدعي نفسها بمسألة أصغر
  11. حالة الأساس توقف السلسلة، والخطوة الذاتية تقترب منها
  12. بدون حالة الأساس يمتلئ المكدس ويتوقف البرنامج
  13. الدالة isPrime أداة تكتبها مرة وتستخدمها دائمًا
  14. الدالتان gcd وpower تبنيان حلولًا لمسائل أكبر
  15. أسئلة المراجعة
الجزء الأول: كيف تعمل الاستدعاءات

دوال كثيرة قد تحمل الاسم نفسه إذا اختلفت معاملاتها

أنت تستخدم هذه الفكرة دون أن تنتبه: println تقبل نصًا أو عددًا أو حرفًا، لأن في Java دوال عدة باسم println. تعريف أكثر من دالة بالاسم نفسه ومعاملات مختلفة يسمى التحميل الزائد (overloading).

ثلاث دوال باسم واحد

توقّع أي دالة تُنفَّذ في كل سطر. ثم اضغط «تتبّع سطرًا سطرًا» و«دخول» لتتأكد. أضف نسخة رابعة تأخذ أربعة أعداد صحيحة، واستدعِها.

public class OverloadedAdd {
    static int add(int a, int b) {
        return a + b;
    }

    static double add(double a, double b) {
        return a + b;
    }

    static int add(int a, int b, int c) {
        return a + b + c;
    }

    public static void main(String[] args) {
        System.out.println(add(3, 4));
        System.out.println(add(3.5, 4.0));
        System.out.println(add(1, 2, 3));
    }
}
الناتج
7
7.5
6

تختار Java الدالة من عدد الوسائط وأنواعها

  1. تستبعد ما يخالف عدد الوسائط
  2. تفضّل المطابقة التامة للأنواع
  3. وإلا فتقبل تحويلًا إلى نوع أوسعمثل char إلى int، وint إلى double. ولا تحوّل إلى نوع أضيق أبدًا.

أي دالة تختار Java في كل استدعاء؟ انتبه إلى السطر الأخير.

public class PickOverload {
    static void show(int n) {
        System.out.println("int: " + n);
    }

    static void show(double n) {
        System.out.println("double: " + n);
    }

    static void show(String n) {
        System.out.println("String: " + n);
    }

    public static void main(String[] args) {
        show(5);
        show(5.0);
        show("5");
        show('A');
    }
}
أظهر الناتج
الناتج
int: 5
double: 5.0
String: 5
int: 65

لماذا؟ لا توجد دالة تأخذ char، ولا يتحول char إلى String تلقائيًا. أقرب نوع أوسع هو int، فيصبح الحرف A رمزه العددي 65.

توجد دالة واحدة تأخذ double، ونحن نعطيها int. ماذا يطبع؟

public class WidenInt {
    static double half(double x) {
        return x / 2;
    }

    public static void main(String[] args) {
        System.out.println(half(5));
    }
}
أظهر الناتج
الناتج
2.5

لماذا؟ يتحول الوسيط 5 إلى 5.0 قبل الدخول إلى الدالة، فتكون القسمة عشرية ولا يضيع الكسر.

توجد دالة واحدة فقط هي static void show(int n). ماذا يحدث مع show(2.5);؟

يجب أن تكون معاملات كل نسخة واضحة الاختلاف

نوع الإرجاع وحده لا يكفي

تختلف الدالتان في نوع الإرجاع فقط. غيّر معامل النسخة الثانية إلى double واستدعِها بالقيمة 1.5.

public class TwiceTwo {
    static int twice(int n) {
        return n * 2;
    }

    static double twice(int n) {
        return n * 2.0;
    }

    public static void main(String[] args) {
        System.out.println(twice(4));
    }
}
رسالة المترجمis already defined in class
أظهر التصحيح
public class TwiceTwo {
    static int twice(int n) {
        return n * 2;
    }

    static double twice(double n) {
        return n * 2.0;
    }

    public static void main(String[] args) {
        System.out.println(twice(4));
        System.out.println(twice(1.5));
    }
}
الناتج
8
3.0

لماذا؟ عند قراءة twice(4) تنظر Java إلى الوسائط فقط، فلا تستطيع التمييز بالنوع المُرجَع. لذلك تعدّ الثانية تعريفًا مكررًا للأولى.

استدعاء محيّر

كلتا الدالتين تحتاج إلى تحويل واحد لتقبل total(1, 2)، فلا تعرف Java أيهما تختار. اجعل الوسيط الثاني 2.0.

public class Ambiguous {
    static double total(int a, double b) {
        return a + b;
    }

    static double total(double a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        System.out.println(total(1, 2));
    }
}
رسالة المترجمreference to total is ambiguous
أظهر التصحيح
public class Ambiguous {
    static double total(int a, double b) {
        return a + b;
    }

    static double total(double a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        System.out.println(total(1, 2.0));
    }
}
الناتج
3.0

لماذا؟ لا يوجد فائز واضح بين الدالتين، فترفض Java التخمين. وبعد التعديل تطابق الدالة الأولى الوسيطين تمامًا.

المتغير يعيش داخل الدالة التي صرّحت عنه فقط

المكان الذي يُعرف فيه اسم المتغير هو مداه (scope). متغيرات main ليست عامة في الصنف كله، بل هي محلية داخل main. والطريق الصحيح لإعطاء قيمة لدالة أخرى هو الوسيط.

الدالة لا ترى متغيرات main

أضف إلى showScore معاملًا int score، ومرّر إليه القيمة عند الاستدعاء.

public class ShowScore {
    static void showScore() {
        System.out.println(score);
    }

    public static void main(String[] args) {
        int score = 90;
        showScore();
    }
}
رسالة المترجمcannot find symbol
أظهر التصحيح
public class ShowScore {
    static void showScore(int score) {
        System.out.println(score);
    }

    public static void main(String[] args) {
        int score = 90;
        showScore(score);
    }
}
الناتج
90

لماذا؟ تبدو الرسالة كأنها خطأ إملائي، مع أن الاسم مكتوب بشكل صحيح. المشكلة هي مكان استخدامه: لا يوجد متغير بهذا الاسم داخل showScore.

متغيران باسم واحد في دالتين مختلفتين. ماذا يطبع؟

public class TwoBoxes {
    static void change() {
        int count = 100;
        System.out.println(count);
    }

    public static void main(String[] args) {
        int count = 1;
        change();
        System.out.println(count);
    }
}
أظهر الناتج
الناتج
100
1

لماذا؟ تشابه الاسمين صدفة: لكل دالة صندوق خاص بها. في المتتبّع ترى count في إطار change() وcount أخرى في إطار main().

تأخذ الدالة نسخة من القيمة، لا المتغير نفسه

عند الاستدعاء تُنسخ قيمة الوسيط إلى صندوق المعامل، ثم لا تبقى أي صلة بين الصندوقين. هذا هو التمرير بالقيمة (pass by value).

تغيّر الدالة معاملها. هل تتغير x في main؟

public class AddTen {
    static void addTen(int n) {
        n = n + 10;
        System.out.println(n);
    }

    public static void main(String[] args) {
        int x = 5;
        addTen(x);
        System.out.println(x);
    }
}
أظهر الناتج
الناتج
15
5

لماذا؟ n نسخة بدأت بالقيمة 5 ثم أصبحت 15، أما x فلم يلمسها أحد. ويختفي الصندوق n عند انتهاء الدالة.

الإرجاع يوصل النتيجة

لكي تصل النتيجة إلى main، تُرجعها الدالة ويُسندها المستدعي إلى المتغير. تتبّع البرنامج سطرًا سطرًا، ثم احذف x = من سطر الاستدعاء وتوقّع الناتج قبل التشغيل.

public class AddTenReturn {
    static int addTen(int n) {
        n = n + 10;
        return n;
    }

    public static void main(String[] args) {
        int x = 5;
        x = addTen(x);
        System.out.println(x);
    }
}
الناتج
15

الدالة swap تفشل، والمتتبّع يكشف السبب

نجح التبديل بالمتغير temp في محاضرة المتغيرات. لننقله إلى دالة، ونتوقع أن يعمل بالطريقة نفسها.

تبديل لا يصل إلى main

توقّع السطرين ثم شغّل. بعدها نفّذ الخطوات أدناه.

public class FailedSwap {
    static void swap(int a, int b) {
        int temp = a;
        a = b;
        b = temp;
        System.out.println("in swap: a = " + a + ", b = " + b);
    }

    public static void main(String[] args) {
        int a = 5;
        int b = 9;
        swap(a, b);
        System.out.println("in main: a = " + a + ", b = " + b);
    }
}
الناتج
in swap: a = 9, b = 5
in main: a = 5, b = 9
  1. اضغط «تتبّع سطرًا سطرًا»، ثم «السطر التالي» حتى swap(a, b);في إطار main() الصندوقان a = 5 وb = 9.
  2. اضغط «دخول»يظهر الإطار swap() فوق main()، وفيه صندوقان آخران بالاسمين والقيمتين نفسها.
  3. اضغط «السطر التالي» حتى } في swapتتبدل القيمتان في إطار swap()، بينما يبقى إطار main() كما هو: a = 5 وb = 9.
  4. اضغط «السطر التالي» مرة أخرىيختفي الإطار swap() ومعه كل ما فعله. لم يبدّل البرنامج سوى نسختين.

لماذا بقيت a وb في main دون تغيير؟

الاسم الجيد يصف مهمة واحدة

اسم ضعيفاسم أفضللماذا
doStuffcalculateAverageالدالة تنجز فعلًا، فليبدأ اسمها بفعل يصفه
checkisPrimeدالة boolean تُقرأ كسؤال
readAndPrintTotalreadInt, printTotalكلمة And تكشف مهمتين، فافصلهما

أي تصميم لحساب مجموع الفاتورة أكثر قابلية لإعادة الاستخدام؟

قسّم برنامج الفاتورة

اقرأ price وquantity، وكلاهما int. اكتب ثلاث دوال: subtotal تُرجع حاصل الضرب، وdiscount تُرجع 10 إذا كان المجموع 100 فأكثر وإلا 0، وprintReceipt تطبع الأسطر الثلاثة كما في المثال: Subtotal: 120 ثم Discount: 10 ثم Total: 110.

import java.util.Scanner;

public class Receipt {
    // Write subtotal, discount and printReceipt here

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int price = input.nextInt();
        int quantity = input.nextInt();
        // Call your methods here

    }
}
stdin
30
4
الناتج المتوقع
Subtotal: 120
Discount: 10
Total: 110
تلميح 1

المعاملات: subtotal(int price, int quantity)، وdiscount(int amount)، وprintReceipt(int amount, int off).

تلميح 2

main يخزن ناتج الدالتين الأوليين في متغيرين، ثم يمررهما إلى printReceipt.

الحل
import java.util.Scanner;

public class Receipt {
    static int subtotal(int price, int quantity) {
        return price * quantity;
    }

    static int discount(int amount) {
        if (amount >= 100) {
            return 10;
        }
        return 0;
    }

    static void printReceipt(int amount, int off) {
        System.out.println("Subtotal: " + amount);
        System.out.println("Discount: " + off);
        System.out.println("Total: " + (amount - off));
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int price = input.nextInt();
        int quantity = input.nextInt();
        int amount = subtotal(price, quantity);
        int off = discount(amount);
        printReceipt(amount, off);
    }
}
الجزء الثالث: الاستدعاء الذاتي والأدوات القابلة لإعادة الاستخدام

الدالة تستطيع أن تستدعي نفسها بمسألة أصغر

هذا هو الاستدعاء الذاتي (recursion)، وتسمى الدالة تراجعية. ليس فيه سحر: كل استدعاء ذاتي هو استدعاء عادي له إطار خاص وصناديق خاصة، كما رأينا في كل دالة.

العد التنازلي

اضغط «تتبّع سطرًا سطرًا»، ثم «دخول» باستمرار. راقب تراكم إطارات countDown() في المكدس، ولكل منها صندوق n بقيمة مختلفة، ثم راقبها تختفي واحدًا بعد الآخر. جرّب countDown(5).

public class CountDown {
    static void countDown(int n) {
        if (n == 0) {
            System.out.println("Go!");
        } else {
            System.out.println(n);
            countDown(n - 1);
        }
    }

    public static void main(String[] args) {
        countDown(3);
    }
}
الناتج
3
2
1
Go!

نقلنا الطباعة إلى ما بعد الاستدعاء الذاتي. ماذا يطبع؟

public class CountUp {
    static void countUp(int n) {
        if (n > 0) {
            countUp(n - 1);
            System.out.println(n);
        }
    }

    public static void main(String[] args) {
        countUp(3);
    }
}
أظهر الناتج
الناتج
1
2
3

لماذا؟ ينتظر الإطار n = 3 انتهاء ما بعده، فلا يطبع إلا في آخر الأمر. أول إطار يصل إلى الطباعة هو n = 1، ثم تعود الإطارات المنتظرة بالترتيب. تأكد من ذلك في المتتبّع.

حالة الأساس توقف السلسلة، والخطوة الذاتية تقترب منها

حالة الأساس

مسألة صغيرة نعرف جوابها دون استدعاء جديد.

if (n <= 1) return 1;
الخطوة الذاتية

نحل الباقي بالدالة نفسها على مسألة أصغر.

n * factorial(n - 1)

المضروب خطوة خطوة

فصلنا الحساب إلى سطرين ليظهر الصندوق smaller عند عودة كل إطار. اضغط «تتبّع سطرًا سطرًا»، ثم «دخول» باستمرار، واكتب ما يُرجعه كل إطار. جرّب 5، ثم 13، وقارن الناتج الثاني بالقيمة الصحيحة 6227020800: لقد تجاوز المدى الذي يتسع له int.

public class Factorial {
    static int factorial(int n) {
        if (n <= 1) {
            return 1;
        }
        int smaller = factorial(n - 1);
        return n * smaller;
    }

    public static void main(String[] args) {
        System.out.println(factorial(4));
    }
}
الناتج
24
الإطارفي طريق الذهاب ينتظرفي طريق العودة يُرجع
factorial(4)factorial(3)4 * 6 = 24
factorial(3)factorial(2)3 * 2 = 6
factorial(2)factorial(1)2 * 1 = 2
factorial(1)لا شيء: حالة الأساس1

مجموع الأعداد ذاتيًا

اكتب sumTo(n) باستدعاء ذاتي دون حلقة: تُرجع 0 عندما تكون n تساوي 0، وإلا تُرجع n + sumTo(n - 1). يقرأ main العدد ويطبع الناتج.

import java.util.Scanner;

public class SumTo {
    static int sumTo(int n) {
        // Replace this line with the base case and the recursive step
        return 0;
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        System.out.println(sumTo(n));
    }
}
stdin
5
الناتج المتوقع
15
تلميح 1

ابدأ بحالة الأساس: if (n == 0).

تلميح 2

ليست لديك حلقة: الاستدعاء الذاتي هو الذي يكرر العمل.

الحل
import java.util.Scanner;

public class SumTo {
    static int sumTo(int n) {
        if (n == 0) {
            return 0;
        }
        return n + sumTo(n - 1);
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        System.out.println(sumTo(n));
    }
}

بدون حالة الأساس يمتلئ المكدس ويتوقف البرنامج

لنكسر البرنامج عمدًا. في مختبر المضروب أعلاه، احذف حالة الأساس لتصبح الدالة كما يلي، ثم اضغط «تشغيل» لا «تتبّع».

static int factorial(int n) {
    int smaller = factorial(n - 1);
    return n * smaller;
}
جرّبها في مختبر المضروب، ثم أعد حالة الأساس.

سترى في أول الناتج Exception in thread "main" java.lang.StackOverflowError، ثم قائمة طويلة تكرر السطر نفسه من factorial. كل سطر منها استدعاء لم ينتهِ بعد.

ما الخطأ في countDown إذا كان الاستدعاء الذاتي countDown(n + 1)، وحالة الأساس n == 0، والبداية countDown(3)؟

الدالة isPrime أداة تكتبها مرة وتستخدمها دائمًا

فحص الأعداد الأولية

العدد الأولي أكبر من 1 ولا يقبل القسمة إلا على 1 وعلى نفسه. لاحظ أن return false يوقف الحلقة فور إيجاد قاسم. غيّر الحد إلى 50، ثم أضف سطرًا يطبع isPrime(1).

public class PrimeList {
    static boolean isPrime(int n) {
        if (n < 2) {
            return false;
        }
        for (int d = 2; d * d <= n; d++) {
            if (n % d == 0) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            if (isPrime(i)) {
                System.out.print(i + " ");
            }
        }
        System.out.println();
    }
}
الناتج
2 3 5 7 11 13 17 19

عدّ الأعداد الأولية

اقرأ n، واستخدم isPrime كما هي لتعدّ الأعداد الأولية من 1 إلى n. اطبع Primes: متبوعًا بالعدد.

import java.util.Scanner;

public class CountPrimes {
    static boolean isPrime(int n) {
        if (n < 2) {
            return false;
        }
        for (int d = 2; d * d <= n; d++) {
            if (n % d == 0) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        // Count the primes from 1 to n, then print the result

    }
}
stdin
10
الناتج المتوقع
Primes: 4
تلميح 1

لا تعدّل isPrime. حلقة وعدّاد وif (isPrime(i)) داخل main تكفي.

الحل
import java.util.Scanner;

public class CountPrimes {
    static boolean isPrime(int n) {
        if (n < 2) {
            return false;
        }
        for (int d = 2; d * d <= n; d++) {
            if (n % d == 0) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int count = 0;
        for (int i = 1; i <= n; i++) {
            if (isPrime(i)) {
                count++;
            }
        }
        System.out.println("Primes: " + count);
    }
}

الدالتان gcd وpower تبنيان حلولًا لمسائل أكبر

أداتان جديدتان

gcd تحسب القاسم المشترك الأكبر بطريقة إقليدس. غيّرت الدالة معامليها، فهل تغيرت x في main؟ تحقق بالمتتبّع. وpower تُرجع long لتتسع للنتائج الكبيرة بقيمة صحيحة تمامًا.

public class MathTools {
    static int gcd(int a, int b) {
        while (b != 0) {
            int remainder = a % b;
            a = b;
            b = remainder;
        }
        return a;
    }

    static long power(int base, int exponent) {
        long result = 1;
        for (int i = 0; i < exponent; i++) {
            result = result * base;
        }
        return result;
    }

    public static void main(String[] args) {
        int x = 84;
        System.out.println(gcd(x, 36));
        System.out.println(x);
        System.out.println(power(2, 10));
        System.out.println(power(2, 40));
        System.out.println(power(7, 0));
    }
}
الناتج
12
84
1024
1099511627776
1

المضاعف المشترك الأصغر

اكتب lcm(a, b) مستخدمًا gcd، علمًا بأن lcm يساوي a / gcd(a, b) * b. اقرأ عددين واطبع LCM: متبوعًا بالناتج.

import java.util.Scanner;

public class Lcm {
    static int gcd(int a, int b) {
        while (b != 0) {
            int remainder = a % b;
            a = b;
            b = remainder;
        }
        return a;
    }

    // Write the lcm method here

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int a = input.nextInt();
        int b = input.nextInt();
        // Print the result here

    }
}
stdin
4
6
الناتج المتوقع
LCM: 12
تلميح 1

جسم الدالة سطر واحد، ويستدعي gcd.

الحل
import java.util.Scanner;

public class Lcm {
    static int gcd(int a, int b) {
        while (b != 0) {
            int remainder = a % b;
            a = b;
            b = remainder;
        }
        return a;
    }

    static int lcm(int a, int b) {
        return a / gcd(a, b) * b;
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int a = input.nextInt();
        int b = input.nextInt();
        System.out.println("LCM: " + lcm(a, b));
    }
}

مجموع القوى

اقرأ base وn، ثم استخدم power لتحسب مجموع القوى من base مرفوعًا إلى 0 حتى base مرفوعًا إلى n. اطبع Sum: متبوعًا بالناتج. مثال: 2 و3 يعطيان 1 + 2 + 4 + 8، أي Sum: 15.

import java.util.Scanner;

public class SumOfPowers {
    static long power(int base, int exponent) {
        long result = 1;
        for (int i = 0; i < exponent; i++) {
            result = result * base;
        }
        return result;
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int base = input.nextInt();
        int n = input.nextInt();
        // Add up the powers here, then print the result

    }
}
stdin
2
3
الناتج المتوقع
Sum: 15
تلميح 1

المجموع يجب أن يكون long، وإلا فلن يتسع للاختبار الأخير.

تلميح 2

الحلقة تبدأ من 0 وتنتهي عند n، وتشمله.

الحل
import java.util.Scanner;

public class SumOfPowers {
    static long power(int base, int exponent) {
        long result = 1;
        for (int i = 0; i < exponent; i++) {
            result = result * base;
        }
        return result;
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int base = input.nextInt();
        int n = input.nextInt();
        long sum = 0;
        for (int i = 0; i <= n; i++) {
            sum = sum + power(base, i);
        }
        System.out.println("Sum: " + sum);
    }
}

أسئلة المراجعة

أي زوج من الدوال يمثل تحميلًا زائدًا صحيحًا؟

ما حالة الأساس في الدالة التراجعية؟

اجمع ما تعلمته اليوم: ماذا يطبع؟

public class ReviewValue {
    static int bump(int n) {
        n++;
        return n;
    }

    static int bump(int n, int by) {
        n += by;
        return n;
    }

    public static void main(String[] args) {
        int n = 1;
        bump(n);
        System.out.println(n);
        n = bump(n, 5);
        System.out.println(n);
    }
}
أظهر الناتج
الناتج
1
6

لماذا؟ الاستدعاء الأول يعدّل نسخة، ولا نستخدم ما يُرجعه، فتبقى n مساوية 1. وفي الثاني تختار Java النسخة ذات المعاملين، ونسند ما تُرجعه إلى n، فتصبح 6.