List<T> 类被实现为在后台使用内部 T[] 数组。如果您使用List<T>(int) 构造函数对其进行初始化,它将分配一个指定大小的数组。如果您使用默认构造函数,它将使用默认容量 4,但在这种情况下,数组只会在第一次添加时分配。
每次向列表中添加元素时,它会首先检查是否已达到容量(即现有的Count是否等于Capacity)。如果是这样,它将创建一个大小是前一个数组两倍的新数组,将所有现有元素复制到其中,然后继续写入新元素。这将在后续添加元素时无限期地发生,直到达到您引用的硬限制 (Int32.MaxValue)。
在性能方面,这意味着添加元素是 O(1) 或 O(n) 操作,具体取决于是否需要增加容量(如 Add 中所述)。但是,由于容量在需要增加时会加倍,因此随着列表变大,这种重新分配的频率会呈指数下降。例如,从 4 开始,容量增加将发生在 4、8、16、32、64、128,... 元素。因此,调用Add n 次时重新分配的总成本大约为 4+8+16+…+n/8+n/4+n/2,仍然对应于 O(n)。
这是一个示例,显示了内部数组的状态以及一系列加法操作:
// ┌┐
var list = new List<char>(); // ││ Count: 0
// └┘ Capacity: 0
// ┌───┬───┬───┬───┐
list.Add('h'); // │ h │ ░ │ ░ │ ░ │ Count: 1
// └───┴───┴───┴───┘ Capacity: 4
// ┌───┬───┬───┬───┐
list.Add('e'); // │ h │ e │ ░ │ ░ │ Count: 2
// └───┴───┴───┴───┘ Capacity: 4
// ┌───┬───┬───┬───┐
list.Add('l'); // │ h │ e │ l │ ░ │ Count: 3
// └───┴───┴───┴───┘ Capacity: 4
// ┌───┬───┬───┬───┐
list.Add('l'); // │ h │ e │ l │ l │ Count: 4
// └───┴───┴───┴───┘ Capacity: 4
// ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('o'); // │ h │ e │ l │ l │ o │ ░ │ ░ │ ░ │ Count: 5
// └───┴───┴───┴───┴───┴───┴───┴───┘ Capacity: 8
// ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add(' '); // │ h │ e │ l │ l │ o │ │ ░ │ ░ │ Count: 6
// └───┴───┴───┴───┴───┴───┴───┴───┘ Capacity: 8
// ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('w'); // │ h │ e │ l │ l │ o │ │ w │ ░ │ Count: 7
// └───┴───┴───┴───┴───┴───┴───┴───┘ Capacity: 8
// ┌───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('o'); // │ h │ e │ l │ l │ o │ │ w │ o │ Count: 8
// └───┴───┴───┴───┴───┴───┴───┴───┘ Capacity: 8
// ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('r'); // │ h │ e │ l │ l │ o │ │ w │ o │ r │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │ Count: 9
// └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ Capacity: 16
// ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('l'); // │ h │ e │ l │ l │ o │ │ w │ o │ r │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │ ░ │ Count: 10
// └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ Capacity: 16
// ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
list.Add('d'); // │ h │ e │ l │ l │ o │ │ w │ o │ r │ l │ d │ ░ │ ░ │ ░ │ ░ │ ░ │ Count: 11
// └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ Capacity: 16
░ 符号表示已分配但仍未使用的空间。这些数组位置将包含default value 用于T。对于char,这将是空字符\0。但是,消费者永远不会看到这些值。
通过AddRange将多个元素相加时,最多只进行一次重新分配。如果将之前的容量翻倍不足以容纳所有新元素,则内部数组会立即增加到新的计数。
与添加不同,删除元素不会自动缩小列表。但是,您可以通过调用 TrimExcess 手动实现这一点。
如comments 中所述,上述某些方面(例如默认初始容量为 4)是从 .NET Framework 4.7.2 的source code 派生的实现细节。然而,核心原则是根深蒂固的,不太可能在其他/未来的框架中改变。