C# List:全面解析与使用技巧 – wiki大全

“`markdown

C# List:全面解析与使用技巧

在 C# 编程中,System.Collections.Generic.List<T> 类是用途最广泛、最基础的集合类型之一。它提供了一个动态数组的功能,能够根据需要自动扩容和收缩,为存储和操作对象集合提供了性能与灵活性的良好平衡。本文将深入全面解析 List<T>,涵盖其核心功能、性能特点以及高效使用的最佳实践。

List<T> 是什么?

List<T> 是一个泛型集合,代表一个强类型对象列表,可以通过索引进行访问。它本质上是围绕一个动态大小的数组进行封装的。当你向 List<T> 中添加元素,当其内部数组空间不足时,它会自动进行扩容(通常是将其容量加倍),并将现有元素复制到新的、更大的数组中。

核心功能与基本用法

1. 声明与初始化

你可以通过多种方式声明和初始化 List<T>

“`csharp
// 声明一个空的整数列表
List numbers = new List();

// 使用集合初始化器声明并初始化带元素的列表
List fruits = new List { “Apple”, “Banana”, “Cherry” };

// 初始化时指定初始容量(例如,避免频繁的内存重新分配)
List temperatures = new List(100);

// 从另一个集合(例如,数组或另一个 List)初始化
string[] colorsArray = { “Red”, “Green”, “Blue” };
List colors = new List(colorsArray);
“`

2. 添加元素

  • Add(T item): 将单个项添加到列表的末尾。
    csharp
    numbers.Add(10); // numbers: [10]
    numbers.Add(20); // numbers: [10, 20]
  • AddRange(IEnumerable<T> collection): 将指定集合的元素添加到列表的末尾。
    csharp
    List<int> moreNumbers = new List<int> { 30, 40 };
    numbers.AddRange(moreNumbers); // numbers: [10, 20, 30, 40]
  • Insert(int index, T item): 在指定索引处插入一个元素。
    csharp
    fruits.Insert(1, "Orange"); // fruits: ["Apple", "Orange", "Banana", "Cherry"]
  • InsertRange(int index, IEnumerable<T> collection): 在指定索引处插入一个集合的元素。

3. 访问元素

可以使用零基索引访问元素:

“`csharp
string firstFruit = fruits[0]; // “Apple”
Console.WriteLine(fruits[2]); // “Banana”

// 遍历列表
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}

// 使用 for 循环遍历
for (int i = 0; i < fruits.Count; i++)
{
Console.WriteLine($”Fruit at index {i}: {fruits[i]}”);
}
“`

4. 删除元素

  • Remove(T item):List<T> 中移除特定对象第一次出现的实例。如果找到并移除了项,则返回 true,否则返回 false
    csharp
    fruits.Remove("Orange"); // fruits: ["Apple", "Banana", "Cherry"]
  • RemoveAt(int index): 移除指定索引处的元素。
    csharp
    fruits.RemoveAt(0); // fruits: ["Banana", "Cherry"]
  • RemoveAll(Predicate<T> match): 移除所有符合指定谓词定义的条件的元素。
    csharp
    List<int> ages = new List<int> { 10, 25, 30, 15, 40 };
    ages.RemoveAll(age => age < 18); // ages: [25, 30, 40]
  • Clear():List<T> 中移除所有元素。
    csharp
    fruits.Clear(); // fruits: []

5. 查找元素

  • Contains(T item): 确定 List<T> 是否包含某个元素。
    csharp
    bool hasBanana = fruits.Contains("Banana"); // true
  • IndexOf(T item): 返回 List<T> 中某个值第一次出现的零基索引。如果未找到,则返回 -1。
    csharp
    int bananaIndex = fruits.IndexOf("Banana"); // 0 (如果 fruits 是 ["Banana", "Cherry"])
  • Find(Predicate<T> match): 搜索符合指定谓词定义的条件的元素,并返回 List<T> 中第一次出现的实例。
    csharp
    string foundFruit = fruits.Find(f => f.StartsWith("B")); // "Banana"
  • FindAll(Predicate<T> match): 检索所有符合指定谓词定义的条件的元素。返回一个新的 List<T>
    csharp
    List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0); // [10, 20, 30, 40]
  • Exists(Predicate<T> match): 确定 List<T> 是否包含符合指定谓词定义的条件的元素。
    csharp
    bool hasEven = numbers.Exists(n => n % 2 == 0); // true

6. 排序与反转

  • Sort(): 使用默认比较器对整个 List<T> 中的元素进行排序。对于自定义类型,T 必须实现 IComparable<T>,或者你可以提供一个 IComparer<T>
    “`csharp
    List unsortedNumbers = new List { 5, 2, 8, 1, 9 };
    unsortedNumbers.Sort(); // [1, 2, 5, 8, 9]

    // 使用 Lambda 表达式进行自定义排序(适用于简单情况)
    List names = new List { “Alice”, “Charlie”, “Bob” };
    names.Sort((a, b) => a.CompareTo(b)); // [“Alice”, “Bob”, “Charlie”]
    * **`Reverse()`:** 反转整个 `List<T>` 中元素的顺序。csharp
    unsortedNumbers.Reverse(); // [9, 8, 5, 2, 1]
    “`

7. 容量与计数

  • Count: 获取 List<T> 中实际包含的元素数量。
  • Capacity: 获取或设置内部数据结构可以容纳的元素总数,而无需重新分配内存。

“`csharp
List myNumbers = new List();
Console.WriteLine($”Count: {myNumbers.Count}, Capacity: {myNumbers.Capacity}”); // Count: 0, Capacity: 0 (或 4,取决于 .NET 版本)

myNumbers.Add(1);
Console.WriteLine($”Count: {myNumbers.Count}, Capacity: {myNumbers.Capacity}”); // Count: 1, Capacity: 4

myNumbers.Add(2);
myNumbers.Add(3);
myNumbers.Add(4);
myNumbers.Add(5); // 这将触发重新分配内存
Console.WriteLine($”Count: {myNumbers.Count}, Capacity: {myNumbers.Capacity}”); // Count: 5, Capacity: 8
``
如果你预先知道列表的大致大小,明确设置
Capacity` 可以通过减少重新分配内存的次数来提高性能。

性能考量

List<T> 对许多常见操作提供了良好的性能,但理解其基于数组的本质至关重要:

  • 在末尾添加/移除 (Add, RemoveAt(Count - 1)): 平均时间复杂度通常为 O(1)。如果需要重新分配内存,则变为 O(N)(因为需要复制数组),但这种开销在多次操作中会被分摊。
  • 按索引访问 (this[int index]): O(1),因为是直接的数组查找。
  • 在中间插入/移除 (Insert, RemoveAt(index),其中 index < Count - 1)): O(N),因为所有后续元素都需要移动。
  • 搜索 (Contains, IndexOf, Find, FindAll): 最坏情况下为 O(N),因为它可能需要遍历所有元素。Sort() 通常是 O(N log N)。

常见陷阱与最佳实践

  1. 在迭代时修改列表:
    在使用 foreach 循环遍历 List<T> 时修改它(添加或删除元素)将抛出 InvalidOperationException
    “`csharp
    // 错误示例:将抛出 InvalidOperationException
    // foreach (int num in numbers)
    // {
    // if (num % 2 == 0)
    // {
    // numbers.Remove(num);
    // }
    // }

    // 正确做法:反向迭代
    for (int i = numbers.Count – 1; i >= 0; i–)
    {
    if (numbers[i] % 2 == 0)
    {
    numbers.RemoveAt(i);
    }
    }

    // 正确做法:使用 RemoveAll 配合谓词
    numbers.RemoveAll(num => num % 2 == 0);

    // 正确做法:创建新列表
    List oddNumbers = numbers.Where(num => num % 2 != 0).ToList();
    “`

  2. 预分配容量:
    如果你知道列表将容纳的大致元素数量,请在初始化时指定容量,以避免多次内存重新分配和数组复制,这可能开销很大。
    csharp
    List<MyObject> largeList = new List<MyObject>(10000); // 好的做法
    // List<MyObject> largeList = new List<MyObject>(); // 如果添加大量项,可能效率低下

  3. 选择正确的集合:

    • List<T> vs. 数组 (T[]): 当集合大小需要动态变化时,使用 List<T>。当大小固定且在创建时已知,或者在对性能要求极高的场景中,使用数组。
    • List<T> vs. LinkedList<T>: LinkedList<T> 在任意位置插入/删除元素(如果你有节点引用)的平均时间复杂度为 O(1),但按索引访问的时间复杂度为 O(N)。List<T> 更适合按索引访问和迭代。
    • List<T> vs. HashSet<T>: HashSet<T> 为添加、删除和检查唯一元素的存在提供了平均 O(1) 的时间复杂度。当你需要一个唯一项的集合并且需要快速查找,且顺序不重要时,使用 HashSet<T>
    • List<T> vs. Dictionary<TKey, TValue>: Dictionary<TKey, TValue> 为按键查找提供了平均 O(1) 的时间复杂度。当你需要将值与唯一键关联时,使用它。
  4. 线程安全:
    List<T> 不是线程安全的。如果多个线程并发访问和修改 List<T>,你必须实现外部同步(例如,使用 lock 语句或 ReaderWriterLockSlim)以防止竞态条件和数据损坏。对于并发场景,可以考虑使用 System.Collections.Concurrent 类型,如 ConcurrentBag<T>ConcurrentQueue<T>

  5. 值类型与引用类型:
    List<T> 对值类型和引用类型都高效。对于值类型(结构体、基本类型),元素直接存储。对于引用类型(类),存储的是对象的引用。

LINQ 与 List<T>

List<T> 与 LINQ (Language Integrated Query) 无缝集成,提供了强大的查询和操作数据的方式。

“`csharp
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}

List products = new List
{
new Product { Name = “Laptop”, Price = 1200, Category = “Electronics” },
new Product { Name = “Mouse”, Price = 25, Category = “Electronics” },
new Product { Name = “Keyboard”, Price = 75, Category = “Electronics” },
new Product { Name = “Desk”, Price = 300, Category = “Furniture” }
};

// 筛选价格低于 100 的产品
var cheapProducts = products.Where(p => p.Price < 100).ToList();
// 结果: [Mouse, Keyboard]

// 选择产品名称
var productNames = products.Select(p => p.Name).ToList();
// 结果: [“Laptop”, “Mouse”, “Keyboard”, “Desk”]

// 按价格排序
var sortedByPrice = products.OrderBy(p => p.Price).ToList();
// 结果: [Mouse, Keyboard, Desk, Laptop]

// 按类别分组
var groupedByCategory = products.GroupBy(p => p.Category);
/
结果示例:
Group “Electronics”: [Laptop, Mouse, Keyboard]
Group “Furniture”: [Desk]
/

// 检查是否有任何产品价格昂贵(超过 1000)
bool anyExpensive = products.Any(p => p.Price > 1000); // true
“`

总结

List<T> 是 C# 中大多数通用场景下功能强大且性能优越的集合。理解其基于数组的实现、容量管理以及各种操作的性能特点是高效使用它的关键。通过遵循最佳实践并充分利用 LINQ,你可以发挥 List<T> 的全部潜力,构建健壮且高效的应用程序。
“`

滚动至顶部