为什么词典没有AddRange?


115

标题已经足够基本了,我为什么不能:

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


2
有很多没有AddRange的东西,这总是让我迷惑不解。像Collection <>。List <>拥有它似乎总是很奇怪,但Collection <>或其他IList和ICollection对象却没有。
蒂姆(Tim)

39
我将在这里提出一个Eric Lippert:“因为没有人设计,指定,实施,测试,记录和发布该功能。”
·穆阿特

3
@ Gabe Moothart-那正是我的假设。我喜欢在其他人身上使用那条线。他们虽然讨厌。:)
蒂姆(Tim)

2
@GabeMoothart说“因为您不能”,“因为没有”,甚至“因为”,会不会更简单?-我猜这说起来没意思吗?---我对您的回答(我想引用或释义)的跟进问题是“为什么没有人设计,指定,实施,测试,记录和发布该功能?”,您很可能会对此感到满意。被迫回答“因为没有人回答”,这与我之前建议的回答相当。我可以设想的唯一其他选择不是讽刺,而是OP真正想知道的。
赛马会2014年

Answers:


76

对原始问题的评论很好地总结了这一点:

因为没有人设计,指定,实施,测试,记录和发布该功能。-@Gabe Moothart

至于为什么呢?好吧,可能是因为无法以适合框架准则的方式对合并词典的行为进行推理。

AddRange之所以不存在,是因为范围对关联容器没有任何意义,因为数据范围允许重复输入。例如,如果您有一个IEnumerable<KeyValuePair<K,T>>that集合,则不能防止重复输入。

添加键值对集合,甚至合并两个字典的行为很简单。但是,如何处理多个重复条目的行为却不是。

处理重复项时该方法的行为是什么?

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

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

当引发异常时,原始词典的状态应该是什么?

Add几乎总是作为原子操作实现的:它成功并更新了集合的状态,或者失败了,并且集合的状态保持不变。由于AddRange会由于重复的错误,方法来保持其行为符合Add将也使其原子通过任何重复抛出一个异常,并保持原来的字典的状态不变。

作为API使用者,必须迭代地删除重复的元素很麻烦,这意味着AddRange应当抛出一个包含所有重复值的异常。

然后选择归结为:

  1. 抛出所有重复项的异常,将原始词典保留下来。
  2. 忽略重复并继续。

有支持两种用例的论点。为此,您是否IgnoreDuplicates在签名中添加标志?

IgnoreDuplicates标志(当设置为true时)还将大大提高速度,因为基础实现将绕过代码进行重复检查。

因此,现在,您有了一个标志,它可以AddRange支持这两种情况,但是具有未记录的副作用(这是Framework设计人员非常努力避免的事情)。

摘要

由于在处理重复项时没有明确,一致和预期的行为,因此不将所有重复项一起处理和不提供开始的方法会更容易。

如果您发现自己不断需要合并字典,那么您当然可以编写自己的扩展方法来合并字典,这种扩展方法将以适合您的应用程序的方式运行。


37
完全错误的,词典应当具有的AddRange(IEnumerable的<KeyValuePair <K,T >>值)
古斯曼

19
如果可以添加,那么它也应该可以添加多个。尝试添加具有重复键的项目时的行为应与添加单个重复键时的行为相同。
Uriah Blatherwick

5
AddMultiple与有所不同AddRange,无论它的实现是如何实现的:您是否使用所有重复键的数组引发异常?还是在遇到的第一个重复键上抛出异常?如果抛出异常,字典的状态应该是什么?原始的还是所有成功的密钥?
艾伦(Alan)

3
好的,所以现在我必须手动迭代我的可枚举对象,并分别添加它们,其中包括所有有关您提到的重复项的警告。从框架中删除它如何解决任何问题?
doug65536 '16

4
@ doug65536因为作为API使用者,您现在可以决定对每个人要做什么Add–要么将每个包裹Add在a中,try...catch然后以这种方式捕获重复项;或使用索引器并用后一个值覆盖第一个值;或ContainsKey在尝试进行之前先检查使用Add,从而保留原始值。如果框架具有AddRangeor 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 2015年

2
好的,ToList()这不是一个好的解决方案,因此我更改了代码。try { mainDic.AddRange(addDic); } catch { do something }如果不确定第三种方法,可以使用。第二种方法效果很好。
ADM-IT

嗨,Panagiotis Kanavos,希望您高兴。
ADM-IT

1
谢谢,我偷了。
metabuddy

14

如果有人像我一样遇到此问题,则可以使用IEnumerable扩展方法来实现“ AddRange”:

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

组合字典时的主要技巧是处理重复键。在上面的代码中,它是part .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,除此之外,它可能会发生多次。当然会抛出相同的ArgumentException结果Add吗?
nicodemus13 2012年

1
@ nicodemus13是的,但是您不知道哪个键引发了异常,只是那个键是重复的。
加仑2012年

1
@Gal已授予,但您可以:在异常消息中返回冲突密钥的名称(对知道他们在做什么的人很有用,我想...),也可以将其作为(的一部分)抛出的ArgumentException的paramName参数,或进行OR-OR运算,可以创建一个新的异常类型(也许足够通用的选项可能是NamedElementException??),或者代替ArgumentException的innerException或作为该参数的innerException抛出,该ArgumentException指定发生冲突的命名元素...我会说几种不同的选择
赛马

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 2015年

1
OP希望添加范围,而不是克隆字典。至于我的示例中方法的名称MethodThatReturnAnotherDic。它来自OP。请再次查看问题和我的答案。
Valamas 2015年

1

如果您要处理一个新的Dictionary(并且没有丢失的现有行),则始终可以使用另一个对象列表中的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 2015年

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(元组文字)的替代解决方案

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使用简单的oneliner替代方法:

MethodThatReturnAnotherDic().ToList.ForEach(kvp => dic.Add(kvp.Key, kvp.Value));
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.