一、 数组 (Array)
数组是C#中最基础、最原始的数据集合形式。理解它,是理解所有高级集合的起点。
1. 数组的本质:连续的内存空间
当你声明一个数组,如 int[] numbers = new int[5];,你实际上是在内存中请求了一块连续的、未被分割的 空间,其大小足以存放5个int类型的数据。
- 连续性 (Contiguous):这是数组最重要的特性。数据肩并肩地存储在一起,就像一排有编号的停车位。
- 固定大小 (Fixed-Size):一旦数组被创建,其大小就不能再改变。你不能让一个长度为5的数组突然容纳第6个元素。
- 类型统一 (Homogeneous):一个数组只能存储相同类型的元素。
2. 数组的声明与使用
csharp
// 1. 声明并初始化大小
int[] scores = new int[5]; // 在内存中分配了5个int的空间,默认值为0
// 2. 通过索引访问 (从0开始)
scores[0] = 95;
scores[1] = 88;
int firstScore = scores[0];
// 3. 声明并直接初始化内容
string[] names = new string[] { "Alice", "Bob", "Charlie" };
// 或者更简洁的语法
string[] namesShort = { "Alice", "Bob", "Charlie" };
// 4. 遍历数组
foreach (string name in names)
{
Console.WriteLine(name);
}
// 5. 获取长度
int nameCount = names.Length; // 结果是 3
3. 数组的优缺点
优点:
- 极高的访问性能 :由于内存是连续的,通过索引
array[i]访问元素是一个 O(1) 操作。CPU可以直接通过基地址 + i * 单个元素大小的公式计算出内存地址,无需任何遍历。这使得数组在需要频繁随机读取的场景下无与伦比。 - 内存效率高:除了数据本身,几乎没有额外的开销。
缺点:
- 大小不可变:这是数组最大的"原罪"。在创建时必须知道所需空间,这在许多动态场景下是不现实的。
- 插入和删除效率低下:在数组中间插入或删除一个元素,需要移动该位置之后的所有元素来填补空位或腾出空间,这是一个 O(n) 操作,非常耗时。
二、List<T> - 动态数组
1. List<T> 初识
List<T> 的内部其实就封装了一个数组。它之所以能"动态"增长,是因为它实现了一套巧妙的**容量管理(Capacity Management)**机制。
- Capacity vs. Count :
Count: 列表实际包含的元素数量。Capacity: 内部数组能够容纳 的元素数量。Capacity >= Count恒成立。
当你向一个List<T>添加元素时,如果Count即将超过Capacity,List<T>会自动执行以下操作:
- 创建一个新的、更大的数组(通常是当前容量的两倍)。
- 将旧数组中的所有元素复制到新数组中。
- 丢弃旧数组,将内部引用指向新数组。
- 在新数组的末尾添加新元素。
csharp
List<int> numbers = new List<int>(); // 初始Capacity通常为0
Console.WriteLine($"Count: {numbers.Count}, Capacity: {numbers.Capacity}");
numbers.Add(1); // 添加元素
Console.WriteLine($"Count: {numbers.Count}, Capacity: {numbers.Capacity}"); // 创建Capacity为4的容器
numbers.Add(2);
numbers.Add(3);
numbers.Add(4);
numbers.Add(5); // 此时会触发内部数组的扩容和复制!此时Capacity为8
Console.WriteLine($"Count: {numbers.Count}, Capacity: {numbers.Capacity}");
2. List<T> 的使用
List<T> 提供了比数组更加丰富的API方法:
1. 添加和插入元素 (Adding and Inserting Elements)
| 方法 (Method) | 说明 (Description) | 示例 (Example) List<string> fruits = new List<string>(); |
|---|---|---|
Add(T item) |
在列表的末尾添加一个元素。 | fruits.Add("Apple"); // ["Apple"] |
AddRange(IEnumerable<T> collection) |
将一个集合中的所有元素添加到列表的末尾。 | var moreFruits = new[] { "Banana", "Cherry" }; fruits.AddRange(moreFruits); // ["Apple", "Banana", "Cherry"] |
Insert(int index, T item) |
在列表的指定索引处插入一个元素。 | fruits.Insert(1, "Orange"); // ["Apple", "Orange", "Banana", "Cherry"] |
InsertRange(int index, IEnumerable<T> collection) |
在列表的指定索引处插入一个集合的所有元素。 | var tropical = new[] { "Mango", "Pineapple" }; fruits.InsertRange(2, tropical); |
Contains(T item) |
判断列表中是否包含指定的元素。返回 bool。 |
bool hasApple = fruits.Contains("Apple"); // true |
Exists(Predicate<T> match) |
判断列表中是否存在满足指定条件的元素。使用Lambda表达式。 | bool hasShortName = fruits.Exists(f => f.Length < 6); // true ("Apple") |
Find(Predicate<T> match) |
搜索满足指定条件的第一个元素并返回它。如果找不到,返回该类型的默认值(如引用类型为null)。 |
string bFruit = fruits.Find(f => f.StartsWith("B")); // "Banana" |
FindAll(Predicate<T> match) |
检索所有满足指定条件的元素,并返回一个包含它们的新 List<T>。 |
var longNameFruits = fruits.FindAll(f => f.Length > 5); // ["Orange", "Banana"] |
IndexOf(T item) |
搜索指定元素,并返回其第一次出现的索引。如果找不到,返回 -1。 |
int index = fruits.IndexOf("Banana"); // 2 |
LastIndexOf(T item) |
搜索指定元素,并返回其最后一次出现的索引。如果找不到,返回 -1。 |
// If fruits = ["A", "B", "A"], LastIndexOf("A") is 2 |
Remove(T item) |
从列表中移除第一次出现的指定元素。成功移除返回 true。 |
fruits.Remove("Apple"); // ["Orange", "Banana", "Apple"] |
RemoveAt(int index) |
移除列表中指定索引处的元素。 | fruits.RemoveAt(1); // ["Apple", "Banana", "Apple"] |
RemoveAll(Predicate<T> match) |
移除所有满足指定条件的元素。返回被移除的元素数量。 | int removedCount = fruits.RemoveAll(f => f == "Apple"); // ["Orange", "Banana"] |
RemoveRange(int index, int count) |
从指定索引开始,移除指定数量的元素。 | fruits.RemoveRange(1, 2); // ["Apple", "Apple"] |
Clear() |
从列表中移除所有元素。Count 变为 0。 |
fruits.Clear(); // [] |
Sort() |
使用默认比较器对列表中的元素进行就地排序(In-place sort)。 | fruits.Sort(); // ["Apple", "Banana", "Cherry"] |
Sort(Comparison<T> comparison) |
使用指定的委托(通常是Lambda)对元素进行就地排序。 | fruits.Sort((a, b) => a.Length.CompareTo(b.Length)); // Sort by length |
Reverse() |
将列表中的元素顺序进行就地反转。 | fruits.Reverse(); // ["Banana", "Apple", "Cherry"] |
ForEach(Action<T> action) |
对列表中的每个元素执行指定的操作。 | fruits.ForEach(f => Console.WriteLine(f.ToUpper())); |
ToArray() |
将列表中的元素复制到一个新的数组中。 | string[] fruitArray = fruits.ToArray(); |
GetRange(int index, int count) |
创建一个新列表,其中包含源列表中从指定索引开始的指定数量的元素。 | var subList = fruits.GetRange(0, 1); // New List containing ["Apple"] |
三、Dictionary等特殊集合
1. Dictionary<TKey, TValue> -- 字典
当你需要通过一个唯一的"键"(Key)来快速查找一个"值"(Value)时,Dictionary是你的首选。
- 本质:基于哈希表(Hash Table)实现。它通过一个哈希函数将Key转换成一个索引,从而实现近乎O(1)的查找、插入和删除性能。
- 核心特性 :Key必须是唯一的,且不可为
null。
csharp
Dictionary<string, int> studentAges = new Dictionary<string, int>();
// 添加键值对
studentAges.Add("Alice", 20);
studentAges["Bob"] = 22; // 更方便的索引器语法
// 查找
if (studentAges.TryGetValue("Alice", out int age))
{
Console.WriteLine($"Alice's age is {age}"); // 推荐用法,避免异常
}
// 遍历
foreach (var pair in studentAges)
{
Console.WriteLine($"Key: {pair.Key}, Value: {pair.Value}");
}
2. HashSet<T> - 唯一数组
当你只关心一个元素是否存在 于集合中,并且需要保证集合中没有重复元素 时,HashSet<T>是完美的选择。
- 本质:同样基于哈希表,但只存储Key(元素本身),没有Value。
- 核心特性:元素唯一,查找效率极高(O(1))。支持高效的集合运算(交集、并集、差集)。
csharp
HashSet<string> uniqueNames = new HashSet<string>();
uniqueNames.Add("Alice");
uniqueNames.Add("Bob");
uniqueNames.Add("Alice"); // 添加失败,因为 "Alice" 已存在
Console.WriteLine(uniqueNames.Count); // 输出: 2
bool hasBob = uniqueNames.Contains("Bob"); // 极快
3. Queue<T> 和 Stack<T> - 队列和堆栈
-
Queue<T>(队列) :先进先出 (FIFO - First-In, First-Out)Enqueue(): 入队(添加到队尾)。Dequeue(): 出队(从队首移除并返回)。
-
Stack<T>(栈) :后进先出 (LIFO - Last-In, First-Out)Push(): 入栈(添加到栈顶)。Pop(): 出栈(从栈顶移除并返回)。
结语
点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文