掌握 C# List:数据结构与操作详解 – wiki大全

“`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> 会执行以下操作:

  1. 创建一个更大的新数组(通常是当前容量的两倍)。
  2. 将旧数组中的所有元素复制到新数组中。
  3. 丢弃旧数组。

这个自动扩容机制虽然方便,但在频繁扩容时可能会产生一定的性能开销,尤其是在列表中间进行大量的插入操作时。因此,如果预先知道列表的大致大小,可以在初始化时指定容量,以减少扩容的次数。

3. 声明和初始化 List<T>

有多种方法可以声明和初始化 List<T>

“`csharp
// 声明一个空的字符串列表
List names = new List();

// 声明一个带有初始容量的整数列表(例如,预估会有100个元素)
List numbers = new List(100);

// 使用集合初始化器初始化列表
List fruits = new List { “Apple”, “Banana”, “Cherry” };

// 从现有集合(例如数组)初始化列表
string[] citiesArray = { “New York”, “London”, “Paris” };
List cities = new List(citiesArray);

// 从 LINQ 查询结果初始化
List evenNumbers = Enumerable.Range(1, 10).Where(n => n % 2 == 0).ToList();
“`

4. List<T> 的主要属性

  • Count: 获取 List<T> 中包含的元素数量。
  • Capacity: 获取或设置 List<T> 可以包含的元素总数,而无需重新调整大小。当 Count 超过 Capacity 时,容量会自动增加。

“`csharp
List myFruits = new 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 = new 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 produce = new List { “Apples”, “Potatoes” };
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): 检查列表中是否包含指定元素。返回 truefalse
  • IndexOf(T item): 返回列表中第一次出现指定元素的零基索引。如果未找到,返回 -1。
  • Find(Predicate<T> match): 查找并返回符合指定条件的第一个元素。如果未找到,返回元素的默认值(对于引用类型是 null,对于值类型是 0 等)。
  • FindLast(Predicate<T> match): 查找并返回符合指定条件的最后一个元素。
  • FindAll(Predicate<T> match): 查找并返回所有符合指定条件的元素,结果是一个新的 List<T>

“`csharp
List scores = new List { 85, 92, 78, 92, 65, 95 };

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 allScoresOver90 = scores.FindAll(s => s > 90); // {92, 92, 95}
“`

5.5 排序元素

  • Sort(): 使用默认比较器对整个 List<T> 中的元素进行就地排序。元素类型必须实现 IComparable<T>IComparable
  • Sort(IComparer<T> comparer): 使用指定的比较器对元素进行排序。
  • Sort(Comparison<T> comparison): 使用指定的 Comparison<T> 委托进行排序。

“`csharp
List unsortedFruits = new List { “Banana”, “Apple”, “Cherry”, “Date” };
unsortedFruits.Sort(); // {“Apple”, “Banana”, “Cherry”, “Date”}

List unsortedNumbers = new List { 5, 2, 8, 1, 9 };
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 people = new 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> 可以显著提高代码的可读性、可维护性和性能。
“`
希望能帮到您!

滚动至顶部