[練習題] C# 中檢查 Collection 是否有項目的幾種方式

今天 (2025-04-06) 早上在 Facebook -  台灣 .NET  技術愛好找論壇裡看到 Will 保哥轉貼了一則 X 貼文,
要檢查一個集合物件是否有元素在內,會使用哪一種語法來檢查 … 

在 C# 開發中,經常需要判斷一個集合是否有包含任何元素。雖然這是一個簡單的判斷,但其實背後有不少值得探討的細節。就來瞭解這四種常見的做法,並分析它們的優缺點與適合的使用情境。

原文出處

 

四種判斷方式

作者「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 的相關資料

 

有關 .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

 

以上

純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力