c#泛型集合

在 C# 中,泛型集合(Generic Collections)是基于泛型(Generic)机制设计的一系列集合类,位于System.Collections.Generic命名空间下。它们解决了非泛型集合(如ArrayListHashtable)的类型不安全和装箱 / 拆箱性能损耗问题,能够存储指定类型的元素,提供了更高效、类型安全的数据存储和操作方式。

泛型集合的优势

  1. 类型安全:编译时检查元素类型,避免将错误类型的元素添加到集合中(非泛型集合默认存储object类型,可能导致运行时错误)。

  2. 性能优化:无需装箱(值类型转object)和拆箱(object转值类型)操作,减少性能损耗。

  3. 代码复用:通过泛型参数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>,增加了CountAddRemove等集合操作方法,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]
动态扩容需手动创建新数组并复制元素自动扩容,无需手动处理
常用方法仅支持 LengthArray.Copy 等静态方法支持 AddRemoveSort 等丰富方法
适用场景长度已知且固定的场景长度不确定或需频繁增删元素的场景

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. 使用 TupleValueTuple

适合临时存储少量不同类型的数据。

// 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中低

选择建议

  1. 优先使用自定义类 / 结构体:若数据结构明确,这是最安全、高效的方案。

  2. 临时数据用 ValueTuple:适合简单场景,无需额外类型定义。

  3. 灵活场景用 dynamicExpandoObject:如解析 JSON、动态配置等。

  4. 避免 objectArrayList:除非兼容性要求,否则装箱开销过大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张謹礧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值