מפת סביבת הריצה ומערכת הטיפוסים¶
עד עכשיו בנינו תוכנית שהקומפיילר וה־linker תרגמו לקוד מכונה ולמבני PE שהמערכת טוענת. כשאנחנו כותבים C# נוסף שחקן: מנוע הריצה מנהל טיפוסים, קומפילציה, חריגות וזיכרון בזמן שהתהליך עובד. כדי לחקור עלות או כשל אנחנו צריכים לדעת באיזה שלב הוא נוצר. השורה שנראית כמו העתקה יכולה לשכפל ערך, להעתיק reference או ליצור object חדש באמצעות boxing.
נתחיל בפרויקט console חדש עם SDK של .NET 10 ונבדוק את סביבת הבנייה. הפקודות מיועדות ל־PowerShell בתיקיית עבודה חדשה.
dotnet --info
dotnet new console -n TypeLab -f net10.0
Set-Location TypeLab
dotnet build -c Release
dotnet run -c Release --no-build
ה־SDK כולל את כלי הבנייה והקומפיילר. ה־runtime הוא המערכת שמריצה את האפליקציה, וה־target framework קובע איזה חוזה API הפרויקט מבקש. גרסת השפה קובעת אילו תחבירים וכללי קומפילציה מותרים. הבחירה ב־net10.0 אינה אומרת שכל מחשב שמותקן עליו SDK אחר יוכל לבנות את הפרויקט, והפצה framework-dependent עדיין דורשת runtime מתאים אצל המשתמש. בפרק הזה נשתמש ב־C# 14 היציבה, בלי להפעיל preview.
החליפו את Program.cs בקוד הבא. זוהי תוכנית מלאה שמפרידה בין שלוש פעולות שנראות דומות במבט ראשון.
using System.Reflection;
var original = new Counter { Value = 7 };
var copy = original;
copy.Value++;
object boxed = original;
original.Value = 30;
var unboxed = (Counter)boxed;
Console.WriteLine($"values: {original.Value}, {copy.Value}, {unboxed.Value}");
var first = new Holder { Value = 7 };
var second = first;
second.Value++;
Console.WriteLine($"same: {ReferenceEquals(first, second)}, value: {first.Value}");
Console.WriteLine(typeof(Counter).IsValueType);
Console.WriteLine(Assembly.GetExecutingAssembly().GetName().Name);
Console.WriteLine(typeof(object).Assembly.GetName().Name);
struct Counter { public int Value; }
sealed class Holder { public int Value; }
הפלט הראשון מכיל את הערכים 30, 8 ו־7. ההשמה ל־copy העתיקה את תוכן המבנה. ההמרה ל־object יצרה box שמכיל עותק נוסף של המבנה, וה־unboxing שלף ממנו ערך. לעומת זאת שני המשתנים של Holder מפנים לאותו אובייקט, ולכן שינוי דרך second נראה דרך first. ההבחנה היא בסמנטיקה, לא בכלל גאוגרפי שלפיו כל struct נמצא במחסנית וכל class בהכרח מוקצה בכל ביטוי חדש. שדה מבנה בתוך אובייקט נשמר כחלק ממנו, ו־JIT עשוי להעלים הקצאות והעתקות אם ההתנהגות נשמרת.
נשנה את Counter כך שיכיל גם מערך. העתקת המבנה תעתיק את ה־reference למערך, לא את תוכן המערך. לכן ערך יכול להכיל שיתוף של אובייקטים אחרים. לפני שנחליט שהעתקה היא snapshot נשאל מה נמצא בכל שדה והאם הוא mutable. זאת אותה שאלת בעלות שהכרנו ב־C++, אבל הכלים שמבטאים אותה שונים.
ב־assembly נשמרים IL ו־metadata, לרבות הגדרות טיפוסים, שיטות ו־references ל־assemblies אחרים. ה־metadata אינו רק שם לתיעוד: מנוע הריצה משתמש בו כדי לפתור קריאות, לזהות טיפוסים ולדעת אילו מקומות מכילים references. במהלך GC אין צורך לנחש אם כל מספר הוא מצביע. הגבול משתנה כשאנחנו מכניסים pointers ו־interop, ונחקור אותו בהמשך.
הוסיפו מדידה של GC.GetAllocatedBytesForCurrentThread סביב לולאה שמכניסה מיליון Counter-ים ל־List
ההיכרות הרשמית עם מערכת הטיפוסים של C# מספקת את אוצר המילים. בהמשך נשתמש בו כדי להסביר את ה־layout, את הקוד שנוצר ואת שורשי הזיכרון שנראה ב־debugger.