כברירת מחדל, C# מעבירה כל ארגומנט לפי ערך: המתודה מקבלת עותק, והשמה לפרמטר לא משפיעה על הקורא. המגדירים ref, out ו-in מעבירים במקום זאת את המשתנה של הקורא עצמו.
פלט:
by value: 50
by ref: 60
מילת המפתח מופיעה פעמיים: ברשימת הפרמטרים (ref int score) ובמקום הקריאה (ref score). הכתיבה שלה בקריאה היא חובה, וזה מכוון: כל מי שקורא את AddPoints(ref score) יודע ש-score עשוי להשתנות. אם שוכחים אותה, הקומפיילר מדווח CS1620, Argument 1 must be passed with the 'ref' keyword.
ref: קריאה וכתיבה של המשתנה של הקורא
פרמטר ref הוא כינוי למשתנה של הקורא. המתודה יכולה לקרוא את הערך הנוכחי ולהחליף אותו. הדוגמה הקלאסית היא החלפה (swap), שבלתי אפשרית עם פרמטרים רגילים:
פלט:
Linus Ada
volume 100
כללים ל-ref:
- הארגומנט חייב להיות משתנה (משתנה מקומי, פרמטר, שדה או איבר במערך), לא ליטרל או ביטוי. גם מאפיין לא מתאים:
Clamp(ref player.Volume, 0, 100)היא שגיאה CS0206,A non ref-returning property or indexer may not be used as an out or ref value. העתיקו את המאפיין למשתנה מקומי, העבירו אותו, והציבו אותו בחזרה. - חייבים להציב ערך במשתנה בוודאות לפני הקריאה.
int n; Clamp(ref n, 0, 10);היא CS0165,Use of unassigned local variable 'n'.
out: תוצאות נוספות
out מיועד לערכים שהמתודה מפיקה. הקורא לא צריך לאתחל את המשתנה, והמתודה חייבת להציב בו ערך בכל מסלול לפני שהיא חוזרת:
static void Split(string fullName, out string first, out string last)
{
// error CS0177: The out parameter 'last' must be assigned to before control leaves the current method
first = fullName.Split(' ')[0];
}
בתוך המתודה, פרמטר out נחשב כלא מאותחל עד שכותבים אליו, ולכן אי אפשר לקרוא את הערך הישן של הקורא. זה ההבדל מ-ref: ref הוא דו-כיווני, out הוא חד-כיווני מהמתודה אל הקורא.
תבנית TryParse
השימוש המוכר ביותר ב-out הוא תבנית ה-Try בספריית הבסיס: מחזירים bool להצלחה ושמים את התוצאה בפרמטר out, כך שקלט שגוי אף פעם לא זורק חריגה:
פלט:
"vip25" is a coupon: 25% off
"42" is a quantity: 42
"BOGUS" is not recognized
"" is not recognized
out var quantity (C# 7.0) מצהיר על המשתנה ישר בתוך הקריאה; הטיפוס שלו מגיע מהפרמטר. בכישלון, TryParse מציב בו 0 ומחזיר false. כשחשוב לכם רק אם הפענוח הצליח, השליכו את הערך עם out _: bool isNumber = int.TryParse(input, out _);.
שימו לב ש-TryGetDiscount מציבה ערך ב-rate בשני המסלולים: פעם אחת במפורש ופעם אחת כשהיא מעבירה אותו ל-Dictionary.TryGetValue, שהיא בעצמה קריאת out.
out מול החזרת tuple
לפני C# 7, out הייתה הדרך העיקרית להחזיר כמה ערכים. היום tuples עושים את זה בצורה ישירה יותר:
// with out parameters
static void MinMax(int[] values, out int min, out int max) { ... }
MinMax(temps, out int lo, out int hi);
// with a tuple
static (int Min, int Max) MinMax(int[] values) { ... }
var (lo, hi) = MinMax(temps);
השתמשו ב-tuple (או במחלקה קטנה) לכמה תוצאות פשוטות. שמרו את out לתבנית ה-Try, שבה ערך ההחזרה bool מאפשר לקריאה לשבת בתוך תנאי של if.
טיפוסי הפניה: שינוי האובייקט מול החלפתו
העברה של מופע מחלקה לפי ערך כבר מאפשרת למתודה לשנות את האובייקט, כי הערך שמועתק הוא הפניה לאותו אובייקט. ref מוסיף יכולת אחת: להחליף את האובייקט שהמשתנה של הקורא מצביע עליו.
פלט:
after AddItem: 2 items
after ResetByValue: 2 items
after ResetByRef: 0 items
ResetByValue הפנתה את העותק שלה של ההפניה לרשימה חדשה; הקורא אף פעם לא ראה את זה. ResetByRef שינתה את המשתנה של הקורא עצמו. בקוד יומיומי רק לעיתים רחוקות צריך ref על פרמטר מסוג מחלקה: החזירו את האובייקט החדש במקום.
ב-structs ההבדל גדול יותר, כי ארגומנט מסוג struct מועתק שדה אחר שדה. מתודה שחייבת לשנות struct של הקורא (Point, struct הגדרות גדול) צריכה ref.
in: קריאה בלבד לפי הפניה
C# 7.2 הוסיפה את in, שמעביר לפי הפניה אבל אוסר השמה. המטרה שלו היא ביצועים: struct גדול לא מועתק בכל קריאה, והקורא מקבל הבטחה שהוא לא ישתנה.
// C# 7.2 and later
struct Matrix4
{
public double M11, M12, M13, M14, M21, M22, M23, M24,
M31, M32, M33, M34, M41, M42, M43, M44;
}
static double Trace(in Matrix4 m) => m.M11 + m.M22 + m.M33 + m.M44;
static void Reset(in Matrix4 m)
{
m.M11 = 0;
// error CS8332: Cannot assign to a member of variable 'm' or use it as the right hand side of a ref assignment because it is a readonly variable
}
במקום הקריאה in הוא אופציונלי: גם Trace(matrix) וגם Trace(in matrix) עובדים. ל-int, double, DateTime ולטיפוסים קטנים אחרים, in לא מרוויח כלום ויכול להיות אפילו מעט איטי יותר; השתמשו בו ל-structs עם כמה שדות שמעבירים לעיתים קרובות.
הגבלות
- פרמטרי
refו-outלא יכולים להיות אופציונליים (CS1741). פרמטרinיכול:static void Connect(in int retries = 3)חוקי. - העמסות לא יכולות להיבדל רק בין
refל-out(אוin), כי ברמת סביבת הריצה הם זהים.F(int x)ו-F(ref int x)יכולים להתקיים יחד. - מתודות
asyncואיטרטורים (מתודות שמשתמשות ב-yield return) לא יכולים לקבל פרמטריref,outאוin. החזירו מהן tuple או אובייקט תוצאה במקום. - למבדה או פונקציה מקומית לא יכולות ללכוד פרמטר
ref,outאוinשל המתודה העוטפת; העתיקו אותו קודם למשתנה מקומי.
השוואה בין ref, out ו-in
| הקורא חייב להציב ערך קודם | המתודה חייבת להציב ערך | המתודה יכולה לקרוא | מילת מפתח בקריאה | |
|---|---|---|---|---|
| (בלי) | כן | לא | כן (עותק) | אין |
ref | כן | לא | כן | חובה |
out | לא | כן, בכל מסלול | רק אחרי השמה | חובה |
in | כן | אסור | כן | אופציונלית |
שאלות נפוצות
מה ההבדל בין ref ל-out ב-C#?
שניהם מעבירים את המשתנה של הקורא עצמו ולא עותק. עם ref, חייבים להציב ערך במשתנה לפני הקריאה, והמתודה יכולה לקרוא אותו ולשנות אותו. עם out, לא צריך להציב ערך קודם, המתודה לא יכולה לקרוא אותו לפני שכתבה אליו, והיא חייבת להציב בו ערך לפני שהיא חוזרת (אחרת CS0177). out אומר "זו תוצאה נוספת".
מה המשמעות של out var ב-C#?
מאז C# 7.0 אפשר להצהיר על משתנה ה-out בתוך הקריאה: if (int.TryParse(text, out var number)). הטיפוס של המשתנה מוסק מהפרמטר, והוא בטווח גם אחרי ההוראה. out _ משליך תוצאה שלא צריך.
האם אובייקטים מועברים לפי הפניה ב-C#?
לא, כל ארגומנט מועבר לפי ערך אלא אם כותבים ref, out או in. במחלקה, הערך שמועתק הוא הפניה, כך שהמתודה יכולה לשנות את השדות של האובייקט והקורא רואה את זה. מה שהיא לא יכולה לעשות בלי ref הוא לגרום למשתנה של הקורא להצביע על אובייקט אחר.
מה זה מגדיר הפרמטר in ב-C#?
in (C# 7.2 ואילך) מעביר ארגומנט לפי הפניה אבל לקריאה בלבד: המתודה לא יכולה להציב בו ערך. הוא קיים כדי להימנע מהעתקה של structs גדולים בכל קריאה. לטיפוסים קטנים כמו int או DateTime אין לו תועלת.
למה מופיעה השגיאה "Argument 1 must be passed with the 'ref' keyword"?
המתודה מצהירה על פרמטר ref והקריאה השמיטה את מילת המפתח. C# דורשת ref (ו-out) גם במקום הקריאה, כדי שהקוראים יראו שהמשתנה עשוי להשתנות: Increment(ref count);, לא Increment(count);.