ทำไม Dictionary ไม่มี AddRange


115

ชื่อเรื่องเป็นพื้นฐานเพียงพอทำไมฉันไม่:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic.AddRange(MethodThatReturnAnotherDic());


2
มีจำนวนมากที่ไม่มี AddRange ซึ่งทำให้ฉันประหลาดใจมาโดยตลอด ชอบคอลเลกชัน <> ดูเหมือนจะแปลกเสมอที่ List <> มีมัน แต่ไม่ใช่ Collection <> หรือวัตถุ IList และ ICollection อื่น ๆ
ทิม

39
ฉันจะดึง Eric Lippert มาที่นี่: "เพราะไม่เคยมีใครออกแบบระบุใช้งานทดสอบจัดทำเอกสารและจัดส่งคุณลักษณะนั้น"
Gabe Moothart

3
@ Gabe Moothart - นั่นคือสิ่งที่ฉันคาดเดา ฉันชอบใช้บรรทัดนั้นกับคนอื่น พวกเขาเกลียดมัน :)
ทิม

2
@GabeMoothart มันจะไม่ง่ายไปกว่านี้ที่จะพูดว่า "Because you can't" หรือ "Because it does" หรือ "Because."? - ฉันเดาว่ามันไม่สนุกที่จะพูดหรืออะไร? --- คำถามติดตามของคุณสำหรับคำตอบของคุณ (ฉันเดาว่ายกมาหรือถอดความ) คือ "ทำไมไม่เคยมีใครออกแบบระบุใช้งานทดสอบจัดทำเอกสารและจัดส่งคุณลักษณะนั้น" ซึ่งคุณอาจจะเป็นได้ บังคับให้ตอบกลับด้วยคำว่า "เพราะไม่มีใครทำ" ซึ่งใกล้เคียงกับคำตอบที่ฉันแนะนำไว้ก่อนหน้านี้ ทางเลือกเดียวที่ฉันสามารถจินตนาการได้ไม่ใช่การประชดประชันและเป็นสิ่งที่ OP สงสัยจริงๆ
Code Jockey

คำตอบ:


76

ความคิดเห็นของคำถามเดิมสรุปได้ค่อนข้างดี:

เนื่องจากไม่เคยมีใครออกแบบระบุใช้งานทดสอบจัดทำเอกสารและจัดส่งคุณลักษณะนั้น - @Gabe Moothart

ทำไม? อาจเป็นเพราะพฤติกรรมของการรวมพจนานุกรมไม่สามารถให้เหตุผลในลักษณะที่สอดคล้องกับแนวทางของ Framework ได้

AddRangeไม่มีอยู่เนื่องจากช่วงไม่มีความหมายใด ๆ กับคอนเทนเนอร์ที่เชื่อมโยงเนื่องจากช่วงของข้อมูลอนุญาตให้มีรายการที่ซ้ำกัน เช่นหากคุณมีIEnumerable<KeyValuePair<K,T>>คอลเล็กชันนั้นจะไม่ป้องกันรายการที่ซ้ำกัน

ลักษณะการทำงานของการเพิ่มคอลเลกชันของคู่คีย์ - ค่าหรือแม้กระทั่งการรวมพจนานุกรมสองพจนานุกรมก็ตรงไปตรงมา อย่างไรก็ตามพฤติกรรมในการจัดการกับรายการที่ซ้ำกันหลายรายการไม่เป็นเช่นนั้น

พฤติกรรมของเมธอดควรเป็นอย่างไรเมื่อเกี่ยวข้องกับรายการที่ซ้ำกัน?

มีอย่างน้อยสามวิธีที่ฉันคิดได้:

  1. โยนข้อยกเว้นสำหรับรายการแรกที่ซ้ำกัน
  2. โยนข้อยกเว้นที่มีรายการที่ซ้ำกันทั้งหมด
  3. ละเว้นรายการที่ซ้ำกัน

เมื่อเกิดข้อยกเว้นสถานะของพจนานุกรมต้นฉบับควรเป็นอย่างไร?

Addมักจะถูกนำไปใช้เป็นการดำเนินการแบบปรมาณู: มันประสบความสำเร็จและอัปเดตสถานะของคอลเลกชันหรือล้มเหลวและสถานะของคอลเลกชันจะไม่เปลี่ยนแปลง เนื่องจากAddRangeอาจล้มเหลวเนื่องจากข้อผิดพลาดที่ซ้ำกันวิธีที่จะทำให้พฤติกรรมของมันสอดคล้องกันAddก็คือการทำให้เป็นปรมาณูด้วยการทิ้งข้อยกเว้นสำหรับสิ่งที่ซ้ำกันและปล่อยให้สถานะของพจนานุกรมต้นฉบับไม่เปลี่ยนแปลง

ในฐานะที่เป็นผู้บริโภค API ก็จะน่าเบื่อที่จะต้องซ้ำลบองค์ประกอบที่ซ้ำกันซึ่งหมายถึงว่าAddRangeควรโยนข้อยกเว้นเดียวที่มีทุกค่าที่ซ้ำกัน

ทางเลือกจะเดือดลงไปที่:

  1. ทิ้งข้อยกเว้นกับรายการที่ซ้ำกันทั้งหมดทิ้งพจนานุกรมต้นฉบับไว้เพียงอย่างเดียว
  2. ละเว้นรายการที่ซ้ำกันและดำเนินการต่อ

มีข้อโต้แย้งในการสนับสนุนทั้งกรณีการใช้งาน ในการทำเช่นนั้นคุณเพิ่มIgnoreDuplicatesธงลงในลายเซ็นหรือไม่?

การIgnoreDuplicatesตั้งค่าสถานะ (เมื่อตั้งค่าเป็นจริง) จะช่วยเพิ่มความเร็วอย่างมีนัยสำคัญเนื่องจากการใช้งานพื้นฐานจะข้ามรหัสสำหรับการตรวจสอบซ้ำ

ตอนนี้คุณมีแฟล็กที่อนุญาตให้AddRangeรองรับทั้งสองกรณี แต่มีผลข้างเคียงที่ไม่มีเอกสาร (ซึ่งเป็นสิ่งที่นักออกแบบ Framework ทำงานอย่างหนักเพื่อหลีกเลี่ยง)

สรุป

เนื่องจากไม่มีพฤติกรรมที่ชัดเจนสอดคล้องและคาดหวังเมื่อต้องจัดการกับรายการที่ซ้ำกันจึงง่ายกว่าที่จะไม่จัดการกับสิ่งเหล่านี้ทั้งหมดเข้าด้วยกันและไม่ได้ระบุวิธีการเริ่มต้นด้วย

หากคุณพบว่าตัวเองต้องรวมพจนานุกรมอย่างต่อเนื่องคุณสามารถเขียนวิธีการขยายของคุณเองเพื่อรวมพจนานุกรมซึ่งจะทำงานในลักษณะที่เหมาะกับแอปพลิเคชันของคุณ


37
เป็นเท็จโดยสิ้นเชิงพจนานุกรมควรมี AddRange (IEnumerable <KeyValuePair <K, T >> ค่า)
Gusman

19
หากสามารถมีเพิ่มได้ก็ควรจะสามารถเพิ่มหลาย ๆ ลักษณะการทำงานเมื่อคุณพยายามเพิ่มรายการที่มีคีย์ที่ซ้ำกันควรจะเหมือนกับเมื่อคุณเพิ่มคีย์ที่ซ้ำกันเพียงรายการเดียว
Uriah Blatherwick

5
AddMultipleแตกต่างจากที่AddRangeไม่ว่าการใช้งานจะเป็นไปได้: คุณโยนข้อยกเว้นด้วยอาร์เรย์ของคีย์ที่ซ้ำกันทั้งหมดหรือไม่? หรือคุณโยนข้อยกเว้นให้กับคีย์ที่ซ้ำกันครั้งแรกที่คุณพบ? สถานะของพจนานุกรมควรเป็นอย่างไรหากมีข้อยกเว้น Pristine หรือกุญแจทั้งหมดที่ทำสำเร็จ?
Alan

3
เอาล่ะตอนนี้ฉันต้องทำซ้ำการแจงนับของฉันด้วยตนเองและเพิ่มทีละรายการพร้อมคำเตือนทั้งหมดเกี่ยวกับรายการที่ซ้ำกันที่คุณกล่าวถึง การละเว้นจากกรอบจะแก้ปัญหาอะไรได้อย่างไร?
doug65536

4
@ doug65536 เพราะคุณเป็นผู้บริโภค API ที่สามารถตอนนี้ตัดสินใจว่าคุณต้องการจะทำอย่างไรกับแต่ละบุคคลAdd- ทั้งห่อแต่ละAddในtry...catchและจับซ้ำกันว่าวิธีการ; หรือใช้ตัวสร้างดัชนีและเขียนทับค่าแรกด้วยค่าในภายหลัง หรือตรวจสอบล่วงหน้าโดยใช้ContainsKeyก่อนที่จะพยายามAddรักษาค่าดั้งเดิมไว้ หากเฟรมเวิร์กมีAddRangeหรือAddMultipleวิธีการวิธีง่ายๆเพียงวิธีเดียวในการสื่อสารสิ่งที่เกิดขึ้นจะต้องผ่านข้อยกเว้นและการจัดการและการกู้คืนที่เกี่ยวข้องจะไม่ซับซ้อนน้อยลง
Zev Spitz

36

ฉันมีวิธีแก้ปัญหา:

Dictionary<string, string> mainDic = new Dictionary<string, string>() { 
    { "Key1", "Value1" },
    { "Key2", "Value2.1" },
};
Dictionary<string, string> additionalDic= new Dictionary<string, string>() { 
    { "Key2", "Value2.2" },
    { "Key3", "Value3" },
};
mainDic.AddRangeOverride(additionalDic); // Overrides all existing keys
// or
mainDic.AddRangeNewOnly(additionalDic); // Adds new keys only
// or
mainDic.AddRange(additionalDic); // Throws an error if keys already exist
// or
if (!mainDic.ContainsKeys(additionalDic.Keys)) // Checks if keys don't exist
{
    mainDic.AddRange(additionalDic);
}

...

namespace MyProject.Helper
{
  public static class CollectionHelper
  {
    public static void AddRangeOverride<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => dic[x.Key] = x.Value);
    }

    public static void AddRangeNewOnly<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => { if (!dic.ContainsKey(x.Key)) dic.Add(x.Key, x.Value); });
    }

    public static void AddRange<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => dic.Add(x.Key, x.Value));
    }

    public static bool ContainsKeys<TKey, TValue>(this IDictionary<TKey, TValue> dic, IEnumerable<TKey> keys)
    {
        bool result = false;
        keys.ForEachOrBreak((x) => { result = dic.ContainsKey(x); return result; });
        return result;
    }

    public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
    {
        foreach (var item in source)
            action(item);
    }

    public static void ForEachOrBreak<T>(this IEnumerable<T> source, Func<T, bool> func)
    {
        foreach (var item in source)
        {
            bool result = func(item);
            if (result) break;
        }
    }
  }
}

มีความสุข.


คุณไม่จำเป็นต้องToList()ใช้พจนานุกรมคือIEnumerable<KeyValuePair<TKey,TValue>ไฟล์. นอกจากนี้วิธีการที่สองและสามจะเกิดขึ้นหากคุณเพิ่มค่าคีย์ที่มีอยู่ ไม่ใช่ความคิดที่ดีคุณกำลังมองหาTryAdd? สุดท้ายตัวที่สองสามารถแทนที่ด้วยWhere(pair->!dic.ContainsKey(pair.Key)...
Panagiotis Kanavos

2
โอเคToList()ไม่ใช่วิธีแก้ปัญหาที่ดีฉันจึงเปลี่ยนรหัส คุณสามารถใช้ได้try { mainDic.AddRange(addDic); } catch { do something }หากคุณไม่แน่ใจสำหรับวิธีที่สาม วิธีที่สองทำงานได้อย่างสมบูรณ์
ADM-IT

สวัสดี Panagiotis Kanavos ฉันหวังว่าคุณจะมีความสุข
ADM-IT

1
ขอบคุณฉันขโมยสิ่งนี้
metabuddy

14

ในกรณีที่มีคนเจอคำถามนี้เหมือนตัวเอง - เป็นไปได้ที่จะบรรลุ "AddRange" โดยใช้วิธีการขยาย IEnumerable:

var combined =
    dict1.Union(dict2)
        .GroupBy(kvp => kvp.Key)
        .Select(grp => grp.First())
        .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

เคล็ดลับหลักในการรวมพจนานุกรมคือการจัดการกับคีย์ที่ซ้ำกัน .Select(grp => grp.First())ในโค้ดข้างต้นนั้นเป็นส่วนหนึ่ง ในกรณีนี้จะใช้องค์ประกอบแรกจากกลุ่มของรายการที่ซ้ำกัน แต่คุณสามารถใช้ตรรกะที่ซับซ้อนกว่านี้ได้หากจำเป็น


จะเกิดอะไรขึ้นถ้าdict1 ไม่ใช้ตัวเปรียบเทียบความเท่าเทียมเริ่มต้น
mjwills

วิธี Linq ช่วยให้คุณส่งผ่าน IEqualityComparer เมื่อเกี่ยวข้อง:var combined = dict1.Concat(dict2).GroupBy(kvp => kvp.Key, dict1.Comparer).ToDictionary(grp => grp.Key, grp=> grp.First(), dict1.Comparer);
Kyle McClellan

12

ฉันเดาว่าไม่มีผลลัพธ์ที่เหมาะสมสำหรับผู้ใช้ว่าเกิดอะไรขึ้น เนื่องจากคุณไม่สามารถมีคีย์ซ้ำในพจนานุกรมได้คุณจะจัดการกับการรวมพจนานุกรมสองชุดโดยที่บางคีย์ตัดกันได้อย่างไร แน่นอนว่าคุณสามารถพูดว่า: "ฉันไม่สนใจ" แต่นั่นเป็นการทำลายแบบแผนในการส่งคืนเท็จ / โยนข้อยกเว้นสำหรับการกดคีย์ซ้ำ


5
มันแตกต่างจากที่ที่คุณมีคีย์ - แคลชเมื่อคุณโทรAddอย่างไรนอกเหนือจากนั้นอาจเกิดขึ้นได้มากกว่าหนึ่งครั้ง มันจะโยนเดียวArgumentExceptionที่Addไม่แน่นอน?
nicodemus13

1
@ nicodemus13 ใช่ แต่คุณจะไม่รู้ว่าคีย์ใดส่ง Exception เพียงคีย์บางคีย์เท่านั้นที่เป็นคีย์ซ้ำ
Gal

1
@Gal ได้รับ แต่คุณสามารถ: คืนชื่อของคีย์ที่ขัดแย้งกันในข้อความยกเว้น (มีประโยชน์สำหรับคนที่รู้ว่าพวกเขากำลังทำอะไรฉันคิดว่า ... ) หรือคุณสามารถใส่เป็น (ส่วนหนึ่งของ?) อาร์กิวเมนต์ paramName กับ ArgumentException ที่คุณโยน OR-OR คุณสามารถสร้างประเภทข้อยกเว้นใหม่ (บางทีตัวเลือกทั่วไปที่เพียงพออาจเป็นNamedElementException??) โยนแทนหรือเป็น innerException ของ ArgumentException ซึ่งระบุองค์ประกอบที่ตั้งชื่อที่ขัดแย้งกัน ... ตัวเลือกที่แตกต่างกันเล็กน้อยฉันจะบอกว่า
Code Jockey

7

คุณสามารถทำได้

Dictionary<string, string> dic = new Dictionary<string, string>();
// dictionary other items already added.
MethodThatReturnAnotherDic(dic);

public void MethodThatReturnAnotherDic(Dictionary<string, string> dic)
{
    dic.Add(.., ..);
}

หรือใช้ List สำหรับ addrange และ / หรือใช้รูปแบบด้านบน

List<KeyValuePair<string, string>>

1
พจนานุกรมมีตัวสร้างที่ยอมรับพจนานุกรมอื่นอยู่แล้ว
Panagiotis Kanavos

1
OP ต้องการเพิ่มช่วงไม่ใช่เพื่อโคลนพจนานุกรม MethodThatReturnAnotherDicในฐานะที่เป็นชื่อของวิธีในตัวอย่างของฉัน มันมาจาก OP โปรดตรวจสอบคำถามและคำตอบของฉันอีกครั้ง
Valamas

1

หากคุณกำลังจัดการกับพจนานุกรมใหม่ (และคุณไม่มีแถวที่จะสูญเสีย) คุณสามารถใช้ ToDictionary () จากรายการวัตถุอื่นได้ตลอดเวลา

ดังนั้นในกรณีของคุณคุณจะทำสิ่งนี้:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic = SomeList.ToDictionary(x => x.Attribute1, x => x.Attribute2);

5
คุณไม่จำเป็นต้องสร้างพจนานุกรมใหม่เพียงแค่เขียนDictionary<string, string> dic = SomeList.ToDictionary...
Panagiotis Kanavos

1

หากคุณรู้ว่าคุณจะไม่มีคีย์ซ้ำคุณสามารถทำได้:

dic = dic.Union(MethodThatReturnAnotherDic()).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

จะทำให้เกิดข้อยกเว้นหากมีคู่คีย์ / ค่าที่ซ้ำกัน

ฉันไม่รู้ว่าทำไมสิ่งนี้ถึงไม่อยู่ในกรอบ ควรจะเป็น. ไม่มีความแน่นอน เพียงแค่โยนข้อยกเว้น ในกรณีของรหัสนี้จะมีข้อยกเว้น


จะเกิดอะไรขึ้นถ้าพจนานุกรมต้นฉบับถูกสร้างขึ้นโดยใช้var caseInsensitiveDictionary = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase);?
mjwills

1

อย่าลังเลที่จะใช้วิธีการขยายเช่นนี้:

public static Dictionary<T, U> AddRange<T, U>(this Dictionary<T, U> destination, Dictionary<T, U> source)
{
  if (destination == null) destination = new Dictionary<T, U>();
  foreach (var e in source)
    destination.Add(e.Key, e.Value);
  return destination;
}

0

นี่คือทางเลือกอื่นโดยใช้ c # 7 ValueTuples (tuple literals)

public static class DictionaryExtensions
{
    public static Dictionary<TKey, TValue> AddRange<TKey, TValue>(this Dictionary<TKey, TValue> source,  IEnumerable<ValueTuple<TKey, TValue>> kvps)
    {
        foreach (var kvp in kvps)
            source.Add(kvp.Item1, kvp.Item2);

        return source;
    }

    public static void AddTo<TKey, TValue>(this IEnumerable<ValueTuple<TKey, TValue>> source, Dictionary<TKey, TValue> target)
    {
        target.AddRange(source);
    }
}

ใช้แล้วชอบ

segments
    .Zip(values, (s, v) => (s.AsSpan().StartsWith("{") ? s.Trim('{', '}') : null, v))
    .Where(zip => zip.Item1 != null)
    .AddTo(queryParams);

0

ดังที่คนอื่น ๆ ได้กล่าวถึงสาเหตุที่Dictionary<TKey,TVal>.AddRangeไม่นำมาใช้เนื่องจากมีหลายวิธีที่คุณอาจต้องการจัดการกรณีที่คุณมีรายการซ้ำ และนี่ก็เป็นกรณีที่Collectionหรือการเชื่อมต่อเช่นIDictionary<TKey,TVal>,ICollection<T>ฯลฯ

List<T>ใช้งานได้เท่านั้นและคุณจะทราบว่าIList<T>อินเทอร์เฟซไม่ทำงานด้วยเหตุผลเดียวกัน: ที่คาดไว้ลักษณะการทำงานที่เมื่อเพิ่มช่วงของค่าลงในคอลเล็กชันอาจแตกต่างกันไปในวงกว้างขึ้นอยู่กับบริบท

บริบทของคำถามของคุณแสดงให้เห็นว่าคุณไม่ต้องกังวลเกี่ยวกับรายการที่ซ้ำกันซึ่งในกรณีนี้คุณมีทางเลือกในการใช้งานง่ายโดยใช้ Linq:

MethodThatReturnAnotherDic().ToList.ForEach(kvp => dic.Add(kvp.Key, kvp.Value));
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.