在 C# 中,泛型集合(Generic Collections)是基于泛型(Generic)机制设计的一系列集合类,位于System.Collections.Generic
命名空间下。它们解决了非泛型集合(如ArrayList
、Hashtable
)的类型不安全和装箱 / 拆箱性能损耗问题,能够存储指定类型的元素,提供了更高效、类型安全的数据存储和操作方式。
泛型集合的优势
-
类型安全:编译时检查元素类型,避免将错误类型的元素添加到集合中(非泛型集合默认存储
object
类型,可能导致运行时错误)。 -
性能优化:无需装箱(值类型转
object
)和拆箱(object
转值类型)操作,减少性能损耗。 -
代码复用:通过泛型参数
T
,一套集合逻辑可适用于多种数据类型。
常用泛型集合类型
1. List<T>
:动态数组
-
功能:可动态调整大小的有序集合,类似数组但支持动态扩容,提供丰富的增删改查方法。
-
常用操作:
using System.Collections.Generic; // 初始化(存储int类型) List<int> numbers = new List<int>(); // 添加元素 numbers.Add(10); numbers.AddRange(new int[] { 20, 30 }); // 批量添加 // 访问元素(索引从0开始) int first = numbers[0]; // 插入/删除 numbers.Insert(1, 15); // 在索引1处插入15 numbers.Remove(20); // 删除值为20的元素 numbers.RemoveAt(0); // 删除索引0处的元素 // 查找 bool has30 = numbers.Contains(30); // 是否包含30 int index = numbers.IndexOf(30); // 查找30的索引 // 排序与反转 numbers.Sort(); // 升序排序 numbers.Reverse(); // 反转元素顺序 // 转换为数组 int[] arr = numbers.ToArray();
2. Dictionary<TKey, TValue>
:键值对集合
-
功能:存储键值对(Key-Value)映射,通过键快速查找值,键唯一且不可重复。
-
常用操作:
// 初始化(键为string,值为int) Dictionary<string, int> scores = new Dictionary<string, int>(); // 添加键值对 scores.Add("Alice", 90); scores["Bob"] = 85; // 另一种添加方式(若键已存在则覆盖) // 访问值 int aliceScore = scores["Alice"]; // 检查键是否存在 if (scores.ContainsKey("Bob")) { Console.WriteLine(scores["Bob"]); } // 获取所有键/值 foreach (string key in scores.Keys) { ... } foreach (int value in scores.Values) { ... } // 遍历键值对 foreach (var pair in scores) { Console.WriteLine($"{pair.Key}: {pair.Value}"); } // 删除 scores.Remove("Alice");
3. HashSet<T>
:无序唯一集合
-
功能:存储不重复的元素,无序且查找效率高(基于哈希表实现),适合去重场景。
-
常用操作:
// 初始化(存储string类型) HashSet<string> names = new HashSet<string>(); // 添加元素(重复元素会被忽略) names.Add("Alice"); names.Add("Bob"); names.Add("Alice"); // 无效,集合中仍只有一个"Alice" // 检查包含 bool hasBob = names.Contains("Bob"); // 集合运算(交集、并集等) HashSet<string> otherNames = new HashSet<string> { "Bob", "Charlie" }; names.IntersectWith(otherNames); // 交集:{"Bob"} names.UnionWith(otherNames); // 并集:{"Alice", "Bob", "Charlie"} // 删除 names.Remove("Alice");
4. Queue<T>
:队列(先进先出)
-
功能:遵循 FIFO(First-In-First-Out)原则,适合需要顺序处理的场景(如任务队列)。
-
常用操作:
Queue<string> queue = new Queue<string>(); // 入队(添加到末尾) queue.Enqueue("Task1"); queue.Enqueue("Task2"); // 出队(移除并返回头部元素) string firstTask = queue.Dequeue(); // 返回"Task1" // 查看头部元素(不删除) string nextTask = queue.Peek(); // 返回"Task2" // 检查是否为空 bool isEmpty = queue.Count == 0;
5. Stack<T>
:栈(后进先出)
-
功能:遵循 LIFO(Last-In-First-Out)原则,适合需要逆序处理的场景(如表达式计算、撤销操作)。
-
常用操作:
Stack<int> stack = new Stack<int>(); // 入栈(添加到顶部) stack.Push(1); stack.Push(2); // 出栈(移除并返回顶部元素) int top = stack.Pop(); // 返回2 // 查看顶部元素(不删除) int nextTop = stack.Peek(); // 返回1
6. LinkedList<T>
:双向链表
-
功能:元素通过节点(
LinkedListNode<T>
)连接,每个节点包含前一个和后一个节点的引用,适合频繁插入 / 删除中间元素的场景(比List<T>
高效)。 -
常用操作:
LinkedList<string> linkedList = new LinkedList<string>(); // 添加元素 linkedList.AddFirst("First"); // 添加到头部 linkedList.AddLast("Last"); // 添加到尾部 LinkedListNode<string> node = linkedList.AddAfter(linkedList.First, "Middle"); // 添加到指定节点后 // 访问节点 string first = linkedList.First.Value; string last = linkedList.Last.Value; // 删除节点 linkedList.Remove("Middle"); linkedList.RemoveFirst();
7. SortedList<TKey, TValue>
:排序键值对
-
功能:类似
Dictionary<TKey, TValue>
,但键会自动按升序排序,兼具数组(按索引访问)和字典(按键访问)的特性。 -
特点:查找效率低于
Dictionary
,但适合需要有序键的场景。
8. ICollection<T>
与 IEnumerable<T>
:接口
-
IEnumerable<T>
:所有泛型集合的基础接口,定义了GetEnumerator()
方法,支持foreach
遍历。 -
ICollection<T>
:继承IEnumerable<T>
,增加了Count
、Add
、Remove
等集合操作方法,List<T>
、Dictionary<TKey, TValue>
等均实现此接口。
泛型集合 vs 非泛型集合
特性 | 泛型集合(如List<T> ) | 非泛型集合(如ArrayList ) |
---|---|---|
类型安全 | 编译时检查,仅允许T 类型元素 | 存储object ,允许任意类型(不安全) |
性能 | 无装箱 / 拆箱,效率高 | 频繁装箱 / 拆箱,性能损耗大 |
代码可读性 | 明确指定类型,可读性强 | 需强制类型转换,易出错 |
适用场景 | 几乎所有场景,推荐优先使用 | 兼容旧代码,不推荐新开发使用 |
总结
泛型集合是 C# 中处理数据的核心工具,根据不同的业务场景选择合适的类型:
-
动态数组、随机访问 →
List<T>
-
键值映射 →
Dictionary<TKey, TValue>
-
去重、集合运算 →
HashSet<T>
-
先进先出 →
Queue<T>
-
后进先出 →
Stack<T>
-
频繁插入删除中间元素 →
LinkedList<T>
使用时需引用命名空间 using System.Collections.Generic;
,并根据元素类型指定泛型参数(如List<string>
、Dictionary<int, string>
)。
动态数组
在 C# 中,动态数组(Dynamic Array) 是一种可以在运行时动态调整大小的数组结构。与普通数组(固定长度)不同,动态数组的容量会随着元素的添加或删除自动扩展或收缩。C# 提供了多种实现动态数组的方式,其中最常用的是泛型集合类 List<T>
。
1. List<T>
:动态数组的首选实现
List<T>
是 C# 中最常用的动态数组实现,位于 System.Collections.Generic
命名空间下。它封装了动态扩容的逻辑,提供了丰富的操作方法。
基本用法
using System.Collections.Generic;
// 创建一个存储整数的动态数组
List<int> numbers = new List<int>();
// 添加元素(自动扩容)
numbers.Add(10);
numbers.Add(20);
numbers.AddRange(new int[] { 30, 40 }); // 批量添加
// 访问元素(索引从0开始)
int first = numbers[0]; // 10
// 插入元素
numbers.Insert(1, 15); // 在索引1处插入15,结果:[10, 15, 20, 30, 40]
// 删除元素
numbers.Remove(20); // 删除值为20的元素
numbers.RemoveAt(0); // 删除索引0处的元素
// 查找元素
bool contains30 = numbers.Contains(30); // true
int index = numbers.IndexOf(30); // 1
// 获取元素数量和容量
int count = numbers.Count; // 元素数量:3
int capacity = numbers.Capacity; // 内部数组容量(自动扩容,通常≥Count)
动态扩容机制
-
当添加元素导致
Count > Capacity
时,List<T>
会自动创建一个更大的新数组(通常是原容量的 2 倍),并将原数组元素复制到新数组。 -
示例:初始容量为 4,添加第 5 个元素时,容量自动扩容为 8。
2. 动态数组 vs 普通数组
特性 | 普通数组(T[] ) | 动态数组(List<T> ) |
---|---|---|
长度固定性 | 长度必须在创建时指定,不可变 | 长度随元素增减自动调整 |
初始化方式 | int[] arr = new int[5]; | List<int> list = new List<int>(); |
元素访问 | 通过索引直接访问(如 arr[0] ) | 同普通数组(如 list[0] ) |
动态扩容 | 需手动创建新数组并复制元素 | 自动扩容,无需手动处理 |
常用方法 | 仅支持 Length 、Array.Copy 等静态方法 | 支持 Add 、Remove 、Sort 等丰富方法 |
适用场景 | 长度已知且固定的场景 | 长度不确定或需频繁增删元素的场景 |
3. 手动实现简单动态数组
通过封装普通数组,可手动实现一个简化版的动态数组,帮助理解其工作原理:
public class SimpleDynamicArray<T>
{
private T[] _array; // 内部数组
private int _count; // 当前元素数量
public SimpleDynamicArray()
{
_array = new T[4]; // 初始容量
_count = 0;
}
public int Count => _count;
// 添加元素
public void Add(T item)
{
if (_count == _array.Length)
{
// 容量不足时扩容为2倍
T[] newArray = new T[_array.Length * 2];
Array.Copy(_array, newArray, _count);
_array = newArray;
}
_array[_count++] = item;
}
// 通过索引访问元素
public T this[int index]
{
get
{
if (index < 0 || index >= _count)
throw new IndexOutOfRangeException();
return _array[index];
}
set
{
if (index < 0 || index >= _count)
throw new IndexOutOfRangeException();
_array[index] = value;
}
}
// 其他方法(如Remove、Insert等)可类似实现
}
4. 动态数组的性能考虑
-
优势:动态扩容避免了手动管理数组大小的麻烦,适合元素数量不确定的场景。
-
潜在开销:
-
扩容成本:当容量不足时,扩容和元素复制操作的时间复杂度为 O(n)。
-
内存占用:内部数组容量通常大于实际元素数量,可能浪费内存。
-
优化建议:
-
若已知大致元素数量,可通过构造函数指定初始容量:
List<int> list = new List<int>(100); // 初始容量100,减少扩容次数
-
使用
TrimExcess() 方法释放多余内存: list.TrimExcess(); // 将容量调整为实际元素数量
5. 其他动态数组相关类
Array.Resize 方法
普通数组可通过 Array.Resize 方法调整大小,但需注意这会创建新数组并复制元素:
int[] arr = { 1, 2, 3 };
Array.Resize(ref arr, 5); // 数组变为 {1, 2, 3, 0, 0}
ArrayList(非泛型)
System.Collections.ArrayList 是早期的动态数组实现,不支持泛型,存储 object 类型,存在装箱 / 拆箱开销,不推荐使用:
ArrayList list = new ArrayList();
list.Add(10); // 装箱:int → object
int value = (int)list[0]; // 拆箱:object → int
总结
-
推荐使用
List<T>
作为动态数组的首选,它提供了类型安全、高效的动态扩容机制。 -
理解动态数组的扩容原理,合理设置初始容量以减少性能开销。
-
避免使用非泛型的
ArrayList
,防止装箱 / 拆箱带来的性能问题。
如何将不同类型的数据存储在一起
在 C# 中,存储不同类型的数据通常有以下几种方式,各有优缺点和适用场景。以下是最常见的解决方案:
1. 使用 object
类型(装箱 / 拆箱)
将所有数据转换为基类 object
,利用 C# 的多态性存储任意类型。
List<object> mixedList = new List<object>();
mixedList.Add(123); // int → 装箱为object
mixedList.Add("Hello"); // string(引用类型无需装箱)
mixedList.Add(new DateTime()); // DateTime → 装箱为object
// 访问时需手动拆箱(可能引发InvalidCastException)
int num = (int)mixedList[0]; // 拆箱
string str = (string)mixedList[1];
-
优点:简单直接,无需额外类型定义。
-
缺点
:
-
装箱 / 拆箱开销:值类型(如
int
,DateTime
)频繁转换会降低性能。 -
类型不安全:运行时可能抛出
InvalidCastException
。
-
2. 使用泛型集合 List<dynamic>
dynamic 关键字在运行时动态解析类型,避免编译时类型检查。
List<dynamic> dynamicList = new List<dynamic>();
dynamicList.Add(123); // 无需装箱
dynamicList.Add("Hello");
dynamicList.Add(new { Name = "Alice" }); // 匿名类型也可存储
// 访问时无需显式转换
int num = dynamicList[0]; // 动态类型解析
string str = dynamicList[1];
-
优点:
-
无需装箱拆箱(值类型以原始类型存储)。
-
支持匿名类型等复杂结构。
-
-
缺点:
-
运行时类型风险:若类型不匹配会在运行时抛出异常。
-
性能开销:动态解析比静态类型稍慢。
-
3. 使用自定义类 / 结构体(推荐方案)
定义一个包含所需字段的类,每个字段使用明确的类型。
public class MixedData
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime BirthDate { get; set; }
}
// 使用方式
List<MixedData> dataList = new List<MixedData>();
dataList.Add(new MixedData {
Id = 1,
Name = "Alice",
BirthDate = DateTime.Now
});
-
优点:
-
类型安全:编译时检查类型。
-
可读性高:字段含义明确。
-
性能最优:无需装箱拆箱。
-
-
缺点:需提前定义类型,不够灵活。
4. 使用 Tuple
或 ValueTuple
适合临时存储少量不同类型的数据。
// Tuple(旧版,需通过Item1、Item2访问)
var tupleList = new List<Tuple<int, string>>();
tupleList.Add(Tuple.Create(1, "Alice"));
// ValueTuple(C# 7.0+,推荐,支持命名元素)
var valueTupleList = new List<(int Id, string Name)>();
valueTupleList.Add((1, "Alice"));
// 访问方式
int id = valueTupleList[0].Id;
string name = valueTupleList[0].Name;
-
优点:简洁,无需额外类定义。
-
缺点:
-
元素数量有限(最多 8 个)。
-
不适合复杂或大量数据。
-
5. 使用 Dictionary<string, object>
以键值对形式存储不同类型的值,类似动态对象。
var dict = new Dictionary<string, object>();
dict["Id"] = 123;
dict["Name"] = "Alice";
dict["IsActive"] = true;
// 访问时需转换类型
int id = (int)dict["Id"];
bool isActive = (bool)dict["IsActive"];
-
优点:灵活,可动态添加 / 删除键值对。
-
缺点:
-
类型不安全,需手动转换。
-
键名硬编码,易出错。
-
6. 使用接口或抽象类(面向对象设计)
定义公共接口,让不同类型实现该接口。
public interface IDataItem { }
public class Person : IDataItem
{
public string Name { get; set; }
}
public class Product : IDataItem
{
public decimal Price { get; set; }
}
// 使用方式
List<IDataItem> items = new List<IDataItem>();
items.Add(new Person { Name = "Alice" });
items.Add(new Product { Price = 9.99m });
// 通过模式匹配访问
foreach (var item in items)
{
if (item is Person person)
Console.WriteLine(person.Name);
else if (item is Product product)
Console.WriteLine(product.Price);
}
-
优点:
-
类型安全,符合面向对象原则。
-
可扩展,易于添加新类型。
-
-
缺点:需设计接口和继承体系,复杂度较高。
7. 使用 ExpandoObject
(动态对象)
System.Dynamic.ExpandoObject 是动态类型的字典,支持运行时添加属性。
dynamic person = new ExpandoObject();
person.Name = "Alice";
person.Age = 30;
// 直接访问属性(无需转换)
Console.WriteLine(person.Name); // "Alice"
-
优点:
-
语法简洁,像普通对象一样访问属性。
-
无需提前定义类型。
-
-
缺点:
-
编译时无类型检查,易出错。
-
性能略低于静态类型。
-
性能对比
方案 | 装箱开销 | 类型安全 | 性能(相对) |
---|---|---|---|
List<object> | 有 | 否 | 低 |
List<dynamic> | 无 | 否 | 中 |
自定义类 / 结构体 | 无 | 是 | 高 |
Dictionary<string, object> | 有 | 否 | 中 |
ExpandoObject | 无 | 否 | 中低 |
选择建议
-
优先使用自定义类 / 结构体:若数据结构明确,这是最安全、高效的方案。
-
临时数据用
ValueTuple
:适合简单场景,无需额外类型定义。 -
灵活场景用
dynamic
或ExpandoObject
:如解析 JSON、动态配置等。 -
避免
object
和ArrayList
:除非兼容性要求,否则装箱开销过大。