310. Jagged arrays vs Multi-dimensional arrays

אז פעם שעברה הכרנו קצת את הקונספט של Jagged Arrays וראינו מה ההבדל ביניהם לMulti-dimensional Arrays.

כזכור, מערך רב מימדי ממומש ע”י מערך חד-מימדי רצוף באורך של מספר התאים שלו.

לעומת זאת, Jagged Array ממומש ע”י מספר מערכים בזכרון שלאו דווקא רצופים זה אחר זה.

עושה רושם שגישה אל מערך רב מימדי אמורה להיות מהירה יותר, שהרי המערך כולו יושב רצוף בזכרון.

עם זאת, באופן מפתיע, מסתבר שגישה אל מערך שהוא Jagged היא יותר מהירה.

הרצתי את הקוד השקול הזה:

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
43
44
45
46
private static void JaggedArrayTest(int width, int height, int length)
{
int[][][] matrix;
// Initialize array
// ...
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
for (int k = 0; k < length; k++)
{
matrix[i][j][k] = i*j*k;
}
}
}
sw.Stop();
Console.WriteLine("Jagged Array: {0}", sw.ElapsedMilliseconds);
}
private static void MultidimensionalArrayTest(int width, int height, int length)
{
int[,,] matrix = new int[width,height,length];
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
for (int k = 0; k < length; k++)
{
matrix[i, j, k] = i*j*k;
}
}
}
sw.Stop();
Console.WriteLine("Multidimensional: {0}", sw.ElapsedMilliseconds);
}

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

באופן דומה, אפשר להריץ קוד של קריאות ולראות שגם שם המערך הרב מימדי מפסיד בביצועים.


למה זה קורה?

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

הקריאה לפונקציה היא מה שעולה את רוב הזמן.

אז למה לא מרגישים את זה בJagged Array? הרי גם שם אנחנו ניגשים בIndexer, ואפילו פעמיים!

ובכן, הקומפיילר מקמפל גישה למערך חד מימדי בצורה יותר מהירה מגישה לפונקציה: זה מתבצע ע"י פקודות IL ייעודיות לכך:

  • stelem.ref – להשמה במערך
  • ldelem.ref – לקריאה ממערך

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

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

1
2
call instance void int32[0...,0...,0...]::Set(int32, int32, int32, int32)
call instance int32 int32[0...,0...,0...]::Get(int32, int32, int32)

לקריאה לפונקציה יש Overhead נוסף, מאחר וצריך לדחוף את הפרמטרים המתאימים במחסנית וכו’.


אז מה המסקנה?

אם אתם משתמשים בקונספט של מערכים רב-מימדיים, והביצועים ממש קריטיים לכם, כדאי לשקול להשתמש בJagged Arrays.

אחרת, אין סיבה לא להשתמש במערכים רב-מימדיים רגילים.

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

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

סוף שבוע רב מימדי טוב!

שתף

309. Jagged Array

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

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

השנייה היא באמצעות משהו שנקרא Jagged Array. מי שתכנת בJava כנראה יצא לו להתקל בקונספט הבא: נייצג מערך דו מימדי בתור מערך חד מימדי של מערכים חד מימדיים:

1
int[][] matrix;

לייצוג זה יש יתרונות וחסרונות. מאחר ומדובר במערך של מערכים, ניתן לדאוג שלכל "שורה" יהיה אורך אחר. מצד שני, אם אנחנו רוצים להשתמש בו בתור מערך דו-מימדי, השימוש הוא מעט פחות נוח, למשל ככה יראה אתחול של מערך דו מימדי כזה בגודל 3 על 4:

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

למי שבא מJava, הקוד הבא לא עובד:

1
int[][] matrix = new int[3][4];

בהמשך נדבר על השוואות ביצועים בין מערך דו מימדי לJagged Array.

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

שתף

308. Array Indexer

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

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

הדרך הראשונה היא באמצעות הפונקציות GetValue וSetValue של המחלקה האבסטרקטית Array ממנה יורשים כל המערכים:

(ChessPiece הוא איזשהו enum)

1
2
3
ChessPiece[,] board = new ChessPiece[8,8];
board.SetValue(ChessPiece.Pawn, 1, 0);
ChessPiece king = (ChessPiece)board.GetValue(0, 4);

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

מצד שני, זה לא Typed-safe.

בנוסף, יש פה Boxing, מאחר והפונקציות האלה מקבלות ומחזירות object.

מה שנחמד במערכים, זה שלכל מערך יש Indexer שהוא Typed-safe. אפשר להשתמש בו כך:

1
2
3
ChessPiece[,] board = new ChessPiece[8,8];
board[1, 0] = ChessPiece.Pawn;
ChessPiece king = board[0, 4];

שימו לב שבניגוד לדוגמה הקודמת, הדוגמה הזאת קורית בלי Boxing!

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

1
2
3
int[,,,,,,,,,] tensor = new int[1,2,3,4,5,6,7,8,9,10];
tensor[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] = 42;
int fourtyTwo = tensor[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

במחלקה גנרית משלנו אי אפשר לעשות משהו כזה – אם היינו מממשים בעצמנו מערך, לא היינו יכולים להנות מFeature כזה.

המשך יום מאונדקס לטובה!

שתף

307. Non zero based arrays

כולנו מכירים מערכים בC#.

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

מסתבר שבC# מערכים יכולים להתחיל גם מאינדקס שאינו 0. איך זה אפשרי? הרי אנחנו מכירים שהדרך של לאתחל מערך היא בצורה הזאת:

1
int[] myArray = new int[6];

איפה אפשר בדיוק להכניס את הlower bound של המערך כאן?

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

זה נראה בערך ככה:

1
int[] myArray = (int[]) Array.CreateInstance(typeof (int), 6);

אלא שיש Overloadים שמאפשרים לנו לציין את הLower bound:

1
Array myArray = Array.CreateInstance(typeof (int), new[] {6}, new[] {2});

אם ננסה לעשות הסבה למערך שהוא Typed safe, לא נצליח:

1
int[] myArray = (int[]) Array.CreateInstance(typeof (int), new[] {6}, new[] {2});

נקבל את הException הבא:

Unable to cast object of type ‘System.Int32[*]’ to type ‘System.Int32[]’.

לא כל כך ברור מהו הטיפוס הזה של המערך.

נוכל לגשת ע"י שימוש בפונקציות של Array, שהן לא Typed safe (ומבצעות boxing):

1
2
3
4
Array myArray = Array.CreateInstance(typeof (int), new[] {6}, new[] {2});
myArray.SetValue(3, 7);
Console.WriteLine(myArray.GetValue(7)); // 3

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

1
2
3
4
int[,] myMatrix =
(int[,]) Array.CreateInstance(typeof (int), new[] {6, 8}, new[] {2, 3});
myMatrix[7, 10] = 3;

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

בכל מקרה הדבר בעיקר מעניין, אבל לא הייתי ממליץ להשתמש בזה, מאחר ורובנו רגילים כי מערכים הם Zero based ולכן אם מישהו יתקל בקוד כזה הוא עשוי לשבור את הראש מה בדיוק קורה פה.

יום לא Zero-based טוב!

שתף

306. MapReduce

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

אמרתי שהפעולה הזאת דומה לפעולה Map Reduce מתכנות פונקציונאלי.

התבלבלתי – הדבר לא נכון והפעם אני אסביר קצת על Map Reduce.

הדרך הפשוטה ביותר להסביר את Map Reduce היא כזאת: נניח שיש לנו אוסף רב של עובדים. על כל עובד אנחנו יודעים מספר פרטים, למשל, את הגיל שלו, את המשכורת שלו, את המחלקה אליה הוא שייך, את מספר הילדים שיש לו וכו’.

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

כך אנחנו מקבלים בעצם מיפוי של מאפיין אחד של העובדים (למשל מחלקה) לנתון שמייצג משהו אודות כל העובדים עם המאפיין הנ”ל.

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

איך כותבים דבר כזה?

מתבקש להשתמש בGroup By (ראו טיפ מספר 115-116) ובSelect (ראו טיפ מספר 92).

הדבר נראה בערך ככה:

1
2
3
4
5
6
7
8
9
10
11
12
public static IDictionary<TPivot, TResult> MapReduce<TSource, TPivot, TResult>
(this IEnumerable<TSource> source,
Func<TSource, TPivot> groupSelector,
Func<IGrouping<TPivot, TSource>, TResult> valueSelector)
{
Dictionary<TPivot, TResult> result =
source.GroupBy(x => groupSelector(x))
.Select(x => new {Key = x.Key, Value = valueSelector(x)})
.ToDictionary(x => x.Key, x => x.Value);
return result;
}

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

1
2
3
IDictionary<string, double> departmentsToAverageSalary =
employers.MapReduce(employer => employer.Department,
department => department.Average(employer => employer.Salary));

מה זה נותן לנו?

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

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

(וחג שמח!)

שתף

305. Scan extension method

הכרנו בעבר (טיפ מספר 102) את הפונקציה Aggregate, המאפשרת לנו לחשב את התוצאה של פעולה איטרטיבית.

פונקציה זו מקבילה לפעולה Reduce מתכנות פונקציונאלי:

בנוסף, הכרנו את הפונקציה Select (טיפ מספר 92) הממפה לנו אוסף של איברים לאוסף אחר של איברים ע”י הפעלת פונקציה מסוימת על כל אחד מהאיברים באוסף.

פונקציה זו מקבילה לפעולה Map מתכנות פונקציונאלי.

בנוסף, קיימת פעולה נוספת בתכנות פונקציונאלי המשלבת את שתי הפעולות, שנקראת Map-Reduce, היא מבצעת איטרציות כמו Map, אבל מחזירה אוסף של תוצאות של האיטרציות (בדומה לReduce).

לצערנו, אין פונקציה כזאת בSystem.Linq, אבל נוכל לממש אותה (למעשה, מימשו אותה בReactive Extensions בגרסאות המוקדמות, והיום זה נמצא בהרחבה אחרת שנקראת Interactive Extensions).

המימוש נראה כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static IEnumerable<TAccumulate> Scan<TSource, TAccumulate>
(this IEnumerable<TSource> source,
TAccumulate seed,
Func<TAccumulate, TSource, TAccumulate> func)
{
TAccumulate accumulate = seed;
foreach (TSource current in source)
{
yield return accumulate;
accumulate = func(accumulate, current);
}
yield return accumulate;
}

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

1
2
3
4
5
6
7
8
9
IEnumerable<int> sums =
Enumerable.Range(1, 10).Scan(0, (result, current) => current + result);
foreach (int sum in sums)
{
Console.WriteLine(sum);
}
// 0,1,3,6,10,15,21,28,36,45,55

בברכת זה שאני לא נמצא, לא אומר שהטיפ היומי מת,

המשך יום טוב!

שתף

303. Event subscription hack

[נכתב ע”י עמית יוגב]

רובנו מכירים את הבעיה כשאנחנו מנסים לעשות Raise לevent אבל אף אחד לא רשום אליו:

1
2
3
4
5
6
7
8
9
10
public class EventHack
{
public event Action<string> OnAdd;
public void Add(string numbers)
{
OnAdd(numbers);
//Calculation...
}
}

אם נקרא לOnAdd ואף אחד לא יהיה רשום אליו – נקבל שגיאת System.NullReferenceException 😞

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

1
2
3
4
5
6
7
public void RaiseOnAdd(string numbers)
{
if(OnAdd!=null)
{
OnAdd(numbers);
}
}

אבל זה מכוער ויוצר הרבה קוד מיותר

מה אפשר לעשות במקום?

נאתחל את הevent בצורה הבאה:

1
public event Action<string> OnAdd = delegate{};

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

המשך יום נחמד 😃

שתף

304. string Intern

[נכתב ע”י שני אלחרר]

אהלן.

הטיפ היומי הוא על תכולה נפלאה בCLR שעוזרת לנו לחסוך בזיכרון ובזמני ריצה – “String Interning”.

לCLR של .Net יש Pool של stringים שאנחנו יכולים להשתמש בו, ולרוב משתמשים בו (כשאנחנו משתמשים בstring שידוע בזמן קימפול). והוא עוזר לנו לחסוך זיכרון ואפילו לקצר את זמני הריצה באופן ישיר.

איך הוא עוזר לנו לחסוך זיכרון ?

אם אני אעשה את הפעולה הבאה :

1
2
3
string a = "a" + 1;
string b = "a" + 1;
Console.Out.WriteLine(ReferenceEquals(a, b));

אקבל בפלט את הערך false, זה אומר שאני מחזיק את המחרוזת "a1" פעמים בזיכרון.

אם אריץ את הפקודה הבאה :

1
2
3
string a = "a";
string b = "a";
Console.Out.WriteLine(ReferenceEquals(a, b));

אקבל את הערך True, זאת מכיוון שאנחנו מקבלים את הStringים אוטומטית מהPool כאשר אנחנו עושים השמה של stringים שהם קבועים בזמן קמפול.

כלומר, איני צריך לבקש מהCLR את הstring מהPool אם אני שם ערך שהוא קבוע בזמן קימפול, אבל אם יש לי string שנבנה באופן דינאמי, אוכל לבקש את הערך שלו כך :

1
2
3
string a = string.Intern("a" + 1);
string b = string.Intern("a" + 1);
Console.Out.WriteLine(ReferenceEquals(a, b));

ולכן גם במקרה הזה אקבל את הערך true, כמובן שבמקום הביטוי "a" + 1 אוכל להכניס כל ביטוי שנבנה בזמן ריצה.

נוכל לחסוך כך זיכרון בכך שיש לי ייצוג של כל string אך ורק פעם אחת בזיכרון, ובזמן ריצה בכך שאם אני משווה stringים, אז השוואת ה-ReferenceEquals (שמתבצעת ראשונה) תחזיר true – וזוהי ההשוואה המהירה ביותר, ולכן אנחנו בודקים Referenceים לפני שאנחנו בודקים השוואה של תו לתו בין המחרוזות.

במקרה שאנחנו יוצרים Stringים דינאמית כדי לכתוב לLog או לConsole, ואנחנו מבינים שלא נצטרך לעשות איתו הרבה פעולות – אין צורך לבצע string.Intern על המחרוזת, כי זה יקח יותר זמן לחפש אותו בPool מאשר להשתמש ולזרוק את המחרוזת.

בברכת יום ירוק וממוחזר.

שתף

302. ExpectedException

[נכתב ע”י ג’ייסון פיין]

שלום,

היום נתקלתי במשהו שלא הכרתי בעולם הUnit Testing שיכול להיות מאוד שימושי ורציתי לשתף אתכם בו.

לפעמים אנחנו רוצים לכתוב Unit Test שאנחנו יודעים שאמור להזרק בו Exception ואנחנו מעוניינים לבדוק שזה קורה באמת ושהException שנזרק הוא הException שאותו ציפינו לקבל.

הרבה פעמים אני רואה קוד שנראה ככה בערך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[TestMethod]
public void DivideTest()
{
bool exceptionThrown = false;
try
{
float f = 1000f / 0f;
}
catch (DivideByZeroException)
{
exceptionThrown = true;
}
Assert.IsTrue(exceptionThrown,"No DivideByZeroException was thrown when dividing a float by zero");
}

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

פה נכנס לעניין הAttribute ExpectedException שמציין על מתודות של Test שאנחנו מצפים שייזרק שם Exception ומאיזה סוג הוא אמור להיות. אם הException לא נזרק או שנזרק אבל מסוג אחר אז הTest יכשל.

בעזרת שימוש בAttribute הזה הבדיקה תראה כך:

1
2
3
4
5
6
[TestMethod]
[ExpectedException(typeof(System.DivideByZeroException))]
public void DivideTest()
{
float f = 1000f/0f;
}

כמו שאתם רואים - הרבה יותר פשוט וקריא.

לעומת זאת יש פה גם חסרונות:

  • אם אנחנו רוצים לבדוק שהException שנזרק מכיל שגיאה מסויימת אז אנחנו לא יכולים – למשל עבור השגיאה OracleException הType הוא תמיד אותו הType אבל הטקסט של השגיאה מכיל משהו אחר כל פעם – ולכן לא נוכל לבדוק ככה שהשגיאה שנזרקה היא הנכונה. רצוי לציין שיש לזה פתרונות בNUnit אבל לא בMS-Test.
  • אם היה לכם UT שבדק כמה Exception ים אז תצטרכו להפרידו לUT ים נפרדים. כמובן שזה לא באמת בעיה היות וככה צריך שהUT יהיו בכל מקרה ולא יבדקו כמה דברים באותו Test.

בברכת "Unit Test גם יכול להיות לא כורעני",

שתף

301. IReadOnlyList{T}, IReadOnlyDictionary{TKey,TValue}

[מבוסס על הכתבה הזאת]

הפעם יהיה טיפ מהעתיד – אנחנו נדבר על יכולות שהוסיפו בFramework 4.5.

עוד בFramework 1.0, התלוננו מתכנתים רבים על כך שאין ממשקים של ICollection או IDictionary שהם Readonly, כלומר חסרי הפונקציות Add, Remove ופעולות שמשנות את הCollection.

התגובה של מיקרוסופט הייתה פשוטה: מבחינתנו כל Collection דיפולטית הוא Readonly. כלומר, אם אתם רוצים לקרוא לאחת הפונקציות שמשנות את הCollection (למשל Add או Remove) – תתכוננו לחטוף Exception, אלא אם כן אתם יודעים שהCollection הוא לא ReadOnly. (ותוכלו לבדוק אם הוא ReadOnly לפי הProperty של ICollection בשם IsReadOnly)

אם נסתכל בהערות של ICollection, למשל, נראה את הדבר הבא:

Throws:
System.NotSupportedException: The System.Collections.Generic.ICollection is read-only.

כמובן, הדבר הציק למתכנתים, אבל הם הושקטו ע”י מיקרוסופט, כשהוציאו את הטיפוס ReadOnlyCollection, שמקל על המימוש של Collection שהוא ReadOnly, אבל יותר מזאת: הוא Type-safed! יחי הGenerics. (ראו גם טיפ מספר 4)

הנושא קיבל תשומת לב מחדש, כשבFramework 4.0 הציגו את הCovariance ואת הContravariance. (ראו גם טיפים מספר 36-40)

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

1
2
IEnumerable<Triangle> triangles = new List<Triangle>();
IEnumerable<Shape> shapes = triangles;

הכל טוב ויפה, אבל לפעמים יש לנו מבנה שהוא טיפה יותר מורכב מIEnumerable, למשל IList שיש לו Indexer לפי אינדקס, או IDictionary, שמאפשר לנו לקבל ערך לפי מפתח. (ולבדוק מי המפתחות/הערכים)

עם זאת, מיקרוסופט עדיין לא הוסיפו ממשקים כאלה לFramework 4.0.

כפי שוודאי יכולתם לנחש, לFramework 4.5, כן נוספו ממשקים בשמות IReadOnlyDictionary<TKey, TValue> ו IReadOnlyList<T>.


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

הרי ראינו שמיקרוסופט לא היו להוטים להוסיף אותם אי פעם לFramework.

ובכן, מה שקרה זה שהחל מFramework 4.5, יש תמיכה של מיקרוסופט במשהו שנקרא Windows RT (Realtime). בגדול זהו API שהולך להחליף את הWindows 32 API הוותיק. בין השאר מה שהולך לקרות זה שהולכת להיות תמיכה שקופה בתקשורת בין שפות, למשל C# יוכל לדבר עם JavaScript ועם COM בצורה מאוד אלגנטית. הWindows RT ידע להמיר למשל את הממשקים המתאימים מCOM לממשקים המתאימים ב.net.

(אם למישהו בא לתקן/לדייק, הוא מוזמן)

מסתבר שבCOM יש ממשקים בשם IVectorView<V> וIMapView<S,U>, שמייצגים למעשה Dictionary וList שהם ReadOnly. לכן, בשביל ה Interoperability, מיקרסופט היו חייבים להוסיף ממשקים שקולים גם ב.net.

אוקיי, עכשיו בואו נסתכל על הממשקים:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface IReadOnlyList<out T> : IEnumerable<T>
{
int Count { get; }
T this[int index] { get; }
}
public interface IReadOnlyDictionary<TKey, TValue> :
IEnumerable<KeyValuePair<TKey, TValue>>
{
int Count { get; }
TValue this[TKey key] { get; }
IEnumerable<TKey> Keys { get; }
IEnumerable<TValue> Values { get; }
bool ContainsKey(TKey key);
bool TryGetValue(TKey key, out TValue value);
}

יש כמה דברים מעניינים להגיד:

דבר ראשון: היה אפשר לחשוב ש IList<T>וIDictionary<TKey, TValue> ירשו מממשקים אלה בהתאמה, אך לא כך המצב. הסיבה לכך היא שאם זה היה ככה – לא הייתה תאימות לאחור, מאחר וכשמממשים ממשק באופן Explicitly (ראו טיפ מספר 82-83), מציינים עבור כל מתודה לאיזה ממשק היא שייכת. והמתודות שנמצאות בממשקים שהם ReadOnly כבר לא תהינה שייכות לממשקים שהם לא ReadOnly. כלומר, ההזדמנות היחידה לדאוג לכך שהממשקים ירשו מהממשקים שהם ReadOnly, הייתה בFramework 2.0, אבל הם פספסו את ההזדמנות.

דבר שני: היה אפשר לחשוב שיהיה ממשק בשם IReadOnlyCollection<out T> שיש לו… Property בשם Count. הדבר הוא נשקל, אבל לבסוף ויתרו עליו. הסיבה היא כזאת: מבחינתם, ברגע שאתה יוצר ממשק חדש, אתה מתחיל עם מינוס אלף נקודות. כעת, מכאן צריך לתת כמה שיותר נימוקים לקיום הממשק כדי לצבור יותר ויותר נקודות.

אם אין לך מספיק נקודות, כנראה אין סיבה מספיק טובה לממשק. ואכן זה כך. סביר להניח שאם נרצה Property בשם Count, זה מאחר והוא יכול להשתנות, כלומר אפשר להוסיף איברים לCollection. מאחר וזה כך, אפשר לממש כבר את הממשק ICollection. ואנחנו כבר יודעים (ראו גם טיפ מספר 15) שהExtension Method של Count() יודע לגשת לProperty הזה כאשר אנחנו מממשים ICollection.

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

המשך יום מהעתיד טוב.

שתף