今天 (2025-04-06) 早上在 Facebook - 台湾 .NET 技术爱好找论坛里看到 Will 保哥转贴了一则 X 贴文,
要检查一个集合物件是否有元素在内,会使用哪一种语法来检查 …
在 C# 开发中,经常需要判断一个集合是否有包含任何元素。虽然这是一个简单的判断,但其实背后有不少值得探讨的细节。就来了解这四种常见的做法,并分析它们的优缺点与适合的使用情境。
原文出处
- https://x.com/okyrylchuk/status/1908546268227625423
四种判断方式
作者「Oleg Kyrylchuk」列出了四种判断 List 不为 null 且有元素的方式
- Classic way
- List.Count way
- Enumerable.Any way
- 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, Array | Count > 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 内容
方法 | Size | Mean (ns) | 备注 |
Count(list) | 10 | 2.07 ns | 最快,稳定 O(1) |
NullConditional(list) | 10 | 2.23 ns | 类似 Count,但加了 null 安全 |
PatternMatching(list) | 10 | 2.08 ns | 几乎与 Count 相同,新的语法 |
Any(list) | 10 | 2.73 ns | 较慢一点,但仍非常快 |
Any(lazy) | 10 | 27.04 ns | 明显较慢,会开始逐一取得集合中的资料 |
Count(list) | 1,000,000 | 2.03 ns | 一样快,证明是 O(1) |
Any(list) | 1,000,000 | 2.66 ns | 稍慢,但仍可接受 |
Any(lazy) | 1,000,000 | 26.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
以上
纯粹是在写兴趣的,用写程式、写文章来抒解工作压力