280. ToDictionary extension method

לפעמים יש לנו איזשהו אוסף ואנחנו מעוניינים ליצור איזשהו Dictionary ממנו.

דוגמה אחת היא הדוגמה שראינו פעם קודמת:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ICollection<Tuple<string, int>> keys = stateAndIdToName.Keys;
IEnumerable<int> israeliIds =
from key in keys
where key.Item1 == "Isreal"
select key.Item2;
Dictionary<int, string> isrealiIdToName =
new Dictionary<int, string>();
foreach (int israeliId in israeliIds)
{
isrealiIdToName[israeliId] =
stateAndIdToName["Israel", israeliId];
}

למרבה המזל, לא חייבים לעבוד כל כך קשה כדי ליצור Dictionary כזה. בSystem.Linq יש Extension Method לIEnumerable<T> בשם ToDictionary שמאפשר לנו ליצור Dictionary בצורה פשוטה ע"י העברת שני Delegate – הראשון בוחר מפתח, והשני בוחר ערך.

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Dictionary<TKey, TValue> ToDictionary<TSource, TKey, TValue>
(this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TValue> valueSelector)
{
Dictionary<TKey, TValue> result = new Dictionary<TKey, TValue>();
foreach (TSource current insource)
{
result.Add(keySelector(current), valueSelector(current));
}
return result;
}

למשל, את הקוד של הפעם הקודמת נוכל לכתוב כך:

1
2
3
Dictionary<int,string> isrealiIdToName =
israeliIds.ToDictionary(id => id,
id => stateAndIdToName["Israel", id]);

יותר יפה 😃

זה מאוד שימושי כשרוצים לבנות משאילתת LINQ איזשהו Dictionary, ע"י שילוב עם Anonymous types:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
IEnumerable<string> bandsMembers =
new[]
{
@"Queen\Brian May",
@"Queen\Roger Taylor",
@"Queen\Freddie Mercury",
@"Queen\John Deacon",
@"Scorpions\Klaus Meine",
@"Scorpions\Rudolf Schenker",
@"Scorpions\Paweł Mąciwoda",
@"Scorpions\James Kottak",
@"Scorpions\Matthias Jabs"
};

ואנחנו מעוניינים למפות שמות של אנשים לקידומת שלהם. נוכל ליצור שאילתת LINQ וליצור ממנה Dictionary 😃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var membersAndBands =
from member in bandsMembers
let seperator = member.IndexOf(@"\")
let bandName = member.Substring(0, seperator)
let name = member.Substring(seperator + 1)
select new
{
BandName = bandName,
Name = name
};
Dictionary<string, string> memberToBand =
membersAndBands.ToDictionary(x => x.Name,
x => x.BandName);

ככה אנחנו משתמשים בAnonymous Type רק כתווך זמני לעיבוד.

ראו גם טיפים על LINQ ואת טיפ מספר 90.

המשך יום למילון טוב

שתף

279. Double keyed dictionary strikes back

בהמשך לפעם שעברה,

קיימת עוד שיטה לממש Dictionary רב מפתחות, ע”י שימוש בDictionary מקונן.

במקום להשתמש בDictionary כKey, ניתן להשתמש בDictionary שממפה את המפתחות שלו לDictionary אחר:

1
2
Dictionary<string, Dictionary<int, string>> stateAndIdToName =
new Dictionary<string, Dictionary<int, string>>();

למי שאי פעם נתקל בשוויון (יותר נכון איזומורפיזם):

$ {\left({A^B}\right)}^C \cong A^{B \times C} $

(מתורת הקבוצות, אבל אפשר להבין אותו גם מספרית)

השוויון הזה בדיוק אומר ששני המבנים האלה זהים במובן מסוים:

כלומר אם יש לי Dictionary שממפה זוג $ (x,y) $ לערך $ z $, אני יכול ליצור Dictionary מתאים שממפה את $ x $ לDictionary שממפה את $ y $ ל$ z $.

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

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
public class DoubleKeyDictionary<TKey1, TKey2, TValue> :
Dictionary<TKey1, IDictionary<TKey2, TValue>>
{
private IDictionary<TKey2, TValue> GetOrCreateInnerDictionary(TKey1 key1)
{
IDictionary<TKey2, TValue> innerDictionary;
if (!this.TryGetValue(key1, out innerDictionary))
{
innerDictionary = new Dictionary<TKey2, TValue>();
this[key1] = innerDictionary;
}
return innerDictionary;
}
public void Add(TKey1 key1, TKey2 key2, TValue value)
{
IDictionary<TKey2, TValue> innerDictionary =
GetOrCreateInnerDictionary(key1);
innerDictionary.Add(key2, value);
}
public TValue this[TKey1 key1, TKey2 key2]
{
get
{
return this[key1][key2];
}
set
{
IDictionary<TKey2, TValue> innerDictionary =
GetOrCreateInnerDictionary(key1);
innerDictionary[key2] = value;
}
}
public bool ContainsKey(TKey1 key1, TKey2 key2)
{
IDictionary<TKey2, TValue> innerDictionary;
return this.TryGetValue(key1, out innerDictionary) &&
innerDictionary.ContainsKey(key2);
}
public bool TryGetValue(TKey1 key1, TKey2 key2, out TValue result)
{
result = default(TValue);
IDictionary<TKey2, TValue> innerDictionary;
return this.TryGetValue(key1, out innerDictionary) &&
innerDictionary.TryGetValue(key2, out result);
}
}

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

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

1
IDictionary<int, string> israeliIdToName = stateAndIdToName["Israel"];

עם המימוש הקודם נצטרך לעבוד יותר קשה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ICollection<Tuple<string, int>> keys = stateAndIdToName.Keys;
IEnumerable<int> israeliIds =
from key in keys
where key.Item1 == "Isreal"
select key.Item2;
Dictionary<int, string> isrealiIdToName =
new Dictionary<int, string>();
foreach (int israeliId in israeliIds)
{
isrealiIdToName[israeliId] =
stateAndIdToName["Israel", israeliId];
}

שימו לב ששוב הירושה נותנת לנו בעיקר API יותר נוח, אבל ניתן ליצור Extension Methods מתאימים שיעשו API נוח אחר (אמנם בלי שם נוח וCollection Initializer, אבל עם מתודות נוחות מספיק)

שתף

278. Double keyed Dictionary

לפעמים אנחנו מעוניינים בDictionary שמבוסס יותר ממפתח אחד,

למה הכוונה?

אנחנו רגילים לDictionary עם מפתח אחד, זה בד”כ מיפוי בין איזשהו מפתח (חד חד ערכי) לערך אחר.

למשל, נניח מיפוי בין תעודת זהות של אדם, לשם שלו.

אבל ניתן להרחיב את הקונספט לDictionary מבוסס מספר מפתחות,

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

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

הדרך הפשוטה ביותר ליצור מימוש כזה היא ע”י שימוש באיזושהי מחלקה המייצגת זוג, למשל KeyValuePair, או Tuple (אם יש לכם יותר מזל ואתם מתכנתים בFramework 4.0, ראו גם טיפים 17, 126)

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

1
2
3
4
5
6
Dictionary<Tuple<string, int>, string> stateAndIdToName =
new Dictionary<Tuple<string, int>, string>()
{
{Tuple.Create("Israel", 314159260), "Haim Pi"},
{Tuple.Create("Egypt", 314159260), "Achmad Pi"},
};

(ראו גם טיפ מספר 18, 126)

וגישה לDictionary מתבצעת כך:

1
2
string israeliPiMinister =
stateAndIdToName[Tuple.Create("Israel", 314159260)];

ניתן גם לרשת מDictionary כדי להוסיף Overloadים נוחים:

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
public class DoubleKeyDictionary<TKey1, TKey2, TValue> :
Dictionary<Tuple<TKey1, TKey2>, TValue>
{
public void Add(TKey1 key1, TKey2 key2, TValue value)
{
this.Add(Tuple.Create(key1, key2), value);
}
public TValue this[TKey1 key1, TKey2 key2]
{
get
{
return this[Tuple.Create(key1, key2)];
}
set
{
this[Tuple.Create(key1, key2)] = value;
}
}
public bool ContainsKey(TKey1 key1, TKey2 key2)
{
return this.ContainsKey(Tuple.Create(key1, key2));
}
public bool TryGetValue(TKey1 key1, TKey2 key2, out TValue result)
{
return this.TryGetValue(Tuple.Create(key1, key2), out result);
}
}

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

  • את האפשרות להגדיר את הDictionary בצורה יותר נוחה: DoubleKeyDictionary<string, int, string> במקום Dictionary<Tuple<string, int>, string>
  • את הIndexer הנוח המאפשר לנו לכתוב כך:

    1
    2
    string israeliPiMinister = stateAndIdToName["Israel", 314159260];
    stateAndIdToName["Israel", 314159260] = "Haim Pi";
  • את פונקציית הAdd, המאפשרת לנו להגדיר את הDictionary בצורה יותר נוחה (ראו גם טיפ מספר 18):

    1
    2
    3
    4
    5
    6
    DoubleKeyDictionary<string, int, string> stateAndIdToName =
    new DoubleKeyDictionary<string, int, string>()
    {
    {"Israel", 314159260, "Haim Pi"},
    {"Egypt", 314159260, "Achmad Pi"},
    };

בשביל שתי המתודות הנוספות, אפשר גם לא לרשת, ולהפוך אותן לExtension Methods של IDictionary<Tuple<TKey1, TKey2>, TValue>. החל מC# 6.0 אפשר גם להפוך את Add לExtension Method מתאים בשביל לקבל את הסינטקס הנ"ל.

המשך יום פעמיים כי טוב

שתף

277. Freezing mutable objects

התחלתי ללמוד לא מזמן 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
public class Person
{
private string mName;
private string mLastName;
private int mAge;
private bool mIsFrozen = false;
public string Name
{
get
{
return mName;
}
set
{
if (mIsFrozen)
{
throw new ArgumentException("The object is frozen.");
}
mName = value;
}
}
public string LastName
{
get
{
return mLastName;
}
set
{
if (mIsFrozen)
{
throw new ArgumentException("The object is frozen.");
}
mLastName = value;
}
}
public int Age
{
get
{
return mAge;
}
set
{
if (mIsFrozen)
{
throw new ArgumentException("The object is frozen.");
}
mAge = value;
}
}
public void Freeze()
{
mIsFrozen = true;
}
public bool 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
public interface IFreezable : ICloneable
{
void Freeze();
bool IsFrozen
{
get;
}
}

ואז ליצור מחלקת Dictionary משלנו שמקבל מפתחות שהם IFreezableשדואג לקרוא לClone ואז לFreeze ברגע שמכניסים מפתח שהוא לא מוקפא.

ההגדרה של המחלקה היא משהו כזה:

1
2
3
4
public class FreezingDictionary<TKey, TValue> :
IDictionary<TKey, TValue> where TKey : IFreezable
{
}

שבוע קפוא בלי הרבה שינויים טוב!

שתף

276. Type Forwarding in the CLR

[נכתב ע”י ולרי פליסקין]

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

רציתי לשתף אתכם ב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:

1
[assembly : TypeForwardedTo(typeof(MySuperClass))]

עכשיו נקמפל את הMySuperAssembly שמכיל את המחלקה MySuperClass לאחר הrefactoring שבצענו.

עכשיו נותר לנו רק להוסיף reference של הMySuperAssembly לMyAssembly ולקמפל מחדש את MyAssembly.

נקודה חשובה: צריך לשמור על שם הnamespace המקורי של MySuperClass גם לאחר שהעברנו אותו לAssembly אחר.

שתף

275. Distributed Source Control

[נכתב ע”י דניאל קוגל, התמונות מכאן ומכאן]

נושא שמעסיק אותי מעט בזמן האחרון והוא הנושא של ניהול גרסאות.

יצא לי לקרוא ספר נחמד בשם Version Control by Example (אפשר להוריד חינם מכאן)

שמדבר על Version Control System (או בקיצור VCS) ועל Distributed Version Control System (או בקיצור DVCS).

למי שלא מכיר (חמור מאוד!) Version Control System הוא כלי שמאפשר לעשות Revision control או ניהול גרסאות.

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

יצא לי לעבוד בעיקר עם TFS שזה כלי לניהול גרסאות של Microsoft אשר מחליף את Source Safe הישן, גם הוא של Microsoft.

שמעתי על מתכנתים שעובדים ב-SVN אבל לא יצא לי להיתקל באחד ומכאן הרצון שלי לחלוק את מה שקראתי.

אפשר לשייך את ה-TFS, SourceSafe ו-SVN לדור הקודם של ניהול הגרסאות בו ניהול הגרסאות היה מבוצע על שרת אחד מרוכז (Repository),

כל המשתמשים היו מתחברים אליו ומוסיפים לו את השינויים שלהם (commit) כאשר כל commit מבוסס על commit קודם בגלל שרוב ה-VCS-ים שומרים קבצים בדלתאות:
275_1.jpg

כאשר שני משתמשים היו מכניסים שינויים באותו הקובץ:
275_2.jpg

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

המשתמש צריך להתערב ולהצביע על השינויים שיש לבצע (איחוד או דריסה)ובעצם ליצור קובץ חדש אותו מכניסים ל-VCS:
275_3.jpg

כלומר בכל רגע נתון יש רק גירסה אחת נכונה ועדכנית,

אפשר לבצע Branch על מנת להתחיל תהליך פיתוח שמבוסס על נקודה בזמן ולהתקדם משם ואז אנחנו מקבלים משהו שנראה ככה:
275_4.jpg

(כל שורה פה היא בעצם Branch /ציר פיתוח)

בניגוד ל- Centralized VCS שבו כל משתמש שרוצה לשמור את השינויים שלו מוסיף אותם ל-branch הראשי בשרת הריכוזי,

ב-DVCS כל משתמש יכול לקחת כל שינוי שהוא רוצה מכל מחשב של משתמש אחר, קצת כמו peer to peer

וכך בעצם אין שרת אחד וגרסה אחת אלא כל גרסה מורכבת מחלקים שונים, mix and match:
275_5.jpg

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

אפשר להסתכל על DVCS כאילו לכל משתמש יש Repository מקומי על השרת שלו ומולו הוא מתנהל כמו VCS רגיל.

כאשר משתמש רוצה לקחת שינוי (Changeset) או תכולה ((Tag / Label הוא ניגש לשרת של משתמש אחר ולוקח אותם.

כל גירסה מורכבת מאוסף של תכולות שנלקחו מהמחשב הנוכחי או הובאו מכל מחשב אחר.

עדיין אפשר לעבוד עם שרת ריכוזי שיכיל גירסה מסויימת כמו ב-CVCS, רק שעכשיו אפשר גם אחרת. 😃

שתף

274. FileSystemWatcher

[נכתב ע”י ולרי פליסקין]

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

הדרך הנאיבית - מימוש התכולה בצורה עצמאית.

ע”י שימוש במרכבים הבאים:

  1. Timer
  2. שמירת מצב הנוכחי של התיקייה
  3. שמירת נתוני 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
static void Main(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
myWatcher.Renamed += myWatcher_Renamed;
myWatcher.Changed += myWatcher_Changed;
//start observing the changes
myWatcher.EnableRaisingEvents = true;
Console.WriteLine("Waiting to changes");
Console.ReadKey();
}
static void myWatcher_Changed(object sender, FileSystemEventArgs e)
{
Console.WriteLine(string.Format("File was changed, Change type: {0}, file name: {1}", e.ChangeType, e.Name));
}
static void myWatcher_Renamed(object sender, RenamedEventArgs e)
{
Console.WriteLine(string.Format("File was renamed, old name: {0}, new name: {1}", e.OldName, e.Name));
}

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

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

כמה חסרונות:

  1. אי אפשר לקבל את השינוי עצמו בתוכן של הקובץ אלא רק הודעה שהקובץ השתנה.
  2. יש בעיה ידועה (שאולי כבר נפתרה) במימוש FileStream.Flush בWindows Vista ומעלה. המימוש של המתודה לא מעדכן נתוני metadata של הקובץ (הם מתעדכנים רק בקריאה לClose) ולכן לא נקבל עדכון על שינוי בקובץ דרך הFSW. זה לא קורה בגרסאות ווינדוס הקודמות והשינוי נבע מתוך שיקולי ביצועים בעבודה מול מערכת הקבצים.
  3. אם משתמשים בכמה מופעים של FSW על אותו נתיב רק אחד מהם יקבל את העדכון
  4. לא נועד לעקוב אחרי תיקיות/קבצים שעוברים שינויים בקצבים מאוד גדולים.

חג שמח וצפייה נעימה!

שתף

273. Immutable objects and dictionary keys

רובנו מכירים את הטיפוס של Dictionary המאפשר לנו לבצע מיפוי בין Instanceים מסוג אחד לInstanceים מסוג אחר.

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

אחת הטעויות הנפוצות היא לתת לDictionary בתור הKey אובייקט שהוא Mutable. (כלומר, אובייקט שהוא בר שינוי)

למה?

יש לזה מספר סיבות: הסיבה העיקרית היא שDictionary ממפהInstance בודד לInstance בודד אחר.

עכשיו, נניח שמיפינו שני אובייקטים שונים, כל אחד לInstance אחר בDictionary:

זה האובייקט שלנו (הEquals באדיבות הReShaper)

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
public class MyMutableObject
{
public string Name
{
get;
set;
}
public bool Equals(MyMutableObject other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return Equals(other.Name, Name);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (!(obj is MyMutableObject))
{
return false;
}
return Equals((MyMutableObject) obj);
}
public override int GetHashCode()
{
if (Name != null)
{
return Name.GetHashCode();
}
return 0;
}
}

ויצרנו שני מיפויים:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Dictionary<MyMutableObject, int> mutableToNumber =
new Dictionary<MyMutableObject, int>();
MyMutableObject mutable =
new MyMutableObject()
{
Name = "Jason"
};
MyMutableObject mutable2 =
new MyMutableObject()
{
Name = "Jefferson"
};
mutableToNumber[mutable] = 3;
mutableToNumber[mutable2] = 4;

עכשיו שינינו את אחד הmutable:

1
mutable2.Name = "Jason";

עכשיו מה שקורה בעצם זה שיש לנו בDictionary את אותו Key ביחס לEquals פעמיים. במצב זה לא ברור איזה ערך אנחנו מצפים שיחזור.

סיבה נוספת היא המימוש הפנימי של Dictionary: Dictionary משתמש בGetHashCode וEquals כדי לאתר איברים בו.

במידה והGetHashCode שלנו הוא לא קונסיסטנטי עם הEquals, ייתכן שהDictionary שלנו לא יצליח למצוא את הKey, למרות שהוא נמצא בו. הדבר הזה יכול לקרות אם הGetHashCode מתבסס על דברים שהם לא Immutable!

לכן כשאתם כותבים מחלקה שאתם מעוניינים שתהיה Key של Dictionary, תדאגו שהיא תהיה Immutable.

אם אתם לא רוצים לממש לה GetHashCode וEquals זה גם בסדר, אתם יכולים לספק IEqualityComparer לDictionary שמשווה את האובייקטים בצורה נכונה.

חג שמח, בלי שינויים לא שגורים!

שתף

272. Writing your own immutable objects

אז הכרנו בפעם שעברה קצת מה זה Immutable Object.

יכול להיות שהשתכנעתם, ואתם רוצים לרוץ לממש בעצמכם 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
public class MyImmutableClass
{
private readonly string mName;
private readonly int mAge;
public MyImmutableClass(string name, int age)
{
mAge = age;
mName = name;
}
public int Age
{
get
{
return mAge;
}
}
public string Name
{
get
{
return mName;
}
}
}

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

1
2
3
4
5
public void IncreaseAge()
{
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…

המשך יום בלי יותר מדי שינויים טוב

שתף

271. Immutable objects

אחד הקונספטים החזקים בעולם ה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";
aNiceString = aNiceString.Replace("They","We"); // :)

המשך יום בלי יותר מדי שינויים דרסטיים

שתף