在上一篇 .NET Framework 源碼研究系列之---Delegate 中,我們共同探討了 .NET 委託的實現原理。今天,我們將目光投向 .NET 集合框架中使用頻率最高、也是最基礎的類別之一 —— List<T>。
大家都知道,List<T> 與陣列 Array 一樣,本質上都是有序的一維資料結構。但 List<T> 提供了豐富的操作方法,如插入、刪除、排序、搜尋等,極大地簡化了開發工作。那麼,List<T> 在 .NET 源碼層面究竟是如何實現的?我們在享受其便利性的同時,是否需要付出額外的性能代價?從源碼中我們又能汲取哪些設計智慧?帶著這些問題,讓我們啟程深入探索。
首先透過物件瀏覽器觀察 List<T> 的型別定義,可見它實現了 IList<T>、ICollection<T>、IEnumerable<T> 等核心介面,同時也繼承了非泛型的 System.Collections.IList 以保持向後相容。源碼中的定義如下:
public class List<T> : IList<T>, System.Collections.IList
注意:IList<T> 本身繼承自 ICollection<T> 與 IEnumerable<T>,因此物件瀏覽器顯示的介面列表會更加完整。
List<T> 內部維護幾個關鍵欄位:
private const int _defaultCapacity = 4; —— 預設初始容量為 4 個元素。private T[] _items; —— 底層儲存資料的陣列,這是 List<T> 之所以為「陣列包裝器」的鐵證。private int _size; —— 當前實際包含的元素數量(即 Count 屬性返回值)。private int _version; —— 版本號,用於迭代器檢測集合在遍歷過程中是否被修改。private Object _syncRoot; —— 同步鎖物件(非序列化)。static T[] _emptyArray = new T[0]; —— 共享的空陣列實例,用於無參建構函式的初始化。三個建構函式分別對應:無參(使用共享空陣列)、指定容量(預分配陣列)、從 IEnumerable<T> 初始化(若來源實作 ICollection<T> 則直接複製,否則逐項 Add)。
這是 List<T> 最核心、也是最值得深究的部分。Count 代表當前元素數,Capacity 代表底層陣列長度。當 Add 導致 _size == _items.Length 時,觸發 EnsureCapacity 擴容邏輯:
private void EnsureCapacity(int min) {
if (_items.Length < min) {
int newCapacity = _items.Length == 0 ? _defaultCapacity : _items.Length * 2;
if (newCapacity < min) newCapacity = min;
Capacity = newCapacity;
}
}
Capacity 的 setter 會分配新陣列,並透過 Array.Copy 將舊資料遷移。關鍵洞見: 最壞情況下(如在容量臨界點插入元素),單次 Add 操作的時間複雜度會退化為 O(n),因為需要複製所有現有元素。這正是使用 List<T> 而非陣列所付出的「隱形成本」。
細讀源碼會發現,List<T> 的 Insert、RemoveAt、BinarySearch、Sort、Reverse、IndexOf 等幾乎所有資料操作,內部皆直接呼叫 Array 類別對應的靜態方法。這印證了「List<T> 是對 Array 的一層薄包裝」這一論斷,也解釋了為何其性能極接近原生陣列。
List<T> 顯式實作 ICollection.IsSynchronized 並直接返回 false,明確宣告非執行緒安全。多執行緒環境下需自行加鎖或改用 System.Collections.Concurrent 命名空間下的並發集合。
List<T> 實作 IEnumerable<T> 並提供 GetEnumerator 返回結構體 Enumerator,完美貫徹了設計模式中「迭代器模式」的定義:提供一種方法順序訪問聚合物件中各個元素,而又不需暴露該物件的內部表示。 這也是為何 foreach 能夠優雅遍歷 List<T> 的根本原因。
除了包裝 Array 方法,List<T> 還自帶兩個實用方法:
public void TrimExcess() {
int threshold = (int)(((double)_items.Length) * 0.9);
if (_size < threshold) {
Capacity = _size;
}
}
當實際元素數少於容量 90% 時,將容量壓縮至 Count,釋放記憶體。示例:
var list = new List<int>(new[] {0,1,2,4}); // Capacity=4
list.Add(5); // Capacity=8 (擴容)
list.TrimExcess(); // Capacity=5 (壓縮)
public bool TrueForAll(Predicate<T> match) {
if (match == null) ThrowHelper.ThrowArgumentNullException(...);
for (int i = 0; i < _size; i++) {
if (!match(_items[i])) return false;
}
return true;
}
以 O(n) 時間檢查所有元素是否滿足條件,短路求值,效能極佳。
capacity 可避免多次擴容複製,顯著提升效能。LinkedList<T>。ConcurrentBag<T> 等並發集合。透過源碼窺探,List<T> 不失為一個「薄而精」的陣列包裝器,它以極小的封裝成本,換取了豐富的 API 與極佳的緩存局部性。理解其擴容策略與底層陣列特性,是寫出高效能 .NET 代碼的基石。