בפרק זה נלמד לכתוב פונקציות משלנו ולקרוא להן. פונקציות מחלקות את התוכנית לתתי-משימות, מאפשרות שימוש חוזר בקוד, ומשפרות את קריאות הקוד.
הגדרה: פונקציה היא מקבץ של פקודות המאוגדות תחת שם מזוהה, שניתן לקרוא לו (להפעיל אותו) מתוך חלקים שונים בתוכנית, כדי לבצע משימה מוגדרת. פונקציה יכולה לקבל נתונים (פרמטרים) ויכולה גם להחזיר תוצאה לפונקציה שקראה לה. כבר השתמשנו בפונקציות שקיימות בספריות השפה כגון, ()Console.WriteLine
או ()Math.Round
, וגם ראינו ש-Main
היא פונקציה מיוחדת, שממנה מתחיל הכל.
מינוחים וסדר הוראה
ניתן להשתמש בשני מינוחים: פעולה או פונקציה. בספרות עברית רווח המונח פעולה. באנגלית רווח המונח פונקציה (function). תיתקלו גם בשמות אחרים כגון שגרה (procedure), מתודה (method), או שיטה.
הערה: חלק מהמורים מלמדים פונקציות מוקדם יותר - כבר לאחר לולאת for
ולפני לולאת while
או אפילו לפני for
. עם זאת, בסילבוס זה הוחלט ללמד פונקציות לאחר לולאות for
, while
ולולאות מקוננות, כחלק משלב חשיבה אלגוריתמי מתקדם. אני מעדיף ללמד פונקציות מוקדם יותר, אך מתיישר עם הסילבוס ומציג פרק זה בהמשך לפרק 6.
המוטיבציה לכתיבת פונקציות:
- פונקציות שאנו כותבים בעצמנו, הן כלי מרכזי לבניית תוכניות מורכבות: הן מאפשרות לנו לפרק בעיות לתתי-משימות, להימנע מחזרת קוד, ולכתוב תוכניות קריאות ונוחות יותר לתחזוקה.
- פירוק בעייה גדולה שאיננו יודעים לפתור, לבעיות קטנות שאנחנו יודעים לפתור הוא טכניקה חשובה בפתרון בעיות. כאן מתגלות הפונקציות במלוא הדרן.
- פונקציות מאפשרות לנו להישען על פתרון קודם, כדי לפתור בעיות חדשות. כדי להפנים עד כמה זה חשוב, מומלץ לצפות בפרופרסור הרשקוביץ כשהוא מתבדח על ההבדל בין מתמטיקאי לפיזיקאי.
פונקציות: הגדרה כללית והרחבה
פונקציות בתכנות
פונקציה (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). משתנים המוגדרים בתוך פונקציה (משתנים מקומיים) אינם מוכרים מחוץ לפונקציה, ולהפך. לכן יש לתכנן כיצד להעביר מידע פנימה (דרך פרמטרים) והחוצה (ערך החזרה) במידת הצורך.
- ביצועים - קריאה לפונקציה מוסיפה מעט תקורה (overhead) בזמן ריצה עקב מעבר לשליטה וחזרה. במקרים נדירים של קריאות פונקציה מאוד תכופות בתוך לולאות ענק, ייתכן שתהיה השפעה על הביצועים. עם זאת, ברוב המכריע של המקרים עדיף לכתוב קוד קריא ומודולרי באמצעות פונקציות, ולשקול אופטימיזציה רק בעת הצורך.
- הגזמה בפירוק - אף שפרוק לפונקציות קטנות הוא רצוי, פירוק-יתר של קוד לפונקציות רבות מאוד עלול להפוך את המעקב אחר זרימת התוכנית למסובך. חשוב למצוא איזון בבניית הפונקציות כך שכל פונקציה תהיה בגודל סביר ותהיה בעלת אחריות ברורה.
הנחיות לשימוש נכון בפונקציות:
- שם ותיעוד מתאימים - שם פונקציה צריך לתאר בפועל את פעולתה (בפועל באנגלית. כתיבת השם: לפי מוסכמות השפה. למשל בפייתון בשיטת snake_case, וב־C# ב-PascalCase כלומר, אות ראשונה גדולה!!! בשונה מ-Java). בחירת שמות ברורים וכתיבת הערות במידת הצורך מקלים על הבנת תפקיד הפונקציה בתוך התוכנית.
- פונקציה = משימה - כל פונקציה צריכה לבצע משימה ברורה אחת. אם נוצרת פונקציה ארוכה מאוד או כזו שמנסה לבצע כמה דברים שונים, שקול לפצל אותה למספר פונקציות.
- מניעת תלות גלובלית - עדיף להעביר מידע לפונקציות דרך פרמטרים ולהחזיר תוצאות דרך ערך חוזר, מאשר לסמוך על משתנים גלובליים. כך הפונקציה גנרית ושימושית יותר, ותוצאותיה צפויות (פונקציה ללא תלות חיצונית נקראת פונקציה טהורה במונחי תכנות).
סיכום ביניים 1:
פונקציות הן אבני בניין בסיסיות בתכנות מודרני המאפשרות כתיבת קוד DRY (Don’t Repeat Yourself) תוך חלוקת התוכנה לחלקים הגיוניים. באמצעות פונקציות נוכל לבנות תוכניות מורכבות באופן מדורג: נפתח ונבדוק כל פונקציה בנפרד, ואז נשלב אותן יחד לפתרון הבעיה הכללית. בפונקציות נשתמש שוב ושוב לאורך התכנות - הן כלי עוצמתי בהפחתת סיבוכיות הקוד ושיפור הקריאות והתחזוקה שלו.
כותרת פונקציה:
בתחביר של C# ו-Java, כתיבת פונקציה נעשית בתוך מחלקה (class
). עד שנלמד תכנות מונחה-עצמים, נכתוב פונקציות מסוג סטטי (static
) בתוך המחלקה הראשית של התוכנית, לצד הפונקציה Main
. התחביר הוא:
[modifier(s)] [return_type(s)] FunctionName([parameter_list])
{
// גוף הפונקציה: סדרת פעולות שתתבצענה בקריאה לפונקציה
}
חשוב:
- חתימת הפונקציה: כוללת רק את שם הפונקציה והחלק בו מוגדרים הפרמטרים בסדר מסויים. בכל מקרה של שתי פונקציות עם אותו שם - יהיה בהכרח הבדל בחתימה. (כיוון שהשם זהה, ההבדל יהיה בפרמטרים שהן מקבלות).
- לעומת זאת, לשורה הראשונה (כולה) קוראים הגדרת הפונקציה.
- את הפונקציה יש לכתוב מחוץ לפונקציה Main (אך בתוך המחלקה).
דיוק והרחבה
- הדרישה היא לא לקנן פונקציות זו בתוך זו אלא במצבים חריגים: החל מ־C# 7 יש אפשרות לפונקציות מקומיות, אך לא נעסוק בכך. פונקציות מקומיות הן הן המאפשרות (בגרסאות החדשות) לכתוב תוכנית בלי שמופיע
Program, Main
וכל הדברים המסורבלים האלו. כפי שאמרתי בעבר, בכתיבה כזו, אתם כבר בתוךMain
וכשאתם כותבים שם פונקציות, אתם נשענים על היכולת לקנן פונקציות. - סדר ההגדרות אינו חשוב - ניתן להגדיר פונקציה לפני או אחרי
Main
- העיקר שההגדרה נמצאת בטווח המחלקה (בשונה מפייתון - שבה פונקציה חייבת להיות מוגדרת לפני כל מי שקורא לה (ולכן גם לפני ה-Main
)).
- הדרישה היא לא לקנן פונקציות זו בתוך זו אלא במצבים חריגים: החל מ־C# 7 יש אפשרות לפונקציות מקומיות, אך לא נעסוק בכך. פונקציות מקומיות הן הן המאפשרות (בגרסאות החדשות) לכתוב תוכנית בלי שמופיע
- נקפיד להוסיף את המילה
static
(כמו בדוגמאות). כשנלמד עצמים נבין מה זה (וזה אחד המושגים שהכי קשה ללמוד). - הפונקציה מחזירה בדרך כלל ערך אחד. בשלב מתקדם נראה כיצד להחזיר
tuple
: מקבץ של מספר ערכים.
7.1 פונקציות ללא פרמטרים
נתחיל בפונקציות הפשוטות ביותר: פונקציות שאינן מקבלות מידע מהקורא להן. פונקציה כזו תמיד תבצע בדיוק את אותה הפעולה בכל קריאה (אלא אם כן היא קוראת לקלט מהמשתמש או משתמשת במשתנים גלובליים - אפשרויות שקיימות, אך אינן מומלצות). נשתמש בפונקציות ללא פרמטרים כאשר המשימה שאנו רוצים לבצע היא כללית ואינה דורשת מידע חיצוני בכל הרצה.
דוגמא 1: הדפסת שורת כוכביות מספר פעמים — ללא פונקציה
נניח שנרצה להדפיס 3 שורות של כוכביות (בכל שורה 10 כוכביות). נשווה בין שני מימושים: בלי פונקציה ועם פונקציה.
פתרון ללא שימוש בפונקציה
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
15
public static void Print10Stars()
{
for (int i = 0; i < 10; i++)
Console.Write("*");
Console.WriteLine();
}
public static void Main()
{
// קריאה לפונקציה 3 פעמים
Print10Stars();
Print10Stars();
Print10Stars();
}
בפונקציה Print10Stars
השתמשנו בלולאה כדי להדפיס 10 כוכביות ברצף, ואחריה מעבר שורה. בכל קריאה לפונקציה זו נקבל את אותה תוצאה: שורת כוכביות באורך 10. ואכן בחרנו לקרוא לה שלוש פעמים מתוך Main כדי להדפיס 3 שורות זהות. שימו לב כיצד קריאה חוזרת לפונקציה מונעת חזרת קוד: לא היינו צריכים לכתוב שלוש לולאות נפרדות או להעתיק את גוף הפונקציה - מספיק לקרוא לה שוב. אם נרצה בעתיד לשנות את אורך השורה המודפסת, נצטרך לערוך את מספר החזרות במקום אחד בלבד (בתוך גוף הפונקציה).
ה-Main מתחת לפונקציה - כדי שתהיו רגילים ליום שתכתבו פייתון אני פחות מקפיד על הסדר, ועובד לפי מה שנח. ניתן לקפוץ להגדרת כל פונקציה וכל משתנה באמצעות המקש F12. בנוסף מעל להגדרת כל פונקציה מופיעות references (הפניות) ובהן קישור הלוקח אותנו חזרה לפונקציה שקראה לנו. כך ניתן לנווט בקלות בתוך אלפי שורות קוד.
בדוגמה שלעיל, שני המימושים מפיקים את אותו הפלט. במימוש הראשון ללא פונקציה, יש לנו חזרת קוד: בלוק הקוד שמדפיס כוכביות מופיע שלוש פעמים. במימוש השני איגדנו את בלוק הקוד לפונקציה בשם Print10Stars
וקראנו לה שלוש פעמים. המימוש עם הפונקציה נקי ומודולרי יותר: אם נרצה לשנות את אורך השורה או להוסיף פעולה לפני/אחרי ההדפסה, נעשה זאת בתוך הפונקציה ומשם זה ישתקף בכל קריאה. לעומת זאת, במימוש ללא פונקציה היינו צריכים לערוך את השינוי בשלושה מקומות. במקרה זה יכולנו אמנם להשתמש בלולאה חיצונית במקום לשכפל קוד, אך בדוגמאות מסובכות יותר (או כאשר הקוד החוזר אינו רציף) - פונקציות הן הפתרון המועדף למניעת חזרתיות.
דוגמה 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\). שימו לב שסדר הארגומנטים חייב להתאים לסדר הפרמטרים כפי שהוגדרו. טיפוס כל ארגומנט נבדק בזמן הקומפילציה - אם ננסה להעביר ערך מטיפוס לא תואם, נקבל שגיאת קומפילציה. (החריג הוא מקרה שבו קיים implicit conversion כמו למשל אם נרצה לשלוחint
כשמצפים ל-double
, אבל לא להיפך). - פרמטר לעומת ארגומנט: פרמטר הוא חלק מהגדרת הפונקציה (מעין משתנה “תבנית” שהפונקציה מצפה לקבל), ואילו ארגומנט הוא הערך המסוים שאנו מעבירים לקריאה. אפשר לומר שפונקציות מגדירות פרמטרים פורמליים, וכשאנו קוראים להן בפועל אנו מוסרים ערכי ארגומנט. לדוגמה, בפונקציה
PrintSum(int a, int b)
המשתנים a ו-b הם פרמטרים. בקריאהPrintSum(5, 7)
הערכים 5 ו-7 הם הארגומנטים שמועברים לפרמטרים. בכיתה, אני קורא לכולם פרמטרים. - אני ממליץ להקפיד, לפחות בחלק מהמקרים, על שימוש בשמות שונים ב-
Main
לארגומנט ובפונקציה לפרמטר, כדי להמחיש את הנתק בינהם ברגע העברת הארגומנט. ראו בהמשך הרחבה בנושא העברת ערכים.
דוגמה 3: פונקציה המקבלת פרמטר יחיד (אורך)
תרגול: שכתבו את הפונקציה הקודמת שיצרה שורת כוכביות באורך קבוע, כך שתוכל להדפיס שורה באורך גמיש בהתאם לקלט. במקום פונקציה נפרדת לכל אורך, נגדיר פונקציה אחת המקבלת פרמטר שלם הקובע את מספר הכוכביות:
פתרון: PrintStars הדפסת שורת כוכביות באורך נתון:
1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}
}
פתרון שמשתמש בפונקציה שכבר כתבנו - עדיף:
1
2
3
4
5
private static void PrintRectangle(int v1, int v2)
{
for (int i = 0; i < v1; i++)
PrintStars(v2); //prints a row
}
כמובן, פרמטרים יכולים להיות מכל טיפוס - לא רק מספרים. לדוגמה, נוכל לכתוב פונקציה המקבלת מחרוזת ומדפיסה ברכה אישית:
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, הפרמטרlength
מקבל ערך 5. כאשר נשנה בתוך הפונקציה את הערך שלlength
ל־10, זה לא ישפיע על המשתנהn
שמחוץ לפונקציה: ערכו נשאר 5. - פונקציה יכולה לקבל אפס, אחד או מספר רב של פרמטרים. אם הפונקציה לא זקוקה לקלט חיצוני - פשוט נגדיר סוגריים ריקים (כמו בקטע 7.1). אם היא דורשת כמה ערכים, נגדיר את כולם ברשימת הפרמטרים, מופרדים בפסיקים.
- קיימת גם אפשרות להגדרת ערכי ברירת מחדל לפרמטרים (Default Parameters values) כדי להפוך חלק מהם לאופציונליים - נושא זה נדון בנפרד בסוף הפרק.
הרחבה – העברה לפי הפניה: שימוש במילה ref
לעיתים נרצה לאפשר לפונקציה להשפיע על המשתנה שמחוץ לה. עבור טיפוסים שהם אובייקטים (כמו מערך או רשימה), מועבר לפונקציה מצביע לכתובת בזיכרון – שינוי בתוכן המערך יתעדכן גם מחוץ לפונקציה. לעומת זאת, אם נגרום למשתנה המקומי להצביע לאובייקט חדש, ההשפעה לא תצא החוצה (המצביע המקורי לא משתנה).
בשימוש ב־ref
, הפונקציה יכולה לשנות את כתובת ההפניה עצמה – כלומר, גם מחוץ לפונקציה המשתנה יצביע לאובייקט החדש, או שהערך עצמו ישתנה.
מדובר בכלי עוצמתי, אך לעיתים מסוכן, ולכן נהוג להשתמש בו רק במקרים חריגים ומוצדקים. שני השותפים למהלך - הן השולח, והן המקבל משתמשים במילה ref
. כך לפחות יש שקיפות מלאה בנוגע למה שעלול לקרות. הנושא אינו חלק מתכנית הלימוד. לעומת זאת ההבנה שמה שמועבר לפרמטר הוא מצביע לעצם היא תובנה חשובה ככדאי להפנים כמה שיותר מוקדם - כבר כאן, במספרים, ומיד אחר כך במערכים.
דרכים אפשריות לזימון פונקציה
דרך א’: בהוראת השמה:
absolute = Math.Abs(num);
דרך ב’: בהוראת פלט:
Console.WriteLine(Math.Abs(num));
דרך ג’: בתוך ביטוי בוליאני:
if (Math.Sqrt(num) == 10.0)
דרך ד’: בתוך ביטוי חשבוני:
avg = (Math.Abs(num1) + Math.Abs(num2)) / 2.0;
דרך ה’: כפעולה(ות) על עצם: כאן בדוגמא 2 פעולות שתוצאתן מועברת לפעולה שלישית:
- מעבר לאותיות גדולות,
- ולאחריו חיתוך המחרוזת (ניקח מהתו השני, תת מחרוזת באורך 3 תווים)
- התוצאה היא ארגומנט המועבר ל-
WriteLine
string name = "Elisabeth";
// LIS הדפסת
Console.WriteLine(name.ToUpper().Substring(1,3));
בפתרון שאלות, כולל בבגרות, אין התלבטות: באיזו תצורה של פונקציה להשתמש: מניסוח השאלה ניתן ונדרש להסיק באופן חד משמעי מהי הגדרת הפונקציה. לעיתים מקבלים את ההגדרה המלאה בצורה מפורשת. אך בכל אופן, זו מיומנות שחייבים לרכוש.
סיכום ביניים 2:
בחלקים 7.1-7.2 למדנו כיצד להגדיר פונקציות ללא ערך חזרה: פונקציות המבצעות פעולה (כגון חישוב או הדפסה) ואינן מחזירות נתון חזרה למקום הקריאה. ראינו דוגמאות לפונקציות שאינן מקבלות פרמטרים וכאלו המקבלות פרמטרים, והדגשנו את היתרון בגמישות שמקנה העברת פרמטרים. בשלב זה כל הפונקציות שהגדרנו היו עם סוג החזרה void
. בחלק הבא נרחיב את היכולת של פונקציות ונדון בפונקציות מחזירות ערך: כיצד פונקציה יכולה לחשב ולהחזיר תוצאה למי שקרא לה. זה יאפשר לנו לכתוב פונקציות כמו Max(a,b)
שמחזירה את הגדול מבין שני מספרים, IsPrime(n)
שמחזירה אמת/שקר אם המספר ראשוני, ועוד.
7.3 העברה וקבלת ערכים מהפונקציה
נחזור לתחביר הבסיסי:
[modifier(s)] [return_type(s)] FunctionName([parameter_list])
{
// גוף הפונקציה: סדרת פעולות שתתבצענה בקריאה לפונקציה
}
אנימציה: העברת פרמטרים לפונקציה וקבלת ערך מוחזר
להצגה בטלפון יש לסובב לרוחב.
{
int n = Function1(42,"alice");
}
שאלה: לאיזה כיוון הפונקציה מחזירה את הערך (התוצאה)?
מהנסיון שצברנו עד כה, די ברור שהפונקציה מחזירה את הערך לכיוון שמאל. והכל בסדר עם זה פרט לעובדה שזה לא נכון. ראשית נתבונן בכמה דוגמאות שממחישות עד כמה ברור שהפונקציה מחזירה ערך לכיוון שמאל:
- \(f(x) = x^2\) הערך עובר שמאלה.
;()string name = Console.ReadLine
זוהי השמה וכבר אמרנו שכיוון ההשמה הוא שמאלה.;double num = double.Parse(Console.ReadLine())
ושוב, הערך של הפעולהReadLine
נכנס שמאלה לתוך הפונקציהParse
ומשם ממשיך שמאלה בהשמה לתוך המשתנהn
.;int n2 = Math.Round(num,2)
שוב שמאלה.
עד כאן הכל טוב. אז למה זה שגוי?
בעצם אין כיוון להחזרה, ואם בכלל, צריך לשים לב שיש קדימות דווקא להחזרה ימינה. נסתכל לדוגמא על הכתיב המשונה הבא שמאפיין עבודה עם עצמים. נשתמש כאן במחרוזת ונטפל בה. הערך שמחזירה הפונקציה אינו חוזר ימינה או שמאלה, הוא פשוט נמצא שם, ומשתמש ימינה דווקא, על ידי שימוש בנקודה כמו בדוגמא הבאה:
string s = "Hello, World!";
s = s.ToUpper().Replace("WORLD", "C#").Replace("HELLO,", "Go");
ToUpper
פעולה ללא פרמטרים, מגדילה הכל לאותיות גדולות. היא מחזירהstring
.- הטיפול בערך המוחזר (כעת “HELLO, WORLD”), ממשיך ימינה (בכיוון הקריאה באנגלית). התו נקודה משמש לשרשור פעולות.
- הפעולה
Replace
פועלת על העצם שהוחזר. היא מקבלת שני פרמטרים: 1. מה מחפשים. 2. מה לשים במקום. - התוצאה - הערך המוחזר - הוא
#HELLO, C
. - המחרוזת המקורית
s
לא משתנה כתוצאה מזה. כל פעולה כזו יוצרת עצם חדש מטיפוס מחרוזת. - העצם שלנו (כרגע
#HELLO, C
) יעבור טרנספורמציה נוספת (ושוב אם לדייק, יווצר עצם חדש). הפונקציה האחרונה בשרשרת תחליף את"HELLO"
ב-"Go"
. - התוצאה של כל התהליך הזה שבו פעלו 3 פונקציות, תיכנס למחרוזת s.
- כדי שיהיה מעניין. המחרוזת
s
לא משתנה. היא פונה כעת לעצם חדש בזיכרון שהתוכן שלו#Go C
.
סיכום ביניים 3: העברה וקבלת ערכים מהפונקציה
- פונקציות מקבלות פרמטרים, כולל ערכי ברירת מחדל (
int n = 0
). - ערך החזרה אינו זורם שמאלה/ימינה, אלא מוחזר ישירות ומיושם במקום הקריאה.
- בשרשור שיטות (כדוגמת
s.ToUpper().Replace(...)
), ההחזרה נעשית דרך נקודה, ימינה, וכל פעולה מחזירה מצביעה לאובייקט (קיים או חדש).
תרגול
⬅ עברו לתרגול 7.1 - פונקציות void: פעולות ללא פרמטרים
⬅ עברו לתרגול 7.2 - פונקציות המקבלות פרמטרים
⬅ עברו לתרגול 7.3 - פונקציות המקבלות ומחזירות ערך