[练习题] C# 中检查 Collection 是否有项目的几种方式

今天 (2025-04-06) 早上在 Facebook -  台湾 .NET  技术爱好找论坛里看到 Will 保哥转贴了一则 X 贴文,
要检查一个集合物件是否有元素在内,会使用哪一种语法来检查 … 

在 C# 开发中,经常需要判断一个集合是否有包含任何元素。虽然这是一个简单的判断,但其实背后有不少值得探讨的细节。就来了解这四种常见的做法,并分析它们的优缺点与适合的使用情境。

原文出处

  • https://x.com/okyrylchuk/status/1908546268227625423

 

四种判断方式

作者「Oleg Kyrylchuk」列出了四种判断 List 不为 null 且有元素的方式

  1. Classic way
  2. List.Count way
  3. Enumerable.Any way
  4. Pattern matching way

1. 传统 Count 判断法

if (list != null && list.Count > 0)

优点
  - 适用于 List<T>、Array 等实作了 ICollection 的类型
  - Count 为 O(1),效能稳定

缺点
  - 不适用于 IEnumerable<T>(如延迟查询的 LINQ)

2. Null-safe Count 判断法

if (list?.Count > 0)

优点
  - 语法简洁,能处理 null
  - 与上面一样是 O(1) 操作
	
缺点
  - 仅适用于实作 ICollection 的集合

3. 使用 Any()

if (list?.Any() == true)

优点
  - 适用于所有 IEnumerable<T>,包括延迟查询
  - 口语化的表达:「是否有任何元素?」
	
缺点
  - 效能最差情况为 O(n),但通常只需检查第一笔

4. C# 9 Pattern Matching 写法

if (list is { Count: > 0 })

优点
  - 语法简洁
  - 支援 null 并为 O(1) 操作
  - 适合喜欢新语法表示方式的开发者

缺点
  - 仅适用于 C# 9 或更新版本
  - 仅适用于 ICollection
  - 不认识或不熟悉新语法的开发者会不适应

 

建议使用的情境

整理一下不同情境下的使用建议

集合型别建议写法
List, ArrayCount > 0 或 Pattern Matching
IEnumerable使用 Any()
Lazy LINQ 查询使用 Any()
喜欢比较新的语法Pattern Matching
需支援 null 判断?.Count > 0 或 Any()

小结

  • 如果集合型别已知是 List<T>、Array 等 ➜ 用 .Count 属性。
  • 如果来源是 LINQ 查询或无法确认类型 ➜ 使用 .Any() 判断是否有资料更合适。

 

时间複杂度 O(1) 是什么意思?

当我们说一个操作是 O(1),表示这个操作无论资料量有多大,执行时间几乎不变,是「常数时间」操作。

  • 不管集合有几个元素,这个操作花费的时间都差不多。
  • 不会因为清单变长,执行时间就变长。

举个例子:

如果使用的是 list.Count

如果 list 是 List<T>、Array、或实作了 ICollection 的集合,那 Count 属性只是回传一个栏位值,这是一个已经计算好的值:

public int Count => _count;

所以取得这个值是 O(1),非常快,不会随着资料笔数变多而变慢。

如果你用的是 list.Any(),这种会透过 foreach 检查第一笔资料的操作:

public static bool Any<T>(this IEnumerable<T> source)
{
    foreach (var item in source)
    {
        return true;
    }
    return false;
}

虽然平均来说也很快(通常一笔就结束),但最坏情况下还是可能要走完整个集合(例如空集合),所以它是 O(n)。

Big-O 表示「当输入资料量变大时,操作的执行时间(或空间)会怎么成长」。

例如:

list.Count;      // O(1)
list.Any();      // O(n)
list.Contains(x) // O(n)

常见的时间複杂度 Big-O 分类

Big-O名称说明(举例)
O(1)常数时间不管资料有多少,时间几乎不变。如:读取 .Count 属性
O(n)线性时间跑一次整个资料。如:.Any() 对 IEnumerable
O(n²)平方时间巢状迴圈,如两层 for 迴圈比较所有组合
O(log n)对数时间如:Binary Search(二分搜寻)
O(n log n)线性对数时间常见于排序(如 QuickSort、MergeSort)
O(2ⁿ)指数时间演算法会爆炸性成长,常见于暴力递迴

Big-O 关心的不是「实际执行时间」,而是「当资料变多时,会变几倍慢?」

  • O(1):1 笔、10 万笔都差不多快(例如读取变数)
  • O(n):资料越多,越慢(例如扫描 list)
  • O(n²):资料多一点,时间暴增(例如比对所有组合)

以下列出有关 Big O notation 的相关资料

  • Wiki - 大O符号
  • 那些听起来很专业的「演算法 Algorithm」跟「Big O notation」到底是什么? | by 00如是说 | Coding Fighter | Medium
  • 演算法 | Big O 複杂度| Pseudocode 伪代码 | 越南放大镜 X 下班资工系

 

有关 .Count   属性与 .Count()  方法的差异

在 C# 中,.Count 与 .Count() 虽然名字相似,实际上是两种不同的实作方式,各自适用于不同场景。

.Count 属性

  • 属于 ICollection 或 ICollection<T> 接口
  • 适用于:List<T>、Array、HashSet<T> 等
  • O(1) 时间複杂度,直接取得现成的栏位值
  • 效能最佳,无需遍历集合
var list = new List<int> { 1, 2, 3 };
int count = list.Count; // 直接取得栏位值,快速

.Count() 方法

  • 属于 LINQ 扩充方法:System.Linq.Enumerable.Count()
  • 适用于所有 IEnumerable<T>,包括 lazy 资料来源
  • O(n) 时间複杂度,需要遍历整个集合(除非最佳化)
  • 常见于处理延迟查询时
IEnumerable<int> query = Enumerable.Range(1, 1000).Where(x => x % 2 == 0);
int count = query.Count(); // 遍历整个集合

 

让 ChatGPT 写个 Benchmark 程式码

这边是写在 LINQPad 8.x 里面的程式码,然后使用 BenchmarkDotNet 的 NuGet 套件(可透过 LINQPad 的 F4 -> Add NuGet 加入)

void Main()
{
    // 执行 benchmark
    var config = DefaultConfig.Instance;
    BenchmarkRunner.Run<CollectionCheckBenchmarks>(config);
}

[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
public class CollectionCheckBenchmarks
{
    // 测试两种资料大小:10 与 1_000_000
    [Params(10, 1_000_000)]
    public int Size;

    private List<int> list;            // 实体集合(支援 ICollection)
    private IEnumerable<int> lazy;     // 延迟查询集合(仅支援 IEnumerable)

    // 测试前先初始化集合
    [GlobalSetup]
    public void Setup()
    {
        list = Enumerable.Range(1, Size).ToList();
        lazy = Enumerable.Range(1, Size).Where(x => x > 0); // 使用 Where 模拟 lazy source
    }

    // 各种判断方式对 list 的效能测试
    [Benchmark] public bool Count_List() => CheckCount(list);
    [Benchmark] public bool NullConditional_List() => CheckNullConditional(list);
    [Benchmark] public bool Any_List() => CheckAny(list);
    [Benchmark] public bool PatternMatching_List() => CheckPatternMatching(list);

    // lazy enumerable 仅能使用 Any()
    [Benchmark] public bool Any_Lazy() => CheckAny(lazy);

    // 使用 is ICollection<T> 并检查 Count
    private static bool CheckCount<T>(IEnumerable<T> source)
    {
        if (source is ICollection<T> collection)
        {
            return collection.Count > 0;
        }
        return false;
    }

    // 使用 null 条件运算子检查 Count
    private static bool CheckNullConditional<T>(IEnumerable<T>? source)
    {
        try
        {
            return (source as ICollection<T>)?.Count > 0;
        }
        catch
        {
            return false;
        }
    }

    // 使用 Any() 方法(适用所有 IEnumerable,遇第一笔即返回)
    private static bool CheckAny<T>(IEnumerable<T>? source)
    {
        return source?.Any() == true;
    }

    // 使用 C# 9 pattern matching 检查 Count
    private static bool CheckPatternMatching<T>(IEnumerable<T>? source)
    {
        return source is ICollection<T> { Count: > 0 };
    }
}

P.S.  如果要使用 LINQPad  执行 Benchmark.NET 的程式,记得要调整设定

最后跑出来的结果

// * Summary *

BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3624)
11th Gen Intel Core i7-1165G7 2.80GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK 9.0.102
  [Host] : .NET 8.0.12 (8.0.1224.60305), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI

Job=.NET 8.0  Runtime=.NET 8.0  

| Method               | Size    | Mean      | Error     | StdDev    | Gen0   | Allocated |
|--------------------- |-------- |----------:|----------:|----------:|-------:|----------:|
| Count_List           | 10      |  2.067 ns | 0.0642 ns | 0.0600 ns |      - |         - |
| NullConditional_List | 10      |  2.234 ns | 0.0196 ns | 0.0163 ns |      - |         - |
| Any_List             | 10      |  2.735 ns | 0.0777 ns | 0.0832 ns |      - |         - |
| PatternMatching_List | 10      |  2.079 ns | 0.0641 ns | 0.0877 ns |      - |         - |
| Any_Lazy             | 10      | 27.044 ns | 0.5593 ns | 0.8371 ns | 0.0153 |      96 B |
| Count_List           | 1000000 |  2.028 ns | 0.0478 ns | 0.0424 ns |      - |         - |
| NullConditional_List | 1000000 |  2.243 ns | 0.0255 ns | 0.0226 ns |      - |         - |
| Any_List             | 1000000 |  2.656 ns | 0.0288 ns | 0.0225 ns |      - |         - |
| PatternMatching_List | 1000000 |  1.989 ns | 0.0083 ns | 0.0070 ns |      - |         - |
| Any_Lazy             | 1000000 | 26.317 ns | 0.5421 ns | 0.7049 ns | 0.0153 |      96 B |
// * Warnings *
MultimodalDistribution
  CollectionCheckBenchmarks.Any_Lazy: .NET 8.0 -> It seems that the distribution is bimodal (mValue = 3.27)
// * Hints *
Outliers
  CollectionCheckBenchmarks.NullConditional_List: .NET 8.0 -> 2 outliers were removed (3.46 ns, 3.49 ns)
  CollectionCheckBenchmarks.PatternMatching_List: .NET 8.0 -> 3 outliers were removed (3.71 ns..4.08 ns)
  CollectionCheckBenchmarks.Count_List: .NET 8.0           -> 1 outlier  was  removed (3.33 ns)
  CollectionCheckBenchmarks.NullConditional_List: .NET 8.0 -> 1 outlier  was  removed (3.59 ns)
  CollectionCheckBenchmarks.Any_List: .NET 8.0             -> 3 outliers were removed (4.06 ns..4.18 ns)
  CollectionCheckBenchmarks.PatternMatching_List: .NET 8.0 -> 2 outliers were removed (3.29 ns, 3.45 ns)
  CollectionCheckBenchmarks.Any_Lazy: .NET 8.0             -> 2 outliers were removed (31.62 ns, 32.41 ns)

// * Legends *
  Size      : Value of the 'Size' parameter
  Mean      : Arithmetic mean of all measurements
  Error     : Half of 99.9% confidence interval
  StdDev    : Standard deviation of all measurements
  Gen0      : GC Generation 0 collects per 1000 operations
  Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
  1 ns      : 1 Nanosecond (0.000000001 sec)

// * Diagnostic Output - MemoryDiagnoser *


// ***** BenchmarkRunner: End *****
Run time: 00:05:09 (309.58 sec), executed benchmarks: 10

Global total time: 00:05:10 (310.37 sec), executed benchmarks: 10


Benchmark 结果整理(.NET 8.0)

以下请 ChatGPT 帮我解说 Summary 内容

方法SizeMean (ns)备注
Count(list)102.07 ns 最快,稳定 O(1)
NullConditional(list)102.23 ns类似 Count,但加了 null 安全
PatternMatching(list)102.08 ns几乎与 Count 相同,新的语法
Any(list)102.73 ns较慢一点,但仍非常快
Any(lazy)1027.04 ns明显较慢,会开始逐一取得集合中的资料
Count(list)1,000,0002.03 ns一样快,证明是 O(1)
Any(list)1,000,0002.66 ns稍慢,但仍可接受
Any(lazy)1,000,00026.32 ns效能一致但分配记忆体(96 B)

结论

  • 对于 List 或 Array(具体集合):
    • Count 和 PatternMatching 表现几乎相同,都是极快的 O(1)
    • Any() 也很快,但略微多一些成本(需进入列举器)
  • 对于 Lazy Enumerable(如 LINQ):
    • 只能使用 Any(),但会分配记忆体(如列举器)
    • 效能相较 List 明显慢,但仍在可接受範围内(~26ns)
  • NullConditional 虽然简洁,效能略低一点点,但差距不大,可用于强调 null-safety。

 

VS2022 的 ConsoleApp 专案版本

另外也在 VS2022 里建立个 ConsoleApp 来执行,大致上程式码都一样,不过另外加上设定产出 Markdown 与 CSV 格式的报告

执行时在 Console 里输入指令

dotnet run --configuration Release

执行结果

BenchmarkDotNet 的预设输出资料夹为:

<目前执行的工作目录>\BenchmarkDotNet.Artifacts\results\

而 Visual Studio 预设的「目前工作目录」是专案根目录,所以会在这个路径看到报告:

Collection_Check_Benchmark\BenchmarkDotNet.Artifacts\results

Markdown Report - default

Markdown Report - Github

Html Report

 

以上

纯粹是在写兴趣的,用写程式、写文章来抒解工作压力

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章