为什么 Dictionary 没有 AddRange?

标题很简单,为什么我不能:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic.AddRange(MethodThatReturnAnotherDic());
108310 次浏览

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

因为从来没有人设计、指定、实现、测试、记录和发布过这个特性

为什么?嗯,可能是因为合并字典的行为不能以符合框架指导方针的方式进行推理。

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

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

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

我至少能想到三种解决办法:

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

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

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

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

选择归结为:

  1. 抛出一个包含所有重复项的异常,而不使用原始字典。
  2. 忽略副本,继续。

支持这两种用例是有争议的。要做到这一点,您是否在签名中添加了 IgnoreDuplicates标志?

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

现在,您有了一个允许 AddRange同时支持这两种情况的标志,但是有一个没有文档说明的副作用(这是 Framework 设计人员非常努力避免的)。

摘要

由于在处理重复数据时没有明确、一致和预期的行为,因此不将它们全部处理在一起,也不提供开始时使用的方法会更容易。

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

我的猜测是缺乏适当的输出给用户发生了什么。 由于字典中不能有重复键,那么如何处理两个键相交的字典的合并呢?当然,您可以说: “ I don’t care”,但这打破了返回 false/为重复键引发异常的惯例。

你可以的

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<KeyValuePair<string, string>>

如果您正在处理 w/a new Dictionary (并且您没有可丢失的现有行) ,那么您总是可以从另一个对象列表中使用 ToDictionary ()。

所以,在你的情况下,你会这样做:

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

我有个办法:

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

玩得开心。

如果有人像我一样遇到这个问题——可以通过使用 IEnumable 扩展方法来实现“ 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())部分。在这种情况下,它只是从重复的组中获取第一个元素,但是如果需要,您可以在那里实现更复杂的逻辑。

如果你知道你不会有重复的钥匙,你可以这样做:

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

如果存在重复的键/值对,它将引发异常。

我不知道为什么这不在框架中; 应该在。没有不确定性,只是抛出一个异常。在此代码的情况下,它确实会抛出异常。

可以像下面这样随意使用扩展方法:

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

下面是使用 c # 7 ValueTuples (tuple 文本)的替代解决方案

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

正如其他人已经提到的,Dictionary<TKey,TVal>.AddRange没有实现的原因是因为有各种各样的方法可以处理有重复的情况。对于 CollectionIDictionary<TKey,TVal>ICollection<T>等接口也是如此。

只有 List<T>实现了它,而且您会注意到 IList<T>接口没有这样做,原因是相同的: 在向集合添加一系列值时,意料之中的行为可能会有很大的不同,这取决于上下文。

您的问题的上下文表明您并不担心重复,在这种情况下,您可以使用 Linq 来替代简单的一线程:

MethodThatReturnAnotherDic().ToList.ForEach(kvp => dic.Add(kvp.Key, kvp.Value));

使用 Concat():

dic.Concat(MethodThatReturnAnotherDic());