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 כדי שיחזרו טיפוסים מהסוג המתאים.

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

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

שתף