06 - 数组TArray
本文主要说明了UE5中有关容器类数组TArray
的相关概念和常见操作。
TArray
TArray
是UE5提供的数组容器类,负责组织一系列同类型元素。
声明与初始化
首先需要对数组进行声明:
TArray<int32> IntArray;
然后是数组的初始化操作,主要有.Init()
和括号初始化两种:
// 两者等效
IntArray.Init(10, 5);
IntArray = {10, 10, 10, 10, 10};
此外,还有一种类似std::vector::resize()
的操作,就是SetNum()
。该函数可直接设置数组元素的数量,如新数量大于当前数量,则使用元素类型的默认构造函数新建元素;如新数量小于当前数量,SetNum()
将移除元素。
添加元素
当新元素添加到数组时,数组的分配器将根据需要分配内存。当前数组大小超出时,默认分配器将添加足够的内存,用于存储多个新元素。
可通过Add()
和Emplace()
在数组末尾添加一个元素,它俩的区别和C++std::vector
中push_back()
与emplace_back()
类似:
Add
(或Push
)将元素类型的实例复制(或移动)到数组中。Emplace
使用给定参数构建元素类型的新实例。
如果不想在末尾添加一个重复元素,可用AddUnique()
,该函数通过运算符==
判断两元素是否相等,可根据需要重载该运算符。
如果想要一次性在末尾添加多个元素,可用Append()
:
// TArray
IntArray.Append({1, 2, 3});
// C风格数组
int32 CTypeArr[] = {1, 2, 3};
IntArray.Append(CTypeArr, UE_ARRAY_COUNT(CTypeArr));
如果想要在数组指定索引处添加单个或多个元素,可使用Insert()
:
// 单个元素
IntArray.Insert(1, 2);
// 多个元素 - TArray
IntArray.Insert({1, 2}, 4);
// 多个元素 - C风格数组
IntArray.Insert(CTypeArr, UE_ARRAY_COUNT(CTypeArr), 4);
遍历元素
和C++类似,TArray
支持基于范围,索引和迭代器的数组元素遍历。
首先是基于范围的遍历:
for (auto& Str : StrArr)
{
// ...
}
然后是基于索引的遍历,这里使用成员函数Num()
查看当前数组的元素个数:
for (int32 Index = 0; Index != StrArr.Num(); ++Index)
{
// ...
}
最后是通过迭代器的遍历,它通过函数CreateIterator()
或CreateConstIterator()
创建元素的读写或只读访问迭代器:
for (auto It = IntArray.CreateConstIterator(); It; ++It)
{
// ......
}
需要注意的是,不要在遍历容器的过程中修改这个容器,在多线程环境下可能会出现一些问题。
排序
可使用Sort()
(快速排序实现),HeapSort()
(堆排序实现)对数组元素进行不稳定排序;也可使用StableSort()
(归并排序实现)对数组元素进行稳定排序。
数组元素默认按照所属类型的operator<
排序,也可通过lambda表达式进行自定义排序:
StrArr.Sort();
// 字典序: ["!","Brave","Hello","of","Tomorrow","World"]
StrArr.HeapSort([](const FString& A, const FString& B) {
return A.Len() < B.Len();
});
// 长度: ["!","of","Hello","Brave","World","Tomorrow"]
查询
TArray
还提供了一系列查询函数,方便用户进行相关操作:
询问数组属性: | 函数 | 用途 | | —————————————— | ———————————————————— | |
Num()
| 获取数组元素个数 | |GetData()
| 获取指向数组首个元素的指针,方便C风格API
需要注意的是,获取指针后如果数组不存在或被更改,指针将会无效 | |GetTypeSize()
| 获取元素类型大小 | |IsValidIndex(i)
| 询问索引i
是否有效 | |CountBytes()
和GetAllocatedSize()
| 估算数组当前内存占用量 |获取数组元素的引用:
函数 | 用途 |
---|---|
operator[] | 按索引获取数组中指定元素的引用 |
Last(i) | 返回数组反向索引i 的元素,默认i 为0 |
Top() | 同Last(0) |
- 元素查询类:
函数 | 用途 |
---|---|
Contains(x) | 询问数组是否包含特定元素x |
ContainsByPredicate(lambda -> bool) | 询问数组是否包含符合lambda表达式要求的元素 |
Find(x) | 询问数组是否出现元素x ,是就返回首次索引,否就返回INDEX_NONE |
FindLast(x) | 返回元素x 最后出现的索引,否就返回INDEX_NONE |
IndexOfByKey(x) | Find() 将参数类型的转换为数组元素的类型后才进行比较,而该函数直接将参数和数组元素比较,因此需要实现两种类型的operator== 。 |
IndexOfByPredicate(lambda -> bool) | 返回首个符合lambda表达式要求元素的索引 |
FindByKey(x) | 和IndexOfByKey(x) 类似,但返回指向元素的指针,找不到返回nullptr |
FindByPredicate(lambda -> bool) | 和IndexOfByPredicate(lambda -> bool) 类似,但返回指向元素的指针 |
FilterByPredicate(lambda -> bool) | 返回符合lambda表达式的元素数组 |
移除
TArray
也提供了许多用于移除数组元素的函数:
Remove
系列,移除元素时会将被移除元素的后续元素前移:
函数 | 用途 |
---|---|
Remove(x) | 移除数组中所有值为x 的元素 |
RemoveSingle(x) | 移除数组中首个值为x 的元素 |
RemoveAt(i) | 移除指定索引处的元素 |
RemoveAll(lambda -> bool) | 移除所有符合lambda表达式的元素 |
RemoveSwap
系列,直接用数组末尾元素代替被删除的元素以减小移动开销:
函数 | 用途 |
---|---|
RemoveSwap(x) | 同Remove(x) |
RemoveAtSwap(i) | 同RemoveAt(i) |
RemoveAllSwap(lambda -> bool) | 同RemoveAll(lambda -> bool) |
Empty(x)
:清空数组所有元素,并为数组预留x
个位置,默认为0。
运算符
TArray
也提供了一些运算符方便用户进行常用操作:
operator+=
:同Append(arr)
,进行数组的串联;operator==
和operator!=
:用于判断两数组是否相同(元素顺序和数量均相同);MoveTemp(arr)
:移动语义,将arr
移动赋值给目标数组,然后清空自身;BulkSerialize()
或operator<<
:用于序列化数组;Swap()
和SwapMemory()
:用于交换两个索引上的元素值,Swap()
更安全;
堆化
TArray
还支持二叉堆操作,堆的根节点位于索引0
,索引N
节点的左右子节点索引为2N+1
和2N+2
:
Heapify()
:将当前数组转换为二叉堆,如果没有用lambda表达式实现排序逻辑(此后该堆的所有操作均需要此表达式),默认使用operator<
;HeapPush(x)
:向堆中添加元素x
;HeapPop()
:移除并返回堆顶元素;HeapPopDiscard()
:只移除堆顶元素;HeapRemoveAt(i)
:移除索引i
处的元素,并维护堆;HeapTop()
:获取堆顶元素;
内存管理
TArray
在添加元素时会进行内存扩充,接下来看看它内存管理的主要内容:
Slack
为避免每次添加元素时重新分配内存,分配器提供的内存通常会超过必要内存,使之后调用 Add()
时不会因重新分配内存而降低性能。同样,删除元素通常不会释放内存。此操作会使数组拥有Slack
,也就是当前未使用的有效预分配元素个数,可通过GetSlack()
获取。
数组中内存分配的元素个数Max
与数组中实际存储的元素个数Num
间的差值即为数组中的Slack
:
TArray<int32> SlackArray;
// SlackArray.GetSlack() == 0
// SlackArray.Num() == 0
// SlackArray.Max() == 0
SlackArray.Add(1);
// SlackArray.GetSlack() == 3
// SlackArray.Num() == 1
// SlackArray.Max() == 4
SlackArray.Add(2);
SlackArray.Add(3);
SlackArray.Add(4);
SlackArray.Add(5);
// SlackArray.GetSlack() == 17
// SlackArray.Num() == 5
// SlackArray.Max() == 22
元素扩充时的内存分配个数遵循此公式(非节省内存模式下): \[ \nonumber \mathrm{初次扩容:FirstGrow=4} \\ \mathrm{后续扩容:Grow=Num()+Num() \times \frac{3}{8}+16} \]
在第一次扩充时(.Add(1)
时),内存分配个数为4;在第二次扩充时(.Add(5)
时),内存分配的元素数为5 + 5*3/8 + 16 = 22
。计算完内存分配的元素个数后需要按内存对齐的规则修改个数,这里22 * 4B = 88B = 16B * 5.5
已经对齐了。
如果要搞性能优化,管理Slack
就成了必修课。TArray
提供如下函数让开发者管理Slack
:
Empty(x)
:释放内存,并请求x
个元素的Slack
。SlackArray.Empty(); // SlackArray.GetSlack() == 0 // SlackArray.Num() == 0 // SlackArray.Max() == 0 SlackArray.Empty(3); // SlackArray.GetSlack() == 3 // SlackArray.Num() == 0 // SlackArray.Max() == 3 SlackArray.Add(1); SlackArray.Add(2); SlackArray.Add(3); // SlackArray.GetSlack() == 0 // SlackArray.Num() == 3 // SlackArray.Max() == 3
Reset(x)
:如果Slack
已满足要求,不释放内存;否则分配更多内存。SlackArray.Reset(0); // SlackArray.GetSlack() == 3 // SlackArray.Num() == 0 // SlackArray.Max() == 3 SlackArray.Reset(10); // SlackArray.GetSlack() == 10 // SlackArray.Num() == 0 // SlackArray.Max() == 10
Shrink()
:移除所有Slack
,把内存分配调整为保存当前元素所需的最小内存。SlackArray.Add(5); SlackArray.Add(10); SlackArray.Add(15); SlackArray.Add(20); // SlackArray.GetSlack() == 6 // SlackArray.Num() == 4 // SlackArray.Max() == 10 SlackArray.Shrink(); // SlackArray.GetSlack() == 0 // SlackArray.Num() == 4 // SlackArray.Max() == 4
Reserve(x)
:希望数组能存x
个元素,如果不够存就单向扩容内存。
原始内存操作
一些高级开发常常要直接操作内存,TArray
提供如下函数方便开发者进行相关操作:
Add/InsertUninitialized()
:
参考资料
UE5 虚幻引擎UEC++从基础到进阶_哔哩哔哩_bilibili
Array Containers in Unreal Engine | 虚幻引擎 5.6 文档 | Epic Developer Community