Tại sao từ điển không có AddRange?


115

Tiêu đề là đủ cơ bản, tại sao tôi không thể:

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


2
Có rất nhiều thứ không có AddRange, điều này luôn khiến tôi hoang mang. Thích Bộ sưu tập <>. Luôn luôn có vẻ kỳ lạ rằng Danh sách <> có nó, nhưng không phải Bộ sưu tập <> hoặc các đối tượng IList và ICollection khác.
Tim

39
Tôi sẽ lấy một Eric Lippert ở đây: "bởi vì không ai từng thiết kế, chỉ định, triển khai, thử nghiệm, ghi chép và vận chuyển tính năng đó."
Gabe Moothart

3
@ Gabe Moothart - đó chính xác là những gì tôi đã giả định. Tôi thích sử dụng dòng đó trên người khác. Họ ghét nó mặc dù. :)
Tim

2
@GabeMoothart sẽ không đơn giản hơn nếu nói "Bởi vì bạn không thể" hoặc "Bởi vì nó không" hoặc thậm chí "Bởi vì." - Tôi đoán điều đó không vui khi nói hay sao? --- Câu hỏi tiếp theo của tôi cho câu trả lời (tôi đoán là được trích dẫn hoặc diễn giải) của bạn là "Tại sao không ai từng thiết kế, chỉ định, triển khai, thử nghiệm, lập tài liệu và gửi tính năng đó?", Mà bạn rất có thể buộc phải trả lời bằng "Bởi vì không ai đã làm", ngang bằng với những câu trả lời tôi đã đề xuất trước đó. Lựa chọn khác duy nhất mà tôi có thể hình dung không mang tính châm biếm và đó là điều mà OP thực sự đang băn khoăn.
Mã Jockey

Câu trả lời:


76

Một nhận xét cho câu hỏi ban đầu tóm tắt điều này khá tốt:

bởi vì không ai từng thiết kế, chỉ định, triển khai, thử nghiệm, ghi lại và vận chuyển tính năng đó. - @Gabe Moothart

Tại sao? Chà, có thể là do hành vi hợp nhất các từ điển không thể được lý giải theo cách phù hợp với các nguyên tắc của Khung.

AddRangekhông tồn tại bởi vì một phạm vi không có bất kỳ ý nghĩa nào đối với một vùng chứa kết hợp, vì phạm vi dữ liệu cho phép các mục nhập trùng lặp. Ví dụ: nếu bạn có một IEnumerable<KeyValuePair<K,T>>bộ sưu tập không bảo vệ khỏi các mục trùng lặp.

Hành vi thêm một bộ sưu tập các cặp khóa-giá trị hoặc thậm chí hợp nhất hai từ điển là dễ dàng. Tuy nhiên, hành vi của cách đối phó với nhiều mục nhập trùng lặp thì không.

Hành vi của phương thức sẽ như thế nào khi nó xử lý một bản sao?

Có ít nhất ba giải pháp tôi có thể nghĩ đến:

  1. ném một ngoại lệ cho mục nhập đầu tiên trùng lặp
  2. ném một ngoại lệ có chứa tất cả các mục trùng lặp
  3. Bỏ qua các bản sao

Khi một ngoại lệ được ném ra, trạng thái của từ điển gốc sẽ như thế nào?

Addhầu như luôn luôn được thực hiện như một hoạt động nguyên tử: nó thành công và cập nhật trạng thái của bộ sưu tập, hoặc nó không thành công và trạng thái của bộ sưu tập được giữ nguyên. Có AddRangethể không thành công do lỗi trùng lặp, cách để giữ cho hành vi của nó nhất quán Addlà cũng làm cho nó trở thành nguyên tử bằng cách ném một ngoại lệ vào bất kỳ bản sao nào và giữ nguyên trạng thái của từ điển gốc là không thay đổi.

Là một người tiêu dùng API, sẽ thật tẻ nhạt nếu phải xóa lặp đi lặp lại các phần tử trùng lặp, điều này ngụ ý rằng AddRangenên ném một ngoại lệ duy nhất chứa tất cả các giá trị trùng lặp.

Sự lựa chọn sau đó sẽ tóm tắt thành:

  1. Bỏ một ngoại lệ với tất cả các bản sao, để nguyên từ điển gốc.
  2. Bỏ qua các bản sao và tiếp tục.

Có những lập luận ủng hộ cả hai trường hợp sử dụng. Để làm điều đó, bạn có thêm một IgnoreDuplicateslá cờ vào chữ ký không?

Các IgnoreDuplicateslá cờ (khi thiết lập là true) cũng sẽ cung cấp một tốc độ đáng kể lên, như việc thực hiện cơ bản sẽ bỏ qua các mã để kiểm tra trùng lặp.

Vì vậy, bây giờ, bạn có một cờ cho phép AddRangehỗ trợ cả hai trường hợp, nhưng có tác dụng phụ không có tài liệu (đó là điều mà các nhà thiết kế Framework đã làm việc rất vất vả để tránh).

Tóm lược

Vì không có hành vi rõ ràng, nhất quán và được mong đợi khi xử lý các bản sao, nên việc không giải quyết tất cả chúng cùng nhau và không cung cấp phương pháp để bắt đầu sẽ dễ dàng hơn.

Nếu bạn thấy mình liên tục phải kết hợp các từ điển, tất nhiên bạn có thể viết phương thức mở rộng của riêng mình để hợp nhất các từ điển, phương thức này sẽ hoạt động theo cách hoạt động cho (các) ứng dụng của bạn.


37
Hoàn toàn sai, từ điển phải có AddRange (IEnumerable <KeyValuePair <K, T >> Values)
Gusman

19
Nếu nó có thể có Thêm, nó cũng có thể có thêm nhiều. Hành vi khi bạn cố gắng thêm các mục có khóa trùng lặp phải giống như khi bạn thêm một khóa trùng lặp.
Uriah Blatherwick,

5
AddMultiplekhác với AddRange, bất kể việc triển khai nó sẽ trở nên khó khăn: Bạn có ném một ngoại lệ với một mảng tất cả các khóa trùng lặp không? Hay bạn ném một ngoại lệ trên khóa trùng lặp đầu tiên mà bạn gặp phải? Trạng thái của từ điển sẽ như thế nào nếu một ngoại lệ được ném ra? Nguyên sơ, hay tất cả các chìa khóa đã thành công?
Alan

3
Được rồi, vì vậy bây giờ tôi phải lặp lại danh sách của mình theo cách thủ công và thêm chúng riêng lẻ, với tất cả các cảnh báo về các bản sao mà bạn đã đề cập. Làm thế nào để loại bỏ nó khỏi khuôn khổ giải quyết bất cứ điều gì?
doug65536

4
@ doug65536 Bởi vì bạn, với tư cách là người tiêu dùng API, giờ đây có thể quyết định những gì bạn muốn làm với từng cá nhân Add- hoặc gói từng người Addtrong một try...catchvà bắt các bản sao theo cách đó; hoặc sử dụng trình lập chỉ mục và ghi đè giá trị đầu tiên bằng giá trị sau đó; hoặc kiểm tra ContainsKeytrước bằng cách sử dụng trước khi thử Add, do đó bảo toàn giá trị ban đầu. Nếu khung công tác có một AddRangehoặc AddMultiplephương pháp, thì cách đơn giản duy nhất để truyền đạt những gì đã xảy ra là thông qua một ngoại lệ, và việc xử lý và khôi phục liên quan sẽ không kém phần phức tạp.
Zev Spitz

36

Tôi có một số giải pháp:

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;
        }
    }
  }
}

Chúc vui vẻ.


Bạn không cần ToList(), từ điển là một IEnumerable<KeyValuePair<TKey,TValue>. Ngoài ra, các phương thức phương thức thứ hai và thứ ba sẽ ném nếu bạn thêm một giá trị khóa hiện có. Bạn đang tìm kiếm không phải là một ý tưởng TryAddhay? Cuối cùng, thứ hai có thể được thay thế bằngWhere(pair->!dic.ContainsKey(pair.Key)...
Panagiotis Kanavos

2
Ok, ToList()không phải là giải pháp tốt nên tôi đã thay đổi mã. Bạn có thể sử dụng try { mainDic.AddRange(addDic); } catch { do something }nếu bạn không chắc chắn về phương pháp thứ ba. Phương pháp thứ hai hoạt động hoàn hảo.
ADM-IT

Xin chào Panagiotis Kanavos, tôi hy vọng bạn hạnh phúc.
ADM-IT

1
Cảm ơn bạn, tôi đã lấy trộm cái này.
metabuddy

14

Trong trường hợp ai đó gặp câu hỏi này giống như tôi - bạn có thể đạt được "AddRange" bằng cách sử dụng các phương thức mở rộng IEnumerable:

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

Thủ thuật chính khi kết hợp các từ điển là xử lý các khóa trùng lặp. Trong đoạn mã trên, đó là một phần .Select(grp => grp.First()). Trong trường hợp này, nó chỉ cần lấy phần tử đầu tiên từ nhóm các bản sao nhưng bạn có thể triển khai logic phức tạp hơn ở đó nếu cần.


Điều gì xảy ra nếu dict1 không sử dụng trình so sánh bình đẳng mặc định?
mjwills

Phương pháp LINQ cho phép bạn vượt qua trong IEqualityComparer khi có liên quan:var combined = dict1.Concat(dict2).GroupBy(kvp => kvp.Key, dict1.Comparer).ToDictionary(grp => grp.Key, grp=> grp.First(), dict1.Comparer);
Kyle McClellan

12

Tôi đoán là thiếu đầu ra thích hợp cho người dùng về những gì đã xảy ra. Vì bạn không thể có các khóa lặp lại trong từ điển, bạn sẽ xử lý như thế nào khi hợp nhất hai từ điển trong đó một số khóa giao nhau? Chắc chắn bạn có thể nói: "Tôi không quan tâm" nhưng điều đó phá vỡ quy ước trả về false / ném một ngoại lệ cho các khóa lặp lại.


5
Điều đó khác gì so với việc bạn bị đụng phím khi gọi Add, ngoài việc nó có thể xảy ra nhiều lần. Nó sẽ ném cùng ArgumentExceptionAddkhông, chắc chắn?
nicodemus 13

1
@ nicodemus13 Có, nhưng bạn sẽ không biết khóa nào đã ném Ngoại lệ, chỉ có một số khóa là lặp lại.
Gal

1
@Gal được cấp, nhưng bạn có thể: trả lại tên của khóa xung đột trong thông báo ngoại lệ (hữu ích cho ai đó biết họ đang làm gì, tôi cho là ...), HOẶC bạn có thể đặt nó là (một phần của?) đối số paramName cho đối số ArgumentException bạn ném, HOẶC HOẶC, bạn có thể tạo một loại ngoại lệ mới (có lẽ là một tùy chọn đủ chung chung có thể là NamedElementException??), được ném thay vì hoặc là bên trong của một ArgumentException, chỉ định phần tử được đặt tên xung đột ... một vài lựa chọn khác nhau, tôi muốn nói
Mã Jockey

7

Bạn có thể làm điều này

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

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

hoặc sử dụng Danh sách cho dải bổ sung và / hoặc sử dụng mẫu ở trên.

List<KeyValuePair<string, string>>

1
Từ điển đã có một phương thức khởi tạo chấp nhận một từ điển khác .
Panagiotis Kanavos

1
OP muốn thêm dải, không phải sao chép từ điển. Đối với tên của phương thức trong ví dụ của tôi MethodThatReturnAnotherDic. Nó đến từ OP. Vui lòng xem lại câu hỏi và câu trả lời của tôi một lần nữa.
Valamas

1

Nếu bạn đang xử lý một Từ điển mới (và bạn không có hàng hiện có để mất), bạn luôn có thể sử dụng ToDictionary () từ một danh sách các đối tượng khác.

Vì vậy, trong trường hợp của bạn, bạn sẽ làm như sau:

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

5
Bạn thậm chí không cần phải tạo ra một từ điển mới, chỉ cần ghiDictionary<string, string> dic = SomeList.ToDictionary...
Panagiotis Kanavos

1

Nếu bạn biết mình sẽ không có các khóa trùng lặp, bạn có thể làm:

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

Nó sẽ ném ra một ngoại lệ nếu có một cặp khóa / giá trị trùng lặp.

Tôi không biết tại sao điều này không có trong khuôn khổ; nên là. Không có gì không chắc chắn; chỉ cần ném một ngoại lệ. Trong trường hợp của mã này, nó ném một ngoại lệ.


Điều gì sẽ xảy ra nếu từ điển gốc được tạo bằng cách sử dụng var caseInsensitiveDictionary = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase);?
mjwills

1

Hãy sử dụng phương pháp mở rộng như thế này:

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

Đây là một giải pháp thay thế bằng cách sử dụng c # 7 ValueTuples (tuple chữ)

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);
    }
}

Được sử dụng như

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

0

Như những người khác đã đề cập, lý do tại sao Dictionary<TKey,TVal>.AddRangekhông được triển khai là vì có nhiều cách khác nhau mà bạn có thể muốn xử lý các trường hợp bạn có bản sao. Đây cũng là trường hợp cho Collectionhoặc giao diện như IDictionary<TKey,TVal>, ICollection<T>vv

Chỉ List<T>triển khai nó và bạn sẽ lưu ý rằng IList<T>giao diện không, vì những lý do tương tự: hành vi mong đợi khi thêm một loạt giá trị vào một tập hợp có thể rất khác nhau, tùy thuộc vào ngữ cảnh.

Bối cảnh câu hỏi của bạn cho thấy bạn không lo lắng về các bản sao, trong trường hợp đó, bạn có một giải pháp thay thế oneliner đơn giản, sử dụng Linq:

MethodThatReturnAnotherDic().ToList.ForEach(kvp => dic.Add(kvp.Key, kvp.Value));
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.