מצב אובייקט והעברת נתונים¶
נניח שאנחנו כותבים מונה שמייצג כמה byte-ים עובדו. ב־C++ התרגלנו לשאול אם הפרמטר מועבר כערך, כ־reference או כמצביע. ב־C# אותה שאלה קיימת, אבל reference type שעובר לפרמטר רגיל עדיין מועבר כערך: הערך שמועתק הוא ה־reference. נבדיל בין שינוי אובייקט, החלפת המשתנה שקורא לנו והעתקת מבנה.
התוכנית הבאה מלאה. הריצו אותה בלי לשנות את חתימות הפונקציות, ואז נעקוב אחרי כל גבול קריאה.
var meter = new Meter();
Change(meter);
Console.WriteLine(meter.Bytes);
Replace(ref meter);
Console.WriteLine(meter.Bytes);
static void Change(Meter value)
{
value.Add(5);
value = new Meter();
value.Add(99);
}
static void Replace(ref Meter value)
{
value = new Meter();
value.Add(20);
}
sealed class Meter
{
public long Bytes { get; private set; }
public void Add(long amount)
{
if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amount));
Bytes = checked(Bytes + amount);
}
}
הפלט הוא 5 ואז 20. הפונקציה Change משנה את האובייקט המקורי דרך reference מועתק, אבל ההשמה למשתנה המקומי שלה אינה מחליפה את המשתנה של הקורא. הפונקציה Replace מקבלת ref למיקום שמחזיק את ה־reference, ולכן יכולה להחליף אותו. המילה sealed סוגרת את הירושה מן הטיפוס, וה־private setter מונע מכל צרכן לעקוף את בדיקת הגודל. זאת הגנה על invariant ברמת API; היא עדיין אינה הופכת את Add לאטומית בין threads.
הפרמטר out מיועד למסלול שמחזיר ערך דרך משתנה שעל הפונקציה לאתחל לפני יציאה רגילה. הפרמטר in מספק readonly reference, אבל אינו הופך את הגרף שאליו הערך מצביע לבלתי משתנה. אם struct מכיל List
הדוגמה הבאה מדגימה defensive copy. הריצו אותה כתוכנית נפרדת.
var counter = new Counter();
Console.WriteLine(Read(in counter));
Console.WriteLine(counter.Value);
static int Read(in Counter value) => value.Next();
struct Counter
{
public int Value;
public int Next() => ++Value;
}
הקריאה מחזירה 1 והערך המקורי נשאר 0. השיטה Next אינה readonly, ולכן הקריאה דרך readonly reference צריכה להגן על הערך המקורי באמצעות עותק. אל תסיקו שכל קריאה דרך in מעתיקה תמיד: שיטה שמסומנת readonly אינה דורשת את אותה הגנה, וה־JIT עשוי לייעל ייצוג בלי לשנות את התוצאה הנצפית. התיקון העיצובי אינו להוסיף readonly למתודה שמשנה שדה, אלא להפריד קריאה משינוי או להעביר ref כששינוי הוא החוזה.
הירושה מציגה סוג אחר של aliasing: משתנה מטיפוס בסיס עשוי להחזיק אובייקט של טיפוס נגזר. קריאה virtual נבחרת לפי הטיפוס בזמן ריצה. הממשק interface מאפשר לתאר יכולת בלי לחייב בסיס משותף, אבל העברת struct דרך interface עלולה ליצור box. בפרק הבא נראה איך generic constraints יכולים לשמור את הפעולה על הטיפוס עצמו.
בחוזה שלנו נרצה לציין מי רשאי לשנות את המצב ומתי. מבנה readonly מתאים לנתון קטן שמייצג תוצאה, ואובייקט מתאים לזהות משותפת שהמצב שלה משתנה. ההחלטה אינה תחרות בין stack ל־heap אלא בחירה בסמנטיקה שתמנע קוד מפתיע. בשלב האבחון נבדוק IL או allocations כדי לזהות עותקים ו־boxing שנוצרו בדרך.
התיעוד על העברת פרמטרים ועל מבנים לקריאה בלבד מגדיר את גבולות השינוי. ההבטחה שנוכל להוכיח היא לגבי פעולה דרך reference מסוים, ולא לגבי כל מי שעשוי להחזיק reference אחר לאותו מצב.