使用 !=null、Count > 0 和 .Any() 时的 C# 最佳实践

C# Best Practices when using !=null, Count > 0, and .Any()

出于好奇,!=nullCount > 0.Any() 之间的根本区别是什么?使用它们的最佳时间是什么时候? - 对于架构和性能。

我知道 .Any() 用于 IEnumerable,而不是列表,但我发现自己在允许的情况下可以互换使用它们 (!=null and Count > 0)。如果这是不好的做法,我不想养成习惯。

这取决于你的目标。一个List可以不为null,但是有Count == 0Any() 怎么样 - 在 LINQ 风格中使用它,例如:

if(MyEnumerable.Any(x => x.Id == myId))
{
    // do something
}
  !=null

检查列表是否为空。这可能代表也可能不代表与“无项目”相同的事物。我建议完全避免对列表使用 null,因为含义不明确。使用空集合来表示“没有项目”。如果您需要其他东西来表示“不存在”,请使用 maybe/optional 类型,或者如果它们默认为 non-nullable 则可能为空引用。这应该是向 reader 发出的更明确的信号,即“无项目”和“不存在”是可能需要以不同方式处理的不同状态。

.Any()

这是 LINQ 的一部分,因此适用于任何可枚举集合。很清楚也很方便,但是会比查一个属性稍微慢一点。但如果您想检查某些特定值的存在,它允许内联过滤。

.Count > 0 or .Length > 0

这仅适用于具有 Count/Length 属性 的集合,但应该是最快的,因为它只是进行比较。至少假设属性很简单,不会做很多工作。

但请注意,除非您 运行 代码 非常 频繁,否则性能问题不是很重要。因此,在尝试进行任何优化之前先测量性能。

Enumerable.Any 的工作方式如下

public static bool Any<TSource>(this IEnumerable<TSource> source)
{
    if (source == null)
        throw Error.ArgumentNull(nameof (source));
    using (IEnumerator<TSource> enumerator = source.GetEnumerator())
    {
        // Returns very early if there are any items
        if (enumerator.MoveNext())
            return true;
    }
    return false;
}

有一些开销,但实际上并没有那么多,因此我假设 Any() 的性能与 List<T>.Count 相当(IEnumerable 扩展试图回退到这个值,因此这也应该是可比较的)。

Clarity-wise,最终归结为品味问题。我个人喜欢 Any() 的表现力,因为它对我来说更接近自然语言。

rows.Any()

让我想不到

rows.Count == 0

但没那么多。

null 比较以完全不同的方式工作。如果您不清楚这一点,您应该了解 C# 中的值和引用类型(警告:最近情况有所不同,因为值类型不允许 null 在当前版本的 C#/.NET 中默认。

总结:引用类型的存储方式不同,实际变量(某种程度上)只是指向数据存储位置的指针(并非 100% 准确)。 C++ 中的经典指针可以采用值 0 来指示没有它们指向的数据,这被转移(至少作为一个概念)到 C#(和其他 C-like 语言作为 Java).无论如何,由于在 C# 中不能直接访问指针,因此发明了 null 来指示变量不指向实际数据(“没有与该变量关联的数据”)。因此,将 List<int> 变量与 null 进行比较意味着您询问列表实例是否已经创建,这是必要的,但对于存储的实际列表项来说并不是一个充分的标准。

TL;DR:Any() 实际上会调用 ICollection.Count,如果集合类型实现了它。否则它将尝试选择“最佳”方法来检查集合是否为 non-empty.

检查 null 与检查空值有很大不同,将它与空值检查进行比较没有任何意义。


Paul Kertscher 已经回答了您的问题的实质,但是为了帮助将来查询此类问题,值得记住的是,事实上,dotnet 的源代码可以在线获得。

例如,LINQ Any() 方法的代码可在此处找到:https://github.com/dotnet/runtime/blob/main/src/libraries/System.Linq/src/System/Linq/AnyAll.cs (and there's plenty more in the rest of the runtime repo, with the libraries here)。它比 Paul 在他的回答中包含的方法版本稍微复杂一些……他可能引用了不同版本的运行时。不过,这是最新的实现(原始来源中的评论):

public static bool Any<TSource>(this IEnumerable<TSource> source)
{
    if (source == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    if (source is ICollection<TSource> collectionoft)
    {
        return collectionoft.Count != 0;
    }
    else if (source is IIListProvider<TSource> listProv)
    {
        // Note that this check differs from the corresponding check in
        // Count (whereas otherwise this method parallels it).  If the count
        // can't be retrieved cheaply, that likely means we'd need to iterate
        // through the entire sequence in order to get the count, and in that
        // case, we'll generally be better off falling through to the logic
        // below that only enumerates at most a single element.
        int count = listProv.GetCount(onlyIfCheap: true);
        if (count >= 0)
        {
            return count != 0;
        }
    }
    else if (source is ICollection collection)
    {
        return collection.Count != 0;
    }

    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
        return e.MoveNext();
    }
}

ICollection 的代码是 System.Collection, and IListProvider is part of System.Linq 的一部分。

您可以看到 Any() 可以通过多种方式来测试基础集合中是否包含任何项目,并且您可以合理地假设它们是按 speed/efficiency 的顺序进行尝试的。显然,您 可以 编写 ICollection.Count { get } 的一个非常糟糕的实现,但那将是一件愚蠢的事情,并且很高兴认为微软在任何时候都没有这样做dotnet 运行时附带的集合类型。 (顺便说一句,arrays 通过显式接口实现 ICollection.Count,将其映射到它们的 Length 属性,这就是为什么 Any() 不需要关心检查 Length)

我还没有查找 IListProvider<T>.GetCount 的任何实现。接口方法的文档是这样说的:

If true then the count should only be calculated if doing so is quick (sure or likely to be constant time), otherwise -1 should be returned.

同样,糟糕的实现可能存在,但可能不存在。

使用 Any() 而不是 Count 通常没有任何问题。通过一些 ifs 和强制转换调用方法和 运行 的额外开销通常可以忽略不计。只需确保不要将 CountCount() 混为一谈,因为后者可能需要遍历整个集合,这可能是一件相对昂贵且耗时的事情,而且如果你只是想要做的是检查是否为空。