Menu

C# ref, out ו-in: העברת פרמטרים לפי הפניה

איך עובדים פרמטרי ref, out ו-in ב-C#: העברת המשתנה עצמו במקום עותק, out לתוצאות נוספות ותבנית TryParse, out var, פרמטרי in לקריאה בלבד, כללי ההשמה הוודאית, ומה משתנה כשהארגומנט הוא טיפוס הפניה.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

כברירת מחדל, 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);.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל