141. More Type and generics

ראינו פעם שעברה כיצד ניתן להזריק לType גנרי פרמטרים גנריים.

לפעמים אנחנו מעוניינים לעשות להפך – יש לנו Type גנרי שהוזרקו אליו הפרמטרים, ואנחנו מעוניינים לקבל את הטיפוס הגנרי (ללא הפרמטרים הגנריים המוזרקים), או לחלופין לקבל את הפרמטרים הגנריים המוזרקים.

כדי להשיג את הType הגנרי ללא הפרמטרים הגנריים המוזרקים, נוכל להשתמש בפונקציהGetGenericTypeDefinition:

1
2
Type closedDictionaryType = typeof (Dictionary<string,int>);
Type unboundDictionaryType = closedDictionaryType.GetGenericTypeDefinition(); // typeof(Dictionary<,>)

כדי לחלץ את הארגומנטים הגנריים נוכל להשתמש בפונקציה GetGenericArguments המחזירה לנו מערך של הארגומנטים הגנריים:

1
2
Type[] genericArguments = closedDictionaryType.GetGenericArguments();
// new[] {typeof (string), typeof (int)}

אם נשתמש GetGenericArguments על טיפוס שהוא Unbound, נקבל את הפרמטרים "הגנריים":

1
2
3
4
5
6
7
8
9
Type[] genericArguments = unboundDictionaryType.GetGenericArguments();
foreach (Type genericArgument in genericArguments)
{
Console.WriteLine(genericArgument);
}
// Prints:
// TKey
// TValue

הבעיה עם הפונקציות האלה היא שאם נקרא להן על טיפוס שאינו גנרי, נקבל Exception.

נוכל למנוע זאת ע"י בדיקה האם הטיפוס הנתון הוא גנרי:

הProperty ששמו IsGenericType מציין האם הטיפוס הנתון הוא גנרי (בין אם הוא closed ובין אם הוא unbound)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (closedDictionaryType.IsGenericType)
{
// True
}
if (unboundDictionaryType.IsGenericType)
{
// True
}
if (typeof(string).IsGenericType)
{
// False
}

יש גם Property בשם IsGenericTypeDefinition שמציין האם הטיפוס הנתון הוא unbound:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (closedDictionaryType.IsGenericTypeDefinition)
{
// False
}
if (unboundDictionaryType.IsGenericTypeDefinition)
{
// True
}
if (typeof(string).IsGenericTypeDefinition)
{
// False
}

בנוסף, נוכל לראות האם טיפוס מסוים הוא פרמטר גנרי (כמו שראינו למעלה):

1
2
3
4
5
6
7
Type keyType =
unboundDictionaryType.GetGenericArguments()[0];
if (keyType.IsGenericParameter)
{
// True
}

לבסוף, נוכל לראות את הConstraintים שיש על הטיפוסים הגנריים:

למשל, נניח שיש לנו מחלקה כזו:

1
2
3
public class Contrainted<T> where T : IComparable
{
}

(ראו גם טיפ מספר 29,30)

אז הקוד הבא ידפיס

1
2
3
4
5
6
7
8
9
10
11
12
13
Type comparableType = typeof (Contrainted<>);
Type firstGenericType =
comparableType.GetGenericArguments()[0];
Type[] constraints =
firstGenericType.GetGenericParameterConstraints();
foreach (Type constraint in constraints)
{
Console.WriteLine(constraint);
}
// typeof(IComparable)

ראו גם טיפ מספר 317

שיהיה שבוע טיפוסי טוב

שתף

140. MakeGenericType method

בהמשך לשבוע הטיפוסי הטוב,

הכרנו קצת את Type וראינו איך אפשר ליצור instance חדש מType נתון.

נניח שיש לנו כמו אתמול Type שהשגנו אותו מאיזשהו מקום (למשל, מתוך קונפיגורציה).

איך נוכל ליצור List של טיפוסים כאלה?

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

1
2
3
4
5
public IList CreateList(Type givenType)
{
List<givenType> list = new List<givenType>();
return list;
}

אלא שקוד זה לא יתקמפל:

The type or namespace name ‘givenType’ could not be found (are you missing a using directive or an assembly reference?)

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

כדי לפתור בעיה זו, נוכל להשתמש בפונקציה MakeGenericType של Type, המקבלת Type של טיפוס גנרי Unbound, ומזריקה אליו את הפרמטרים הגנריים שלו.

למשל:

1
2
3
4
Type unboundList = typeof (List<>);
Type stringList =
unboundList.MakeGenericType(typeof (string)); // typeof(List<string>)

או

1
2
3
4
Type unboundDictionary = typeof (Dictionary<,>);
Type stringList =
unboundDictionary.MakeGenericType(typeof (string), typeof(int)); // typeof(Dictionary<string, int>)

נוכל להשתמש בזה כדי לפתור את הבעיה המקורית:

1
2
3
4
5
6
7
8
9
10
11
public IList CreateList(Type givenType)
{
Type unboundListType = typeof (List<>);
Type givenTypeList =
unboundListType.MakeGenericType(givenType);
IList list = (IList)Activator.CreateInstance(givenTypeList);
return list;
}

קצת טרמינולוגיה:

במחלקה

1
2
3
public class List<T>
{
}

הטיפוס List<T> נקרא open generic type (כיוון שT יכול להיות כל דבר)

כאשר הטיפוס הגנרי מצוין, למשל List<int>, טיפוס זה נקרא closed generic type.

כאשר אנחנו יוצרים טיפוס כזה

1
Type unboundDictionary = typeof (Dictionary<,>);

הטיפוס נקרא Unbound generic type.

בזמן ריצה, לא קיימים open generic types.

לא ניתן ליצור instanceים של unbound generic typeים:

1
2
3
4
Type unboundDictionary = typeof (Dictionary<,>);
object dictionary =
Activator.CreateInstance(unboundDictionary);

נקבל Exception כזה:

Cannot create an instance of System.Collections.Generic.Dictionary`2[TKey,TValue] because Type.ContainsGenericParameters is true.

סופ"ש טיפוסי טוב

שתף

139. Activator CreateInstance

עד כה ראינו מספר דרכים להשיג Type בזמן ריצה.

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

אחד הדברים המגניבים ביותר שאפשר לעשות על Type כזה הוא ליצור instance חדש מהסוג שלו בזמן ריצה.

למשל, נניח שיש לנו מחלקה המייצגת צורה:

1
2
3
public abstract class Shape
{
}

עכשיו, כל אחד יכול לממש צורה בדרך שלו. אנחנו רוצים לאפשר ליצור צורה לפי השם שלה:

1
2
3
4
5
6
public static class ShapeFactory
{
public static Shape CreateByName(string name)
{
}
}

הדרך האינטואיטיבית היא לעשות משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static class ShapeFactory
{
public static Shape CreateByName(string name)
{
if (name == "Triangle")
{
return new Triangle();
}
else if (name == "Square")
{
return new Square();
}
else if (name == "Circle")
{
return new Circle();
}
return null;
}
}

או כזה:

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
public static class ShapeFactory
{
public static Shape CreateByName(string name)
{
switch (name)
{
case "Triangle":
{
return new Triangle();
}
case "Square":
{
return new Square();
}
case "Circle":
{
return new Circle();
}
default:
{
return null;
}
}
}
}

המימוש הזה נחמד, אבל יש איתו שתי בעיות עיקריות:

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

ראו גם טיפ 50.

כדי לפתור את זה, נוכל להשתמש בReflection כדי ליצור instance של צורה לפי הType שלה באופן דינאמי.

(נוכל למשל, להשתמש בType.GetType כדי להשיג את הType של הצורה לפי השם שלו)

כדי ליצור instance חדש לפי Type, קיימת הפונקציה הסטטית Activator.CreateInstance.

הOverload הכי פשוט שלה מקבל Type ופרמטרים, וקורא לConstructor הכי מתאים של הType עם הפרמטרים.

נוכל להשתמש בו למשל כדי לממש את הShapeFactory:

1
2
3
4
5
6
7
8
9
public static class ShapeFactory
{
public static Shape CreateByName(string name)
{
// This isn't accurate
Type givenType = Type.GetType(name);
return (Shape)Activator.CreateInstance(givenType);
}
}

ככה יצרנו בעצם צורה בהינתן השם שלה בזמן ריצה.

(שימו לב שהדרך שבה אנחנו משיגים את הType היא לא מדויקת, כיוון שצריך גם לציין את הNamespace (ולעתים גם את הAssemblyQualifiedName).)

נוכל גם ליצור instance של טיפוס שמקבל בConstructor שלו פרמטרים, למשל:

השורה הבאה:

1
2
Person meir =
(Person) Activator.CreateInstance(typeof (Person), "Meir", "Ariel");

מבצעת בסופו של דבר קריאה לConstructor כמו השורה הזאת:

1
Person meir = new Person("Meir", "Ariel");

הדבר ממש מגניב, אבל לכל דבר יש מחיר.

המחיר כאן הוא בביצועים – יצירת אובייקט בצורה דינמית באמצעות הפונקציהActivator.CreateInstance מוסיף מספר מילישניות לזמן שלוקח ליצור את האובייקט. זה די הרבה באופן יחסי, מאחר והCLR מאוד מהיר ביצירת אובייקטים (קריאה רגילה לnew לוקחת בערך 10 נאנו שניות).

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

נראה בעתיד דרכים יותר יעילות לעשות דברים דומים..

המשך יום טיפוסי

שתף

138. Obtaining the type of a declared variable

אתמול הכרנו את הפונקציה GetType המאפשרת לנו להשיג את הType האמיתי של instance של אובייקט שיש לנו.

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

לדוגמה:

כתבנו פונקציה כזו:

1
2
3
4
public static Type GetVariableType(object value)
{
return value.GetType();
}

עכשיו אנחנו קוראים לה ככה:

1
2
3
IList myList = new List<string>();
Type myListType = GetVariableType(myList);
// typeof(List<string>)

אלא שהעברנו לפונקציה משתנה מסוג IList, והיינו רוצים לדעת מה הסוג של המשתנה שהועבר לפונקציה… (כלומר מאיזה טיפוס המשתמש בפונקציה שלנו ראה אותו)

נוכל לפתור זאת בצורה הבאה:

נהפוך את המתודה לגנרית:

1
2
3
4
public static Type GetVariableType<T>(T value)
{
return value.GetType();
}

ובמקום להחזיר את GetType של הvalue, נחזיר את הסוג של T:

1
2
3
4
public static Type GetVariableType<T>(T value)
{
return typeof (T);
}

אם עכשיו נבצע את השורות שראינו מעלה נקבל באמת הטיפוס שציפינו לקבל:

1
2
IList myList = new List<string>();
Type myListType = GetVariableType(myList); // typeof(IList)

מה סוד הקסם?

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

(טיפ מספר 28, וטיפ מספר 69)

מה שקורה כאן, הוא בגלל שהמשתנה שלנו מסוג IList, אז הטיפוס הגנרי שהקומפיילר מעביר לפונקציה.

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

1
2
3
4
5
6
7
8
public static bool IsTypeTooExplicit<T>(T value)
{
Type givenType = typeof(T);
// Prefer interfaces. (See also tip #81)
return (!givenType.IsInterface &&
givenType.GetInterfaces().Any());
}

המשך יום טיפוסי טוב

שתף

137. GetType

בהמשך לשבוע הטיפוסי,

ראינו שאנחנו מסוגלים לקבל Type באמצעות הKeyword ששמו typeof.

בנוסף, קיימת אפשרות לקבל את הType של instance של אובייקט נתון, באמצעות המתודה GetType. מתודה זו היא של object.

למשל:

1
2
3
4
5
6
7
8
IEnumerable<string> myStrings = new List<string>();
Type enumerableType = typeof (IEnumerable<string>);
Type instanceType = myStrings.GetType(); // typeof(List<string>)
if (instanceType == enumerableType)
{
// False
}

בשונה מהKeyword ששמו typeof, הפונקציה GetType נקראת בזמן ריצה וכך הטיפוס מפוענח בזמן ריצה לפי הטיפוס של הinstance.

קיימת עוד פונקציה ששמה GetType המאפשרת לנו לקבל Type לפי הFullName שלו (זוכרים מאתמול?):

1
2
3
4
5
6
Type myType = Type.GetType("System.Int32");
if (myType == typeof(int))
{
// True
}

היא מחפשת את הType שאנחנו מחפשים מהAssemblyים (הDLLים) הטעונים לנו בזכרון.


אחד השימושים הלא נכונים שראיתי של הפונקציה GetType הוא להשתמש בו בכדי לבדוק מה הסוג שלthis, ולפיו לבצע פעולה, כלומר משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Shape
{
public void Draw()
{
if (this.GetType() == typeof(Shape))
{
// Draw a shape
}
if (this.GetType() == typeof(Triangle))
{
// Draw a triangle
}
else if (this.GetType() == typeof(Circle))
{
// Draw a shape
}
// ...
}
}

הדבר הזה לא נכון, מאחר ומחלקה לא אמורה להכיר את המחלקות בת שלה. (אחד העקרונות של OOP)

הפתרון הנכון הוא להפוך את Draw לפונקציה וירטואלית:

1
2
3
4
5
6
7
public class Shape
{
public virtual void Draw()
{
// Draw a shape
}
}

ולדרוס אותה במחלקות בת:

1
2
3
4
5
6
7
public class Circle : Shape
{
public override void Draw()
{
// Draw a circle
}
}

עוד שימוש לא נכון בGetType הוא להשתמש בו בכדי לוודא שאובייקט הוא מסוג טיפוס ידוע, בשביל לעשות הסבה בטוחה.

כלומר לעשות משהו כזה:

1
2
3
4
5
6
object instance;
if (instance.GetType() == typeof(Shape))
{
Shape shape = (Shape) instance;
}

מה הבעיה בקטע קוד הזה?

יכול להיות שinstance הוא טיפוס שיורש מShape (למשל Circle), ואז לא נכנס לתנאי (כי GetType יחזיר typeof(Circle) ולא typeof(Shape)), ולכן לא תתבצע ההסבה.

הדרך הנכונה היא להשתמש בKeywordים ששמם is או as:

1
2
3
4
if (instance is Shape)
{
Shape shape = (Shape) instance;
}

כזכור, is בודק שמופע של אובייקט הוא מטיפוס נתון או יורש ממנו, ומאחר וCircle יורש מShape, התנאי מתקיים.

לחלופין אפשר להשתמש בas:

1
2
3
4
5
6
Shape shape = instance as Shape;
if (shape != null)
{
// ...
}

ראו גם טיפ מספר 12.

(שימו לב שגם שילוב של שני האנטי-טיפים הוא לא נכון, כלומר לכתוב if (this is Sparta) זו לא כתיבה נכונה, בדומה לGetType)

המשך שבוע טיפוסי טוב

שתף

136. typeof keyword

אחד הדברים המגניבים ביותר שאפשר לעשות בnet הוא לכתוב קוד שמסוגל לחלץ מידע על קוד שכתבנו.

כאשר התוכנה שלנו מתקמפלת, חלק מהמידע בה נעלם, כגון שמות של משתנים לוקאליים והערות, אבל רוב המידע נשאר נגיש. (למשל שמות של מחלקות שלנו והMemberים שלהן)

קיימת לנו גישה למידע זה, הנקרא גם Metadata, באמצעות Reflection המאפשר לנו לקבל מחלקות המתארות את המידע בו אנחנו מעוניינים.

נכיר בטיפ זה את Type – טיפוס של הFramework המתאר טיפוסים אחרים, ואת הkeyword ששמו typeof.


כאמור המחלקה Type מתארת טיפוסים netים אחרים. השימוש הפשוט ביותר במחלקה זו הוא באמצעות הkeyword ששמו typeof:

1
Type intType = typeof (int);

ככה קיבלנו instance של Type המתאר את הטיפוס int.

נוכל לעשות כל מיני דברים נחמדים כמו:

1
2
3
Console.WriteLine(intType.Name); // Int32
Console.WriteLine(intType.Namespace); // System
Console.WriteLine(intType.FullName); // System.Int32

או להדפיס, למשל, את כל הממשקים שהוא מממש:

1
2
3
4
5
6
7
8
9
foreach (Type currentInterface in intType.GetInterfaces())
{
Console.WriteLine(currentInterface.FullName);
}
// System.IComparable
// System.IFormattable
// System.IConvertible
// System.IComparable`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
// System.IEquatable`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

את הKeyword ששמו typeof ניתן להפעיל על כל טיפוס שידוע בזמן קימפול, והנה מספר דוגמאות:

1
2
3
4
5
Type interfaceType = typeof (IComparable); // Works on interfaces
Type arrayType = typeof (int[]); // Works on arrays
Type twoDimensionalArrayType = typeof (int[,]); // Even if they have a larger dimension
Type listType = typeof (List<int>); // Even on generic types
Type unboundListType = typeof (List<>); // Also on unbound generic types

הדוגמה האחרונה היא יותר חריגה, מאחר ואם ננסה לכתוב קוד כזה:

1
List<> myList = new List<>();

הוא לא יתקמפל. בJava, קיים פתרון באמצעות Wildcards. בC# אין פתרון לדבר כזה בזמן קימפול, אבל באמצעות Reflection אפשר לעשות דברים דומים לזה בזמן ריצה.


קצת בקשר לFullName: ראינו בדוגמה למעלה שהדפסה של FullName של טיפוס נראית בעצם כמו שרשור של הNamespace שלו עם הName שלו.

הדבר נכון בד"כ, אבל יש שני מקרים בהם הוא לא נכון:

  • עבור טיפוסים מקוננים (Nested Types), למשל:
1
2
3
4
5
6
public class MyType
{
public class MyNestedType
{
}
}

נקבל:

1
2
3
4
Type myNestedType = typeof(MyType.MyNestedType);
Console.WriteLine(myNestedType.Name); // MyNestedType
Console.WriteLine(myNestedType.Namespace); // MyNamespace
Console.WriteLine(myNestedType.FullName); // MyNamespace.MyType+MyNestedType
  • עבור טיפוסים גנריים: נקבל בName את שם המחלקה משורשרת עם ` שאחריו מופיע את מספר הארגומנטים הגנריים:
1
2
3
Type genericType = typeof(Dictionary<int, string>);
Console.WriteLine(genericType.Name); // Dictionary`2
Console.WriteLine(genericType.Namespace); // System.Collections.Generic

אבל בFullName נקבל משהו קצת אחר:

1
2
Console.WriteLine(genericType.FullName);
// System.Collections.Generic.Dictionary`2[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

שימו לב שמצוינים בסוגריים הטיפוסים הגנריים

  • אם הטיפוס הגנרי היה unbound, כלומר בלי פרמטרים גנריים מצוינים, היינו באמת מקבלים משהו כזה:
1
2
Type unboundType = typeof(Dictionary<,>);
Console.WriteLine(unboundType.FullName); // System.Collections.Generic.Dictionary`2

שיהיה שבוע טיפוסי (עד כמה שאפשר)

שתף

135. params keyword

בהמשך לטיפ היומי של אתמול ובאופן שלא כל כך מתקשר לשאר הטיפים של השבוע,

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

קיימת תמיכה לזה בשפת C#, באמצעות הKeyword params, אותו ניתן להצמיד לפרמטר האחרון בפונקציה, בתנאי שהוא מסוג מערך.

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

נוכל לעשות משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
public static int Sum(params int[] numbers)
{
int sum = 0;
foreach (int number in numbers)
{
sum += number;
}
return sum;
}

ולקרוא לה כך:

1
2
3
int sumOfFourNumbers = Sum(1, 2, 4, 8); // 15
int sumOfFiveNumbers = Sum(1, 2, 4, 8, 16); // 31
int sumOfTheNumbers = Sum(4, 8, 15, 16, 23, 42); // 108

שימו לב שאנחנו יכולים לציין כמה פרמטרים שאנחנו רוצים.

קיימת אפשרות גם להעביר פשוט מערך, אם מישהו מעדיף את הקוד הזה, או לחלופין יש לו מערך שהוא רוצה להעביר:

1
2
int[] numbers = new int[] { 4, 8, 15, 16, 23, 42 };
int sumOfTheNumbers = Sum(numbers); // 108

מגבלות: לא נוכל לשלוח פרמטרים בparams בתור ref או out.

בנוסף, הפרמטר של הparams חייב להיות האחרון בפרמטרים. (אתם מסוגלים לחשוב מדוע?)

מבחינת overloading, יש לפונקציה עם params עדיפות נמוכה יותר בזמן קימפול ממתודה עם אותו שם ומספר הפרמטרים שציינו:

כלומר אם נניח נוסיף את הפונקציה הזאת

1
2
3
4
public static int Sum(int x, int y, int z)
{
return 0;
}

אזי הקריאה הבאה:

1
int weirdSum = Sum(1, 2, 3); // 0

מחזירה 0.

params יכול להפוך API למאוד נוח, במיוחד כoverload לפונקציות שמצפות לקבל מערך או IEnumerable<T> כלשהו.

סופ"ש באורך לא מוגבל

שתף

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 האלה אפשר לשנות, אבל האנלוגיה מובנת.

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

שתף

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.

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

שתף

132. Automatic properties and structs

ראינו בעבר (טיפ מספר 89) כי בC# 3.0 נוספה אפשרות להגדיר Properties בצורה יותר נוחה.

הדבר עובד טוב למחלקות, אך האם הוא עובד גם לstructים?

במבט ראשון, נראה שזה עובד:

1
2
3
4
5
6
public struct Point3D
{
public int X { get; set; }
public int Y { get; set; }
public int Z { get; set; }
}

הקוד מתקמפל ועובד.

כעת נקשה על העניין, נהפוך את הsetterים להיות private:

1
2
3
4
5
6
public struct Point3D
{
public int X { get; private set; }
public int Y { get; private set; }
public int Z { get; private set; }
}

עדיין מתקמפל, אבל עכשיו כבר אי אפשר לאתחל את הProperties מבחוץ.

אז נוסיף Constructor שיאתחל לנו אותם:

1
2
3
4
5
6
7
8
9
10
11
12
13
public struct Point3D
{
public Point3D(int x, int y, int z)
{
this.X = x;
this.Y = y;
this.Z = z;
}
public int X { get; private set; }
public int Y { get; private set; }
public int Z { get; private set; }
}

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

Backing field for automatically implemented property ‘Point3D.Z’ must be fully assigned before control is returned to the caller. Consider calling the default constructor from a constructor initializer.
Backing field for automatically implemented property ‘Point3D.Y’ must be fully assigned before control is returned to the caller. Consider calling the default constructor from a constructor initializer.
Backing field for automatically implemented property ‘Point3D.X’ must be fully assigned before control is returned to the caller. Consider calling the default constructor from a constructor initializer.
The ‘this’ object cannot be used before all of its fields are assigned to

מה קורה פה?

זוכרים שאתמול הזכרנו שבConstructor שאינו דיפולטי של struct אנחנו מחויבים לאתחל את כל השדות?

ובכן, הAuto-Properties בעצם יוצרים לנו שדות מאחורי הקלעים, שאותם לא איתחלנו בConstructor. לכן הקוד אינו מתקמפל.

איך נוכל לפתור את הבעיה?

קיימות מספר אופציות:

  • אופציה ראשונה הוא מה שמציע לנו הקומפיילר: נקרא לConstructor הדיפולטי:
1
2
3
4
5
6
7
public Point3D(int x, int y, int z) :
this()
{
this.X = x;
this.Y = y;
this.Z = z;
}

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

  • אופציה שנייה היא להסיר את הConstructor הזה. ככה אי-אפשר לאתחל את הProperties האלה
  • אופציה שלישית היא לעשות מימוש שהוא לא Auto-Property ואז לאתחל ישירות את השדות בערכים האמיתיים שלהם, אבל רק אתחול אחד

הייתי הולך על הפתרון השלישי, אבל כל אחד ומה שנראה לו.

המשך יום מובנה טוב

שתף