134. The difference between out and ref keywords

ראינו ביום ראשון, שאם מעבירים struct למתודה שמקבלת struct, מועבר העתק שלו.

לכן, אם נרצה להעביר struct למתודה, כך שהמתודה תשנה אותו, לא נוכל לעשות זאת.

בעיה זו מוכרת עוד מימי C/C++ העליזים. שם פתרנו את הבעיה ע”י העברת פוינטר לstruct, או לחלופין השתמשנו בSyntax של & שהעביר את האובייקט כReference.

בC# יש שני keywords המאפשרים לנו להתמודד עם בעיה כזו. שמות הKeywords הם out וref והם מאפשרים לנו להעביר אובייקט לפונקציה “ולשנות אותו”.

יש שתי בעיות שהKeywords האלה פותרים:

הבעיה הראשונה: שינוי reference של אובייקט מסוג reference type בקריאה למתודה:

1
2
3
4
public static void ReverseString(string value)
{
value = new string(value.Reverse().ToArray());
}

פונקציה זו מקבלת מחרוזת והופכת אותה. נראה טוב, לא?

אלא שאם נריץ את הקוד הבא נקבל להפתעתנו

1
2
3
string myString = "This ain't a palindrome";
ReverseString(myString);
Console.WriteLine(myString); // This ain't a palindrome

הבעיה היא שאנחנו מקבלים בvalue רק reference שמצביע לmyString.

בפונקציה כשאנחנו משנים את value, אנחנו משנים את המקום שאליו הוא מצביע (כי אנחנו יוצרים שם אובייקט חדש). עם זאת, לא שינינו את ההצבעה של האובייקט myString, ולכן אין סיבה שהוא יצביע לאותו מקום שמצביע עכשיו value.

הבעיה השנייה היא מה שהזכרתי בתחילת הטיפ:

אם נעשה משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Person
{
public int Age
{
get;
set;
}
// ...
}
public static void ChangePerson(Person person)
{
person.Age = 26;
}

אז כשנקרא לפונקציה עם Person כלשהו הגיל שלו ישתנה במידה והוא reference type:

1
2
3
Person myPerson = new Person(){Age = 3};
ChangePerson(myPerson);
Console.WriteLine(myPerson.Age); // 26

למה? כי זה reference type, הפעם לא שינינו בפונקציה ChangePerson את הreference של person ולכן כאשר שינינו את person בפונקציה, הוא שינה גם את myPerson.

אבל אם נשנה את Person להיות value type:

1
2
3
4
5
6
7
8
9
public struct Person
{
public int Age
{
get;
set;
}
// ...
}

אז בקריאה זו:

1
2
3
Person myPerson = new Person(){Age = 3};
ChangePerson(myPerson);
Console.WriteLine(myPerson.Age); // 3

למה? כפי שראינו ביום ראשון, כשאנחנו מעבירים לפונקציה value type (כלומר struct), אנחנו מעבירים לה בעצם עותק שלו. לכן אנחנו משנים כאן רק את העותק person, ולא את המקור myPerson.


אז איך פותרים? יש שני keywords שמאפשרים לנו להתמודד עם בעיות כאלה: ref וout.

אם נסמן במתודה שפרמטר מסוים הוא ref, במידה והוא Reference type, יועבר לנו מצביע לReference שלו (כלומר מצביע למצביע), ואם נשנה אותו, ישתנה גם האובייקט המקורי:

1
2
3
4
public static void ReverseString(ref string value)
{
value = new string(value.Reverse().ToArray());
}

אכן:

1
2
3
string myString = "This ain't a palindrome";
ReverseString(ref myString);
Console.WriteLine(myString); // emordnilap a t'nia sihT

במידה והוא Value type נקבל אותו By reference (כלומר, נקבל מצביע למקום בו הוא יושב בזכרון) וכך נוכל לשנות אותו:

1
2
3
4
public static void ChangePerson(ref Person person)
{
person.Age = 26;
}

ואכן:

1
2
3
Person myPerson = new Person(){Age = 3};
ChangePerson(ref myPerson);
Console.WriteLine(myPerson.Age); // 26

יש לנו עוד Keyword ושמו out. הוא מאוד דומה לref, אבל נועד למטרה אחרת: נניח שאנחנו רוצים לאתחל פרמטר בפונקציה, אזי נסמן אותו בout:

1
2
3
4
5
public static int Divide(int a, int b, out int remainder)
{
remainder = a%b;
return a/b;
}

הפונקציה הזאת מחזירה לנו את השארית ואת החלוקה של מספר מסוים במספר אחר.


תכלס, יכולנו לעשות את זה גם עם ref, לא?

ובכן נראה ששני הKeywords האלה קיימים מטעמי סמנטיקה, כדי שנוכל להגיד אם אנחנו רוצים לאתחל פרמטר בתוך הפונקציה, או לשנות אותו.

זה לא רחוק מהאמת, אבל קיימים מספר הבדלים בין הKeywordים:

אם למשל נוריד שורה מהמתודה האחרונה:

1
2
3
4
public static int Divide(int a, int b, out int remainder)
{
return a/b;
}

הקוד שלנו לא יתקמפל, מאחר ולא אתחלנו משתנה שמסומן בout.

The out parameter ‘remainder’ must be assigned to before control leaves the current method

לעומת זאת, אם נחליף את הout בref, הקוד דווקא כן יתקמפל.

זאת משום שאנחנו מכריזים בקבלת משתנה עם out שאנחנו אחראים לאתחל אותו עד סוף הפונקציה.


בכיוון ההפוך: אם נשאר עם החתימה הזאת:

1
2
3
4
5
public static int Divide(int a, int b, out int remainder)
{
remainder = a%b;
return a/b;
}

ונכתוב קוד כזה:

1
2
3
int remainder;
Divide(100, 9, out remainder);
Console.WriteLine(remainder); // 1

הקוד יתקמפל ויעבוד. מה שמיוחד כאן הוא שהצהרנו על remainder, אבל לא אתחלנו אותו. עם זאת, זה מתקמפל כי out מוודא שאנחנו מאתחלים את remainder בפונקציה.

אם, לעומת זאת, נחליף את הפונקציה לפונקציה עם החתימה הבאה:

1
2
3
4
5
public static int Divide(int a, int b, ref int remainder)
{
remainder = a % b;
return a/b;
}

הקוד הבא כבר לא יתקמפל:

1
2
3
int remainder;
Divide(100, 9, ref remainder); // Compile error
Console.WriteLine(remainder);

נקבל את שגיאת הקימפול:

Use of unassigned local variable ‘remainder’

מה קורה כאן? כמו שout דואג לוודא שאנחנו מאתחלים את הפרמטר לפני שאנחנו יוצאים מהפונקציה, ref דואג שאנחנו נאתחל את המשתנה לפני שנכנס לפונקציה.

זאת מאחר והפונקציה עשויה להשתמש בremainder, ואם היינו רוצים להגיד שהיא לא מסתמכת עליו, היינו צריכים לסמן אותו בout.


למי שלמד על המושגים in parameter, out parameter וin/out parameter , הייתי מציע לעשות את האנלוגיה הבאה:

בלי לסמן כלום על פרמטר – זה אנלוגי להגיד שהוא in parameter

סימון של out – זה אנלוגי להגיד שהוא out parameter

סימון של ref – זה אנלוגי להגיד שהוא in/out parameter

שימו לב שיש פה אנלוגיה, אבל זה לא הכי מדויק, כיוון שכפי שראינו למעלה, גם reference types שאנחנו לא מסמנים באף אחד מהkeywords האלה אפשר לשנות, אבל האנלוגיה מובנת.

המשך יום חיצוני או מצביע מובנה טוב

שתף