Unity开发中最常用的数据结构
- 数组:Array
- 集合(动态数组):List
- 字典:Dictionary<TKey,TValue>
- 哈希集:HashSet
- 链表:LinkedList
- 栈:Stack
- 队列:Queue
数组(Array)
在C#中,数组是一种用于存储固定大小的相同类型元素的数据结构。数组的大小在创建时确定,并且在创建后不能再改变。数组在C#中非常有用,特别是在需要存储一系列相关元素时。
内存模型
连续内存块
数组在托管堆上分配一段连续的内存空间,其大小 = 元素类型大小 × 长度 + 少量对象头(类型对象指针、同步块索引等)。
-
值类型数组(如 int[ ]、Vector3[ ]):元素直接存储在连续内存中,没有额外的间接层。
-
引用类型数组(如 GameObject[ ]):数组本身存储的是引用(指针),每个引用指向堆上的实际对象实例。这些对象实例在堆上可能分散,但引用本身是连续存放的。
这种连续性带来了极高的空间局部性,CPU 缓存能高效预取后续元素,使得顺序遍历性能极佳。
多维数组 vs 锯齿数组
-
多维数组(int[ , ]):同样是连续内存,按行优先顺序排列。访问时通过公式 index = row * cols + col 计算偏移。
-
锯齿数组(int[ ][ ]):本质是"数组的数组",外层数组存储指向内层数组的引用,内层数组各自独立分配。内存连续性差,但可以支持不等长的子数组。
实现原理
类型体系
C# 中所有数组都隐式派生自 System.Array,该类提供了 Length、Rank(维度)、GetValue/SetValue 等方法。数组本身是引用类型,即便元素是值类型。
创建与固定性
使用 new int[10] 创建时,CLR 在堆上分配指定大小的空间,并清零所有元素(保证安全)。数组长度一旦确定就不可变,无法动态增加或删除元素------若要"扩容",只能新建更大数组并拷贝原数据。
索引访问
CLR 对数组索引有边界检查(可通过 unsafe 代码规避),超出范围会抛出 IndexOutOfRangeException。这是安全性的代价,但在 JIT 优化下,常见循环模式可能会被消除冗余检查。
多维数组的特殊性
多维数组在 CLR 中是独立的类型,其访问比锯齿数组略慢(需多次计算偏移)。锯齿数组则等同于多次普通数组访问,更灵活且性能相近。
复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 按索引访问/赋值 | O(1) | 直接计算地址,与数组大小无关 |
| 查找值(无序) | O(n) | 线性搜索 |
| 查找值(有序) | O(log n) | 使用二分查找(Array.BinarySearch) |
| 插入/删除 | O(n) | 因长度固定,通常通过新建数组+拷贝实现;若在中间插入,需移动元素 |
| 遍历 | O(n) | 顺序遍历效率极高,得益于缓存友好 |
| 排序 | O(n log n) | 使用 Array.Sort(内部为快速排序/内省排序) |
Unity 实际应用场景
Unity 开发中,数组无处不在,理解其特性有助于写出高性能代码。
①组件获取
csharp
// GetComponents 返回数组,每次调用都会分配新数组
Rigidbody[] rbs = GetComponents<Rigidbody>();
// 推荐使用非分配版本(若可用)
int count = GetComponents(componentArray); // 将已有数组作为缓存
频繁调用 GetComponents 会产生 GC,应优先使用 TryGetComponent 或缓存数组。
② Mesh 数据
csharp
Mesh mesh = GetComponent<MeshFilter>().mesh;
Vector3[] vertices = mesh.vertices; // 返回副本,修改需重新赋值
int[] triangles = mesh.triangles; // 三角面索引数组
mesh.vertices 返回的是新数组(副本),对大网格应使用 mesh.SetVertices(List<Vector3>) 避免分配。
③ 对象池与固定大小集合
当最大数量已知且稳定时,数组比 List 更轻量:
csharp
Bullet[] bullets = new Bullet[100];
int activeCount = 0;
通过手动维护 activeCount 实现高效管理,避免动态扩容带来的 GC。
④ 粒子系统批量操作
csharp
ParticleSystem.Particle[] particles = new ParticleSystem.Particle[1000];
int count = particleSystem.GetParticles(particles);
// 修改粒子属性
particleSystem.SetParticles(particles, count);
此处数组作为缓冲区,重复使用可减少分配。
⑤ 协程与 yield return 的优化
csharp
// 每次迭代都分配新数组
yield return new WaitForSeconds(1f); // 没问题,但若在循环中频繁创建应缓存
// 缓存数组避免 GC
static readonly WaitForSeconds wait = new WaitForSeconds(1f);
yield return wait;
⑥ 高性能计算与 Job System
在 Unity DOTS 和 Burst 编译器中,原生容器(NativeArray)替代了托管数组,提供:
- 连续内存,可被 Burst 优化
- 不产生 GC
- 支持多线程安全访问(配合安全系统)
NativeArray 本质上是一个结构体,内部指针指向非托管内存,行为类似数组但更底层。
总结
-
内存:连续分配,值类型直接存值,引用类型存引用,缓存友好。
-
原理:派生自 System.Array,长度不可变,有边界检查。
-
复杂度:随机访问 O(1),修改(非长度)O(1),搜索 O(n),插入/删除 O(n)。
-
Unity 实践:适用于数量固定的场景、与底层引擎 API 交互、作为临时缓冲区;注意避免频繁分配导致的 GC;在性能敏感场景优先考虑 NativeArray 或 List 的复用。