KeyCollection 作为 IEnumerable 产生不一致的 LINQ 行为

问题描述 投票:0回答:2

以下代码打印“false”

IEnumerable<string> x = new List<string>();
Console.WriteLine(x.Contains(null));

但是这段代码抛出一个

ArgumentNullException

IEnumerable<string> x = new Dictionary<string, string>().Keys;
Console.WriteLine(x.Contains(null));

我看到 这篇文章 解释了为什么

Dictionary.ContainsKey
在传入 null 时抛出,所以我猜这种行为是相关的。但是,在
ContainsKey
的情况下,我得到漂亮的绿色波浪形,而在
IEnumerable
的情况下,我的应用程序崩溃了:

消费代码不会知道传递给它的

IEnumerable
的底层类型,所以我们需要:

  • 一般情况下不使用
    IEnumerable.Contains()
    和可空类型 or
  • KeyCollection
    转换为列表,然后再将它们视为
    IEnumerable

这是对的,还是我遗漏了什么?

c# linq dictionary nullable
2个回答
1
投票

你可以做其中任何一个,但我推荐第三种方法:为你的项目启用 nullable reference types。这样,您就不能将空值传递给

IEnumerable<string>.Contains
,因为
string
不允许空值。它是自我记录的,怎么样。

如果您将警告视为错误,您将得到的不仅仅是绿色波浪线,而是实际的编译器错误,这意味着没有崩溃的可能性。


0
投票

我假设您想将

Keys
属性公开为允许搜索
IEnumerable<TKey>
null
序列。一种简单的方法是将集合包装在
IEnumerable<TKey>
实现中,隐藏集合的身份:

static IEnumerable<T> HideIdentity<T>(this IEnumerable<T> source)
{
    ArgumentNullException.ThrowIfNull(source);
    foreach (var item in source) yield return item;
}

用法示例:

IEnumerable<string> x = new Dictionary<string, string>().Keys.HideIdentity();

这样 LINQ

Contains
运算符将不会检测到集合实现了
ICollection<T>
接口,并且将遵循枚举集合并使用
TKey
类型的默认比较器比较每个键的缓慢路径。这有两个缺点:

  1. 操作的 CPU 复杂度将是 O(n) 而不是 O(1)。
  2. Dictionary<K,V>.Comparer
    的比较语义将被忽略。因此,如果字典配置为不区分大小写,
    Contains
    将执行区分大小写的搜索。这可能不是你想要的。

更复杂的方法是将集合包装在

ICollection<TKey>
实现中,其中包括在
null
方法中对
Contains
的特殊处理:

class NullTolerantKeyCollection<TKey, TValue> : ICollection<TKey>
{
    private readonly Dictionary<TKey, TValue>.KeyCollection _source;

    public NullTolerantKeyCollection(Dictionary<TKey, TValue>.KeyCollection source)
    {
        ArgumentNullException.ThrowIfNull(source);
        _source = source;
    }

    public int Count => _source.Count;
    public bool IsReadOnly => true;
    public bool Contains(TKey item) => item == null ? false : _source.Contains(item);
    public void CopyTo(TKey[] array, int index) => _source.CopyTo(array, index);
    public void Add(TKey item) => throw new NotSupportedException();
    public bool Remove(TKey item) => throw new NotSupportedException();
    public void Clear() => throw new NotSupportedException();
    public IEnumerator<TKey> GetEnumerator() => _source.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

static NullTolerantKeyCollection<TKey, TValue> NullTolerant<TKey, TValue>(
    this Dictionary<TKey, TValue>.KeyCollection source)
{
    return new NullTolerantKeyCollection<TKey, TValue>(source);
}

用法示例:

IEnumerable<string> x = new Dictionary<string, string>().Keys.NullTolerant();

这样生成的序列将保留底层集合的性能和行为特征。

您在问题中提到了第三个选项:使用

List<T>
LINQ 运算符将集合转换为
ToList
。这将创建密钥的副本,并将在调用
ToList
时返回密钥的快照。如果字典被冻结并且键的数量很少,这可能是一个不错的选择。

© www.soinside.com 2019 - 2024. All rights reserved.