“`markdown
掌握 C# List:数据结构与操作详解
在 C# 编程中,处理集合数据是日常任务。System.Collections.Generic.List<T> 是 .NET Framework 和 .NET Core 中最常用、功能最强大的泛型集合之一。它提供了一个动态大小的数组,允许您存储和操作类型安全的对象列表。本文将深入探讨 List<T> 的数据结构、主要特性以及各种操作,帮助您在 C# 项目中高效地使用它。
1. 什么是 List<T>?
List<T> 是 System.Collections.Generic 命名空间下的一个泛型集合类。它实现了 IList<T>, ICollection<T>, IEnumerable<T>, IReadOnlyList<T>, IReadOnlyCollection<T> 等接口,这意味着它不仅提供了丰富的操作方法,还具有良好的互操作性。
核心特性:
- 动态大小: 与固定大小的数组不同,
List<T>可以根据需要自动增长或缩小,无需手动管理底层数组。 - 类型安全:
List<T>是泛型的,这意味着它在编译时强制类型安全,避免了运行时类型转换错误和装箱/拆箱的性能开销。 - 基于索引访问: 元素可以通过零基索引进行快速访问,就像数组一样。
- 高效插入和删除(部分情况): 在列表末尾添加和删除元素通常非常高效。但在列表中间插入或删除元素可能涉及元素的移动,效率会稍低。
2. List<T> 的底层实现(数据结构)
List<T> 的底层实际上是一个 T[](泛型数组)。当您创建一个 List<T> 时,它会分配一个初始容量的数组。当添加的元素数量超过当前容量时,List<T> 会执行以下操作:
- 创建一个更大的新数组(通常是当前容量的两倍)。
- 将旧数组中的所有元素复制到新数组中。
- 丢弃旧数组。
这个自动扩容机制虽然方便,但在频繁扩容时可能会产生一定的性能开销,尤其是在列表中间进行大量的插入操作时。因此,如果预先知道列表的大致大小,可以在初始化时指定容量,以减少扩容的次数。
3. 声明和初始化 List<T>
有多种方法可以声明和初始化 List<T>:
“`csharp
// 声明一个空的字符串列表
List
// 声明一个带有初始容量的整数列表(例如,预估会有100个元素)
List
// 使用集合初始化器初始化列表
List
// 从现有集合(例如数组)初始化列表
string[] citiesArray = { “New York”, “London”, “Paris” };
List
// 从 LINQ 查询结果初始化
List
“`
4. List<T> 的主要属性
Count: 获取List<T>中包含的元素数量。Capacity: 获取或设置List<T>可以包含的元素总数,而无需重新调整大小。当Count超过Capacity时,容量会自动增加。
“`csharp
List
Console.WriteLine($”Initial Count: {myFruits.Count}, Capacity: {myFruits.Capacity}”); // Count: 0, Capacity: 0
myFruits.Add(“Mango”);
Console.WriteLine($”After Add Count: {myFruits.Count}, Capacity: {myFruits.Capacity}”); // Count: 1, Capacity: 4 (通常默认初始容量为4)
myFruits.Add(“Orange”);
myFruits.Add(“Grape”);
myFruits.Add(“Kiwi”);
Console.WriteLine($”After 4 Adds Count: {myFruits.Count}, Capacity: {myFruits.Capacity}”); // Count: 4, Capacity: 4
myFruits.Add(“Pineapple”); // 触发扩容
Console.WriteLine($”After 5 Adds Count: {myFruits.Count}, Capacity: {myFruits.Capacity}”); // Count: 5, Capacity: 8
“`
5. List<T> 的常用操作
5.1 添加元素
Add(T item): 在列表末尾添加单个元素。AddRange(IEnumerable<T> collection): 在列表末尾添加一个集合的所有元素。Insert(int index, T item): 在指定索引处插入单个元素。InsertRange(int index, IEnumerable<T> collection): 在指定索引处插入一个集合的所有元素。
“`csharp
List
shoppingList.Add(“Milk”); // {“Milk”}
shoppingList.Add(“Bread”); // {“Milk”, “Bread”}
string[] dairy = { “Cheese”, “Yogurt” };
shoppingList.AddRange(dairy); // {“Milk”, “Bread”, “Cheese”, “Yogurt”}
shoppingList.Insert(0, “Eggs”); // {“Eggs”, “Milk”, “Bread”, “Cheese”, “Yogurt”}
List
shoppingList.InsertRange(3, produce); // {“Eggs”, “Milk”, “Bread”, “Apples”, “Potatoes”, “Cheese”, “Yogurt”}
“`
5.2 访问元素
- 索引器
[index]: 通过零基索引访问元素。
“`csharp
string firstItem = shoppingList[0]; // “Eggs”
string thirdItem = shoppingList[2]; // “Bread”
// 修改元素
shoppingList[1] = “Almond Milk”; // {“Eggs”, “Almond Milk”, “Bread”, …}
“`
5.3 删除元素
Remove(T item): 移除列表中第一次出现的指定元素。如果找到并移除,返回true;否则返回false。RemoveAt(int index): 移除指定索引处的元素。RemoveRange(int index, int count): 移除从指定索引开始的指定数量的元素。RemoveAll(Predicate<T> match): 移除所有符合指定条件的元素。Clear(): 移除列表中的所有元素。
“`csharp
shoppingList.Remove(“Bread”); // 移除”Bread”
shoppingList.RemoveAt(0); // 移除”Eggs”
// 假设 shoppingList 现在是 {“Almond Milk”, “Apples”, “Potatoes”, “Cheese”, “Yogurt”}
shoppingList.RemoveRange(1, 2); // 移除”Apples”和”Potatoes”
// shoppingList 现在是 {“Almond Milk”, “Cheese”, “Yogurt”}
shoppingList.Add(“Cheddar”);
shoppingList.Add(“Gouda”);
shoppingList.Add(“Feta”);
// shoppingList 现在是 {“Almond Milk”, “Cheese”, “Yogurt”, “Cheddar”, “Gouda”, “Feta”}
// 移除所有长度大于5的字符串
shoppingList.RemoveAll(item => item.Length > 5);
// shoppingList 现在是 {“Cheese”, “Gouda”, “Feta”}
shoppingList.Clear(); // 清空列表
“`
5.4 搜索元素
Contains(T item): 检查列表中是否包含指定元素。返回true或false。IndexOf(T item): 返回列表中第一次出现指定元素的零基索引。如果未找到,返回 -1。Find(Predicate<T> match): 查找并返回符合指定条件的第一个元素。如果未找到,返回元素的默认值(对于引用类型是null,对于值类型是 0 等)。FindLast(Predicate<T> match): 查找并返回符合指定条件的最后一个元素。FindAll(Predicate<T> match): 查找并返回所有符合指定条件的元素,结果是一个新的List<T>。
“`csharp
List
bool has92 = scores.Contains(92); // true
int indexOf78 = scores.IndexOf(78); // 2
int indexOfFirst92 = scores.IndexOf(92); // 1 (第一次出现的索引)
int indexOfLast92 = scores.LastIndexOf(92); // 3 (最后一次出现的索引)
int firstScoreOver90 = scores.Find(s => s > 90); // 92
int lastScoreOver90 = scores.FindLast(s => s > 90); // 95
List
“`
5.5 排序元素
Sort(): 使用默认比较器对整个List<T>中的元素进行就地排序。元素类型必须实现IComparable<T>或IComparable。Sort(IComparer<T> comparer): 使用指定的比较器对元素进行排序。Sort(Comparison<T> comparison): 使用指定的Comparison<T>委托进行排序。
“`csharp
List
unsortedFruits.Sort(); // {“Apple”, “Banana”, “Cherry”, “Date”}
List
unsortedNumbers.Sort((a, b) => b.CompareTo(a)); // 降序排序: {9, 8, 5, 2, 1}
// 自定义对象排序示例
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
List
{
new Person { Name = “Alice”, Age = 30 },
new Person { Name = “Bob”, Age = 25 },
new Person { Name = “Charlie”, Age = 30 }
};
// 按年龄升序排序,如果年龄相同则按姓名升序
people.Sort((p1, p2) =>
{
int ageComparison = p1.Age.CompareTo(p2.Age);
if (ageComparison != 0) return ageComparison;
return p1.Name.CompareTo(p2.Name);
});
// 结果: Bob (25), Alice (30), Charlie (30)
“`
5.6 迭代元素
foreach循环: 最常用的迭代方式。for循环: 通过索引迭代。ForEach(Action<T> action): 对List<T>中的每个元素执行指定操作。
“`csharp
// foreach 循环
foreach (string fruit in unsortedFruits)
{
Console.WriteLine(fruit);
}
// for 循环
for (int i = 0; i < unsortedFruits.Count; i++)
{
Console.WriteLine($”{i}: {unsortedFruits[i]}”);
}
// ForEach 方法
unsortedFruits.ForEach(fruit => Console.WriteLine($”Fruit: {fruit}”));
“`
6. List<T> 与数组 (Array) 的比较
| 特性 | List<T> |
T[] (数组) |
|---|---|---|
| 大小 | 动态,自动扩容 | 固定大小,创建后不能改变 |
| 类型 | 泛型,类型安全 | 泛型(T[]),类型安全 |
| 性能 | 读写快(通常),插入/删除(中间)可能慢 | 读写非常快 |
| 功能 | 提供丰富的方法(Add, Remove, Sort, Find) | 基础集合,功能需要手动实现或使用 Array 辅助类 |
| 内存 | 可能有额外开销(如 Capacity 大于 Count) |
内存紧凑 |
| 适用场景 | 元素数量不确定,需要频繁增删改查 | 元素数量确定,需要高性能随机访问,不常增删 |
总结: 当你需要一个可以动态增长或缩小的集合,并且需要方便地进行增删改查操作时,List<T> 是最佳选择。如果元素的数量是固定的,并且对性能有极致要求,或者需要直接访问原始内存,那么数组可能更合适。
7. 性能注意事项
- 扩容开销:
List<T>扩容时需要重新分配内存并复制元素。虽然这通常以指数级增长策略(如翻倍)来摊销,使得在大多数情况下平均性能良好,但如果频繁在小容量和大容量之间跳跃,或者在循环中频繁添加大量元素而不预先设置容量,可能会导致性能下降。
建议: 如果知道列表的大致最终大小,在初始化时使用new List<T>(initialCapacity)来指定容量。 - 中间插入/删除: 在
List<T>的中间插入或删除元素会导致其后的所有元素都进行内存移动,这在列表较大时会非常耗时。
替代方案: 如果需要频繁在列表中间进行插入或删除操作,可以考虑使用LinkedList<T>,它在这种情况下效率更高,但随机访问性能较差。
8. 结论
List<T> 是 C# 中一个极其重要且多功能的集合类型。通过了解其底层实现、主要属性和丰富的操作方法,您可以更有效地利用它来组织和操作数据。在日常开发中,正确选择和使用 List<T> 可以显著提高代码的可读性、可维护性和性能。
“`
希望能帮到您!