选择原则:数量固定且需要最小开销用数组;主线程动态集合优先
List<T>;进入 Jobs/Burst 或需要非托管连续内存时使用 NativeArray<T>。为什么连续存储重要
数组、
List<T> 和 NativeArray<T> 都把元素放在连续内存中。顺序遍历时缓存命中率高,按索引访问是 O(1),因此它们通常是 Unity 热路径的首选。区别主要在于:长度是否可变、内存由谁管理、能否进入 Job,以及是否允许托管引用。
Array:固定长度
数组适合出生点、固定技能槽、预计算曲线采样等规模稳定的数据。它的长度创建后不变,但数组中的元素仍可修改。
List:动态长度
List<T> 内部仍然是数组。容量不足时会申请更大的数组并复制元素,所以提前估计容量可以减少扩容与 GC:对顺序无要求时,删除中间元素可使用“末尾覆盖”避免搬移:
这种删除是 O(1),但会改变顺序。
NativeArray:Jobs/Burst 的连续容器
NativeArray<T> 存储非托管数据,可以被 Job 安全访问,并能被 Burst 优化:调度与释放:
实际项目应尽量复用 Persistent 容器,而不是每帧申请。任何读取或释放都必须遵守 JobHandle 依赖。
复杂度
操作 | Array | List | NativeArray |
按索引访问 | O(1) | O(1) | O(1) |
尾部追加 | 不可变长 | 均摊 O(1) | 不可变长 |
中间插入/删除 | 需手动复制 | O(n) | 需手动处理 |
Jobs/Burst | 不可直接并行写 | 不支持 | 支持 |
Unity 常见坑
- 不要在
foreach遍历List<T>时增删它;使用倒序索引或延迟操作队列。
List.Contains是 O(n)。循环中反复查找唯一元素,应考虑HashSet<T>。
- Unity 默认不能直接序列化
IReadOnlyList<T>接口字段;序列化私有数组或 List,再暴露只读视图。
NativeArray不会由 GC 自动回收;必须Dispose。
Allocator.Temp只适合非常短的生命周期;跨帧 Job 使用TempJob,长期数据使用Persistent。
- 不要只凭理论替换容器;用 Profiler 和 Memory Profiler 验证热点。
什么时候选谁
- 数组:Inspector 固定配置、固定槽位、查表数据。
- List:敌人注册表、任务列表、动态 UI 数据。
- NativeArray:大批量数值计算、Job 输入输出、Burst 热循环。