בפרק זה נלמד כיצד לכתוב פעולות (פונקציות), המחלקות את התוכנית לתתי-משימות, מאפשרות שימוש חוזר בקוד, ומשפרות את קריאות הקוד. .
ניתן להשתמש בשני המינוחים. בספרות בעברית רווח המונח פעולה. באנגלית רווח המונח פונקציה.
הערה: חלק מהמורים מלמדים פונקציות מוקדם יותר - כבר לאחר לולאת for
ולפני לולאת while
או אפילו לפני for
. עם זאת, בסילבוס זה הוחלט ללמד פונקציות רק לאחר לולאות for
, while
ולולאות מקוננות, כחלק משלב חשיבה אלגוריתמי מתקדם. אומנם אישית אני ממליץ ללמד פונקציות מוקדם יותר, אך נתיישר עם הסילבוס ונציג את הפרק בהמשך לפרק 6.
הגדרה: פונקציה היא מקבץ של פקודות המאוגדות תחת שם מזוהה, שניתן לקרוא לו (להפעיל אותו) מתוך חלקים שונים בתוכנית כדי לבצע משימה מוגדרת. פונקציה יכולה לקבל נתונים קלט (פרמטרים) ויכולה גם להחזיר תוצאה לפונקציה שקראה לה
- כאשר קוראים לפונקציה, התוכנית עוצרת בנקודת הקריאה, עוברת לביצוע קוד הפונקציה, ולאחר סיום הפונקציה - חוזרת לנקודת הקריאה עם ערך שחושב (אם הפונקציה מחזירה ערך)
- אין כאן מבנה חדש לחלוטין - כבר השתמשנו בפונקציות שקיימות בספריות השפה כגון,
Console.WriteLine()
אוMath.Sqrt()
, וגם ראינו ש-Main היא פונקציה מיוחדת שבה מתחילה התוכנית. יחד עם זאת, פונקציות שאנו כותבים בעצמנו הן כלי מרכזי לבניית תוכניות מורכבות: הן מאפשרות לנו לפרק בעיות לתתי-משימות, להימנע מחזרת קוד, ולכתוב תוכניות קריאות ונוחות יותר לתחזוקה.
בפרק זה נלמד כיצד להגדיר פונקציות משלנו ולקרוא להן.
פונקציות: הגדרה כללית והרחבה
פונקציות בתכנות
פונקציה (function, פעולה), נקראת לעיתים גם שגרה (procedure) או מתודה (method). זוהי יחידת קוד עצמאית בתוך תוכנית, המבצעת משימה מוגדרת.
- לפי פרדיגמת התכנות הפרוצדורלי, מומלץ לפרק תוכניות לפונקציות קטנות ככל האפשר, כך שכל פונקציה מבצעת פעולה פשוטה אחת או כמה פעולות קשורות.
- פונקציות נקראות גם “תת-תוכניות”, משום שכל פונקציה היא כמעין תוכנית קטנה בתוך התוכנית הגדולה.
ריצה של פונקציה:
כאשר קוראים לפונקציה, מתבצעות הפקודות שבתוך הפונקציה (הנקראות “גוף הפונקציה”), ולאחר מכן השליטה חוזרת לתוכנית הקוראת. ניתן לקרוא לאותה פונקציה מספר פעמים במקומות שונים בתוכנית, עם נתונים שונים בכל פעם, ובכך לחסוך כפילות קוד. התרשים הבא ממחיש את זרימת התוכנית בקריאה ל-3 פונקציות:
Hello World מדפיסה] SayHello --> |return| Main Main --> |"3.קריאה ל- ;()AddNumbers"| AddNumbers[הפונקציה AddNumbers
מחשבת 3+5
ומדפיסה את התוצאה] AddNumbers --> |return| Main Main --> |"4.קריאה ל- ;()SayGoodbye"| SayGoodbye[הפונקציה SayGoodbye
מדפיסה Goodbye] SayGoodbye --> |return| Main Main --> End([5.סיום]) style Main fill:#4fc3f7,stroke:#0277bd,stroke-width:4px,color:#fff style SayHello fill:#ffb74d,stroke:#f57c00,stroke-width:2px style AddNumbers fill:#ffb74d,stroke:#f57c00,stroke-width:2px style SayGoodbye fill:#ffb74d,stroke:#f57c00,stroke-width:2px style Start fill:#81c784,stroke:#388e3c,stroke-width:2px style End fill:#e57373,stroke:#d32f2f,stroke-width:2px linkStyle default stroke:#666666,stroke-width:3px
יתרונות חסרונות והנחיות
יתרונות השימוש בפונקציות:
- שימוש חוזר (Reuse) - פונקציה מאפשרת לכתוב קוד פעם אחת ולהריץ אותו מספר פעמים, עם קלטים שונים. בכך אנו נמנעים מחזרת קוד ומפחיתים טעויות.
- ארגון והבנה - פיצול תוכנית לפונקציות יוצר מבנה היררכי ברור. קל יותר להבין ולבדוק חלקי תוכנה קצרים המתמקדים במשימה ספציפית, מאשר להתמודד עם תוכנית ארוכה כמקשה אחת
- גמישות לשינויים - עדכון לוגיקה שקיימת בפונקציה אחת יוחל אוטומטית בכל המקומות שקוראים לפונקציה, ללא צורך לשנות קוד במקומות מרובים.
- בדיקות וניפוי שגיאות - פונקציות קצרות מאפשרות לבדוק כל חלק בנפרד (Unit Testing) ולאתר שגיאות בקלות רבה יותר.
חסרונות ואתגרים:
- מעבר נתונים - פונקציה פועלת בסביבה מבודדת (scope). משתנים המוגדרים בתוך פונקציה (משתנים מקומיים) אינם מוכרים מחוץ לפונקציה, ולהפך. לכן יש לתכנן carefully כיצד להעביר מידע פנימה (דרך פרמטרים) והחוצה (ערך החזרה) במידת הצורך.
- ביצועים - קריאה לפונקציה מוסיפה מעט תקורה (overhead) בזמן ריצה עקב מעבר לשליטה וחזרה. במקרים נדירים של קריאות פונקציה מאוד תכופות בתוך לולאות ענק, ייתכן שתהיה השפעה על הביצועים. עם זאת, ברוב המכריע של המקרים עדיף לכתוב קוד קריא ומודולרי באמצעות פונקציות, ולשקול אופטימיזציה רק בעת הצורך.
- הגזמה בפירוק - אף שפרוק לפונקציות קטנות הוא רצוי, פירוק-יתר של קוד לפונקציות רבות מאוד עלול להפוך את המעקב אחר זרימת התוכנית למסובך. חשוב למצוא איזון בבניית הפונקציות כך שכל פונקציה תהיה בגודל סביר ותהיה בעלת אחריות ברורה.
הנחיות לשימוש נכון בפונקציות:
- שם ותיעוד מתאימים - שם פונקציה צריך לתאר בפועל את פעולתה (בפועל באנגלית, לפי מוסכמות השפה: למשל בפייתון בשיטת snake_case, וב־C# ב-PascalCase). בחירת שמות ברורים וכתיבת הערות במידת הצורך מקלים על הבנת תפקיד הפונקציה בתוך התוכנית.
- פונקציה = משימה - כל פונקציה צריכה לבצע משימה ברורה אחת. אם נוצרת פונקציה ארוכה מאוד או כזו שמנסה לבצע כמה דברים שונים, שקול לפצל אותה למספר פונקציות.
- מניעת תלות גלובלית - עדיף להעביר מידע לפונקציות דרך פרמטרים ולהחזיר תוצאות דרך ערך חוזר, מאשר לסמוך על משתנים גלובליים. כך הפונקציה גנרית ושימושית יותר, ותוצאותיה צפויות (פונקציה ללא תלות חיצונית נקראת פונקציה טהורה במונחי תכנות).
סיכום:
פונקציות הן אבני בניין בסיסיות בתכנות מודרני המאפשרות כתיבת קוד DRY (Don’t Repeat Yourself) תוך חלוקת התוכנה לחלקים הגיוניים. באמצעות פונקציות נוכל לבנות תוכניות מורכבות באופן מדורג: נפתח ונבדוק כל פונקציה בנפרד, ואז נשלב אותן יחד לפתרון הבעיה הכללית. בפונקציות נשתמש שוב ושוב לאורך התכנות - הן כלי עוצמתי בהפחתת סיבוכיות התוכנה ושיפור הקריאות והתחזוקה שלה.
הגדרת פונקציה ב-#C:
בתחביר של C#, הגדרת פונקציה (מתודה) נעשית בתוך מחלקה (class). עד שנלמד תכנות מונחה-עצמים, נכתוב פונקציות סטטיות בתוך המחלקה הראשית של התוכנית, לצד הפונקציה Main. התחביר הבסיסי הוא:
[modifier(s)] [return_type(s)] FunctionName([parameter_list])
{
// גוף הפונקציה: סדרת פעולות שתתבצענה בקריאה לפונקציה
}
חשוב:
- לשורה הראשונה קוראים הגדרת הפונקציה.
- חתימת הפונקציה: כוללת רק את שם הפונקציה והחלק בו מוגדרים הפרמטרים בסדר מסויים. בכל מקרה של שתי פונקציות עם אותו שם - יהיה בהכרח הבדל בחתימה. (כיוון שהשם זהה, ההבדל יהיה בפרמטרים שהן מקבלות).
- את הגדרת הפונקציה יש לכתוב מחוץ לפונקציה Main (ובאותה מחלקה).
דיוק והרחבה
- הדרישה היא לא לקנן פונקציות זו בתוך זו אלא במצבים חריגים: ב־C# 7 יש אפשרות של פונקציות מקומיות, אך לא נעסוק בכך כעת. פונקציות מקומיות הן הן המאפשרות (בגרסאות החדשות) לכתוב תוכנית בלי שמופיע Program, Main וכל הדברים המסורבלים האלו. כפי שאמרתי בעבר, בכתיבה כזו, אתם כבר בתוך Main וכשאתם כותבים שם פונקציות, אתם נשענים על היכולת לקנן פונקציות.
- סדר ההגדרות אינו חשוב - ניתן להגדיר פונקציה לפני או אחרי Main - העיקר שההגדרה נמצאת בטווח המחלקה (בשונה מפייתון - שבה פונקציה חייבת להיות מוגדרת לפני כל מי שקורא לה (ולכן גם לפני ה- Main)).
- נקפיד גם להוסיף את המילה static (כמו בדוגמאות). כשנלמד עצמים נבין מה זה.
7.1 פונקציות ללא פרמטרים
נתחיל בפונקציות הפשוטות ביותר: פונקציות שאינן מקבלות מידע מהקורא להן (פונקציה ללא פרמטרים). פונקציה כזו תמיד תבצע בדיוק את אותה הפעולה בכל קריאה (אלא אם כן היא קוראת לקלט מהמשתמש או משתמשת במשתנים גלובליים - אפשרויות שקיימות אך אינן מומלצות כלל). נשתמש בפונקציות ללא פרמטרים כאשר המשימה שאנו רוצים לבצע היא כללית ואינה דורשת מידע חיצוני בכל הרצה. בקורס יסודות, מבני נתונים, ובבגרות, אין התלבטות במה להשתמש: ניתן להסיק באופן חד משמעי מניסוח השאלה.
דוגמא 1: הדפסת שורת כוכביות מספר פעמים — ללא פונקציה
נניח שנרצה להדפיס שלוש שורות של כוכביות (בכל שורה 10 כוכביות). נשווה בין שני מימושים: בלי פונקציה - תוך חזרת קוד, ועם פונקציה.
פתרון ללא שימוש בפונקציה
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void Main()
{
// שלוש שורות של 10 כוכביות - מימוש ללא פונקציה
for (int i = 0; i < 10; i++)
Console.Write("*");
Console.WriteLine();
for (int i = 0; i < 10; i++)
Console.Write("*");
Console.WriteLine();
for (int i = 0; i < 10; i++)
Console.Write("*");
Console.WriteLine();
}
נחזור לדוגמא 1: הדפסת שורת כוכביות מספר פעמים — עם פונקציה
נגדיר פונקציה המדפיסה שורה של כוכביות. הפונקציה לא מקבלת שום פרמטר (הסוגריים ריקים) ולא מחזירה ערך, ולכן סוג הערך המוחזר מוגדר כ-void. ניתן לקרוא לפונקציה זו מכל מקום בתוך המחלקה (למשל מתוך Main) על-ידי כתיבת שמה ואחריו סוגריים ריקים:
פתרון עם פונקציה
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void PrintStars10()
{
for (int i = 0; i < 10; i++)
Console.Write("*");
Console.WriteLine();
}
public static void Main()
{
// קריאה לפונקציה 3 פעמים
PrintStars10();
PrintStars10();
PrintStars10();
}
בפונקציה PrintStars10 השתמשנו בלולאה כדי להדפיס 10 כוכביות ברצף, ואחריה מעבר שורה. בכל קריאה לפונקציה זו נקבל את אותה תוצאה: שורת כוכביות באורך 10. ואכן בחרנו לקרוא לה שלוש פעמים מתוך Main כדי להדפיס 3 שורות זהות. שימו לב כיצד קריאה חוזרת לפונקציה מונעת חזרת קוד: לא היינו צריכים לכתוב שלוש לולאות נפרדות או להעתיק את גוף הפונקציה - מספיק לקרוא לה שוב. אם נרצה בעתיד לשנות את אורך השורה המודפסת, נצטרך לערוך את מספר החזרות במקום אחד בלבד (בתוך גוף הפונקציה).
בדוגמה שלעיל, שני המימושים מפיקים את אותו הפלט. במימוש הראשון ללא פונקציה, יש לנו חזרת קוד: בלוק הקוד שמדפיס כוכביות מופיע שלוש פעמים. במימוש השני איגדנו את בלוק הקוד לפונקציה בשם PrintStars10 וקראנו לה שלוש פעמים. המימוש עם הפונקציה נקי ומודולרי יותר: אם נרצה לשנות את אורך השורה או להוסיף פעולה לפני/אחרי ההדפסה, נעשה זאת בתוך הפונקציה ומשם זה ישתקף בכל קריאה. לעומת זאת, במימוש ללא פונקציה היינו צריכים לערוך את השינוי בשלושה מקומות. במקרה זה יכולנו אמנם להשתמש בלולאה חיצונית במקום לשכפל קוד, אך בדוגמאות מסובכות יותר (או כאשר הקוד החוזר אינו רציף) - פונקציות הן הפתרון המועדף למניעת חזרתיות.
דוגמה 2: פונקציה ללא קלט המבצעת חישוב
פונקציות ללא פרמטרים עשויות גם לבצע חישוב פנימי ולהציג תוצאה, בלי לקבל מידע מבחוץ. לדוגמה, נכתוב פונקציה המדפיסה את סכום המספרים הזוגיים מ-1 עד 100. הפונקציה תחשב את הסכום באמצעות לולאה, ותדפיס את התוצאה. ניתן לקרוא לפונקציה זו ישירות, ללא צורך בפרמטרים:
SumEven100 - הדפסת סכום הזוגיים עד 100
1
2
3
4
5
6
7
8
public static void SumEven100()
{
int sum = 0;
for (int num = 0; num <= 100; num += 2)
sum += num;
Console.WriteLine($"Sum of even numbers 1-100 is {sum}");
}
בדוגמה זו, הפונקציה SumEven100 לא זקוקה לקלט חיצוני - היא יודעת לסרוק את הטווח 1 עד 100 בעצמה ולחשב את הסכום. היעדר פרמטרים מפשט את השימוש בפונקציה (פשוט קוראים SumEven100()), אך מצד שני הפונקציה אינה גמישה לטווחים אחרים. מה אם נרצה לחשב סכום זוגיים עד 50 או עד 1000? נוכל כמובן לכתוב פונקציה נפרדת לכל טווח, אך זו לא דרך יעילה. כאן עולה הצורך ביכולת להגדיר פונקציה גנרית יותר - כזו שמקבלת פרמטרים לשינוי התנהגותה. נעבור כעת לנושא הפרמטרים.
7.2 העברת פרמטרים לפונקציה
כדי להפוך פונקציה לגמישה וכללית יותר, נגדיר פרמטרים (parameters) - משתנים המופיעים בסוגריים בהגדרת הפונקציה. בעת הקריאה לפונקציה, יש להעביר ארגומנטים (arguments) שהם הערכים המסוימים עבור אותם פרמטרים.
- הפרמטרים מתנהגים כמשתנים מקומיים בתוך הפונקציה, ומאפשרים לקוד הפונקציה לעבוד על נתונים שסופקו מבחוץ.
- תחביר פרמטרים: ברשימת הפרמטרים אנו מציינים עבור כל פרמטר טיפוס ושם משתנה. אם יש יותר מפרמטר אחד, מפרידים ביניהם בפסיק. לדוגמה, פונקציה שמקבלת שני מספרים שלמים יכולה להיות מוגדרת כך:
public static void PrintSum(int a, int b) { Console.WriteLine($"{a} + {b} = {a + b}"); }
כעת, בקריאה לפונקציה יש לספק שני ארגומנטים מתאימים, למשל PrintSum(5, 7) ידפיס את השורה 5 + 7 = 12. שימו לב שסדר הארגומנטים חייב להתאים לסדר הפרמטרים כפי שהוגדרו. טיפוס כל ארגומנט נבדק בזמן הקומפילציה - אם ננסה להעביר ערך מטיפוס לא תואם, נקבל שגיאת קומפילציה.
- פרמטר לעומת ארגומנט: פרמטר הוא חלק מהגדרת הפונקציה (מעין משתנה “תבנית” שהפונקציה מצפה לקבל), ואילו ארגומנט הוא הערך המסוים שאנו מעבירים לקריאה. אפשר לומר שפונקציות מגדירות פרמטרים פורמליים, וכשאנו קוראים להן בפועל אנו מוסרים ערכי ארגומנט. לדוגמה, בפונקציה PrintSum(int a, int b) - a ו-b הם פרמטרים; בקריאה PrintSum(5, 7) - 5 ו-7 הם הארגומנטים.
דוגמה 3: פונקציה עם פרמטר יחיד (אורך)
תרגול: שכתבו את הפונקציה הקודמת שיצרה שורת כוכביות באורך קבוע, כך שתוכל להדפיס שורה באורך גמיש בהתאם לקלט. במקום פונקציה נפרדת לכל אורך, נגדיר פונקציה אחת עם פרמטר שלם הקובע את מספר הכוכביות:
PrintStars - פתרון: הדפסת שורת כוכביות באורך נתון
1
2
3
4
5
6
7
8
9
10
11
12
public static void PrintStars(int length)
{
for (int i = 0; i < length; i++)
Console.Write("*");
Console.WriteLine();
}
public static void Main()
{
PrintStars(5); // *****
PrintStars(10); // **********
PrintStars(3); // ***
}
הפונקציה PrintStars מקבלת פרמטר יחיד length. בכל קריאה, הערך שנמסר (ארגומנט) יוכנס למשתנה length ויקבע את מספר הפעמים שהלולאה תרוץ. בתוכנית הדוגמה קראנו לפונקציה עם הערכים 5, 10 ו-3 - ובהתאם הודפסו שורות באורכים מתאימים. כעת הפונקציה גמישה בהרבה: היא יודעת להדפיס שורת כוכביות בכל אורך שנבקש, ללא חזרת קוד. אפשר, כמובן, לשלב כמה פרמטרים. לדוגמה, נכתוב פונקציה שמדפיסה מלבן של כוכביות, עם שני פרמטרים: rows ו-cols הקובעים את ממדי המלבן:
תרגול: כתבו פונקציה PrintRectangle המקבלת שני פרמטרים שלמים - rows (שורות) ו-cols (עמודות), ומדפיסה מלבן כוכביות בגודל המבוקש. לדוגמה, עבור קריאה PrintRectangle(3, 5) הפלט יהיה:
***
***
*****
נסו לחשוב כיצד לכתוב זאת (Tip: השתמשו בלולאה מקוננת), לפני שאתם חושפים את הפתרון.
פתרון. נסו לכתוב את הפונקציה בעצמכם לפני הצפייה
1
2
3
4
5
6
7
8
9
public static void PrintRectangle(int rows, int cols)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
Console.Write("*");
Console.WriteLine();
}
}
כמובן, פרמטרים יכולים להיות מכל טיפוס - לא רק מספרים. לדוגמה, נוכל לכתוב פונקציה המקבלת מחרוזת ומדפיסה ברכה אישית:
SayHello - פונקציה עם פרמטר מטיפוס מחרוזת
public static void SayHello(string userName)
{
Console.WriteLine($"Hello, {userName}!");
}
בקריאה SayHello("Dan")
תודפס ההודעה Hello, Dan!
. בצורה דומה אפשר לקבל בפרמטרים קלטים מטיפוס double (למספרים ממשיים), char (לתו בודד) וכדומה, או לשלב מספר פרמטרים מסוגים שונים. לדוגמה, פונקציה המקבלת שם וכמות: PrintNameMultiple(string name, int times) שתדפיס את השם מספר פעמים לפי הערך (הארגומנט) שיועבר לפרמטר השני.
- הערה על העברת ערכים: בשפות כמו C#, ברירת המחדל היא העברה לפי ערך – כלומר, לפונקציה מועבר עותק של הארגומנט. שינוי בפרמטר בתוך הפונקציה לא משפיע על המשתנה המקורי ששלחנו. למשל, אם נקרא
PrintStars(n)
כאשרn
הוא 5, ונשנה בתוך הפונקציה את הערך ל־10, זה לא ישפיע עלn
שמחוץ לפונקציה.
הרחבה – העברה לפי הפניה (by ref)
לעיתים נרצה לאפשר לפונקציה להשפיע על המשתנה שמחוץ לה. עבור טיפוסים שהם אובייקטים (כמו מערך או רשימה), מועבר לפונקציה מצביע לכתובת בזיכרון – שינוי בתוכן המערך יתעדכן גם מחוץ לפונקציה. לעומת זאת, אם נגרום למשתנה המקומי להצביע לאובייקט חדש, ההשפעה לא תצא החוצה (המצביע המקורי לא משתנה). בשימוש ב־`by ref` (או ref ב־C#), הפונקציה יכולה *לשנות את כתובת ההפניה עצמה* – כלומר, גם מחוץ לפונקציה המשתנה יצביע לאובייקט החדש, או שהערך עצמו ישתנה. מדובר בכלי עוצמתי, אך לעיתים מסוכן, ולכן נהוג להשתמש בו רק במקרים חריגים ומוצדקים.-
הערה על העברת ערכים: בשפות כמו #C, ברירת המחדל היא העברה לפי ערך - כלומר, העתק של הארגומנט מועבר לפונקציה. שינוי בערך הפרמטר בתוך הפונקציה לא ישנה את המשתנה המקורי שנשלח בארגומנט. למשל, אם נקרא PrintStars(n) עם משתנה n שערכו 5, ואז בתוך הפונקציה נשנה את length ל-10, הדבר לא ישפיע על המשתנה n מחוץ לפונקציה.
-
יתכן שנלמד על העברה לפי הפניה (by ref לא בתכנית) - המאפשרת לפונקציה להשפיע חיצונית. הרחבה ממש בקצרה: גם בלי שימוש ב-by ref ההעברה היא תמיד של מצביע לכתובת מסויימת (בין אם זה הכתובת של המספר 5 ובין אם זה הכתובת של מערך או תור או רשימה). אם קיבלנו הפנייה למערך, ואנחנו משנים את תוכן המערך - תהיה השפעה חיצונית. אבל אם קיבלנו מערך והחלטנו להפנות למערך אחרת בתוך התוכנית, מי שנתן לנו את ההפניה לא יתעדכן ומחוץ לפונקציה ההפניה היא למערך המקורי (בדיוק כמו שההפניה בחוץ נותרת למספר 5). השימוש ב-by ref מאפשר להשפיע שלב נוסף כך שנוכל לשנות את המצביע באופן שישנה את המצביע מחוץ לפונקציה. כלומר גם מחוץ לפונצקיה ערך המשתנה יהפוך ל-10 או במקרה שהחלפנו מערך, המצביע שמחוץ לפונקציה יפנה למערך החדש. בסה”כ זהו כלי מסוכן שמשתמשים בו רק במצבים מיוחדים וחריגים.
- פונקציה יכולה לקבל אפס, אחד או מספר רב של פרמטרים. אם הפונקציה לא זקוקה לקלט חיצוני - פשוט נגדיר סוגריים ריקים (כמו בקטע 7.1). אם היא דורשת כמה ערכים, נגדיר את כולם ברשימת הפרמטרים, מופרדים בפסיקים.
- קיימת גם אפשרות להגדרת ערכי ברירת מחדל לפרמטרים (Default Parameters) כדי להפוך חלק מהם לאופציונליים - נושא זה נדון בנפרד.
סיכום ביניים
בחלקים 7.1-7.2 למדנו כיצד להגדיר פונקציות ללא ערך חזרה: פונקציות המבצעות פעולה (כגון חישוב או הדפסה) ואינן מחזירות נתון חזרה למקום הקריאה. ראינו דוגמאות לפונקציות ללא פרמטרים ועם פרמטרים, והדגשנו את היתרון בגמישות שמקנה העברת פרמטרים. בשלב זה כל הפונקציות שהגדרנו היו עם סוג החזרה void. בחלק הבא נרחיב את היכולת של פונקציות ונדון בפונקציות מחזירות ערך: כיצד פונקציה יכולה לחשב ולהחזיר תוצאה למי שקרא לה. זה יאפשר לנו לכתוב פונקציות כמו Max(a,b) שמחזירה את הגדול מבין שני מספרים, IsPrime(n) שמחזירה אמת/שקר אם המספר ראשוני, ועוד.
איפה הפרק הבא??? וצריך לסיים את השאלות ולפצל החוצה.
תרגילים לפרק 7 — חלק 1
תרגול 7.1 - פונקציות ללא פרמטרים תרגול בכתיבת פונקציות שאינן מקבלות קלט (ולא מחזירות ערך)
7.1.1 - הדפסת המספרים 1 עד 10
כתבו פונקציה בשם PrintOneToTen() המדפיסה את כל המספרים מ־1 עד 10 ברצף (באותה שורה או בשורות נפרדות, לפי בחירתכם).
אין פתרון
7.1.2 - הדפסת מספרים זוגיים
כתבו פונקציה PrintEvens20() המדפיסה את כל המספרים הזוגיים בין 2 ל-20 (כולל).
אין פתרון
7.1.3 - הדפסת האלפבית האנגלי
כתבו פונקציה PrintAlphabet() שמדפיסה את כל האותיות באנגלית מראשית האלפבית (A) ועד סופו (Z). טיפ: ניתן לבצע איטרציה על אותיות בדומה לאיטרציה מספרית, באמצעות טיפוס הנתונים char. (זכרו שהתרשים הפנימי של תווי יוניקוד מגדיר סדר - למשל ‘A’+1 הוא ‘B’).
פתרון
1
2
3
4
5
6
7
8
public static void PrintAlphabet()
{
for (char ch = 'A'; ch <= 'Z'; ch++)
{
Console.Write(ch + " ");
}
Console.WriteLine();
}
7.1.4 - מלבן כוכביות 5×5
כתבו פונקציה StarSquare5() המדפיסה ריבוע של כוכביות בגודל 5 על 5. (כלומר, 5 שורות ובכל שורה 5 כוכביות).
אין פתרון
7.1.5 - סכום המספרים 1-50
כתבו פונקציה SumOneToFifty() שמחשבת ומדפיסה את סכום כל המספרים מ־1 עד 50. מומלץ לוודא שהתוצאה המודפסת נכונה (ניתן לחשב את הסכום גם באמצעות נוסחה ידועה).
אין פתרון
תרגילים לפרק 7 — חלק 2
תרגול 7.2 - פונקציות עם פרמטרים
תרגול בכתיבת פונקציות המקבלות ערכי קלט כפרמטרים (ללא ערכי חזרה)
7.2.1 - זוגי או אי-זוגי?
א. כתבו פונקציה PrintEvenOrOdd(int n) המקבלת מספר שלם n ומדפיסה הודעה המציינת האם n הוא זוגי או אי-זוגי. למשל, עבור הקריאה PrintEvenOrOdd(13) תודפס ההודעה: 13 is odd.
ב. כתבו פונקציה Main הקולטת מהמשתמש מספר שלם אחד, וקוראת לפונקציה שכתבתם כדי להציג את התוצאה.
אין פתרון
7.2.2 - המקסימום מבין שלושה
א. כתבו פונקציה PrintMax3(int a, int b, int c)
המדפיסה את הערך המקסימלי מבין שלושת המספרים a, b, c. לדוגמה, הקריאה PrintMax3(7, -2, 7)
תדפיס: Max is 7.
ב. הוסיפו בתוכנית פונקציית Main שקולטת שלושה מספרים שלמים מהמשתמש, וקוראת ל-PrintMax3 עם שלושת הערכים שהוקלדו.
אין פתרון
7.2.3 - הדפסת תו N פעמים
א. צרו פונקציה PrintLine(char ch, int count)
המקבלת תו (Character) ומספר שלם, ומדפיסה בשורה אחת את התו שהתקבל count פעמים. למשל, PrintLine('#', 5)
תפיק את השורה: #####.
ב. כתבו פונקציה Main שקוראת לפונקציה זו מספר פעמים, עם פרמטרים שונים לפי בחירתכם, להדגמת הפעולה (ניתן לבקש קלט מהמשתמש עבור התו ומספר הפעמים).
אין פתרון
7.2.4 - משולש כוכביות גמיש
כתבו פונקציה PrintTriangle(int n)
המדפיסה משולש כוכביות בן n שורות. בשורה הראשונה יודפס כוכבית אחת, בשנייה 2, וכן הלאה עד לשורה ה-n שתכיל n כוכביות. לדוגמה, PrintTriangle(4)
יפיק:
*
**
***
****
אין פתרון
7.2.5 - כל המחלקים של מספר (מבוסס 5.2.3)
א. כתבו פונקציה PrintDivisors(int num) שמקבלת מספר שלם חיובי num ומדפיסה את כל המחלקים (divisors) החיוביים של num. למשל, עבור הקלט 28 הפלט יהיה: 1, 2, 4, 7, 14, 28 (ניתן להדפיס ברצף מופרד בפסיקים או בשורות נפרדות).
ב. (אתגר) נסו לייעל את הפונקציה כך שלא תבצע איטרציות מיותרות מעבר לנדרש.
פתרון
1
2
3
4
5
6
7
8
9
public static void PrintDivisors(int num)
{
for (int candidate = 1; candidate <= num; candidate++)
{
if (num % candidate == 0)
Console.Write(candidate + " ");
}
Console.WriteLine();
}
7.2.6 - בדיקת מספר ראשוני
א. כתבו פונקציה public static void PrintIsPrime(int num)
המקבלת מספר שלם חיובי ובודקת האם הוא ראשוני. הפונקציה תדפיס הודעה מתאימה, למשל: 17 is prime או 18 is not prime.
ב. הוסיפו בתוכנית פונקציית Main הקולטת מספר מהמשתמש, וקוראת לפונקציה שכתבתם כדי להציג את ההודעה המתאימה.
אין פתרון
7.2.7 - סדרת פיבונאצ’י
א. כתבו פונקציה PrintFibonacci(int n) שמקבלת מספר שלם חיובי n ומדפיסה את n האיברים הראשונים בסדרת פיבונאצ’י. סדרת פיבונאצ’י מתחילה בערכים 0, 1, וכל איבר לאחר מכן הוא סכום שני האיברים הקודמים לו. לדוגמה, עבור הקריאה PrintFibonacci(8) הפלט יהיה: 0 1 1 2 3 5 8 13.
ב. צרו פונקציה Main הקולטת מהמשתמש מספר חיובי אחד, וקוראת ל-PrintFibonacci עם הערך שהוזן.
פתרון
1
2
3
4
5
6
7
8
9
10
11
12
public static void PrintFibonacci(int n)
{
int a = 0, b = 1;
for (int i = 1; i <= n; i++)
{
Console.Write($" {a}");
int next = a + b;
a = b;
b = next;
}
Console.WriteLine();
}
7.2.8 - הדפסת שם מספר פעמים
כתבו פונקציה PrintNameMultiple(string name, int times) המקבלת שם (מחרוזת) ומספר שלם times, ומדפיסה את השם שהתקבל times פעמים ברצף אחד אחרי השני. למשל, הקריאה PrintNameMultiple(“Noa”, 3) תדפיס: Noa Noa Noa (באותה שורה או בשורות שונות - לפי החלטתכם).
אין פתרון
7.2.9 - ממוצע ציונים
פונקציה PrintAverage(int grade1, int grade2, int grade3) מקבלת שלושה ציונים (בין 0 ל-100) ומדפיסה את הממוצע שלהם. לדוגמה, PrintAverage(90, 85, 100) תדפיס: Average = 91.67 (אפשר לעגל את התוצאה לשני מקומות עשרוניים). כתבו את הפונקציה הנ”ל, והוסיפו תוכנית ראשית הקולטת שלושה ציונים מהמשתמש ומשתמשת בפונקציה כדי להציג את הממוצע.