133. Avoid boxing when passing an interface as a struct

[נכתב ע”י דניאל קוגל]

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface IPrintable
{
void Print();
}
public struct Point3D : IPrintable
{
public int X { get; private set; }
public int Y { get; private set; }
public int Z { get; private set; }
public void Print()
{
Console.WriteLine("({0},{1},{2})",
this.X, this.Y, this.Z);
}
}

באופן דומה, כאשר נגדיר מתודה שמקבלת ממשק:

1
2
3
4
public static void Print(IPrintable printable)
{
printable.Print();
}

ונעביר לה את struct שמממש את הממשק, יתבצע boxing בקריאה למתודה:

1
2
3
IL_001c: ldloc.0
IL_001d: box Point3D
IL_0022: call void Program::Print(class IPrintable)

אבל אם נגדיר את המתודה שלנו גנרית עם אילוצים:

1
2
3
4
5
public static void Print<TPrintable>(TPrintable printable)
where TPrintable : IPrintable
{
printable.Print();
}

הקומפיילר ידע למנוע את ה-boxing ולקרוא למתודה ישירות:

1
2
IL_0027: ldloc.0
IL_0028: call void Program::Print<valuetype Point3D>(!!0)

המסקנה היא שאפשר להשתמש בGenerics כדי למנוע boxing של struct בקריאה לפונקציה שמקבלinterface.

אם נסתכל על הגוף של המתודה הגנרית, הוא נראה כך:

1
2
3
4
5
6
7
8
9
.method private hidebysig static void Print<(IPrintable) TPrintable>(!!TPrintable printable) cil managed
{
// Code size 14 (0xe)
.maxstack 8
IL_0000: ldarga.s printable
IL_0002: constrained. !!TPrintable
IL_0008: callvirt instance void IPrintable::Print()
IL_000d: ret
} // end of method Program::Print

כפי שרואים נוספה פקודה של constrained לפני הפקודה callvirt, הסיבה היא שהמהדר לא יודע האם TPrintable הוא value type או reference type אבל הוא כן יודע שצריך להעביר כתובת כלשהי למתודה בתור ה-this שלה מכיוון שמדובר במתודה של ממשק אשר לא יכול להיות סטטית (כלומר חייב להיות לה this).

לאחר שקיבלנו את הכתובת של הפרמטר ודחפנו אותו למחסנית (IL_0000), כאשר אנו קוראים ל-callvirt שלפניו יש constrained אז במידה ו-TPrintable הוא reference type אז על המחסנית בעצם נמצא pointer שמצביע ל-pointer שכן הפרמטר מכיל כתובת ולכן מתבצע dereference כדי שה-this יצביע לאן שצריך וניתן לקרוא למתודה בעזרת Callvirt.

במידה ו-TPrintable הוא value type אז הכתובת של הפרמטר בעצם מכילה את הכתובת של ה-value type וניתן להעביר אותה כמו שהיא למתודה בעזרת Call.

המשך יום גנרי ומובנה

שתף