容器体系¶
UE 提供了一整套自研容器(不是 STL):以 TArray(动态数组)、TMap(哈希映射)、TSet(哈希集合) 为三大主力,另有 TStaticArray、TDeque、TQueue、TArrayView 等针对特定场景的类型
一句话总结
- TArray → 顺序存储、随机访问、遍历为主(≈
std::vector) - TMap → 按 key 查找/更新(≈
std::unordered_map) - TSet → 去重与存在性判断(≈
std::unordered_set) - TArrayView / TConstArrayView → 零拷贝只读视图(≈
std::span)
flowchart TD
A[需要存放一批数据] --> B{按 key 查找?}
B -->|是| C{key 需要有序?}
C -->|是| D[TSortedMap]
C -->|否| E[TMap]
B -->|否| F{需要去重?}
F -->|是| G[TSet]
F -->|否| H{顺序遍历 / 随机访问?}
H -->|是| I[TArray]
H -->|否| J[按场景选 TDeque / TQueue / TList]
1 三大主力容器¶
| TArray | TMap | TSet | |
|---|---|---|---|
| 结构 | 连续内存数组 | 哈希表(key→value) | 哈希表(元素即 key) |
| 访问 | 下标 O(1) | key 查找 O(1) 均摊 | 存在性 O(1) 均摊 |
| 有序性 | 保持插入顺序 | 无序(哈希顺序) | 无序 |
| 允许重复 | ✅ | key 唯一 | 元素唯一 |
| 典型 | 列表、批量数据、遍历 | 字典、缓存、索引 | 去重、标记、集合运算 |
2 完整容器清单¶
| 容器 | 说明 | 适用 |
|---|---|---|
TArray<T> |
动态数组(引擎最常用) | 绝大多数"一批东西" |
TMap<K,V> |
哈希映射 | 按 key 查/改 |
TSet<T> |
哈希集合 | 去重、存在性 |
TSortedMap<K,V> |
有序映射(内部数组 + 二分) | 数据量小、需要 有序遍历 |
TMultiMap<K,V> |
一键多值 | 一键对应多个结果 |
TStaticArray<T,N> |
固定大小(可栈上) | 数量已知,免堆分配(≈ std::array) |
TArray<T, TInlineAllocator<N>> |
小数组 内联 优化 | 大多数情况元素很少,避免堆分配 |
TDeque<T> |
双端队列 | 两端频繁插入删除 |
TQueue<T> |
无锁 队列(单生产者单消费者) | 跨线程传递数据 |
TCircularBuffer / TCircularQueue |
环形缓冲 | 固定容量、覆盖最旧数据 |
TBitArray |
位数组(位压缩) | 布尔标志大批量、掩码 |
TSparseArray<T> |
稀疏数组(引擎内部大量使用) | 频繁增删且需稳定索引 |
TList<T> |
双向链表 | 极少用(缓存不友好) |
TArrayView<T> / TConstArrayView<T> |
非拥有视图 | 零拷贝传参 |
3 TArray:动态数组¶
3.1 常用 API¶
| 操作 | API | 备注 |
|---|---|---|
| 添加 | Add / Emplace |
Emplace 原地构造,省一次拷贝 |
| 插入 | Insert(Item, Index) |
O(n),会移动元素 |
| 批量追加 | Append / Append(TArray) |
— |
| 删除(保序) | RemoveAt(Index) |
O(n) |
| 删除(不保序) | RemoveAtSwap(Index) |
O(1),速度快但打乱顺序 |
| 删除匹配项 | Remove(Item) / RemoveSingle / RemoveAll(Pred) |
— |
| 删除末尾 | Pop(bAllowShrinking) |
O(1) |
| 查找 | Find / IndexOfByKey / FindByPredicate / Contains |
Find 返回下标,FindByPredicate 返回指针 |
| 排序 | Sort(不稳定)/ StableSort / HeapSort |
传谓词可自定义 |
| 容量 | Reserve / Empty / Reset / Shrink |
Reset 保容量,Empty 释放 |
| 状态 | Num / IsValidIndex / IsEmpty |
Num 是元素数(不是容量) |
TArray<FString> Names;
Names.Reserve(100); // 预分配,避免反复扩容
Names.Emplace(TEXT("Alice")); // 原地构造
Names.Add(TEXT("Bob"));
if (Names.IsValidIndex(0)) { /* ... */ }
Names.Sort([](const FString& A, const FString& B) { return A < B; });
// 高效移除:不关心顺序时用 RemoveAtSwap
Names.RemoveAtSwap(0);
3.2 迭代中删除的正确写法¶
// ✅ 方式一:倒序遍历
for (int32 i = Array.Num() - 1; i >= 0; --i)
{
if (ShouldRemove(Array[i])) { Array.RemoveAtSwap(i); }
}
// ✅ 方式二:RemoveAll(推荐,内部处理索引)
Array.RemoveAll([](const T& E) { return E.bShouldRemove; });
// ✅ 方式三:迭代器
for (auto It = Array.CreateIterator(); It; ++It)
{
if (ShouldRemove(*It)) { It.RemoveCurrent(); }
}
// ❌ 正序遍历 + RemoveAt:会漏掉元素
3.3 自定义分配器¶
// 小数组内联优化:元素少时不分配堆内存
TArray<int32, TInlineAllocator<8>> SmallList;
SmallList.Reserve(8); // 直接放在栈上的内联缓冲区里
// 固定容量(永不重新分配)
TArray<int32, TFixedAllocator<16>> FixedList;
对"通常只有几个元素"的容器,TInlineAllocator 能显著减少堆分配次数。
TArray 最大的坑:引用失效
只要可能发生增删,就不要长期持有元素指针/引用;需要稳定引用请改用 TArray<TObjectPtr<T>>(存指针)或 TSparseArray
4 TMap:哈希映射¶
TMap<FName, int32> ScoreMap;
ScoreMap.Reserve(64);
ScoreMap.Add(TEXT("Alice"), 100);
ScoreMap.Emplace(TEXT("Bob"), 80);
// 查找:返回指针,找不到返回 nullptr(最常用)
if (int32* Score = ScoreMap.Find(TEXT("Alice")))
{
*Score += 10;
}
// 不存在则插入并返回引用
int32& Ref = ScoreMap.FindOrAdd(TEXT("Carol")); // 默认构造 0
Ref = 50;
// 一定存在时才用(不存在会断言)
int32 Val = ScoreMap.FindChecked(TEXT("Bob"));
// 不存在时返回默认值副本(不是指针)
int32 Copy = ScoreMap.FindRef(TEXT("Nobody")); // 0
// 遍历:注意 TMap 是无序的
for (const TPair<FName, int32>& Pair : ScoreMap)
{
UE_LOG(LogTemp, Log, TEXT("%s: %d"), *Pair.Key.ToString(), Pair.Value);
}
| 关键点 | 说明 |
|---|---|
| 无序 | 迭代顺序由哈希决定,不同运行/不同平台可能不同 |
| 需要有序 | 用 TSortedMap,或取出 keys 自己排序 |
| Key 要求 | 必须支持 GetTypeHash(Key) 与 operator== |
| Key 类型选择 | 优先 FName/整数;避免用 FString 做高频 key |
| 性能 | 提前 Reserve 能减少 rehash |
TMap 迭代顺序影响确定性
依赖 TMap 迭代顺序的逻辑(尤其 帧同步/回放/随机)会导致不同步:需要确定性的场合请对 key 排序后再遍历,或改用有序容器
5 TSet / TSortedMap / TMultiMap¶
// TSet:去重 + 快速存在性判断
TSet<FName> Visited;
Visited.Add(TEXT("RoomA"));
if (Visited.Contains(TEXT("RoomA"))) { }
Visited.Remove(TEXT("RoomA"));
// TSortedMap:需要按 key 有序遍历的小数据量场景
TSortedMap<int32, FString> SortedByLevel;
SortedByLevel.Add(3, TEXT("High"));
SortedByLevel.Add(1, TEXT("Low"));
// 遍历时按 key 升序
// TMultiMap:一个 key 对应多个 value
TMultiMap<FName, AActor*> ActorsByTag;
ActorsByTag.Add(TEXT("Enemy"), EnemyA);
ActorsByTag.Add(TEXT("Enemy"), EnemyB);
| 容器 | 查找 | 插入 | 有序 | 适用 |
|---|---|---|---|---|
TMap |
O(1) | O(1) | ❌ | 通用字典 |
TSortedMap |
O(log n) | O(n) | ✅ | 数据量小 + 必须有序 |
TSet |
O(1) | O(1) | ❌ | 去重、标记 |
TMultiMap |
O(1) | O(1) | ❌ | 一键多值 |
6 视图:零拷贝传参¶
// 接受任意数组的只读视图,不产生拷贝
void ProcessItems(TConstArrayView<FItem> Items)
{
for (const FItem& Item : Items) { /* 只读 */ }
}
// 从 TArray 或裸数组构造
TArray<FItem> MyItems;
ProcessItems(MyItems); // 隐式转换
ProcessItems(MakeArrayView(RawPtr, Count)); // 从指针 + 长度
// 需要可写视图
void ModifyItems(TArrayView<FItem> Items) { Items[0].Value = 1; }
视图的生命周期
TArrayView 不拥有数据,只是"指向某段数组的引用"。被指向的容器一旦被修改(尤其是增删导致重分配)或销毁,视图立刻失效
7 与反射 / GC / 网络的关系¶
容器里的对象引用必须用 UPROPERTY
| 关系 | 说明 |
|---|---|
| GC | 带 UPROPERTY 的容器会被 GC 遍历,追踪其中的对象引用 |
| 序列化 | TArray / TMap / TSet 均支持 UPROPERTY 存读档 |
| 网络复制 | 容器可作为 Replicated 属性整体复制 |
| 反射 | 容器类型有对应的 FArrayProperty / FMapProperty / FSetProperty |
8 性能与最佳实践¶
优化要点
- 预分配:已知规模就
Reserve,避免反复扩容 + 拷贝 Emplace优于Add:原地构造,省一次临时对象- 不需要顺序就用
RemoveAtSwap(O(1) 换 O(n)) - 大对象别按值存:存
TObjectPtr<T>或指针,避免拷贝 - 小数组用
TInlineAllocator:省堆分配 - 传参用
TConstArrayView:零拷贝 - 高频 key 用
FName/整数,别用FString - 能用 TArray 就别用 TMap:连续内存遍历快得多,只有真需要按 key 查才用映射
线程安全
UE 容器 默认不是线程安全的。跨线程访问同一容器必须加锁(FCriticalSection 等);跨线程传递数据用 TQueue(无锁)这类专门容器
9 常见坑汇总¶
| 坑 | 说明 |
|---|---|
| 元素引用失效 | 持有 &Array[i] 后增删导致重分配 → 悬垂 |
| 迭代中修改 | 遍历时增删 → 迭代器失效;用 RemoveAll 或倒序/迭代器方式 |
忘记 UPROPERTY |
容器内对象引用不被 GC 追踪 |
| 依赖 TMap 顺序 | 迭代顺序不稳定,破坏确定性 |
正序 RemoveAt |
循环中删元素会跳元素 |
Num() 当成容量 |
Num() 是元素数,容量要 Max() |
| 按值传大数组 | 函数签名写 TArray<T> Param 会整体拷贝 |
| 频繁重分配 | 循环里 Add 却不 Reserve |
10 如何选择¶
| 需求 | 选择 |
|---|---|
| 一批有序数据、遍历 | TArray |
| 按 key 查/改 | TMap |
| 去重、判断存在 | TSet |
| 需要按 key 有序遍历(量小) | TSortedMap |
| 一键多值 | TMultiMap |
| 元素数固定且少 | TStaticArray / TArray + TInlineAllocator |
| 两端增删 | TDeque |
| 跨线程传递 | TQueue(无锁) |
| 固定容量的历史/缓冲 | TCircularBuffer / TCircularQueue |
| 大量布尔标志 | TBitArray |
| 只读传参 | TConstArrayView |
与其它笔记的联系
- 垃圾回收:容器里的对象引用必须
UPROPERTY才会被追踪 - 反射系统:容器对应
FArrayProperty/FMapProperty/FSetProperty - 字符串体系:做 key 优先
FName,做展示用FText - 网络同步:容器可作为复制属性,但要注意带宽与顺序确定性
评论区
欢迎在评论区指出文档错误,为文档提供宝贵意见,或写下你的疑问