并发 ToLookup() 转换?

Concurrent ToLookup() conversion?

如何使 ToLookup() 并发?我有一些这样的代码:

myRepository.GetAllContacts().ToLookup( c => c.COMPANY_ID);

我想要一个类似这样的结构:

new ConcurrentDicitonary<String,IEnumerable<Contact>>();  // <Company,List of Contacts in company>

因为每个 COMPANY_ID 可以映射到多个 Contact,我不能简单地使用 ToDictionary

这似乎是一个简单的问题,但正如其他(有问题的)答案所显示的那样,解决方案并不简单。

现有答案存在问题

就目前而言,两种提议的解决方案都会创建某种字典,每次您在任何给定键处枚举 IEnumerable<Contact> 时,过滤后的 IEnumerable<Contact> 都会通过枚举从头开始重新创建并过滤原始集合。本质上,您在字典中存储的是 logic 以获得所需的过滤 Contact 集合,而不是 actual 集合。

因此,您将一遍又一遍地列举原始 IEnumerable<Contact>。从 thread-safety 的角度来看,这是危险的,即使它有效 - 这样做没有任何好处,只有开销。

建议的解决方案

你说得对,out-of-the-box thread-safe Lookup/ILookup<TKey, TValue> 的最佳选择似乎是 ConcurrentDictionary<TKey, TValue>,其中 TValue 来自 IEnumerable<Contact> .它提供查找功能的超集 并且 是 thread-safe 如果您正确构建它。在基础 Class 库中没有 ready-to-use 扩展方法,所以你可以自己实现:

IEnumerable<Contact> contacts = GetAllContacts();
ConcurrentDictionary<string, IReadOnlyList<Contact>> dict = new ConcurrentDictionary<string, IReadOnlyList<Contact>>();

foreach (IGrouping<string, Contact> group in contacts.GroupBy(c => c.COMPANY_ID))
{
    if (!dict.TryAdd(group.Key, group.ToArray())) {
        throw new InvalidOperationException("Key already added.");
    }
}

这看起来与其他人提供的非常相似,但有一个重要区别:我的字典 TValue 是一个 materialised 集合(特别是 Contact[] 冒充如 IReadOnlyList<Contact>)。每次将其从字典中拉出并枚举时,它不会从头开始重建。

哦,而且我只列举来源 IEnumerable<Contact> 一次,永远 - 不是真的 life-changing,但很贴心。

你仍然可以使用 ConcurrentDictionary<string, IEnumerable<Contact>> 作为你的字典类型(你可以替换我上面例子中的字典类型,它仍然会按预期编译和工作) - 只要确保你只添加 materialised,最好是在构建字典时将不可变集合添加到字典中。

选择您的 TValue 类型:IReadOnlyList<T>

的替代品

(超出原题范围)

IReadOnlyList<T> 是我能想到的最通用的 general-purpose quasi-immutable 集合接口(显然 IReadOnlyCollection<T> 除外),它向调用者传达集合已实现并且将来不太可能改变。

如果我在自己的代码中使用它,我实际上会使用 Contact[] 作为我字典的 TValue 进行任何私人和内部调用(出于性能原因放弃 "read-only" 的舒适性).对于任何 public API,我会坚持使用 IReadOnlyList<T>ReadOnlyCollection<T> 来强调 TValue 集合的 read-only 方面。

如果采用外部依赖项是一个可行的选择,您还可以将 Microsoft 的 System.Collections.Immutable NuGet 添加到您的项目并使用 ImmutableDictionary<string, ImmutableArray<Contact>> 来存储您的查找。 ImmutableDictionary<TKey, TValue> 是一个不可变的 thread-safe 字典。 ImmutableArray<T> 是一个轻量级数组包装器,它具有强大的不变性保证,并且还具有通过结构枚举器和 re-implementations 某些完全避免枚举器分配的某些 LINQ 方法实现的可靠性能特征。

List<T> 对于 TValue 来说是一个糟糕的选择,因为它 a) 可变性和 b) 它倾向于分配长度大于 List<T>.Count 的内部缓冲区数组(除非你明确使用 List<T>.TrimExcess)。当您将内容存储在字典中时,它很可能会存活一段时间,因此分配您不会真正使用的内存(就像 List<T> 那样)并不是一个好主意。

编辑

现在我必须添加:.NET 当前的 Lookup<Tkey, TValue> 实现由 LINQ 的 ToLookup 返回 实际上看起来是 thread-safe .但是,我发现的 none 规范对 Lookup<TKey, TValue> 上实例方法的 thread-safety 做出了任何保证(MSDN 特别声明它们不能保证是 thread-safe),这意味着 lookup thread-safety 是一个实现细节,而不是万无一失的保证。因此,我上面所说的关于使用 ConcurrentDictionary<TKey, TValue> 的所有内容仍然适用。