Value מול Reference, ו-out
מה זה?
שינוי פרמטר בתוך פונקציה — האם זה משפיע על המשתנה המקורי בחוץ, או לא? התשובה תלויה בטיפוס: int (Value Type) שנשלח לפונקציה מקבל עותק — שינוי בפנים לא משפיע בחוץ. אבל אובייקט (Reference Type) שנשלח לפונקציה משתף את אותה הפניה — שינוי בתוך האובייקט (לא החלפתו לגמרי) כן ישפיע בחוץ. זה ההבדל שכבר נרמז בשיעור על טיפוסים פרימיטיביים מול Object — עכשיו רואים אותו בפעולה.
מילות מפתח שחשוב לזכור
• Value Type — טיפוס שמועבר כעותק של הערך (int, double, bool, char, struct)
• Reference Type — טיפוס שמועבר כהפניה (reference) לאותו אובייקט בזיכרון (string, מחלקות, List<T>)
• out — מילת מפתח שגורמת לפרמטר להיות “יציאה” — הפונקציה חייבת לתת לו ערך, וההשפעה נראית בחוץ
// Value Type — copy, changing it inside does not affect the outside
static void TryDouble(int number)
{
number = number * 2; // affects only the local copy
}
int x = 5;
TryDouble(x);
Console.WriteLine(x); // still 5 — the function only changed a copy// Reference Type — changing inside the object does affect the outside
static void AddItem(List<string> list)
{
list.Add("new"); // changes the shared object itself
}
List<string> items = new List<string> { "a", "b" };
AddItem(items);
Console.WriteLine(items.Count); // 3 — the shared object changed// out — when you want a function to "return" more than one value
static bool TryDivide(int a, int b, out int result)
{
if (b == 0)
{
result = 0;
return false;
}
result = a / b;
return true;
}
if (TryDivide(10, 2, out int answer))
{
Console.WriteLine($"The result: {answer}"); // 5
}הסבר עיקרי
למה int לא מושפע אבל List<string> כן — כשמעבירים int x לפונקציה, C# מעתיקה את הערך (5) לפרמטר המקומי — זה משתנה נפרד לגמרי מ-x המקורי. כשמעבירים List<string> items, C# מעתיקה רק את ההפניה (הכתובת) לאותו אובייקט ברשימה — הפרמטר המקומי מצביע לאותו אובייקט ממש, כך ש-list.Add(...) משנה את האובייקט שגם items בחוץ מצביע אליו.
ההבדל בין “לשנות את האובייקט” ל"להחליף את ההפניה" — חשוב להבחין: list.Add("חדש") משנה את תוכן האובייקט הקיים — זה נראה בחוץ. אבל list = new List<string>() (בתוך הפונקציה) רק מחליף למה הפרמטר המקומי מצביע — לא משפיע על items בחוץ, כי זו רק החלפת ההפניה המקומית, לא שינוי האובייקט המשותף.
מתי out נחוץ — פונקציה רגילה יכולה להחזיר רק ערך אחד עם return. out מאפשר “להחזיר” גם דרך פרמטר — שימושי כש-TryParse/TryDivide צריכים גם תוצאת בוליאן (הצליח/לא) וגם את הערך המחושב עצמו, בלי לבנות אובייקט מיוחד רק בשביל זה.
יתרונות
Value Types מגנים מפני שינוי בלתי-צפוי — פונקציה לא יכולה “לזיין” משתנה int שנשלח אליה; Reference Types חוסכים העתקה יקרה של מבני נתונים גדולים בכל קריאת פונקציה; out מאפשר “החזרת” כמה ערכים בלי ליצור מחלקה מיוחדת רק לשם כך.
חסרונות
ההבחנה Value/Reference יכולה להפתיע מפתחים מתחילים — שינוי בתוך פונקציה עובד “אחרת” לגבי אובייקטים לעומת פרימיטיבים; out שובר קריאות אם משתמשים בו יותר מדי — פונקציה עם כמה פרמטרי out קשה לקרוא בנקודת הקריאה.
נקודות חשובות
• Value Type מועבר כעותק; Reference Type מועבר כהפניה לאותו אובייקט
• שינוי תוכן אובייקט (Reference) בתוך פונקציה נראה בחוץ; החלפת ההפניה עצמה — לא
• out מאפשר לפונקציה “להחזיר” ערך נוסף דרך פרמטר, בנוסף ל-return
• TryParse/TryDivide הם השימוש הכי נפוץ ב-out — הצלחה בוליאנית + תוצאה
טעויות נפוצות
• לצפות ששינוי int/double בתוך פונקציה ישפיע על המשתנה המקורי בחוץ
• לבלבל בין “לשנות אובייקט” (list.Add, נראה בחוץ) ל"להחליף הפניה" (list = new List<>(), לא נראה בחוץ)
• לשכוח לתת ערך לפרמטר out בכל נתיב קוד אפשרי — שגיאת קומפילציה
סיכום
Value Types (int, double, bool וכו’) מועברים לפונקציות כעותק — שינוי בפנים לא משפיע בחוץ. Reference Types (string, מחלקות, List<T>) מועברים כהפניה — שינוי בתוך האובייקט המשותף כן נראה בחוץ, אך החלפת ההפניה עצמה לא. out מאפשר לפונקציה “להחזיר” ערך נוסף דרך פרמטר, מעבר ל-return הרגיל.
דוקומנטציה רשמית
Microsoft Learn — Value types and reference types
Microsoft Learn — out parameter modifier
תרגילים
תרגיל 1 — Value Type לא משתנה
המשימה: כתבו פונקציה void AddTen(int number) שמנסה להוסיף 10 לפרמטר, וקראו לה על משתנה int. הדפיסו את המשתנה לפני ואחרי הקריאה.
בדיקה: הערך המודפס לפני ואחרי הקריאה זהה — הפונקציה לא שינתה את המשתנה המקורי.
תרגיל 2 — Reference Type כן משתנה
המשימה: כתבו פונקציה void AddItem(List<int> numbers) שמוסיפה את המספר 99 לרשימה שהתקבלה, וקראו לה על List<int> קיימת. הדפיסו את מספר האיברים לפני ואחרי.
בדיקה: מספר האיברים גדל ב-1 אחרי הקריאה — הרשימה המקורית השתנתה.
פרויקט מסכם
המשימה: כתבו פונקציה bool TryGetDiscount(double price, string couponCode, out double finalPrice) שמחשבת מחיר אחרי הנחה.
דרישות:
- אם
couponCodeשווה ל-"SAVE10"—finalPrice=price * 0.9, הפונקציה מחזירהtrue - עבור כל קוד קופון אחר —
finalPrice=price(ללא שינוי), הפונקציה מחזירהfalse - ב-
Main, קראו לפונקציה עם קוד קופון תקין ולא תקין, והדפיסו הודעה מתאימה לכל מקרה בעזרת ה-boolוה-outשהתקבלו
בדיקה: קופון "SAVE10" על מחיר 100 מחזיר true ו-finalPrice = 90; קופון לא קיים מחזיר false ו-finalPrice שווה למחיר המקורי.
מה בפרק הבא
בפרק הבא — Nullable Types: איך מייצגים “אין ערך” גם עבור טיפוס פרימיטיבי כמו int, שבברירת מחדל תמיד חייב להכיל מספר כלשהו.