“`markdown
C# List:全面解析与使用技巧
在 C# 编程中,System.Collections.Generic.List<T> 类是用途最广泛、最基础的集合类型之一。它提供了一个动态数组的功能,能够根据需要自动扩容和收缩,为存储和操作对象集合提供了性能与灵活性的良好平衡。本文将深入全面解析 List<T>,涵盖其核心功能、性能特点以及高效使用的最佳实践。
List<T> 是什么?
List<T> 是一个泛型集合,代表一个强类型对象列表,可以通过索引进行访问。它本质上是围绕一个动态大小的数组进行封装的。当你向 List<T> 中添加元素,当其内部数组空间不足时,它会自动进行扩容(通常是将其容量加倍),并将现有元素复制到新的、更大的数组中。
核心功能与基本用法
1. 声明与初始化
你可以通过多种方式声明和初始化 List<T>:
“`csharp
// 声明一个空的整数列表
List
// 使用集合初始化器声明并初始化带元素的列表
List
// 初始化时指定初始容量(例如,避免频繁的内存重新分配)
List
// 从另一个集合(例如,数组或另一个 List)初始化
string[] colorsArray = { “Red”, “Green”, “Blue” };
List
“`
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"); // trueIndexOf(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
ListunsortedNumbers = new List { 5, 2, 8, 1, 9 };
unsortedNumbers.Sort(); // [1, 2, 5, 8, 9]// 使用 Lambda 表达式进行自定义排序(适用于简单情况)
Listnames = 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
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)。
常见陷阱与最佳实践
-
在迭代时修改列表:
在使用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);// 正确做法:创建新列表
ListoddNumbers = numbers.Where(num => num % 2 != 0).ToList();
“` -
预分配容量:
如果你知道列表将容纳的大致元素数量,请在初始化时指定容量,以避免多次内存重新分配和数组复制,这可能开销很大。
csharp
List<MyObject> largeList = new List<MyObject>(10000); // 好的做法
// List<MyObject> largeList = new List<MyObject>(); // 如果添加大量项,可能效率低下 -
选择正确的集合:
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) 的时间复杂度。当你需要将值与唯一键关联时,使用它。
-
线程安全:
List<T>不是线程安全的。如果多个线程并发访问和修改List<T>,你必须实现外部同步(例如,使用lock语句或ReaderWriterLockSlim)以防止竞态条件和数据损坏。对于并发场景,可以考虑使用System.Collections.Concurrent类型,如ConcurrentBag<T>或ConcurrentQueue<T>。 -
值类型与引用类型:
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
{
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> 的全部潜力,构建健壮且高效的应用程序。
“`