【Unity3D补充知识点】常用数据结构分析-数组(Array)

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 的复用。

相关推荐
格林威2 小时前
Baumer相机铝箔表面针孔检测:提升包装阻隔性的 7 个核心策略,附 OpenCV+Halcon 实战代码!
开发语言·人工智能·数码相机·opencv·计算机视觉·c#·工业相机
w-白兰地2 小时前
配置Unity中的ADB环境变量
unity·adb·游戏引擎
程序员zgh2 小时前
C++ 环形队列 从原理到实例演示
c语言·开发语言·数据结构·c++·学习
Trouvaille ~2 小时前
【优选算法篇】拓扑排序——逻辑先后与任务依赖的终极拆解
数据结构·c++·算法·leetcode·青少年编程·蓝桥杯·拓扑学
RDCJM3 小时前
C#数据库操作系列---SqlSugar完结篇
网络·数据库·c#
CylMK3 小时前
题解:UVA1218 完美的服务 Perfect Service
数据结构·c++·算法·深度优先·图论
丶小鱼丶3 小时前
数据结构和算法之【阻塞队列】上篇
java·数据结构
mxwin3 小时前
Unity Shader 几何着色器:动态生成图元与顶点拓扑修改
unity·游戏引擎·着色器
zs宝来了3 小时前
Redis 数据结构底层实现:intset、ziplist、skiplist 深度剖析
数据结构·redis·源码解析·skiplist·ziplist·intset