368. The difference between generic parameter types

בהמשך לטיפים על טיפוסיים גנריים:

קיים הבדל בCLR בין התייחסות לפרמטר גנרי שהוא Reference type לפרמטר גנרי שהוא value type.

כאשר הCLR נתקל בטיפוס (או מתודה) גנרי הוא יוצר קוד מתאים של הטיפוס (או המתודה) לפרמטר הגנרי.

בפעם הבאה כשיתקל הCLR באותו טיפוס/מתודה עם אותו פרמטר גנרי, הוא ישתמש באותו קוד שיצר בפעם הקודמת.


מה שכתוב כאן נכון, אבל קיים הבדל דק בין פרמטר גנרי שהוא value type לפרמטר גנרי שהוא reference type. (ראו גם טיפ מספר 131)

נתחיל בreference type: בפעם הראשונה שהCLR נתקל בטיפוס גנרי עם פרמטר גנרי מסוג reference type הוא יוצר קוד מתאים. בפעם הבאה שהCLR יתקל באותו טיפוס גנרי עם פרמטר גנרי אחר שהוא reference type, הוא ישתמש באותו קוד שיצר עבור הreference type הקודם.

הדבר הזה אפשרי מאחר וכל הreference types הם באותו גודל. למשל, נניח שיש לנו את הטיפוס הזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyList<T>
{
private T[] mValues = new T[4];
public void Add(T value)
{
mValues[0] = value;
}
public T GetIndex(int index)
{
return mValues[0];
}
}

אז כאשר נציב ערכים שונים של T שהם Reference types נצפה לראות את המחלקות האלה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DogList
{
private Dog[] mValues = new Dog[4];
public void Add(Dog value)
{
mValues[0] = value;
}
public Dog GetIndex(int index)
{
return mValues[0];
}
}
public class CatList
{
private Cat[] mValues = new Cat[4];
public void Add(Cat value)
{
mValues[0] = value;
}
public Cat GetIndex(int index)
{
return mValues[0];
}
}

(כאשר Cat וDog הם Reference types)

אם נסתכל בIL שנוצר למתודות הנ"ל נראה IL דומה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.method public hidebysig instance void Add(class Dog 'value') cil managed
{
.maxstack 8
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld class mValues
L_0007: ldc.i4.0
L_0008: ldarg.1
L_0009: stelem.ref
L_000a: ret
}
.method public hidebysig instance class Dog GetIndex(int32 index) cil managed
{
.maxstack 2
.locals init (
[0] class Dog CS$1$0000)
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld class mValues
L_0007: ldc.i4.0
L_0008: ldelem.ref
L_0009: stloc.0
L_000a: br.s L_000c
L_000c: ldloc.0
L_000d: ret
}

מה שאנחנו רואים זה שההתייחסויות למחלקה נמצאים במספר מקומות (בBold), אבל מאחר וכל הקצאות הזכרון הן באותו גודל (כלומר ביחידות מידה של Reference), אפשר לעשות Reuse לIL כפי שהוא.

למען האמת אפשר לשנות את הקוד ל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.method public hidebysig instance class Dog GetIndex(int32 index) cil managed
{
.maxstack 2
.locals init (
[0] class object CS$1$0000)
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld class mValues
L_0007: ldc.i4.0
L_0008: ldelem.ref
L_0009: stloc.0
L_000a: br.s L_000c
L_000c: ldloc.0
L_000d: ret
}

לקמפל מחדש ולראות שזה עדיין עובד!

לעומת זאת אם המחלקה שלנו היא Value Type, אז נראה משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ColorList
{
private Color[] mValues = new Color[4];
public void Add(Color value)
{
mValues[0] = value;
}
public Color GetIndex(int index)
{
return mValues[0];
}
}

מתקמפל ל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.method public hidebysig instance void Add(valuetype Color 'value') cil managed
{
.maxstack 8
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld valuetype Color [] ColorList::mValues
L_0007: ldc.i4.0
L_0008: ldelema Color
L_000d: ldarg.1
L_000e: stobj Color
L_0013: ret
}
.method public hidebysig instance valuetype Color GetIndex(int32 index) cil managed
{
.maxstack 2
.locals init (
[0] valuetype Color CS$1$0000)
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld valuetype Color [] ColorList::mValues
L_0007: ldc.i4.0
L_0008: ldelema Color
L_000d: ldobj Color
L_0012: stloc.0
L_0013: br.s L_0015
L_0015: ldloc.0
L_0016: ret
}

אנחנו רואים פה הבדל מאחר ויש צורך להעתיק את הערך של הValue type למשתנה על Stack.

מאחר ולכל Value type מוקצה גודל שונה על הStack, לא ניתן לעשות כאן Reuse.

לכן כל פעם שנתקל בValue type שטרם נראה, הCLR יאלץ ליצור קוד מתאים חדש, מאחר ולא יוכל למחזר את הקוד.


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

שתף