为什么Dictionary没有AddRange方法?

159

题目很基础,为什么我不能:

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

2
有很多东西没有AddRange,这一直让我感到困惑。比如Collection<>。总觉得很奇怪,List<>有它,但Collection<>或其他IList和ICollection对象却没有。 - Tim
50
我将模仿Eric Lippert的做法:“因为没有人设计、规定、实现、测试、记录并发布该功能。” - Gabe Moothart
5
@ Gabe Moothart - 这正是我所想的。 我喜欢向其他人使用那句话,但他们却不喜欢。 :) - Tim
2
@GabeMoothart,说“因为你不能”、“因为它不行”或者甚至是“因为。”不是更简单吗?——我猜这样说可能没有那么有趣之类的?--- 我对你的回答(我猜测是引用或转述)的跟进问题是:“为什么没有人设计、规定、实现、测试、文档化和发布该功能?”你很可能被迫回答“因为没有人这样做”,这与我之前提出的回答相当。我能想象的唯一另一个选项是不带讽刺意味的,也是OP实际上想知道的。 - Code Jockey
1
您可以间接地这样做: MethodThatReturnAnotherDic().ToList.ForEach(kvp => dic.Add(kvp.Key, kvp.Value));正如其他人所提到的,您可能需要小心处理重复项。 - Ama
显示剩余2条评论
13个回答

96

对原问题的评论已经很好地概括了:

因为没有人设计,指定,实现,测试,文档化和交付该特性。 - @Gabe Moothart

至于为什么呢?嗯,很可能是因为合并字典的行为无法按照框架指南的方式进行推理。

AddRange不存在,因为对于关联容器来说,范围没有任何意义,因为数据范围允许有重复条目。例如,如果您有一个IEnumerable<KeyValuePair<K,T>>这个集合不会防止重复项。

添加键值对集合,甚至合并两个字典的行为很直观。然而,如何处理多个重复条目的行为则不是那么明确。

当方法处理重复项时,应该是什么行为呢?

我能想到至少三种解决方案:

  1. 抛出第一个重复项的异常
  2. 抛出包含所有重复项的异常
  3. 忽略重复项

当抛出异常时,原始字典的状态应该是什么?

Add几乎总是实现为原子操作:它成功并更新了集合的状态,或者失败,集合的状态保持不变。由于AddRange可能由于重复错误而失败,因此使其行为与Add一致的方法是通过在任何重复项上抛出异常来使其成为原子操作,并将原始字典的状态保持不变。

作为API使用者,逐个删除重复元素会很繁琐,这意味着AddRange应该抛出一个包含所有重复值的单个异常。

然后选择就只有:

  1. 抛出包含所有重复项的异常,保持原始字典不变。
  2. 忽略重复项并继续进行。

有支持两种用例的观点。为了实现这一点,您需要在签名中添加一个IgnoreDuplicates标志吗?

IgnoreDuplicates标志(设置为true时)也会提供显著的速度提升,因为底层实现将绕过重复检查的代码。

现在,您有一个允许AddRange支持两种情况的标志,但是存在未记录的副作用(这是框架设计人员非常努力避免的事情)。

摘要

由于处理重复项时没有明确、一致和预期的行为,因此最好一起不处理它们,并且不提供该方法。

如果您发现自己不断地必须合并字典,当然可以编写自己的扩展方法来合并字典,以适合您的应用程序。


57
完全不正确,一个字典应该有AddRange(IEnumerable<KeyValuePair<K, T>> Values)方法。 - Gusman
35
如果可以添加单个项目,那么也应该能够添加多个项目。当您尝试添加具有重复键的项目时,其行为应与添加单个重复键的项目时相同。 - Uriah Blatherwick
6
AddMultipleAddRange 不同,但无论如何实现都会有些问题:当遇到重复键时,是使用包含所有重复键的数组来抛出异常?还是在遇到第一个重复键时立即抛出异常?如果抛出异常,字典的状态应该是什么?是原始状态还是已成功添加的所有键值对? - Alan
3
好的,现在我需要手动迭代我的可枚举对象并逐个添加它们,同时要注意你提到的重复问题。那么不将其包含在框架中如何解决任何问题呢? - doug65536
7
因为您作为API的使用者,现在可以决定如何处理每个独立的“Add”操作--可以将每个“Add”操作包装在“try...catch”中并通过这种方式捕获重复项;或者使用索引器并用后来的值覆盖第一个值;或在尝试“Add”之前使用“ContainsKey”进行预检查,从而保留原始值。如果框架有一个“AddRange”或“AddMultiple”方法,唯一简单的传达发生了什么的方法是通过异常,其处理和恢复也同样复杂。 - Zev Spitz
显示剩余4条评论

59
我有一些解决方案:
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
1
谢谢,我抄袭了这个。 - HamsterWithPitchfork

21

如果有人像我一样遇到这个问题 - 可以使用IEnumerable扩展方法来实现"AddRange":

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

我的猜测是缺乏向用户适当输出发生了什么的内容。 由于字典中不能有重复的键,那么当两个字典有部分键相交时,你该如何处理合并这两个字典呢?当然,你可以说:“我不在乎”,但这会违反对于重复键应该返回false/抛出异常的惯例。


5
跟你在调用“Add”方法时出现密钥冲突的情况有何不同呢,除了它可能会发生多次以外?这肯定会抛出与“Add”相同的ArgumentException异常,对吧? - nicodemus13
1
是的,但你不会知道哪个键抛出了异常,只知道某个键是重复的。 - Gal
1
@Gal 确实,但你可以:在异常消息中返回冲突键的名称(对于知道自己在做什么的人很有用,我想...),或者将其作为 paramName 参数的一部分抛出 ArgumentException 异常,或者创建一个新的异常类型(也许一个足够通用的选项可能是 NamedElementException??),抛出它而不是 ArgumentException 或作为其 innerException,指定冲突的命名元素... 有几种不同的选择,我想说。 - 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

4

随意使用扩展方法,例如:

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

2

只需使用Concat()函数:

dic.Concat(MethodThatReturnAnotherDic());

3
“Dictionary” 没有 “Concat” 方法 - “Concat” 来自 Linq,将返回一个键值对集合(可能包含重复的键),而不是字典。 - D Stanley

2
为什么字典没有AddRange方法?
List.AddRange是一种快速连续块复制的方法,用于像列表、向量或数组这样的集合,在内存中以连续布局的方式存储数据,即每个元素都在上一个元素之后,以规则的偏移量存储。Range提示了它的内部结构,即仅有一个范围(元素或内存)。
字典在内部看起来与列表非常不同。这是由于哈希表(即字典)存储其条目的方式:它们不像数组或列表中看到的那样在内存中连续排列,而是字典的元素分散在多个哈希桶中,每个桶包含多个条目,其中许多为空,因此您不能将整个范围块复制到例如列表中,否则您将获得一堆空条目,字典通常通过其接口隐藏这些空条目。
由于这种分割,我们别无选择,只能手动迭代遍历字典的条目,以便将仅有效的键和值提取到连续的数组、列表或另一个字典中。请参见Microsoft的参考实现,其中使用for实现了CopyToList.AddRange使用Array.Copy。这类似于Buffer.BlockCopy,其在C语言中的等价物是memcpy。它存在的目的是为了对大块连续数据进行高效复制,而你需要的是所有这些数据。将这些数据添加到listA的末尾很容易实现,但合并两个字典的条目并不那么简单。而且你的建议需要解决这个问题。
您提出的概念方法是有用的,确实可以添加。但称其为AddRange是不准确的。我可能会称其为AddEntries。任何实现都需要解决一些有趣的问题,其中一些在这里已经被其他人评论过了。

1
如果你正在处理一个新的字典(而且你没有现有的行可以丢失),你可以随时使用来自另一个对象列表的ToDictionary()方法。
因此,在您的情况下,您需要执行以下操作:
Dictionary<string, string> dic = new Dictionary<string, string>();
dic = SomeList.ToDictionary(x => x.Attribute1, x => x.Attribute2);

6
您甚至不需要创建一个新的字典,只需编写 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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接