Menu

Dictionary ב-C#: הוספה, TryGetValue, מעבר בלולאה ומיון

Dictionary<TKey, TValue> ממפה מפתחות לערכים עם חיפוש מהיר. למדו איך מוסיפים ומעדכנים רשומות, קוראים בבטחה עם TryGetValue, עוברים על רשומות KeyValuePair, סופרים הופעות, מתעלמים מאותיות גדולות וקטנות במפתחות, ושומרים על מפתחות ממוינים עם SortedDictionary.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

Dictionary<TKey, TValue> מאחסן ערכים תחת מפתחות ייחודיים ומוצא ערך לפי המפתח שלו בזמן קבוע בקירוב, לא משנה כמה רשומות יש. זו המקבילה של C# ל-hash map: ספר טלפונים משם למספר, מטמון ממזהה לרשומה, ספירה לכל מילה.

יצירת מילון וקריאת ערכים

פלט:

12
2.50
2
True
False

שתי צורות האתחול עושות את אותו הדבר. הצורה ["key"] = value (C# 6) משתמשת ב-indexer, כך שמפתח חוזר דורס; הצורה { key, value } קוראת ל-Add, כך שמפתח חוזר זורק כשהשורה רצה.

ContainsKey הוא חיפוש hash והוא מהיר. ContainsValue צריך לסרוק כל רשומה, כי הערכים אינם מאונדקסים.

Add מול ה-indexer מול TryAdd

יש שלוש דרכים להכניס רשומה, והן נבדלות רק במה שקורה כשהמפתח כבר קיים:

פלט:

26
Caught ArgumentException
True
False
31

זה ש-Add זורק על כפילות הוא פיצ'ר: הוא חושף נתונים שהיו אמורים להיות ייחודיים ולא היו. השתמשו ב-indexer כש"הכנס או עדכן" זה מה שאתם מתכוונים אליו, וב-TryAdd (.NET Core 2.0 ואילך) כשהערך הראשון צריך לנצח.

מפתחות לא יכולים להיות null. Add(null, ...) או dict[null] זורקים ArgumentNullException. ערכים יכולים להיות null כשטיפוס הערך מאפשר זאת.

KeyNotFoundException ו-TryGetValue

קריאה של מפתח שלא קיים עם ה-indexer זורקת KeyNotFoundException. זו השגיאה הנפוצה ביותר עם מילונים, והפתרון הוא כמעט תמיד TryGetValue.

פלט:

Caught KeyNotFoundException
Found ana@example.com
Missing, value is null: True
no email

TryGetValue עושה חיפוש hash אחד ומדווחת על הצלחה כ-bool. התבנית if (dict.ContainsKey(k)) { var v = dict[k]; } עובדת, אבל מחפשת את המפתח פעמיים. כשהמפתח חסר, משתנה ה-out מקבל את ערך ברירת המחדל של הטיפוס (null, 0, false).

ב-.NET Core 2.0 ואילך יש גם GetValueOrDefault(key, fallback), שמחזירה את ערך החלופה כשהמפתח חסר: emails.GetValueOrDefault(103, "no email").

עדכון והסרה של רשומות

פלט:

2
True
False
1
0

cart["milk"] += 1 זורק KeyNotFoundException אם milk עוד לא במילון, כי הוא קורא לפני שהוא כותב. Remove מחזירה false במקום לזרוק על מפתח חסר, כך שאין צורך לבדוק ContainsKey קודם.

מעבר בלולאה: KeyValuePair, Keys ו-Values

לולאת foreach על מילון מפיקה פריטים מסוג KeyValuePair<TKey, TValue>, לכל אחד Key ו-Value.

פלט:

Ana: 88
Ben: 72
Chloe: 95
Ana Ben Chloe 
Total 255
77

pair.Value הוא לקריאה בלבד, כך שעדכון ערכים פירושו כתיבה דרך ה-indexer. הלולאה האחרונה עוברת על עותק מסוג List<string> של המפתחות, וזה תמיד בטוח; מעבר ישיר על scores.Keys תוך דריסת ערכים קיימים מותר ב-.NET Core 3.0 ואילך, אבל זורק InvalidOperationException ב-.NET Framework.

הוספת מפתח חדש בתוך foreach על אותו מילון זורקת InvalidOperationException בכל גרסה. הסרה בזמן המעבר זורקת ב-.NET Framework ומותרת החל מ-.NET Core 3.0. קוד שחייב לרוץ בכל מקום אוסף קודם את המפתחות להסרה, ואז מסיר אותם אחרי הלולאה.

עם C# 7 ו-.NET Core 2.0 ואילך, אפשר לפרק את KeyValuePair בכותרת הלולאה:

foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}

ספירה עם מילון

ספירת הופעות היא השימוש הקלאסי. קראו את הספירה הנוכחית עם TryGetValue (מפתח חסר נותן 0), ואז כתבו בחזרה.

פלט:

the 3
cat 1
and 2
dog 1
bird 1

אותה צורה מקבצת פריטים: Dictionary<string, List<Order>>, שבו שולפים את הרשימה עם TryGetValue, יוצרים ושומרים אחת אם היא חסרה, ואז עושים לה Add. לספירה ולקיבוץ חד-פעמיים, LINQ עושה את זה בביטוי אחד: words.GroupBy(w => w).ToDictionary(g => g.Key, g => g.Count()). ראו LINQ.

מפתחות שאינם רגישים לאותיות גדולות וקטנות עם comparer

מפתחות מחרוזת מושווים בדיוק כברירת מחדל: "Apple" ו-"apple" הם שני מפתחות. העבירו IEqualityComparer<string> לבנאי כדי לשנות את זה.

פלט:

False
text/html
1

StringComparer.OrdinalIgnoreCase היא הבחירה הנכונה למזהים כמו headers של HTTP, סיומות קבצים ושמות משתמש. קריאה ל-.ToLower() על כל מפתח לפני שמאחסנים אותו גם עובדת, אבל קל לשכוח אותה במקום אחד.

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

סדר, מיון ו-SortedDictionary

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

כשצריך סדר, מיינו בנקודת השימוש או השתמשו באוסף ממוין:

פלט:

Cairo 210
Lima 340
Oslo 520
By value, highest first:
Oslo 520
Lima 340
Cairo 210
Berlin, Cairo, Lima, Oslo

SortedDictionary<TKey, TValue> שומר את המפתחות שלו ממוינים כל הזמן (הוא עץ מאוזן), כך שחיפושים והכנסות הם O(log n) במקום O(1). השתמשו בו כשאתם עוברים לעיתים קרובות לפי סדר המפתחות; מיינו מילון רגיל עם LINQ כשצריך סדר רק פעם אחת. SortedList<TKey, TValue> היא אפשרות שלישית שמשתמשת בפחות זיכרון, אבל ההכנסה אליה איטית כשהיא גדולה.

עזר מהיר

משימהקוד
יצירהnew Dictionary<string, int>()
הכנסה או דריסהd[k] = v
הכנסה, זריקה על כפילותd.Add(k, v)
הכנסה רק אם חדשd.TryAdd(k, v)
קריאה, זריקה אם חסרd[k]
קריאה בטוחהd.TryGetValue(k, out var v)
המפתח קייםd.ContainsKey(k)
הסרהd.Remove(k) (מחזירה bool)
גודלd.Count
מפתחות, ערכיםd.Keys, d.Values
ממוין לפי מפתחd.OrderBy(p => p.Key) או SortedDictionary
התעלמות מאותיות גדולות וקטנותnew Dictionary<string, T>(StringComparer.OrdinalIgnoreCase)

טעויות נפוצות

  • קריאה של מפתח חסר עם d[k]. זורק KeyNotFoundException; השתמשו ב-TryGetValue.
  • קריאה ל-Add עבור מפתח שאולי קיים. זורק ArgumentException; השתמשו ב-indexer או ב-TryAdd.
  • הוספת מפתחות בתוך foreach על המילון. זורק InvalidOperationException; אספו את השינויים והחילו אותם אחר כך.
  • הסתמכות על סדר המעבר. מיינו, או השתמשו ב-SortedDictionary.
  • שינוי השדות של אובייקט מפתח אחרי שהוכנס. ה-hash code שלו משתנה ואי אפשר יותר למצוא את הרשומה.
  • ContainsKey ואחריו indexer. שני חיפושים; TryGetValue עושה אחד.

שאלות נפוצות

מה ההבדל בין Dictionary.Add ל-indexer ב-C#?

dict.Add(key, value) מכניס רשומה חדשה וזורק ArgumentException אם המפתח כבר קיים. dict[key] = value מכניס את הרשומה אם המפתח חדש ודורס את הערך אם הוא קיים, ואף פעם לא זורק על כפילות. TryAdd(key, value) מכניס רק כשהמפתח חדש, ומחזיר false אחרת.

איך TryGetValue עובדת ב-C#?

dict.TryGetValue(key, out var value) מחזירה true ומציבה את value כשהמפתח קיים, ומחזירה false ומציבה ב-value את ברירת המחדל של הטיפוס שלו כשהוא לא קיים. היא עושה חיפוש אחד, בעוד ש-ContainsKey ואחריו dict[key] עושים שניים, והיא אף פעם לא זורקת KeyNotFoundException.

איך עוברים בלולאה על Dictionary ב-C#?

foreach (KeyValuePair<string, int> pair in dict) נותן כל רשומה עם pair.Key ו-pair.Value. כדי לעבור רק על המפתחות או רק על הערכים, השתמשו ב-dict.Keys או ב-dict.Values. אל תוסיפו מפתחות למילון בתוך הלולאה הזו: זה זורק InvalidOperationException.

האם Dictionary ב-C# שומר על סדר?

שום סדר לא מובטח. מילון שרק הוסיפו לו רשומות בדרך כלל מחזיר אותן בסדר ההכנסה, אבל אחרי Remove רשומות חדשות יכולות למלא את המקום שהתפנה, כך שהסדר משתנה. מיינו כשאתם צריכים סדר: dict.OrderBy(p => p.Key), או השתמשו ב-SortedDictionary<TKey, TValue>, שתמיד מחזיר את הרשומות לפי מפתח.

איך הופכים מפתחות של Dictionary ללא רגישים לאותיות גדולות וקטנות?

העבירו comparer לבנאי: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase). אז "Apple" ו-"apple" הם אותו מפתח בחיפושים, ב-Add וב-ContainsKey. ה-comparer נקבע כשהמילון נוצר.

מהו KeyValuePair ב-C#?

KeyValuePair<TKey, TValue> הוא ה-struct שמילון נותן לכם עבור כל רשומה כשעוברים עליו. יש לו מאפייני Key ו-Value לקריאה בלבד, כך שאי אפשר לשנות רשומה דרכו; כתבו במקום זאת dict[pair.Key] = newValue (אחרי הלולאה, או על עותק של המפתחות).

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל