320. Implementing the virtual method pattern in C# part 2

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

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

ובכן, תזכורת:

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

1
2
3
4
5
6
7
8
9
10
11
Square square = new Square() { Edge = 4 };
square.GetArea(); // 16
Shape shape = new Square() { Edge = 3 };
shape.GetPerimeter(); // 12
RegularTriangle triangle = new RegularTriangle(){Edge = 3};
triangle.GetPerimeter(); // 9
shape = triangle;
shape.GetPerimeter(); // 0 (GetPerimeter doesn't override..)

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

1
2
3
4
5
6
7
8
9
10
11
Square square = new Square() { Edge = 4 };
Square.GetArea(square);
Shape shape = new Square() { Edge = 3 };
Shape.GetPerimeter(shape);
RegularTriangle triangle = new RegularTriangle() { Edge = 3 };
RegularTriangle.GetPerimeter(triangle); // 9
shape = triangle;
Shape.GetPerimeter(shape);

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

1
Shape.GetPerimeter(shape);

תקרא במקום לפונקציה

1
Square.GetPerimeter(shape);

אז איך נוכל לעשות זאת?


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

מה כן אפשר לעשות? אפשר להשתמש בDelegateים!

איך נעשה את זה? כל Instance יחזיק את המתודות האלה:

1
2
3
4
5
6
7
public abstract class Shape
{
protected delegate double GetPerimeterSignature(Shape _this);
protected delegate double GetAreaSignature(Shape _this);
protected GetPerimeterSignature mGetPerimeter;
protected GetAreaSignature mGetArea;
}

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

1
2
3
4
5
6
7
8
9
public static double GetPerimiter(Shape _this)
{
return _this.mGetPerimeter(_this);
}
public static double GetArea(Shape _this)
{
return _this.mGetArea(_this);
}

ונדאג בConstructorים לאתחל את הDelegate במתודות הנכונות:

בShape זה קל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Shape()
{
mGetArea = InnerGetArea;
mGetPerimeter = InnerGetPerimeter;
}
public static double InnerGetPerimeter(Shape _this)
{
return 0;
}
public static double InnerGetArea(Shape _this)
{
return 0;
}

בSquare זה יותר קשה:

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 Square()
{
mGetArea = InnerGetArea;
mGetPerimeter = InnerGetPerimeter;
}
public static double InnerGetPerimeter(Shape _this)
{
return GetPerimeter((Square) _this);
}
public static double InnerGetArea(Shape _this)
{
return GetArea((Square)_this);
}
public static double GetArea(Square _this)
{
return _this.Edge*_this.Edge;
}
public static double GetPerimeter(Square _this)
{
return 4*_this.Edge;
}

שימו לקסם: כשיכתבו את השורה הזאת למשל:

1
2
Shape shape = new Square() { Edge = 3 };
shape.GetPerimeter(); // 12

היא תתרגם ל

1
2
Shape shape = new Square() { Edge = 3 };
Shape.GetPerimeter(shape);

שתקרא ל

1
2
3
4
public static double GetPerimiter(Shape _this)
{
return _this.mGetPerimeter(_this);
}

שתקרא ל

1
2
3
4
public static double InnerGetPerimeter(Shape _this)
{
return GetPerimeter((Square) _this);
}

שתקרא ל

1
2
3
4
public static double GetPerimeter(Square _this)
{
return 4*_this.Edge;
}

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

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


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

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

המשך יום וירטואלי טוב!

שתף

319. Implementing the virtual method pattern in C# part 1

הכרנו בעבר את המושג של מתודות וירטואליות, ואפילו דיברנו על Multiple dynamic dispatch.

(ראו גם טיפים מספר 221-235)

בפעמים הקרובות אנחנו ננסה להבין איך מימשו (או איך היה אפשר לממש) פונקציות וירטואליות בC#.

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

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

לכן נניח למעשה שאין לנו פונקציות של Instance, אלא רק פונקציות סטטיות.


אז מה אנחנו רוצים להשיג? נתבסס על הדוגמאות קוד מטיפ מספר 221:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
public abstract class Shape
{
public virtual double GetPerimeter()
{
return 0;
}
public virtual double GetArea()
{
return 0;
}
}
public class RegularTriangle : Shape
{
public double Edge { get; set; }
public double GetPerimeter()
{
return Edge*3;
}
public override double GetArea()
{
return Edge*Edge*Math.Sqrt(3)/2;
}
}
public class Square : Shape
{
public double Edge { get; set; }
public override double GetArea()
{
return Edge*Edge;
}
public override double GetPerimeter()
{
return 4*Edge;
}
}

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

1
2
3
4
5
6
7
8
9
10
11
Square square = new Square() { Edge = 4 };
square.GetArea(); // 16
Shape shape = new Square() { Edge = 3 };
shape.GetPerimeter(); // 12
RegularTriangle triangle = new RegularTriangle(){Edge = 3};
triangle.GetPerimeter(); // 9
shape = triangle;
shape.GetPerimeter(); // 0 (GetPerimeter doesn't override..)

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


אז איך מממשים? נתחיל בכך שאין לנו פונקציות של Instance בכלל.

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

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
30
31
32
33
34
35
36
37
38
39
40
41
42
public abstract class Shape
{
public static double GetPerimeter(Shape _this)
{
return 0;
}
public static double GetArea(Shape _this)
{
return 0;
}
}
public class RegularTriangle : Shape
{
public double Edge { get; set; }
public static double GetPerimeter(RegularTriangle _this)
{
return _this.Edge*3;
}
public static double GetArea(RegularTriangle _this)
{
return _this.Edge*_this.Edge*Math.Sqrt(3)/2;
}
}
public class Square : Shape
{
public double Edge { get; set; }
public static double GetArea(Square _this)
{
return _this.Edge*_this.Edge;
}
public static double GetPerimeter(Square _this)
{
return 4*_this.Edge;
}
}

כעת כשיקרא הקוד הזה:

1
2
Square square = new Square() { Edge = 4 };
square.GetArea(); // 16

הקומפיילר החכם ידע לתרגם אותו לקריאה הזאת:

1
2
Square square = new Square() { Edge = 4 };
Square.GetArea(square); // 16

ואת הקוד הזה

1
2
Shape shape = new Square() { Edge = 3 };
shape.GetPerimeter();

הוא יתרגם לקוד הבא:

1
2
Shape shape = new Square() { Edge = 3 };
Shape.GetPerimeter(shape); // 0

למי מכם שתכנת בשפות כמו פייתון, בוודאי יצא להתקל בפונקציות של instance שמקבלות את this (או את self) בתור הפרמטר הראשון של הפונקציה.

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

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

את הניסיון לפתרון הזה נראה בפעם הבאה.

המשך יום וירטואלי טוב.

שתף

318. Why Nullable{T} struct isn't true

עבר הכרנו את הטיפוס Nullable (טיפ מספר 35).

ראינו בטיפ מספר 31 את הConstraintים המיוחדים

1
2
where T : struct
where T : class

מה שמעניין הוא שאם נכתוב קוד כזה:

1
2
3
public static void MyGenericMethod<T>()
{
}

לא נוכל להעביר אליו טיפוס שהוא Nullable אם אחד הConstraintים המיוחדים מלמעלה נמצא.


מה קורה כאן?

הרי Nullable הוא struct בעצמו: אם נסתכל בMetadata שלו נראה:

1
public struct Nullable<T> where T : struct

ובכן, אם אכן יכולנו להעביר למתודה הזאת

1
2
3
public static void MyGenericMethod<T>() where T : struct
{
}

טיפוס שהוא Nullable, זה היה אומר שNullable מקיים את התנאי where T : struct. ואז היינו יכולים לכתוב:

1
Nullable<Nullable<int>> myNullableNullable;

אבל מאחר ואין לכך משמעות, החליטו ליצור פאץ’ ולא לאפשר לConstraint לקבל את Nullable. אם תקראו בMSDN תראו שהתיאור הוא Non nullable value type.

סופ"ש לא Nullable טוב!

שתף

317. GenericParameterAttributes

פגשנו בעבר (טיפ מספר 29) את הFeature של Generic Constraints ואת את הGeneric Constraints המיוחדים שאפשר לשים לפרמטרים גנריים (טיפים מספר 31,33).

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

לפעמים מעניין אותנו לדעת אם יש על פרמטר גנרי אחת מההגבלות המיוחדות, כי אחרת נחטוף Exception כשננסה לקרוא לMakeGenericMethod/MakeGenericType (טיפים מספר 140, 149) עם פרמטר גנרי לא מתאים.

אם נחפש את הConstraintים המיוחדים בדרך אליה אנו רגילים, אנחנו נתאכזב לראות שהם אינם מופיעים ב GetGenericParameterConstraints. מסתבר שיש דרך אחרת לגלות איזה Constraintים מיוחדים יש לטיפוס גנרי, והיא כזאת:

1
2
3
public static void MyGenericMethod<T>() where T : new()
{
}

נשיג את הMethodInfo:

1
2
MethodInfo methodInfo =
this.GetType().GetMethod("MyGenericMethod");

כעת יש לType איזשהו Property בשם GenericParameterAttributes שהוא Enum עם הערכים הבאים:

1
2
3
4
5
6
7
8
9
10
11
12
[Flags]
public enum GenericParameterAttributes
{
None = 0,
Covariant = 1,
Contravariant = 2,
VarianceMask = Contravariant | Covariant,
ReferenceTypeConstraint = 4,
NotNullableValueTypeConstraint = 8,
DefaultConstructorConstraint = 16,
SpecialConstraintMask = DefaultConstructorConstraint | NotNullableValueTypeConstraint | ReferenceTypeConstraint,
}

שימו לב שהוא Enum של Flags (ראו טיפ מספר 22) ולכן נוכל לבדוק אם יש Constraint מסוים בצורה הבאה (ראו גם טיפ מספר 72):

1
2
3
4
5
if (genericArgument.GenericParameterAttributes.HasFlag
(GenericParameterAttributes.DefaultConstructorConstraint))
{
// true, because T : new()
}

באופן דומה יש ערכים מתאימים בEnum לבדיקות אחרות, למשל האם מתקיים שT הוא struct, class וכו’.

משהו שאולי כדאי לדעת, זה שאם שמתם משהו כזה:

1
2
3
public static void MyGenericMethod<T>() where T : struct
{
}

הEnum יכיל גם DefaultConstructorConstraint וגם NotNullableValueTypeConstraint, כי לכל Value Type יש Constructor דיפולטי. (ראו גם טיפ מספר 131)

המשך יום גנרי עם תנאים מיוחדים טוב.

שתף

316. Multidimensional jagged array

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

למשל, נניח שיש לנו מתודה נחמדה כזאת:

1
2
3
public static void MyMethod<T>(T[,] value)
{
}

אפשר לקרוא לה ככה:

1
MyMethod(new int[,] {});

היינו מצפים שגם יתקיים שאפשר לקרוא לה כך:

1
MyMethod(new int[][,] {});

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

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

1
MyMethod(new int[,][] {});

מה בעצם קורה כאן? יכול להיות שזה נראה ממש מוזר, אבל בואו ננסה לאתחל מערך כזה:

1
int[,][] superMatrix = new[,] {{new[]{1}}};

כאמור, זה מערך דו מימדי של מערכים חד מימדיים. כלומר, מטריצה של מערכים חד-מימדיים. איך היינו מצפים לגשת אליה?

ככה:

1
2
superMatrix[0, 0][0] = 2;
Console.WriteLine(superMatrix[0, 0][0]);

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


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

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

המשך יום תכלת טוב.

שתף

315. IsNumeric

לפעמים עולה לנו הצורך לבדוק עבור Type מסוים האם הוא מייצג ערך מספרי או לא של הFramework או לא.

יכולות להיות לכך כמה סיבות – למשל, אנחנו רוצים לבדוק האם לאפשר לטיפוס מסוים וואלידאציה ברמת הטווח (כלומר – לאפשר לבדוק שערך של טיפוס מסוים הוא בטווח מסוים, למשל בין לבדוק שהמספר חיובי וקטן מ10)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static bool IsNumeric(this Type type)
{
TypeCode code = Type.GetTypeCode(type);
if (code == TypeCode.Byte ||
code == TypeCode.Decimal ||
code == TypeCode.Double ||
code == TypeCode.Int16 ||
code == TypeCode.Int32 ||
code == TypeCode.Int64 ||
code == TypeCode.SByte ||
code == TypeCode.Single ||
code == TypeCode.UInt16 ||
code == TypeCode.UInt32 ||
code == TypeCode.UInt64)
{
return true;
}
return false;
}

לא הכי יפה, אבל משיג את המטרה…

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

שתף

314. 32bit, 64bit and in between

[נכתב ע”י עודד לזר]

יצא לי להתקל בבעיה עם הרמה של סרביס בשרת 64bit אתמול.

הסרביס עבד מעולה על שרת 32bit ולא ראיתי שום סיבה ממשית שהוא לא יעבוד על שרת 64bit, במיוחד כשיש WOW64 (אמולציה של 32bit)

למזלי הייתי בסדנת מיגרציה מ 32bit ל64bit ושם אלי אופק דיבר איתנו על תרחישים אפשריים בתהליך מיגרזציה שכזה ועל כלים ופתרונות לבעיות שעלולות לצוץ.

אז הנה כמה נקודות שכדאי שתכירו:

  1. מושגים בסיסיים:
    • 32bit – x86 (“תואמי אינטל 😃”)
    • 64bit:
      • x64
      • AMD64
      • EM64T
  2. כשאתם יוצרים solution בvisual studio אתם יכולים לבחור לאיזה platform הוא יתקמפל. המשמעות היא שאחרי שתקפלו יודלקו / לא יודלקו מס’ דגלים בclr header של הassembly וכאשר תריצו את הassembly הclr יסתכל על הדגלים האלו ויחליט כיצד לקמפל את הassembly.
  3. אם הassembly נטען לפלטפורמה מסוימת, גם כל הreferenced assemblies נטענים לאותה הפלטפורמה. ז”א שאם יש exe שעולה כ64bit גם כל הreferenced assemblies צריכים להיות ב64bit או anycpu (שזה אומר שהביט של ILONLY מודלק אבל לא 32bit required)
  4. אפשר להתסכל על הheader של הקובץ עם מס’ כלים, ביניהם:
    • corflags – מאפשר לראות ולערוך את clr header של assembly
    • dumpbin: מאפשר לראות headers של קובץ (לא בהכרח assembly)
  5. בtaskmanager אפשר לדעת אם exe עלה כ64bit או 32bit לפי כוכבית שיש ליד השם שלו.
  6. ל-Dependency walker יש גרסאת 32bit ו64bit ושימו לב שאם אתם רוצים לבדוק dependencies של קובץ, תרוצו עליו עם הגרסא הנכונה של dependency walker

אז איך זה מתחבר אלי? מסתבר שהסרביס שהרמתי קומפל לanycpu וכשהרצתי אותו על שרת 64bit הוא לא הצליח לטעון את אחד הassemblies שקומפל לx86 (ולא לanycpu / x64)

תודה,

שתף

313. TypeCode

הכרנו בעבר (טיפ מספר 137) את הטיפוס Type ודרכים להשתמש בו.

לפעמים אנחנו מעוניינים לדעת האם Type מסוים הוא “פרימיטיבי” במובן מסוים. למשל, כל הטיפוסים הנומריים הבסיסיים, מחרוזות ותאריכים, נחשבים בד”כ לפרימיטיביים.

קיימת דרך נחמדה לעשות זאת, והיא ע”י שימוש בEnum שנקרא TypeCode. הEnum הזה מכיל הרבה טיפוסים שנחשבים בד”כ לפרימיטיביים:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum TypeCode
{
Empty = 0,
Object = 1,
DBNull = 2,
Boolean = 3,
Char = 4,
SByte = 5,
Byte = 6,
Int16 = 7,
UInt16 = 8,
Int32 = 9,
UInt32 = 10,
Int64 = 11,
UInt64 = 12,
Single = 13,
Double = 14,
Decimal = 15,
DateTime = 16,
String = 18,
}

כדי להשיג מType את הTypeCode שלו, יש דרך פשוטה לעשות את זה:

1
2
3
4
TypeCode objectTypeCode = Type.GetTypeCode(typeof(object)); // Object
TypeCode stringTypeCode = Type.GetTypeCode(typeof(string)); // String
TypeCode intTypeCode = Type.GetTypeCode(typeof (int)); // Int32
TypeCode otherTypeCode = Type.GetTypeCode(typeof(FileStream)); // Object

בסה"כ זה שימושי בעיקר כשכותבים Serializer/Deserializer ואז רוצים לטפל בטיפוסים פרימיטיביים בצורה אחת, ובטיפוסים אחרים בצורה אחרת. מה שעושים זה פשוט משווים את הTypeCode שיוצא עםTypeCode.Object, ובמידה וזה יוצא שווה, יודעים שהטיפוס הוא לא טיפוס פרימיבי.

המשך יום טיפוס-צופן טוב.

שתף

312. About Enumerable Repeat

הכרנו בעבר (ראו גם טיפ מספר 153) את הפונקציה Enumerable.Repeat המאפשרת לנו ליצור אוסף המכיל איבר מסוים מספר נתון של פעמים.

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

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

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

1
2
3
4
5
6
Person[] people = new Person[100];
for (int i = 0; i < people.Length; i++)
{
people[i] = new Person();
}

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

1
Person[] people = Enumerable.Repeat(new Person(), 100).ToArray();

אלא שזה לא ייתן לנו אותה תוצאה, מאחר ומה שיקרה בשיטה השנייה שרשמנו זה יווצר מערך שכל התאים מחזיקים Reference לאותו הInstance של Person.

אם היה לנו את הOverload שדיברתי עליו, היינו יכולים לעשות משהו כזה:

1
Person[] people = Enumerable.Repeat(() => new Person(), 100).ToArray();

למעשה זה לא מסובך לממש Overload כזה, וזה נראה בערך ככה:

1
2
3
4
public static IEnumerable<T> Repeat<T>(Func<T> creator, int count)
{
return Enumerable.Range(0, count).Select(x => creator());
}

(ראו גם טיפ מספר 152, 92)

מה הבעייתיות בדבר הזה? אם אנחנו רק רוצים לקרוא לToArray, זה אחלה. זה עושה בדיוק את מה שרצינו.

ברגע שאנחנו לא קוראים לToArray, מתחילה הבעייתיות.

אפשר באופן תמים לחלוטין לכתוב קוד כזה:

1
2
3
4
5
6
7
8
IEnumerable<Person> people = Repeat(() => new Person(), 100);
Random random = new Random();
foreach (Person person in people)
{
person.Id = random.Next();
}

הקוד נראה תקין לחלוטין, אנחנו מאתחלים אוסף של Person, ואז דואגים לאתחל לכל אחד מהם את הId לאיזשהו מספר אקראי (במקום 0).

אלא שאם תריצו את הקוד הבא, תגלו את ההפתעה הבאה:

1
2
3
4
foreach (Person person in people)
{
Console.WriteLine(person.Id); // 0
}

כן, כן, כל הIdים מחזירים 0.

האם הRandom לא עבד טוב? האמת שהוא עבד מצוין. אז מה הבעיה?

כזכור (טיפים על LINQ – למשל מספרים 91-94), כאשר אנחנו מריצים LINQ, כל האוספים שחוזרים לנו הם Lazy.

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

אם נזכר, זה בין השאר מאחר וLINQ מאוד מבוססת תכנות פונקציונאלי, וחלק מהעקרונות של תכנות פונקציונאלי הם אובייקטים שהם Immutable (טיפ מספר 271) וStateless.

לכן כל פעם שנרוץ על people, יחושב מחדש האוסף, ולמעשה יאותחלו מחדש כל איבריו.


מה המסקנה? יש דברים שלא מתערבבים. לא סתם לא הכניסו לLINQ כל מיני Extension Methods כמו ForEach, הסיבה היא שזה נוגד את העקרונות שLINQ נבנה עליהם.

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

ואולי עדיף לא להכניס מתודה כזאת, אלא לאתחל מערך בשיטה הקלאסית.

המשך יום שלא חוזר על עצמו יותר מדי טוב.

שתף

311. Multiple enumeration on a given IEnumerable

לא אחת יוצא לנו לכתוב פונקציות שמקבלות IEnumerable כפרמטר.

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

את הבדיקות האלה אנחנו מבצעים בעזרת הExtension Methods של LINQ. מה שהן עושות מאחורי הקלעים זה קוראות לGetEnumerator() ומתחילות לרוץ על האוסף. (ראו טיפים מספר 51, 15, 91-105 ,291 ועוד)

הדבר הזה יכול להיות מסוכן, במידה ואנחנו קוראים ליותר מExtension Method אחד כזה, מאחר וזה גורם ליותר מריצה אחת על האוסף.

למה זה יכול להיות מסוכן? ראו למשל את הקוד הבא:

1
2
3
4
5
6
7
8
9
10
11
private static int mCallCount = 0;
public IEnumerable<int> GetRange()
{
mCallCount++;
for (int i = 0; i < mCallCount; i++)
{
yield return (i + mCallCount);
}
}

הקוד הזה מחזיר לנו IEnumerable, אבל כל פעם שנרוץ עליו הוא יכיל איברים שונים. בהתחלה הוא יכיל את המספר 1.

אחר כך את המספרים 2,3. אחר כך את המספרים 3,4,5.

(ראו טיפ מספר 54-55 אם אתם לא מכיר את yield return)

למשל אם נריץ את הקוד הבא נקבל:

1
2
3
4
5
6
IEnumerable<int> range = GetRange();
Console.WriteLine(range.First()); // 1
Console.WriteLine(range.First()); // 2
Console.WriteLine(range.First()); // 3
Console.WriteLine(range.First()); // 4

הReSharper יודע לזהות את הסיפור הזה ומציג אזהרה לקוד החל מReSharper 6.

אז מה בעצם אפשר לעשות? מצד אחד אפשר לומר שזו בעיה של המשתמש – אם הוא שלח לנו לפונקציה איזשהו IEnumerable שהוא לא עקבי, זו בעיה שלו, והוא עשוי לקבל פלט לא צפוי מהפונקציה.

אופציה אחרת היא למנוע ריצה על האוסף יותר מפעם אחת, וזה ע"י קריאה לExtension Method ששמו ToList() אשר יוצר רשימה שמכילה את כל איברי הIEnumerable ע"י ריצה עליו, ואז לבצע את כל הפעולות מול הרשימה הזאת. אלא שגם דרך זה אינה חסינה ממספר בעיות: מה אם הIEnumerable הוא אינסופי? או מה אם במהלך הריצה, מוחזר תמיד אותו Reference של אובייקט, אבל האובייקט משתנה (ראו לדוגמה את הטיפ עלScan, המאפשר לעשות דבר כזה, טיפ מספר 305)

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

בכל מקרה כדאי להכיר את הבעייתיות הזו ולהיזהר בחלק משימוש כזה.

המשך יום בלי יותר מריצה אחת.

הערה: דרך נוספת, אם בכל זאת רוצים לעשות binding למשתנה אשר נמצא מחוץ ל- scope, היא להשתמש בעותק מקומי שלו:

(למרות שבמקרה ספציפי זה זה לא נכון).

1
2
3
4
5
6
7
8
9
10
11
private static int mCallCount = 0;
public IEnumerable <int> GetRange()
{
var callCount = mCallCount + 1;
for (int i = 0; i < callCount; i++)
{
yield return (i + callCount);
}
}
שתף