引言:.NET 集合类型的演进之路

在 .NET 开发的早期阶段,集合类型的选择相对有限,主要依赖于 ArrayList 这样的非泛型集合。然而,随着 .NET Framework 2.0 引入泛型,List<T> 成为了开发者的首选。这种演进不仅仅是语法上的变化,更是性能、类型安全和代码可维护性的重大飞跃。

本文将深入探讨从 ArrayListList<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 的性能问题

  1. 装箱与拆箱(Boxing/Unboxing):存储值类型(如 intdouble)时,会发生装箱(将值类型转换为 object),读取时需要拆箱。这会导致额外的 CPU 开销和内存分配。
// 装箱示例
ArrayList list = new ArrayList();
list.Add(42);  // 装箱:int -> object

// 拆箱示例
int value = (int)list[0];  // 拆箱:object -> int
  1. 类型不安全:由于存储的是 object,编译器无法检查类型,容易在运行时抛出 InvalidCastException
ArrayList list = new ArrayList();
list.Add("Hello");
int value = (int)list[0];  // 运行时错误:无法将字符串转换为整数
  1. 性能开销:频繁的装箱/拆箱和类型转换会显著影响性能,尤其是在处理大量数据时。

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 的优势

  1. 类型安全:编译器会检查类型,避免运行时错误。
List<int> list = new List<int>();
list.Add(42);
// list.Add("Hello");  // 编译错误:无法将字符串转换为 int
  1. 无装箱/拆箱:值类型直接存储,无需转换。
List<int> list = new List<int>();
list.Add(42);  // 无装箱
int value = list[0];  // 无拆箱
  1. 性能提升:在频繁操作和大量数据时,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. 总结

ArrayListList<T> 的演进是 .NET 集合类型的一次重大升级。List<T> 通过泛型解决了类型安全和性能问题,成为现代 .NET 开发的首选。在实际项目中,我们应:

  1. 优先使用 List<T>,避免 ArrayList
  2. 预先设置容量,减少扩容开销。
  3. 根据场景选择集合类型,如 LinkedList<T>Dictionary<TKey, TValue>
  4. 优化查询和遍历,避免不必要的内存分配。
  5. 探索高性能 API,如 Span<T>,进一步提升性能。

通过这些优化技巧,你可以在 .NET 项目中写出更高效、更可靠的代码。希望本文能帮助你深入理解 .NET 集合类型,并在实际开发中应用这些知识!