290. EquivalenceRelationEqualityComparer

כזכור, EqualityComparer מאפשר לנו לתת התנהגות לפיה שני איברים נחשבים שווים.

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

זהו יחס R המקיים 3 תנאים:

  1. רפלקסיביות: a R a (כלומר a תמיד שקול לעצמו)
  2. סימטריות: אם a R b אזי גם b R a (כלומר שקילות היא סימטרית)
  3. טרנזטיביות: אם a R b וגם b R c אזי a R c.

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

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

1
HashSet<KeyValuePair<T, T>>

מבנה זה אומר שa שקול לb אם"ם הזוג new KeyValuePair<T, T>(a, b) נמצא בו.

מאחר והיחס סימטרי, נרצה להשתמש בSymmetricEqualityComparer שראינו פעם שעברה. זה יבטיח שאם $ a $ שקול ל$ b $ אזי $ b $ שקול ל$ a $:

1
HashSet<KeyValuePair<T, T>> equivalenceRelation = new HashSet<KeyValuePair<T,T>>(new SymmetricEqualityComparer<T>());

נוכל ליצור ממבנה זה EqualityComparer מתאים:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class EquivalenceRelationEqualityComparer<T> : IEqualityComparer<T>
{
private readonly HashSet<KeyValuePair<T, T>> mEquivalenceRelation;
public EquivalenceRelationEqualityComparer(HashSet<KeyValuePair<T, T>> equivalenceRelation)
{
mEquivalenceRelation = equivalenceRelation;
}
public bool Equals(T x, T y)
{
if (ReferenceEquals(x, y))
{
return true;
}
return mEquivalenceRelation.Contains(new KeyValuePair<T, T>(x, y));
}
public int GetHashCode(T obj)
{
return 0;
}
}

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

המשך יום ששקול ליום מצוין.

שתף

289. SymmetricEqualityComparer

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

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

1
2
3
4
5
6
7
8
var pairs =
from first in candidate
from second in candidate
select new {first, second};
return pairs.All(x =>
candidate.Contains(x.first.Union(x.second), comparer) &&
candidate.Contains(x.first.Intersect(x.second), comparer));

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SymmetricEqualityComparer<TSource> : IEqualityComparer<KeyValuePair<TSource, TSource>>
{
public bool Equals(KeyValuePair<TSource, TSource> x, KeyValuePair<TSource, TSource> y)
{
return(EqualityComparer<TSource>.Default.Equals(x.Key, y.Key) &&
EqualityComparer<TSource>.Default.Equals(x.Value, y.Value)) ||
(EqualityComparer<TSource>.Default.Equals(x.Key, y.Value) &&
EqualityComparer<TSource>.Default.Equals(x.Value, y.Key));
}
public int GetHashCode(KeyValuePair<TSource, TSource> obj)
{
return EqualityComparer<TSource>.Default.GetHashCode(obj.Key) ^
EqualityComparer<TSource>.Default.GetHashCode(obj.Value);
}
}

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

בGetHashCode אנחנו מחזירים את הXOR של שני החלקים של אובייקט. (זו פעולה קומוטטיבית)

שימו לב שכתבתי את זה ספציפית על KeyValuePair<TSource, TSource> במקום על אובייקט יותר גנרי, אבל אפשר להרחיב את זה ע"י קבלת Delegateים שמפרקים את האובייקטים לחלקים (כאן אלה יהיו Delegate שמחזיר את הKey וDelegate שמחזיר את הValue)

נוכל להשתמש בזה בדוגמא של אתמול כך:

1
2
3
4
5
6
7
8
9
10
11
12
SymmetricEqualityComparer<IEnumerable<T>> symmetricComparer =
new SymmetricEqualityComparer<IEnumerable<T>>();
var pairs =
(from first in candidate
from second in candidate
select newKeyValuePair<IEnumerable<T>,IEnumerable<T>>(first, second))
.Distinct(symmetricComparer);
return pairs.All(x =>
candidate.Contains(x.Key.Union(x.Value), comparer) &&
candidate.Contains(x.Key.Intersect(x.Value), comparer));

כך אנחנו עוברים על הזוגות בלי חשיבות לסדר.

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

שתף

288. Computing all topologies of a finite set

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

מדובר באוסף קבוצות ביחס לקבוצה X עם התכונות הבאות:

  1. הקבוצה הריקה והקבוצה X שייכות לטופולוגיה
  2. איחוד (כלשהו) של קבוצות מהטופולוגיה שייך לטופולוגיה
  3. חיתוך (סופי) של קבוצות מהטופולוגיה שייך לטופולוגיה.

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

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

זה נראה כך:

1
2
3
4
5
6
7
8
public static IEnumerable<IEnumerable<IEnumerable<T>>> Topologies<T>(IEnumerable<T> world)
{
IEnumerable<IEnumerable<T>> discreteTopology = GetPowerSet(world);
IEnumerable<IEnumerable<IEnumerable<T>>> candidates = GetPowerSet(discreteTopology);
return candidates.Where(x => IsTopology(x, world));
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static bool IsTopology<T>(IEnumerable<IEnumerable<T>> candidate, IEnumerable<T> world)
{
SetComparer<T> comparer = new SetComparer<T>();
if (!candidate.Contains(Enumerable.Empty<T>(), comparer) ||
!candidate.Contains(world, comparer))
{
return false;
}
var pairs =
from first in candidate
from second in candidate
select new {first, second};
return pairs.All(x =>
candidate.Contains(x.first.Union(x.second), comparer) &&
candidate.Contains(x.first.Intersect(x.second), comparer));
}

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

פרט נחמד שהסתרתי מאיתנו כאן הוא איך מתבצעת השוואת הקבוצות. זאת מתבצעת באמצעות IEqualityComparer (ראו גם טיפים מספר 111-116) שמשווה אוספים כקבוצות:

1
2
3
4
5
6
7
8
9
10
11
12
public class SetComparer<T> : IEqualityComparer<IEnumerable<T>>
{
public bool Equals(IEnumerable<T> x,IEnumerable<T> y)
{
return new HashSet<T>(x).SetEquals(y);
}
public int GetHashCode(IEnumerable<T> obj)
{
return 0;
}
}

שימו לב שהמימוש לא הכי אידאלי, אבל זה המימוש הכי פשוט אפשר לכתוב.

כעת נוכל להריץ את הקוד ולראות מספר טופולוגיות:

על הקבוצה הריקה נקבל את הטופולוגיות הבאות:

$ \{\{\}\} $

על קבוצה עם איבר בודד נקבל את:

$ \{\{\},\{0\}\} $

על קבוצה עם שני איברים נקבל 4 אפשרויות:

$ \{\{\},\{0,1\}\} \\ \{\{\},\{1\},\{0,1\}\} \\ \{\{\},\{0\},\{0,1\}\} \\ \{\{\},\{1\},\{0\},\{0,1\}\}$

על קבוצה עם 3 איברים נקבל 29 אפשרויות:

$ \{\{\},\{0,1,2\}\} \\ \{\{\},\{2\},\{0,1,2\}\} \\ \{\{\},\{1\},\{0,1,2\}\} \\ \{\{\},\{1,2\},\{0,1,2\}\} \\ \{\{\},\{2\},\{1,2\},\{0,1,2\}\} \\ \{\{\},\{1\},\{1,2\},\{0,1,2\}\} \\ \{\{\},\{2\},\{1\},\{1,2\},\{0,1,2\}\} \\ \{\{\},\{0\},\{0,1,2\}\} \\ \{\{\},\{1,2\},\{0\},\{0,1,2\}\} \\ \{\{\},\{0,2\},\{0,1,2\}\} \\ \{\{\},\{2\},\{0,2\},\{0,1,2\}\} \\ \{\{\},\{1\},\{0,2\},\{0,1,2\}\} \\ \{\{\},\{2\},\{1,2\},\{0,2\},\{0,1,2\}\} \\ \{\{\},\{2\},\{1\},\{1,2\},\{0,2\},\{0,1,2\}\} \\ \{\{\},\{0\},\{0,2\},\{0,1,2\}\} \\ \{\{\},\{2\},\{0\},\{0,2\},\{0,1,2\}\} \\ \{\{\},\{2\},\{1,2\},\{0\},\{0,2\},\{0,1,2\}\} \\ \{\{\},\{0,1\},\{0,1,2\}\} \\ \{\{\},\{2\},\{0,1\},\{0,1,2\}\} \\ \{\{\},\{1\},\{0,1\},\{0,1,2\}\} \\ \{\{\},\{1\},\{1,2\},\{0,1\},\{0,1,2\}\} \\ \{\{\},\{2\},\{1\},\{1,2\},\{0,1\},\{0,1,2\}\} \\ \{\{\},\{0\},\{0,1\},\{0,1,2\}\} \\ \{\{\},\{1\},\{0\},\{0,1\},\{0,1,2\}\} \\ \{\{\},\{1\},\{1,2\},\{0\},\{0,1\},\{0,1,2\}\} \\ \{\{\},\{0\},\{0,2\},\{0,1\},\{0,1,2\}\} \\ \{\{\},\{2\},\{0\},\{0,2\},\{0,1\},\{0,1,2\}\} \\ \{\{\},\{1\},\{0\},\{0,2\},\{0,1\},\{0,1,2\}\} \\ \{\{\},\{2\},\{1\},\{1,2\},\{0\},\{0,2\},\{0,1\},\{0,1,2\}\} $

על קבוצה עם 4 איברים נקבל 355 אפשרויות.

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

$ 1,1,4,29,355, \dots $

נכון להיום לא ידועה נוסחה פשוטה לסדרה של מספר האיברים בטופולוגיה ממספר סופי (ראו גם Finite topological space).

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

אפשר לשפר את הקוד שכתבתי, למרות שהוא מדגים שימוש במספר Featureים של השפה והFramework.

יום טופולוגי טוב!

שתף

287. Writing a Quine

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

לקונספט הזה קוראים Quine.

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

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

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

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
using System;
namespace Quine
{
public class Program
{
private const string PROGRAM =
@"using System;
namespace Quine
{
public class Program
{
private const string PROGRAM =
@""#"";
private static void Main(string[] args)
{
string[] programLines =
PROGRAM.Split(new[] {Environment.NewLine},
StringSplitOptions.None);
foreach (string line in programLines)
{
string lineToPrint = line;
if (line.Contains(@""@""""#""""""))
{
string programConstant =
PROGRAM.Replace(@"""""""", @"""""""""""");
lineToPrint =
lineToPrint.Replace(""#"", programConstant);
}
Console.WriteLine(lineToPrint);
}
}
}
}";
private static void Main(string[] args)
{
string[] programLines =
PROGRAM.Split(new[] {Environment.NewLine},
StringSplitOptions.None);
foreach (string line in programLines)
{
string lineToPrint = line;
if (line.Contains(@"@""#"""))
{
string programConstant =
PROGRAM.Replace(@"""", @"""""");
lineToPrint =
lineToPrint.Replace("#", programConstant);
}
Console.WriteLine(lineToPrint);
}
}
}
}

אם תפעילו את התוכנה הזו, היא תדפיס למסך את עצמה 😃

מה הקסם? התוכנה מכילה את עצמה, אלא שבקבוע המייצג אותה, היא אינה יכולה להכיל את הקבוע שוב (שהרי אז תהיה רקורסיה אינסופית)

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

תכלס זה רעיון ממש נחמד וממש נחמד להבין למה זה עובד..

המשך יום רקורסיבי טוב!

שתף

286. ReadOnly enumerable

ראינו בעבר הרחוק (טיפ מספר 4) שניתן לעטוף IListב ReadOnlyCollection כדי שלא יוכלו לשנות אותו.

נניח שיש לנו איזשהו IEnumerable, ואנחנו מעוניינים שלא יוכלו לשנות אותו.

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

נניח שנחזיר את הCollection כך:

1
2
3
4
5
6
private List<int> mTheNumbers =new List<int>() { 4, 8, 15, 16, 23, 42 };
public IEnumerable<int> GetTheNumbers()
{
return mTheNumbers;
}

אלא שאפשר להתחכם איתנו:

1
2
ICollection<int> numbers = (ICollection<int>)GetTheNumbers();
numbers.Add(108);

מה אפשר לעשות?

אופציה אחת היא לעטוף אותו בReadOnlyCollection. הבעיה היא שReadOnlyCollection יודע לעבוד רק מולIList.

אופציה אחרת היא טיפה יותר יצירתית: נחזיר טיפוס אחר:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ReadOnlyEnumerable<T> : IEnumerable<T>
{
private readonly IEnumerable<T> mSource;
public ReadOnlyEnumerable(IEnumerable<T> source)
{
mSource = source;
}
public IEnumerator<T> GetEnumerator()
{
return mSource.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}

ואז להחזיר ככה:

1
2
3
4
public IEnumerable<int> GetTheNumbers()
{
return new ReadOnlyEnumerable<int>(mTheNumbers);
}

אופציה שנייה היא יותר יצירתית והיא נראית ככה:

1
2
3
4
public IEnumerable<int> GetTheNumbers()
{
return mTheNumbers.Select(x => x);
}

מה קורה כאן? מצד אחד זה נראה כאילו לא קרה כלום. מצד שני, מה שקורה כאן זה שמוחזר IEnumerable<int> עם טיפוס אחר.

הקסם בדרך הזאת היא שגם אם אחר כך יעדכנו את mTheNumbers, יעודכן גם הIEnumerable<int> שחוזר מהפונקציה. וזאת למה? כי המתודות של LINQ הן Lazy – כלומר הן מחושבות רק כאשר אנחנו מתחילים לרוץ על הEnumerable! (ראו גם טיפים מספר 90,91)

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

סוף שבוע שלא ניתן לקריאה בלבד טוב!

שתף

285. Explicit interface implementation and your cup of coffee

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

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

(אנחנו נתקלנו בבעיה עם Indexerים ספציפית, אבל קל להיתקל בזה גם בהקשרים אחרים)

בC# מה שעשו, זה אפשרו לעשות מימוש של Explicit Implementation לממשקים, המאפשר לנו לציין לאיזה ממשק שייכת המתודה שאנחנו רוצים לממש. (ראו גם טיפ מספר 82)

אולי תתפלאו לשמוע, אבל יש שפות בהן הדבר הזה לא אפשרי, למשל שפת Java.

בJava אי אפשר לתת לשתי מתודות אותו שם ואתם פרמטרים, ולמעשה גם אין שם Explicit Implementation.

אז איך הם מסתדרים שם?

דבר ראשון, לJava יש תמיכה בCovariance במימוש ממשקים, למה הכוונה?

נניח שיש לנו איזשהו טיפוסים שמממש כמה ממשקים:

1
2
3
public class Dog : IBarkable, IMoveable
{
}

עכשיו נניח שיש לנו כמה Providerים:

1
2
3
4
5
6
7
8
9
public interface IBarkableProvider
{
IBarkable Provide();
}
public interface IMoveableProvider
{
IMoveable Provide();
}

עכשיו נניח שאנחנו רוצים לממש את שני הממשקים האלה של הProviderים. בC# אין לנו ברירה, ואנחנו חייבים לממש אותם Explicitly:

1
2
3
4
5
6
7
8
9
10
11
12
public class Provider : IBarkableProvider, IMoveableProvider
{
IBarkableIBarkableProvider.Provide()
{
// ...
}
IMoveableIMoveableProvider.Provide()
{
// ...
}
}

בJava יש אלטרנטיבה! אפשר לממש פונקציה אחת שמחזירה את שני הטיפוסים!

1
2
3
4
5
6
7
public class Provider :IBarkableProvider, IMoveableProvider
{
public Dog Provide()
{
// ...
}
}

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

ראו גם טיפים מספר 36-40 על Covariance וContravariance.


מה עוד?

הרי לא תמיד הסיטואציה כל כך פשוטה, למשל אם ערכי ההחזר הם string וint, אי אפשר לעשות טריק כזה.

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

1
2
3
4
5
6
7
8
9
10
public class Provider
{
public IBarkableProvider AsBarkableProvider()
{
}
public IMoveableProvider AsMoveableProvider()
{
}
}

ויוצרים מחלקות פנימיות שמממשות אותן:

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 class Provider
{
private class BarkableProvider : IBarkableProvider
{
private readonly Provider mProvider;
public BarkableProvider(Provider provider)
{
mProvider = provider;
}
public IBarkable Provide()
{
// ...
}
}
private class MoveableProvider : IMoveableProvider
{
private readonly Provider mProvider;
public MoveableProvider(Provider provider)
{
mProvider = provider;
}
public IMoveable Provide()
{
// ...
}
}
public IBarkableProvider AsBarkableProvider()
{
return new BarkableProvider(this);
}
public IMoveableProvider AsMoveableProvider()
{
return new MoveableProvider(this);
}
}

מה הטריק? הטריק הוא שהמחלקות הפנימיות יכולות לגשת לכל הMemberים של המחלקה Provider, ולכן לממש את הממשק כאוות נפשן! 😃

(ראו גם טיפ מספר 217)

אמנם בC# אנחנו יכולים לפתור את הבעיות האלה בצורה אחרת בעזרת Explicit implementation, אך אפשר להעתיק את הרעיון למספר למקומות שבהם הוא מתאים.

יום עם פולי קפה טובים טוב

שתף

284. Invertible dictionary

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

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

1
2
3
4
public class InvertibleDictionary<TKey, TValue>
: IDictionary<TKey, TValue>,ILookup<TValue, TKey>
{
}

איך נממש את זה?

נחזיק שני Dictionaryים – אחד מהKey לValue, השני מהValueלאוסף של Key, ונדאג שכל הפונקציות של IDictionaryיעברו דרך שני הDictionaryים:

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
public class InvertibleDictionary<TKey, TValue>
: IDictionary<TKey, TValue>, ILookup<TValue, TKey>
{
private class KeyValuePairGrouping : IGrouping<TValue, TKey>
{
private readonly KeyValuePair<TValue, ICollection<TKey>> mPair;
public KeyValuePairGrouping(KeyValuePair<TValue, ICollection<TKey>> pair)
{
mPair = pair;
}
public IEnumerator<TKey> GetEnumerator()
{
return mPair.Value.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public TValue Key
{
get
{
return mPair.Key;
}
}
}
private IDictionary<TKey, TValue> mKeyToValue = new Dictionary<TKey, TValue>();
private IDictionary<TValue, ICollection<TKey>> mValueToKeys = new Dictionary<TValue, ICollection<TKey>>();
IEnumerator<IGrouping<TValue, TKey>> IEnumerable<IGrouping<TValue, TKey>>.GetEnumerator()
{
return mValueToKeys
.Select(x => new KeyValuePairGrouping(x))
.Cast<IGrouping<TValue, TKey>>()
.GetEnumerator();
}
public void Add(TKey key, TValue value)
{
mKeyToValue.Add(key, value);
ICollection<TKey> valueToKeys;
if (!mValueToKeys.TryGetValue(value, out valueToKeys))
{
valueToKeys = new List<TKey>();
mValueToKeys[value] = valueToKeys;
}
valueToKeys.Add(key);
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
if (mKeyToValue.Remove(item.Key))
{
ICollection<TKey> valueToKey = mValueToKeys[item.Value];
valueToKey.Remove(item.Key);
if (!valueToKey.Any())
{
mValueToKeys.Remove(item.Value);
}
return true;
}
return false;
}
public bool Remove(TKey key)
{
TValue value;
if (!mKeyToValue.TryGetValue(key, out value))
{
return false;
}
return Remove(new KeyValuePair<TKey, TValue>(key, value));
}
public TValue this[TKey key]
{
get
{
return mKeyToValue[key];
}
set
{
TValue keyValue;
if (mKeyToValue.TryGetValue(key, out keyValue))
{
Remove(new KeyValuePair<TKey, TValue>(key, keyValue));
}
Add(key, value);
}
}
#region Dummy Implementation
public void Add(KeyValuePair<TKey, TValue> item)
{
Add(item.Key, item.Value);
}
public void Clear()
{
mKeyToValue.Clear();
mValueToKeys.Clear();
}
public bool Contains(KeyValuePair<TKey, TValue> item)
{
return mKeyToValue.Contains(item);
}
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
mKeyToValue.CopyTo(array, arrayIndex);
}
public bool Contains(TValue key)
{
return mValueToKeys.ContainsKey(key);
}
public bool ContainsKey(TKey key)
{
return mKeyToValue.ContainsKey(key);
}
int ILookup<TValue, TKey>.Count
{
get
{
return mValueToKeys.Count;
}
}
IEnumerable<TKey> ILookup<TValue, TKey>.this[TValue key]
{
get
{
return mValueToKeys[key];
}
}
int ICollection<KeyValuePair<TKey, TValue>>.Count
{
get
{
return mKeyToValue.Count;
}
}
public IEnumerator GetEnumerator()
{
IDictionary<TKey, TValue> dictionary = this;
return dictionary.GetEnumerator();
}
public bool IsReadOnly
{
get
{
return false;
}
}
public bool TryGetValue(TKey key, out TValue value)
{
return mKeyToValue.TryGetValue(key, out value);
}
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
{
return mKeyToValue.GetEnumerator();
}
public ICollection<TKey> Keys
{
get
{
return mKeyToValue.Keys;
}
}
public ICollection<TValue> Values
{
get
{
return mValueToKeys.Keys;
}
}
#endregion
}

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

  • יש הרבה מאוד מתודות לממשק IDictionary. לא הייתי ממליץ לאף אחד לממש אותו בעצמו (למרות שרוב המימושים הם שורה אחת-שתיים בעטיפה שכתובה פה)
  • המימושים הכבדים, באופן לא מפתיע, הם של הפונקציות Add, Remove והIndexer, שמבצעים מניפולציות על הDictionaryים הפנימיים
  • זה גם משהו שעלה בפעם הקודמת שכתבתי על המפה החד חד ערכית – מאחר ויש פה שני Indexerים, יש שתי אפשרויות:
    • הראשונה היא לממש את הממשקים Implicitly, ואז אם שני הטיפוסים שווים, אי אפשר לקרוא לIndexer ישירות (אלא רק דרך הכנסה למשתנה מסוג הממשק המתאים, ראו גם טיפ מספר 82)
    • השנייה, היא לממש לפחות את אחד הממשקיםExplicitly, ואז תמיד אפשר לקרוא לIndexerהנותר ישירות. לIndexer שמימשנו Explicitlyניתן יהיה לקרוא רק דרך הכנס למשתנה מהסוג המתאים. כאן בחרתי בדרך זו, כי נראה לי שיותר חשוב לגשת לערך לפי מפתח, לעומת הכיוון השני
  • שימו לב שיש פה כמה מימושים לGetEnumerator, למעשה יש את המימוש הלא גנרי של IEnumerable שמפנה למימוש של הDictionary, יש מימוש של הLookup שמפנה לDictionary ההפוך. היה אפשר לחשוב שאם נבצע מעבר על הCollection עם הטיפוס המתאים, הקומפיילר יפנה אותנו לפרמטר הגנרי, למשל שהקוד הבא יעבוד:
    1
    2
    3
    4
    foreach (IGrouping<string, int> group in invertible)
    {
    // ...
    }

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

1
2
3
4
5
6
IEnumerable<IGrouping<string,int>> invertibleEnumerable = invertible;
foreach (IGrouping<string, int> group in invertibleEnumerable)
{
// ...
}

  • שימו לב שכדי לממשIEnumerable<IGrouping<TValue, TKey>>, נאלצנו לכתוב מחלקת IGrouping משלנו, מאחר ואין כזאת שהיא public בFramework. בנוסף, היינו צריכים לעשות Select במימוש של GetEnumerator כדי שיחזרו טיפוסים מהסוג המתאים.

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

המשך יום הפיך טוב!

שתף

283. Using Lazy to achieve warm initialization

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

אהלן.

לאחרונה שאל אותי מישהו על בעיה שעלתה לו בתוכנה שכתב : יש לו יותר מThread אחד בתוכנה שמשתמש במשתנים שלוקח הרבה זמן ליצור אותם (מדובר ביותר ממשתנה אחד), כאשר הוא מעוניין בזמן עליה נמוך ובביצועים גבוהים.

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

המשתנים (הפלט) של האתחול הם כאלו:

  1. קונפיגורציה של האפליקציה
  2. חיבור לDB (התצורה של החיבור נקבעת לפי הפלט הראשון)
  3. פלט של שאילתא שמבוצעת מול הDB

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

1
2
3
IConfiguration GetConfiguration();
IDbConnection CreateDbConnection(IConfiguration configuration);
IEnumerable<User> GetUsers(IDbConnection dbConnection);

המימוש ההתחלתי של מנגנון הסנכרון היה ManualResetEvent שנקבע כאשר כל האתחול הסתיים בThread, ושאר הThreadים חיכו לEvent.

בטיפ’ מספר 129 זלינגר מסביר על המחלקה Lazy שקיימת ב.net 4 (ואף בDLLים שאפשר למצוא ברשת, המימוש פשוט), מה שזלינגר לא סיפר לכם זה שLazy תומך בThread Safety מהקופסה, כלומר – שהאתחול יתבצע פעם אחת בלבד, ואם Thread אחר ינסה לגשת למשתנה בזמן האתחול, הוא יחכה עד שהאתחול יסתיים.

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

ניצור שני Fieldים מהסוג Lazy, כאשר לכל אחד הפרמטר הגנרי יהיה לפי הפלט של האתחול :

1
2
3
private Lazy<IConfiguration> mConfiguration;
private Lazy<IDbConnection> mDbConnection;
private Lazy<IEnumerable<User>> mUsers;

בC’tor של מחלקת האתחול, נאתחל אותם :

1
2
3
mConfiguration = new Lazy<IConfiguration>(GetConfiguration);
mDbConnection = new Lazy<IDbConnection>(() => CreateDbConnection(mConfiguration.Value));
mUsers = new Lazy<IEnumerable<User>>(() => GetUsers(mDbConnection.Value));

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

כעת, נוכל לפתוח Thread שמביא את הLazy כדי ליצור את החיבור ברקע (ע"מ שזמן העליה יתקצר במקרה ונרצה להשתמש במשתנים הללו) :

1
ThreadPool.QueueUserWorkItem(x => { var nothing = mUsers.Value; });

השתלשלות העניינים היא כזו :

  1. הLazy של המשתמשים יריץ את הפונקציה של האתחול שלו
  2. הפונקציה של האתחול תגש לLazy של החיבור לDB
  3. הLazy של החיבור לDB יריץ את הפונקציה של האתחול שלו
  4. הפונקציה של האתחול תגש לLazy של הקונפיגורציה
  5. הLazy של הקונפיגורציה תריץ את הפונקציה של האתחול שלו
  6. הקונפיגורציה תובא
  7. החיבור לDB יושג
  8. המשתמשים ייובאו מהDB

בגלל שהמימוש של Lazy הוא כמו של Singleton – (Double null check), אבל פחות סטאטי, הוא מבטיח לנו שהפונקציה שאנחנו נותנים לו בC’tor תורץ רק פעם אחת. בפועל הוא משתמש בנעילה כדי שThreadים אחרים לא יוכלו לגשת לערך בזמן שהוא מאתחל אותו. ולכן הוא מאפשר לנו את ההתנהגות הרצויה במקרה הזה.

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

המשך יום עצל טוב

שתף

282. Two side dictionary

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

הדבר אפשרי אם ורק אם הDictionary הוא חד חד ערכי גם בערכים – כל ערך מופיע לכל היותר פעם אחת.

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

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
72
73
74
75
76
77
78
79
80
81
public class TwoSideDictionary<TKey, TValue>
{
private readonly IDictionary<TKey, TValue> mKeyToValue = new Dictionary<TKey, TValue>();
private readonly IDictionary<TValue, TKey> mValueToKey = new Dictionary<TValue, TKey>();
public void Map(TKey key, TValue value)
{
if (mKeyToValue.ContainsKey(key))
{
throw new ArgumentException("Key already mapped in dictionary", "key");
}
if (mValueToKey.ContainsKey(value))
{
throw new ArgumentException("Value already mapped in dictionary", "value");
}
mKeyToValue[key] = value;
mValueToKey[value] = key;
}
public TValue GetByKey(TKey key)
{
return mKeyToValue[key];
}
public TKey GetByValue(TValue key)
{
return mValueToKey[key];
}
public bool ContainsKey(TKey key)
{
return mKeyToValue.ContainsKey(key);
}
public bool ContainsValue(TValue value)
{
return mValueToKey.ContainsKey(value);
}
public TValue this[TKey key]
{
get
{
return GetByKey(key);
}
set
{
Map(key, value);
}
}
public TKey this[TValue key]
{
get
{
return GetByValue(key);
}
set
{
Map(value, key);
}
}
public IEnumerable<KeyValuePair<TKey, TValue>> KeyToValue
{
get
{
return mKeyToValue;
}
}
public IEnumerable<KeyValuePair<TKey, TValue>> ValueToKey
{
get
{
return mKeyToValue;
}
}
}

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

אפשר גם לממש את הממשק IDictionary<TKey, TValue> ע"י עטיפת הInstance של mKeyToValue.

שבוע גשום טוב!

שתף

281. Lookup, ToLookup

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

דרך אחת לעשות זאת היא להגדיר פשוט Dictionary שהערכים שלו הם מסוג Collection מסוים, למשל:

1
2
IDictionary<string,ICollection<int>> nameToIds =
new Dictionary<string,ICollection<int>>();

ואז להשתמש בכל מיני דרכים שיאפשרו לנו שימוש נוח איתו: ראו למשל טיפ מספר 13 וטיפ מספר 73.

החל מFramework 3.5, קיים הממשק ILookup המגדיר מיפוי כזה:

1
2
3
4
5
6
public interface ILookup<TKey, TElement> : IEnumerable<IGrouping<TKey, TElement>>
{
int Count { get; }
IEnumerable<TElement> this[TKey key] { get; }
bool Contains(TKey key);
}

בואו נשים לב לכמה דברים: הממשק מממשIEnumerable של IGrouping (ראו גם טיפ מספר 115) – הדבר הזה מאפשר לנו לרוץ על כל איברי הLookupבאופן אנלוגי לאופן בו KeyValuePair מאפשר לנו לרוץ על כל איברי הDictionary (ראו גם טיפ מספר 17)

בנוסף, קיים פה Indexer המחזיר לנוIEnumerable<TElement> - כלומר המיפוי אכן ממפה את המפתחות שלנו לאוספים. שימו לב שזה לאICollection<TElement>. בנוסף אין כאן מתודתAdd, או לחלופין Setter לIndexer. למעשה, המבנה הזה לא מיועד להיות בר שינוי, אלא להיות Immutable (אי אפשר להוסיף לאוסף איבר, ואי אפשר למפות מפתח לאוסף מסוים – ראו גם טיפ מספר 271)

אז איך משתמשים בזה? קיים מימוש של הFramework בשם הלא מפתיע Lookup. הבעיה היחידה היא שהConstructorשל Lookup הוא private.

דרך אחת שבה ניתן להשתמש בזה היא ע"י הExtension Method בשם ToLookup: בדומה למה שראינו בפעם שעברה, הExtension Method מקבל Delegate שבוחר את הKey וDelegate שבוחר את הValue וממפה אותם:

1
2
ILookup<string, int> nameToIds =
people.ToLookup(x => x.Name, x => x.Id);

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

סופ"ש רב ערכי טוב!

שתף