引言:.NET 集合类型的演进之路
在 .NET 开发的早期阶段,集合类型的选择相对有限,主要依赖于 ArrayList 这样的非泛型集合。然而,随着 .NET Framework 2.0 引入泛型,List<T> 成为了开发者的首选。这种演进不仅仅是语法上的变化,更是性能、类型安全和代码可维护性的重大飞跃。
本文将深入探讨从 ArrayList 到 List<T> 的演进历程,分析它们的内部实现差异,并通过详细的代码示例展示如何在实际项目中优化集合的使用。无论你是 .NET 新手还是经验丰富的开发者,这篇文章都将帮助你更好地理解集合类型的工作原理,并在性能关键场景中做出明智的选择。
1. ArrayList:非泛型集合的先驱
1.1 ArrayList 的基本概念
ArrayList 是 .NET Framework 1.0 引入的非泛型集合类,位于 System.Collections 命名空间下。它是一个动态数组,可以存储任何类型的对象,因为所有类型都继承自 System.Object。
using System;
using System.Collections;
class Program
{
static void Main()
{
// 创建 ArrayList
ArrayList list = new ArrayList();
// 添加不同类型的数据
list.Add(1); // 整数
list.Add("Hello"); // 字符串
list.Add(3.14); // 浮点数
// 遍历并输出
foreach (var item in list)
{
Console.WriteLine($"值: {item}, 类型: {item.GetType()}");
}
}
}
输出:
值: 1, 类型: System.Int32
值: Hello, 类型: System.String
值: 3.14, 类型: System.Double
1.2 ArrayList 的内部实现
ArrayList 内部使用一个 object[] 数组来存储元素。当数组容量不足时,它会自动扩容,通常是当前容量的 2 倍。
// 简化的 ArrayList 扩容逻辑
public class ArrayList
{
private object[] _items;
private int _size;
public ArrayList()
{
_items = new object[0];
}
public void Add(object item)
{
if (_size == _items.Length)
{
// 扩容:新容量为当前容量的 2 倍,如果是 0 则初始化为 4
int newCapacity = _items.Length == 0 ? 4 : _items.Length * 2;
Array.Resize(ref _items, newCapacity);
}
_items[_size++] = item;
}
}
1.3 ArrayList 的性能问题
- 装箱与拆箱(Boxing/Unboxing):存储值类型(如
int、double)时,会发生装箱(将值类型转换为object),读取时需要拆箱。这会导致额外的 CPU 开销和内存分配。
// 装箱示例
ArrayList list = new ArrayList();
list.Add(42); // 装箱:int -> object
// 拆箱示例
int value = (int)list[0]; // 拆箱:object -> int
- 类型不安全:由于存储的是
object,编译器无法检查类型,容易在运行时抛出InvalidCastException。
ArrayList list = new ArrayList();
list.Add("Hello");
int value = (int)list[0]; // 运行时错误:无法将字符串转换为整数
- 性能开销:频繁的装箱/拆箱和类型转换会显著影响性能,尤其是在处理大量数据时。
2. List:泛型集合的崛起
2.1 List 的基本概念
List<T> 是 .NET Framework 2.0 引入的泛型集合类,位于 System.Collections.Generic 命名空间下。它提供了类型安全、高性能的动态数组实现。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 创建 List<int>
List<int> list = new List<int>();
// 添加数据
list.Add(1);
list.Add(2);
list.Add(3);
// 遍历并输出
foreach (var item in list)
{
Console.WriteLine($"值: {item}, 类型: {item.GetType()}");
}
}
}
输出:
值: 1, 类型: System.Int32
值: 2, 类型: System.Int32
值: 3, 类型: System.Int32
2.2 List 的内部实现
List<T> 内部使用 T[] 数组存储元素,避免了装箱和拆箱。扩容逻辑与 ArrayList 类似,但更高效。
// 简化的 List<T> 实现
public class List<T>
{
private T[] _items;
private int _size;
public List()
{
_items = new T[0];
}
public void Add(T item)
{
if (_size == _items.Length)
{
int newCapacity = _items.Length == 0 ? 4 : _items.Length * 2;
Array.Resize(ref _items, newCapacity);
}
_items[_size++] = item;
}
}
2.3 List 的优势
- 类型安全:编译器会检查类型,避免运行时错误。
List<int> list = new List<int>();
list.Add(42);
// list.Add("Hello"); // 编译错误:无法将字符串转换为 int
- 无装箱/拆箱:值类型直接存储,无需转换。
List<int> list = new List<int>();
list.Add(42); // 无装箱
int value = list[0]; // 无拆箱
- 性能提升:在频繁操作和大量数据时,
List<T>显著优于ArrayList。
3. 性能对比:ArrayList vs List
3.1 测试代码
我们通过一个简单的性能测试来比较两者的差异。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
class Program
{
static void Main()
{
const int count = 1_000_000;
// 测试 ArrayList
Stopwatch sw = Stopwatch.StartNew();
ArrayList arrayList = new ArrayList();
for (int i = 0; i < count; i++)
{
arrayList.Add(i); // 装箱
}
for (int i = 0; i < count; i++)
{
int value = (int)arrayList[i]; // 拆箱
}
sw.Stop();
Console.WriteLine($"ArrayList: {sw.ElapsedMilliseconds} ms");
// 测试 List<int>
sw.Restart();
List<int> list = new List<int>();
for (int i = 0; i < count; i++)
{
list.Add(i); // 无装箱
}
for (int i = 0; i < count; i++)
{
int value = list[i]; // 无拆箱
}
sw.Stop();
Console.WriteLine($"List<int>: {sw.ElapsedMilliseconds} ms");
}
}
典型输出:
ArrayList: 120 ms
List<int>: 30 ms
3.2 结果分析
- ArrayList:由于装箱和拆箱,性能较差,内存分配更多。
- List
:无额外开销,性能显著提升。
4. 实战优化指南
4.1 预先设置容量
如果已知元素数量,可以在初始化时设置容量,避免多次扩容。
// 低效:多次扩容
List<int> list1 = new List<int>();
for (int i = 0; i < 100_000; i++)
{
list1.Add(i);
}
// 高效:一次性分配
List<int> list2 = new List<int>(100_000);
for (int i = 0; i < 100_000; i++)
{
list2.Add(i);
}
4.2 选择合适的集合类型
- List
:通用动态数组,适合大多数场景。 - LinkedList
:频繁插入/删除,适合队列或链表。 - Dictionary
:快速查找,适合键值对。 - HashSet
:去重和快速查找,适合集合运算。
4.3 使用 LINQ 优化查询
LINQ 提供了简洁的查询语法,但要注意性能。
List<int> list = new List<int> { 1, 2, 3, 4, 5 };
// 高效:直接遍历
foreach (var item in list.Where(x => x > 2))
{
Console.WriteLine(item);
}
// 低效:多次枚举
var filtered = list.Where(x => x > 2).ToList(); // 额外分配
foreach (var item in filtered)
{
Console.WriteLine(item);
}
4.4 避免频繁的插入/删除
List<T> 在中间插入或删除元素时,需要移动后续元素,性能较差。
List<int> list = new List<int> { 1, 2, 3, 4, 5 };
// 低效:在中间插入
list.Insert(2, 99); // 需要移动 3,4,5
// 高效:使用 LinkedList<T>
LinkedList<int> linkedList = new LinkedList<int>();
linkedList.AddLast(1);
linkedList.AddLast(2);
var node = linkedList.Find(2);
linkedList.AddAfter(node, 99); // 无需移动
4.5 使用 Span<T> 和 Memory<T> 进行高性能操作
对于高性能场景,可以使用 Span<T> 避免内存分配。
using System;
class Program
{
static void Main()
{
int[] array = { 1, 2, 3, 4, 5 };
Span<int> span = array.AsSpan();
// 零拷贝切片
Span<int> slice = span.Slice(1, 3);
foreach (var item in slice)
{
Console.WriteLine(item); // 输出 2,3,4
}
}
}
5. 总结
从 ArrayList 到 List<T> 的演进是 .NET 集合类型的一次重大升级。List<T> 通过泛型解决了类型安全和性能问题,成为现代 .NET 开发的首选。在实际项目中,我们应:
- 优先使用
List<T>,避免ArrayList。 - 预先设置容量,减少扩容开销。
- 根据场景选择集合类型,如
LinkedList<T>、Dictionary<TKey, TValue>。 - 优化查询和遍历,避免不必要的内存分配。
- 探索高性能 API,如
Span<T>,进一步提升性能。
通过这些优化技巧,你可以在 .NET 项目中写出更高效、更可靠的代码。希望本文能帮助你深入理解 .NET 集合类型,并在实际开发中应用这些知识!
