65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ 和字典

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (22投票s)

2012 年 11 月 26 日

Eclipse

4分钟阅读

viewsIcon

83653

downloadIcon

485

用于使用 LINQ 创建字典的便捷方法。

引言

LINQ 是 .NET 中操作集合的强大技术,但当应用于字典时,其行为并非完全直观。我实现了一些 C# 扩展方法,旨在在使用 LINQ 对字典执行标准操作时提供更方便、更直观的体验。

本文之前已发布在我的博客 cureos.blogspot.com 上。

LINQ 与字典

LINQ 是操作 .NET 中对象集合的非常强大的技术。例如,如果您有一个整数集合,使用 LINQ 从集合中选取偶数是一个简单的任务。
int[] ints = new[] { 1, 3, 6, 8, 9, 10 };
IEnumerable<int> evens = ints.Where(i => i % 2 == 0); 

对于字典也是如此。

Dictionary<int, int> intDict = new Dictionary<int, int> { { 2, 3 }, { 3, 5 }, { 6, 7}};
Dictionary<int, int> evenKeys = intDict.Where(kv => kv.Key % 2 == 0); 

什么?编译错误?!?

实际上,`intDict.Where()` 语句并不完全正确。从 LINQ 的角度来看,`Dictionary<,>` 以及其他实现 `IDictionary<TKey, TValue>` 接口的类实际上被视为 `IEnumerable<KeyValuePair<TKey, TValue>>` 接口的实现。因此(暂时忽略我们也可以使用 `var` 关键字),正确的 `evenKeys` 赋值应该写成:

IEnumerable<KeyValuePair<int, int>> evenKeys = intMap.Where(kv => kv.Key % 2 == 0); 

现在,我猜测在正常情况下,上面的赋值更倾向于返回一个 `Dictionary`。幸运的是,LINQ 还提供了许多 `ToDictionary` 方法重载。因此,为了让 `evenKeys` 赋值返回一个 `Dictionary`,我们只需输入:

Dictionary<int, int> evenKeys = intMap.Where(kv => kv.Key % 2 == 0).ToDictionary();  

什么?!又编译错误?

是的,因为 `ToDictionary` 方法也操作 `IEnumerable<T>` 对象。您需要告诉编译器如何根据这个任意类型 `T` 来设计您的 `Dictionary`。为了正确性,我们的 `evenKeys` 赋值必须表示为:

Dictionary<int, int> evenKeys = 
    intMap.Where(kv => kv.Key % 2 == 0).ToDictionary(kv => kv.Key, kv => kv.Value);

对于字典来说,这种显式性可能显得非常违反直觉,并且互联网上有许多论坛问题表明这种 API 设计确实引起了混淆(例如,这里这里这里)。

经过对这个问题的思考,我相信我已经找到了一种绕过这个困惑障碍的方法。我在一个静态的工具类中实现了以下扩展方法:

public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(
    this IEnumerable<KeyValuePair<TKey, TValue>> source)
{
    return source.ToDictionary(kv => kv.Key, kv => kv.Value);
}  

这个 `ToDictionary` 的重载接收任何实现 `IEnumerable<KeyValuePair<TKey, TValue>>` 接口的对象,这是调用 LINQ 操作字典时常见的返回类型,并使用参数列表中的字典相同的键和值返回一个 `Dictionary<TKey, TValue>` 对象。定义了这个扩展方法后,我现在实际上可以输入:

Dictionary<int, int> evenKeys = intMap.Where(kv => kv.Key % 2 == 0).ToDictionary();

而不会出现烦人的编译错误。

这一切都很好,但我随后决定将这个问题进一步推进。例如,当 `Where()` 扩展方法操作 `Dictionary` 时,如果默认返回一个具有相同键和值的 `Dictionary`,那会不会很好?

嗯,这当然可以做到!这是解决方案:

public static Dictionary<TKey, TValue> Where<TKey, TValue>(
    this IDictionary<TKey, TValue> source, Func<KeyValuePair<TKey, TValue>, bool> predicate)
{
    return Enumerable.Where(source, predicate).ToDictionary(kv => kv.Key, kv => kv.Value);
}  
定义了这个方法后,它有效地隐藏了当 `IEnumerable<T>` 对象也实现 `IDictionary<TKey, TValue>` 接口时的一般 `Where(IEnumerable<T>, Func<>)` 扩展方法。定义了上述 `Where()` 扩展方法后,我们甚至可以应用我们第一个字典 LINQ 尝试而没有编译错误:
Dictionary<int, int> evenKeys = intMap.Where(kv => kv.Key % 2 == 0); 

不幸的是,无论 `intDict` 是 `SortedDictionary` 还是 `SortedList`,或者是另一个实现 `IDictionary<TKey, TValue>` 接口的对象,这个方法总是返回一个 `Dictionary` 对象。目前,我没有可行的方法来返回用作特殊 `Where` 方法输入的字典类型。尽管如此,寻找解决方案仍将继续。

我还将一些类似的 LINQ 扩展方法重载收集到一个我选择称之为... dictionarylinqGithub 项目中!

在这个项目中,您可以找到 `Except`、`Intersect` 和 `Union` 扩展方法的字典重载。由于它们的签名,对于这些方法,实际上已经可以实现一个专门的重载,返回真实的输入字典类型,以及为 `Dictionary<TKey, TValue>` 类提供一个性能更好的指定重载。

还包括了当 `Func<TSource, TResult>` 对象的返回类型是 `KeyValuePair<TKey, TValue>` 时,`Select` 方法的方法重载。为 `dictionarylinq` 添加更多方法重载相对容易,但目前就是这些。

当 `dictionarylinq` 工具类中的扩展方法包含在您的应用程序中时,这些方法将有效地隐藏 `System.Linq.Enumerable` 类中的一般扩展方法。如果您确实希望在特定场景下回退到一般方法,请进行显式转换为 `IEnumerable<>` 接口

IEnumerable<KeyValuePair<int, int>> output = 
    ((IEnumerable<KeyValuePair<int, int>>)input).Where(kv => kv.Key > 3); 

或者显式调用静态方法

IEnumerable<KeyValuePair<int, int>> output = Enumerable.Where(input, kv => kv.Key > 3); 

请注意,Visual Studio 2010 中的 `dictionarylinq` 类库是一个 可移植类库。这意味着该库使用了 .NET Framework 4、Silverlight 和 Windows Phone 7 基类库的“最小公分母”,并且可以轻松配置以支持 Windows 应用商店应用程序。该库可以构建一次,然后被所有 .NET 技术使用,而无需重新构建。在 Visual Studio 2012 中,可移植类库默认受支持。如果您正在运行 Visual Studio 2010 并且尚未安装 Portable Library Tools,只需创建一个新的专用类库项目并包含 `dictionarylinq` 类库的源代码,或者将源代码直接包含在您自己的类库或应用程序中。

我希望这项工作也能对除我之外的其他人有所帮助。有关此工作的疑问和评论,请随时在下方评论。如果您是 Github 用户,请随时通过项目的 Issues 标签报告代码问题。

祝您的字典和 LINQ 之旅顺利!

历史  

2012 年 11 月 26 日:初始版本,改编自 http://cureos.blogspot.com/2011/10/linq-and-dictionaries.html。

© . All rights reserved.