למרבה המזל, לא חייבים לעבוד כל כך קשה כדי ליצור Dictionary כזה. בSystem.Linq יש Extension Method לIEnumerable<T> בשם ToDictionary שמאפשר לנו ליצור Dictionary בצורה פשוטה ע"י העברת שני Delegate – הראשון בוחר מפתח, והשני בוחר ערך.
מה המתודה עושה זה יוצרת Dictionary חדש, רצה על האיברים של האוסף וקוראת על כל ערך באוסף לפונקציה שבוחרת את המפתח, לפונקציה שבוחרת את הערך, וממפה את המפתח לערך.
שימו לב ששוב הירושה נותנת לנו בעיקר API יותר נוח, אבל ניתן ליצור Extension Methods מתאימים שיעשו API נוח אחר (אמנם בלי שם נוח וCollection Initializer, אבל עם מתודות נוחות מספיק)
לפעמים אנחנו מעוניינים בDictionary שמבוסס יותר ממפתח אחד,
למה הכוונה?
אנחנו רגילים לDictionary עם מפתח אחד, זה בד”כ מיפוי בין איזשהו מפתח (חד חד ערכי) לערך אחר.
למשל, נניח מיפוי בין תעודת זהות של אדם, לשם שלו.
אבל ניתן להרחיב את הקונספט לDictionary מבוסס מספר מפתחות,
למשל, המושג של מספר תעודת זהות הוא באמת חד חד ערכי, אבל רק בהקשר של מדינה מסוימת.
לכן היינו רוצים לייצג איזשהו מיפוי מסוים בין מדינות ותעודות זהות, לשמות של אנשים.
הדרך הפשוטה ביותר ליצור מימוש כזה היא ע”י שימוש באיזושהי מחלקה המייצגת זוג, למשל KeyValuePair, או Tuple (אם יש לכם יותר מזל ואתם מתכנתים בFramework 4.0, ראו גם טיפים 17, 126)
בשביל שתי המתודות הנוספות, אפשר גם לא לרשת, ולהפוך אותן לExtension Methods של IDictionary<Tuple<TKey1, TKey2>, TValue>. החל מC# 6.0 אפשר גם להפוך את Add לExtension Method מתאים בשביל לקבל את הסינטקס הנ"ל.
התחלתי ללמוד לא מזמן Ruby, כך שהדבר אולי ישפיע על הפינה.
בRuby בניגוד לJava וC# מחרוזות הן Mutable, כלומר ניתן לשנות את ערכיהן.
למשל, קוד בסגנון הזה יעבוד בRuby:
1
2
3
string helloWorld = "hello World";
helloWorld[0] = "H";
Console.WriteLine(helloWorld); // Hello World
אבל רגע! אמרנו שחשוב שאובייקטים יהיו Immutable, בין השאר כדי שיהיו מפתחות של Dictionary.
אז איך מסתדרים החבר’ה שם בRuby?
ובכן, יש אפשרות להקפיא מחרוזות (ואובייקטים נוספים שהם Mutable) ע"י קריאה למתודה בשם Freeze.
המתודה הזאת גורמת לכך שאם ננסה לשנות את האובייקט, ייזרק לנו Exception. למשל, זה יכול להיות מימוש של אובייקט עם מתודת Freeze בC#:
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
publicclassPerson
{
privatestring mName;
privatestring mLastName;
privateint mAge;
privatebool mIsFrozen = false;
publicstring Name
{
get
{
return mName;
}
set
{
if (mIsFrozen)
{
thrownew ArgumentException("The object is frozen.");
}
mName = value;
}
}
publicstring LastName
{
get
{
return mLastName;
}
set
{
if (mIsFrozen)
{
thrownew ArgumentException("The object is frozen.");
}
mLastName = value;
}
}
publicint Age
{
get
{
return mAge;
}
set
{
if (mIsFrozen)
{
thrownew ArgumentException("The object is frozen.");
}
mAge = value;
}
}
publicvoidFreeze()
{
mIsFrozen = true;
}
publicbool IsFrozen
{
get
{
return mIsFrozen;
}
}
}
ברגע שאובייקט מוקפא, הוא נהיה Immutable – אי אפשר יותר לשנות אותו.
שימו לב – אין דרך "להפשיר" אובייקט ברגע שהקפאנו אותו!
כך הוא יכול לשמש כמפתח בDictionary וכו’ בלי חשש שישתנה.
למעשה, הDictionary בRuby (מה שנקרא Hash), דואג לכך שאם הוא מקבל כמפתח מחרוזת שהיא לא קפואה (ע"י בדיקת הפונקציה frozen?, אצלנו זה הProperty ששמו IsFrozen), הוא משכפל אותה ומקפיא אותה.
רגע, ואם בא לנו "להפשיר" את האובייקט?
אז Ruby מספקת לנו שתי מתודות: dup וclone:
שתיהן משכפלות את האובייקט, רק שאחת יוצרת אובייקט חדש שהוא כבר לא קפוא…
דיברתי פה הרבה על Ruby, אבל אפשר להעתיק את הקונספט גם לC#: אפשר ליצור ממשק פשוט כזה:
1
2
3
4
5
6
7
8
9
publicinterfaceIFreezable : ICloneable
{
voidFreeze();
bool IsFrozen
{
get;
}
}
ואז ליצור מחלקת Dictionary משלנו שמקבל מפתחות שהם IFreezableשדואג לקרוא לClone ואז לFreeze ברגע שמכניסים מפתח שהוא לא מוקפא.
רציתי לשתף אתכם בfeature נחמד של .NET הנקרא: Type forwarding.
נבחן את הבעיה הבאה: יש לנו assembly בשם MyAssembly שבין היתר, מכיל בתוכו מחלקה בשם MySuperClass. ואנחנו משתמשים במחלקה הזאת באפליקציה אחרת שלנו (MyApp), בעזרת Assembly reference סטנדרטי.
יום אחד החלטנו לעשות refactoring לMyAssembly והגענו למסקנה שזה לא מקום טוב עבור MySuperClass והוא צריך לשבת בassembly אחר בשם: MySuperAssembly.
עשינו את כל השינויים, קימפלנו מחדש גם את MyAssembly וגם את MySuperAssembly וגילינו במהרה שהאפליקציה שלנו - MyApp זורקת שגיאה כי היא לא מוצאת יותר את המחלקה MySuperClass, מפני שהוא כבר לא נמצא בMyAssembly.
דרך אחת היא לעשות refactoring באפליקציה שלנו ולהוסיף reference נוסף לMySuperAssembly, אבל לא תמיד אנחנו יכולים לפתוח את הקוד של כל האפליקציה, מה גם שלפעמים הDLL שלנו הוא תשתיתי והרבה אפליקציות אחרות משתמשות בו, הרי לא נעבור ונוסיף עכשיו בכל אחת מהן את הreference לMySuperAssembly.
כאן נכנס לתמונה הattribute הנחמד בשם: TypeForwardedToAttribute
נוציא את המחלקה MySuperClass מתוך MyAssembly ונעביר אותה לMySuperAssembly.
לאחר שהזזנו את המחלקה שלנו מMyAssembly ל MySuperAssembly, נוסיף את השורה הבאה בקובץ AssemblyInfo.cs של MyAssembly:
לפעמים אנחנו נדרשים לדעת על השינוי שהתרחש בקובץ או בתיקיה כלשהי.
הדרך הנאיבית - מימוש התכולה בצורה עצמאית.
ע”י שימוש במרכבים הבאים:
Timer
שמירת מצב הנוכחי של התיקייה
שמירת נתוני metadata של התיקייה והקבצים המעניינים
למזלנו המקרה הזה מספיק נפוץ כדי שתהיה מחלקה מיוחדת בFramework שתטפל לנו בנושא: FileSystemWatcher
המחלקה יושבת בתוך הnamespace ששמו System.IO
היא מאפשרת לנו לקבל בדחיפה עדכונים על שינויים במבנה של התיקייה או קבצים שמעניינים אותנו.
תכולות עיקריות:
Eventים
Created/Renamed/Changed/Removed מתרחשים כאשר קובץ/תיקיית יעד שלנו השתנתה (בהתאם לשם הevent)
Properties
Path – נתיב לתיקיה שאנחנו רוצים לעקוב אחריה
NotifyFilter – איזה סוג שינויים מעניינים אותנו
Filter – באיזה סוגי קבצים מעניינים אותנו השינויים
EnableRaisingEvents – האם להתחיל האזנה לשינויים
Methods
WaitForChanged מתודה סינכרונית שנכנסת להמתנה עד שלא מתרחש שינוי המבוקש ביעד.
נציג כמה דוגמאות:
1
2
3
FileSystemWatcher myWatcher = new FileSystemWatcher(@"C:\test");
var res = myWatcher.WaitForChanged(WatcherChangeTypes.Created);
Console.WriteLine(res.Name);
כתוצאה מ3 השורות קוד האלה התוכנית שלנו תיתקע על השורה השנייה ותשתחרר רק לאחר היווצרותו של קובץ חדש בתיקיית"test" בכונן C. לאחר שניצור קובץ כלשהו בתיקיה הנ"ל נקבל במסך פלט של התוכנית את שם הקובץ שנוצר.
נעדכן טיפה את הדוגמא הראשונה:
1
2
3
FileSystemWatcher myWatcher = new FileSystemWatcher(@"C:\Test","*.log");
var res = myWatcher.WaitForChanged(WatcherChangeTypes.Created | WatcherChangeTypes.Renamed);
Console.WriteLine(res.Name);
ניצור קובץ טקסט חדש בתוך התיקייה ונקרא לו test.txt נראה שאין שום דבר בחלון הפלט של התוכנה שלנו והיא עדיין תקועה. זה קורה משום שכעט אנחנו משתמשים בoverload אחר של הctor של הFSW שמקבל גם מחרוזת פילטר לסינון סוגי הקבצים שמעניין אותנו לקבל את השינויים שלהם. אם נשנה את הקובץ שיצרנו לtext.log מיד נקבל תוצאה בחלון הפלט של התוכנה.
בדוגמא האחרונה נדגים שימוש בeventים של המחלקה שמאפשרים ריצה רגילה של התוכנית שלנו, ללא תקיעה סינכרונית בזמן ההמתנה לשינויים במערכת הקבצים:
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
staticvoidMain(string[] args)
{
//we are interested in changes in Test folder only on log files
FileSystemWatcher myWatcher = new FileSystemWatcher(@"C:\Test", "*.log");
// we are interested in file size changes only
myWatcher.NotifyFilter = NotifyFilters.Size;
//we want to get notifications on rename and changes only
Console.WriteLine(string.Format("File was renamed, old name: {0}, new name: {1}", e.OldName, e.Name));
}
כמו שאתם רואים, המחלקה מאוד שימושית וחושפת הרבה אופציות למשתמש. נזכיר עוד כמה נקודות חשובות
ניתן לעקוב אחרי שינויים בכונן רשת כי הpath מועבר בפורמט UNC.
כמה חסרונות:
אי אפשר לקבל את השינוי עצמו בתוכן של הקובץ אלא רק הודעה שהקובץ השתנה.
יש בעיה ידועה (שאולי כבר נפתרה) במימוש FileStream.Flush בWindows Vista ומעלה. המימוש של המתודה לא מעדכן נתוני metadata של הקובץ (הם מתעדכנים רק בקריאה לClose) ולכן לא נקבל עדכון על שינוי בקובץ דרך הFSW. זה לא קורה בגרסאות ווינדוס הקודמות והשינוי נבע מתוך שיקולי ביצועים בעבודה מול מערכת הקבצים.
אם משתמשים בכמה מופעים של FSW על אותו נתיב רק אחד מהם יקבל את העדכון
לא נועד לעקוב אחרי תיקיות/קבצים שעוברים שינויים בקצבים מאוד גדולים.
עכשיו מה שקורה בעצם זה שיש לנו בDictionary את אותו Key ביחס לEquals פעמיים. במצב זה לא ברור איזה ערך אנחנו מצפים שיחזור.
סיבה נוספת היא המימוש הפנימי של Dictionary: Dictionary משתמש בGetHashCode וEquals כדי לאתר איברים בו.
במידה והGetHashCode שלנו הוא לא קונסיסטנטי עם הEquals, ייתכן שהDictionary שלנו לא יצליח למצוא את הKey, למרות שהוא נמצא בו. הדבר הזה יכול לקרות אם הGetHashCode מתבסס על דברים שהם לא Immutable!
לכן כשאתם כותבים מחלקה שאתם מעוניינים שתהיה Key של Dictionary, תדאגו שהיא תהיה Immutable.
אם אתם לא רוצים לממש לה GetHashCode וEquals זה גם בסדר, אתם יכולים לספק IEqualityComparer לDictionary שמשווה את האובייקטים בצורה נכונה.
יכול להיות שהשתכנעתם, ואתם רוצים לרוץ לממש בעצמכם Immutable Object.
בוודאי תתאכזבו לגלות כי אין איזשהו ממשק שאפשר לרשת ממנו המציין שאובייקט הוא Immutable.
במקום זאת קיים הKeyword ששמו readonly המציין כי Field מסוים ניתן לאתחול רק בConstructor שלו. אם ננסה לאתחל אותו מחוץ לConstructor, נקבל שגיאת קימפול.
למשל:
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
publicclassMyImmutableClass
{
privatereadonlystring mName;
privatereadonlyint mAge;
publicMyImmutableClass(string name, int age)
{
mAge = age;
mName = name;
}
publicint Age
{
get
{
return mAge;
}
}
publicstring Name
{
get
{
return mName;
}
}
}
כעת, אם ננסה לשנות את הערכים של הFields שלנו במקום אחר בקוד, לא נצליח:
1
2
3
4
5
publicvoidIncreaseAge()
{
mAge++;
// Doesn't compile! : A readonly field cannot be assigned to (except in a constructor or a variable initializer)
}
(ראו גם טיפ מספר 14)
לכן הדרך המומלצת לממש Immutable object היא ע"י יצירת מחלקה שכל השדות שלה הן readonly.
שימו לב שזה לא מספיק, כי במידה ואחד הFields הוא לא Immutable בעצמו, נוכל לשנות את הField, ולכן גם את האובייקט עצמו. לכן חשוב לדאוג שגם כל הFieldים של המחלקה שלנו יהיו בעצמם Immutable…
אחד הקונספטים החזקים בעולם הOOP הוא הקונספט של אובייקטים שהם Immutable.
Immutable אומר בלתי ניתן לשינוי, והכוונה לאובייקטים שאנחנו יכולים לקבוע את ערכם, רק בConstructor.
לאחר שנוצר הInstance של האובייקט, האובייקט אינו בר שינוי יותר.
לדבר זה מספר יתרונות:
אנחנו לא צריכים לפחד שמישהו שינה לנו את האובייקט
במידה ואנחנו רוצים להעביר למישהו אחר את האובייקט, אנחנו לא צריכים להעביר לו עותק שלו, אלא מספיק להעביר את הReference, שהרי הוא לא יכול לשנות אותו. (הקונספט הזה מוכר בשפות מסוימות, בהן מקובל שבProperty אף פעם לא מחזירים Instance של Field, אלא רק העתק שלו)
מאחר והאובייקט אינו בר שינוי – אפשר ליצור איזשהו מנגנון שאחראי ליצור את האובייקטים האלה, והוא יחזיק גם Cache שלהם (כך שאם מבקשים את אותו אובייקט פעמיים, מקבלים אותו Instance). הדבר הזה מאפשר בעצם לנו בין השאר להשוות שני Instanceים של אובייקט שנוצר ממנגנון כזה, ע”י הReference שלהם. (במקום לכתוב מנגנון השוואה מתוחכם יותר, ראו גם טיפים 76-80)
אם נסתכל על הFramework, לא חסרים לנו אובייקטים שהםImmutable. ביניהם: string, DateTime, Delegate, Nullable ועוד.
אם נחשוב על זה, גם רוב הStructים שלנו בשפה הםImmutable , שהרי כאשר אנחנו מכניסים למשתנה ערך של Struct, נכנס למעשה עותק של הStruct למשתנה. (ראו גם טיפ מספר 131)
חלק מכם בוודאי חושבים עכשיו:
“string הוא Immutable? לא יכול להיות, הרי אפשר לכתוב:
1
2
string myString = "Hello ";
myString += "World";
"
אז איך זה בעצם עובד?
מה שמקובל לעשות בעבודה עם Immutable Objects הוא ליצור אובייקט חדש במקום לשנות את האובייקט הקיים. מה שבעצם קורה למשל במקרה זה הוא שהקוד מתרגם לקוד הבא:
1
2
string myString = "Hello ";
myString = myString + "World";
והאופרטור + דואג להחזיר לנו מחרוזת חדשה שהיא השרשור של שתי המחרוזות הנ"ל.
מי שבד"כ מתחיל לתכנת בC# מגלה שstring הוא Immutable בדרך טיפה כואבת: ע"י הפונקציה Replace:
1
2
string aNiceString = "They are the champions, my friend";
aNiceString.Replace("They","We");
מה שקורה זה שהקוד הזה נראה עובד, אבל בפועל הוא לא. אחרי כמה זמן של דיבוג, מגלים שReplace מחזיר string חדש השווה לתוצאה של הפעולה:
1
2
string aNiceString = "They are the champions, my friend";