על מערכים גדולים יצא לי שלפעמים לוקח למערך הרב מימדי כמעט פי 2 יותר זמן לבצע פעולה זו מהמערך הJagged.
באופן דומה, אפשר להריץ קוד של קריאות ולראות שגם שם המערך הרב מימדי מפסיד בביצועים.
למה זה קורה?
הסיבה היא כזאת: אמנם מערכים רב מימדיים יושבים ברצף בזכרון, אבל הגישה אליהם מתבצעת באמצעות Indexer. הIndexer הזה הוא למעשה פונקציה לכל דבר, ולכן מתבצעת קריאה לפונקציה כדי להגיע למקום המתאים בזכרון.
הקריאה לפונקציה היא מה שעולה את רוב הזמן.
אז למה לא מרגישים את זה בJagged Array? הרי גם שם אנחנו ניגשים בIndexer, ואפילו פעמיים!
ובכן, הקומפיילר מקמפל גישה למערך חד מימדי בצורה יותר מהירה מגישה לפונקציה: זה מתבצע ע"י פקודות IL ייעודיות לכך:
stelem.ref – להשמה במערך
ldelem.ref – לקריאה ממערך
לכן במערכים שהם Jagged, שהם מערכים חד מימדיים לכל דבר, גישה לאיבר מסוים מתבצעת ע"י גישה מהירה.
במערכים רב-מימדיים רגילים, הגישה מתבצעת ע"י קריאה לאחת מהפונקציות:
בהמשך לאווירת המערכים מפעמים קודמות, קיימות שתי דרכים לייצג מערך דו מימדי:
הראשונה היא הדרך הקלאסית אותה מכיר מי שתכנת בשפות אחרות כמו C, והיא ע”י מערך דו מימדי, שבעצם מדובר בייצוג נוח של הקומפיילר במערך חד מימדי רצוף של כל השורות בזו אחר זו. מאחר והשורות נמצאות ברצף זו אחר זו, מלמדים שעדיף לעבור על מערך כזה כאשר קודם רצים על שורות, ובלולאה פנימית רצים על העמודות של כל שורה.
השנייה היא באמצעות משהו שנקרא Jagged Array. מי שתכנת בJava כנראה יצא לו להתקל בקונספט הבא: נייצג מערך דו מימדי בתור מערך חד מימדי של מערכים חד מימדיים:
1
int[][] matrix;
לייצוג זה יש יתרונות וחסרונות. מאחר ומדובר במערך של מערכים, ניתן לדאוג שלכל "שורה" יהיה אורך אחר. מצד שני, אם אנחנו רוצים להשתמש בו בתור מערך דו-מימדי, השימוש הוא מעט פחות נוח, למשל ככה יראה אתחול של מערך דו מימדי כזה בגודל 3 על 4:
1
2
3
4
5
6
int[][] matrix = newint[3][];
for (int i = 0; i < matrix.Length; i++)
{
matrix[i] = newint[4];
}
למי שבא מJava, הקוד הבא לא עובד:
1
int[][] matrix = newint[3][4];
בהמשך נדבר על השוואות ביצועים בין מערך דו מימדי לJagged Array.
הזכרתי את זה בקטנה פעם הקודמת, אבל מאחר וגיליתי שלא כולם מכירים את זה, נראה לי שזה שווה את הטיפ.
כשאנחנו עובדים עם מערכים נורמליים (כאלה שנוצרים באמצעות האופרטור 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 הזה, למשל:
האמת שלא כל כך ברור מה הולך כאן, ואם למישהו יש הסבר, אני אשמח לשמוע.
בכל מקרה הדבר בעיקר מעניין, אבל לא הייתי ממליץ להשתמש בזה, מאחר ורובנו רגילים כי מערכים הם Zero based ולכן אם מישהו יתקל בקוד כזה הוא עשוי לשבור את הראש מה בדיוק קורה פה.
פעם קודמת סיפרתי קצת על הפונקציה Scan שמבצעת פעולה דומה לAggregate, רק שמחזירה אוסף של כל שלבי האיטרציות.
אמרתי שהפעולה הזאת דומה לפעולה Map Reduce מתכנות פונקציונאלי.
התבלבלתי – הדבר לא נכון והפעם אני אסביר קצת על Map Reduce.
הדרך הפשוטה ביותר להסביר את Map Reduce היא כזאת: נניח שיש לנו אוסף רב של עובדים. על כל עובד אנחנו יודעים מספר פרטים, למשל, את הגיל שלו, את המשכורת שלו, את המחלקה אליה הוא שייך, את מספר הילדים שיש לו וכו’.
מה שאנחנו יכולים לעשות זה דבר כזה: נקבץ את העובדים לפי איזשהו מאפיין, למשל מחלקה. לאחר מכן, נסתכל על איזשהי אגרגציה על קבוצות אלה, למשל ממוצע של הגילאים של העובדים במחלקה, או משכורת מקסימלית.
כך אנחנו מקבלים בעצם מיפוי של מאפיין אחד של העובדים (למשל מחלקה) לנתון שמייצג משהו אודות כל העובדים עם המאפיין הנ”ל.
דבר כזה נקרא Map Reduce של העובדים – קודם אנחנו מקבצים את כל הנתונים לפי נתון מסוים, ולאחר מכן משטחים את כל הקבוצות לנתון בודד שמייצג את הקבוצה.
איך כותבים דבר כזה?
מתבקש להשתמש בGroup By (ראו טיפ מספר 115-116) ובSelect (ראו טיפ מספר 92).
department => department.Average(employer => employer.Salary));
מה זה נותן לנו?
הדבר הזה בעצם מאפשר לנו "למצות מידע" על אוסף רב של נתונים: במקום להשאר עם כמות גדולה של נתונים, אנחנו יכולים לסכם את הנתונים לתמונה יותר כללית המאפשרת לנו לראות סטטיסטיקות שמעניינות אותנו. ככה אנחנו בעצם רואים כיצד מתפלג משתנה מסוים תחת קבוצות שונות.
בנוסף, קיימת פעולה נוספת בתכנות פונקציונאלי המשלבת את שתי הפעולות, שנקראת Map-Reduce, היא מבצעת איטרציות כמו Map, אבל מחזירה אוסף של תוצאות של האיטרציות (בדומה לReduce).
לצערנו, אין פונקציה כזאת בSystem.Linq, אבל נוכל לממש אותה (למעשה, מימשו אותה בReactive Extensions בגרסאות המוקדמות, והיום זה נמצא בהרחבה אחרת שנקראת Interactive Extensions).
הטיפ היומי הוא על תכולה נפלאה ב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 מאשר להשתמש ולזרוק את המחרוזת.
היום נתקלתי במשהו שלא הכרתי בעולם הUnit Testing שיכול להיות מאוד שימושי ורציתי לשתף אתכם בו.
לפעמים אנחנו רוצים לכתוב Unit Test שאנחנו יודעים שאמור להזרק בו Exception ואנחנו מעוניינים לבדוק שזה קורה באמת ושהException שנזרק הוא הException שאותו ציפינו לקבל.
הרבה פעמים אני רואה קוד שנראה ככה בערך:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[TestMethod]
publicvoidDivideTest()
{
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 יכשל.
אם אנחנו רוצים לבדוק שהException שנזרק מכיל שגיאה מסויימת אז אנחנו לא יכולים – למשל עבור השגיאה OracleException הType הוא תמיד אותו הType אבל הטקסט של השגיאה מכיל משהו אחר כל פעם – ולכן לא נוכל לבדוק ככה שהשגיאה שנזרקה היא הנכונה. רצוי לציין שיש לזה פתרונות בNUnit אבל לא בMS-Test.
אם היה לכם UT שבדק כמה Exception ים אז תצטרכו להפרידו לUT ים נפרדים. כמובן שזה לא באמת בעיה היות וככה צריך שהUT יהיו בכל מקרה ולא יבדקו כמה דברים באותו Test.
הפעם יהיה טיפ מהעתיד – אנחנו נדבר על יכולות שהוסיפו ב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; }
boolContainsKey(TKey key);
boolTryGetValue(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 לא היה מועיל לנו יותר מדי.