容器也是类,它们的主要功能是存储数据集。常见的类有 TArray、TMap 和 TSet。它们的大小均为动态,因此可变为所需的任意大小
主要使用的容器类为 TArray。TArray 类负责同类其他对象(称为元素)序列的所有权和组织。TArray 是序列,其元素拥有定义完善的排序,其函数用于确定性地操作对象及其排序
TArray 是虚幻引擎中最常用的容器类。其设计决定了它速度较快、内存消耗较小、安全性高。TArray 类型由两大属性定义:主要为其元素类型和一个任选的分配器
元素类型是将被存储在数组中的对象类型。TArray 被称为同质容器:其所有元素均完全为相同类型。不能进行不同元素类型的混合
分配器经常会被省略,适合最常使用的分配器即为默认设置。它定义对象在内存中的排列方式;以及数组如何进行扩张,以便容纳更多的元素。如默认行为不符合您的要求,可选取多种不同的分配器,或自行进行编写
TArray 是一个数值类型,意味着应该以其他内置类型(如 int32 或浮点)的方式对其进行处理。它被设计为不可被继承,通过 new/delete 在堆上创建/销毁 TArray 均非常规操作。元素也是数值类型,为容器所拥有。数组被销毁时元素也将被销毁。如从另一个 TArray 创建 TArray 变量,将把其元素复制到新变量中;不存在共享状态
创建并填充数组
如要创建数组,将其以此定义:
TArray<int32> IntArray;
这会创建一个空数组,用以保存一个整数序列。元素类型可以是根据普通 C++ 数值规则进行复制和销毁的数据类型,如 int32、FString、TSharedPtr 等。TArray 未指定分配器,因此它采用基于堆的常规分配。此时尚未进行内存分配
TArray 可以多种方式进行填入。一种方式是使用
Init
函数,用大量元素副本填入数组
IntArray.Init(10, 5); // IntArray == [10, 10, 10, 10, 10]
Add
和Emplace
函数可用于在数组末端创建新对象
TArray
StrArr; StrArr.Add(TEXT("Hello")); StrArr.Emplace(TEXT("World")); // StrArr == ["Hello", "World"] 元素被添加时,内存从分配器中被分配。Add 和 Emplace 函数可达到同样效果,但存在细微不同:
Add
函数将吧一个元素类型实例复制(或移动)到数组中Emplace
函数将使用给定的参数构建一个元素类型的新实例因此在 TArray 中,Add 函数将从字符串文字创建一个临时 FString,然后将临时内容移至容器内的新 FString 中;而 Emplace 函数将使用字符串文字直接创建其 FString。最终结果相同,但 Emplace 可避免创建临时文件。对 FString 之类的非浅显值类型而言,临时文件通常有害无益。Push 也可用作 Add 的同义词
总体而言,Emplace 优于 Add。Emplace 可避免在调用点创建不必要的临时文件并将它们复制或传入容器。经验法则:在基本值类型上使用 Add,在其他类型(如自定义类)上使用 Emplace。Emplace 的效率不会比 Add 低,但有时 Add 读取更佳
利用
Append
可将多个元素一次性从另一个 TArray(或指针+大小)添加到一个常规 C 数组:
FString Arr[] = { TEXT("of"), TEXT("Tomorrow") }; StrArr.Append(Arr, ARRAY_COUNT(Arr)); // StrArr == ["Hello", "World", "of", "Tomorrow"]
如尚不存在等值元素,
AddUnique
只添加一个新元素到容器。使用元素类型的运算符 == 检查等值性:
StrArr.AddUnique(TEXT("!")); // StrArr == ["Hello","World","of","Tomorrow","!"] StrArr.AddUnique(TEXT("!")); // StrArr 不变,因为数组中已经存在"!"
与Add、Emplace和Append一样,
Insert
允许在给定索引添加一个单一元素或元素数组的一个副本
StrArr.Insert(TEXT("Brave"), 1); // StrArr == ["Hello","Brave","World","of","Tomorrow","!"]
SetNum
函数可直接设置数组元素的数量
// 如果设置的新数量大于当前数量,则使用元素类型的默认构造函数创建新元素 StrArr.SetNum(8); // StrArr == ["Hello","Brave","World","of","Tomorrow","!","",""] // 如果设置的新数量小于当前数量,SetNum 将移除超出数组长度的元素 StrArr.SetNum(6); // StrArr == ["Hello","Brave","World","of","Tomorrow","!"]
迭代
有多中方法可以在数组元素上进行迭代,推荐方法为C++的
ranged-for
功能:
FString JoinedStr; for (auto& Str : StrArr) { JoinedStr += Str; JoinedStr += TEXT(" "); } // JoinedStr == "Hello Brave World of Tomorrow !"
也可以使用基于索引的常规迭代:
for (int32 Index = 0; Index != StrArr.Num(); ++Index) { JoinedStr += StrArr[Index]; JoinedStr += TEXT(" "); }
还可以通过数组自身的迭代器类型对迭代进行控制。函数
CreateIterator
* 和CreateConstIterator
可分别用于元素的读写和只读访问:*
for (auto It = StrArr.CreateConstIterator(); It; ++It) { JoinedStr += *It(); JoinedStr += TEXT(" "); }
排序
调用
Sort
函数即可对数组进行排序:
StrArr.Sort(); // StrArr == ["!","Brave","Hello","of","Tomorrow","World"]
在这里,值是通过元素类型的运算符
<
进行排序的。在 FString 中,这是一个不区分大小写的词典编纂对比。二进制谓词可提供不同的排序语意,如:
StrArr.Sort([](const FString& A, const FString& B){ return A.len() < B.len(); }); // StrArr == ["!","of","Hello","Brave","World","Tomorrow"] // 现在按字符串长度进行排序。
Sort 并不稳定,等值元素(因为长度相同,此处字符串为等值)的相对排序无法保证。Sort 是使用 quicksort 实现的
HeapSort
函数,无论带或不带二元谓词,均可用于执行对排序。是否选择使用它则取决于特定数据和与 Sort 函数之间的排序效率对比。和 Sort 一样,HeapSort 并不稳定
StrArr.HeapSort([](const FString& A, const FString& B) { return A.Len() < B.Len(); }); // StrArr == ["!","of","Hello","Brave","World","Tomorrow"]
StableSort
可在排序后保证等值元素的相对排序。
StrArr.StableSort([](const FString& A, const FString& B) { return A.Len() < B.Len(); }); // StrArr == ["!","of","Brave","Hello","World","Tomorrow"]
StableSort 作为归并排序实现
查询
使用
Num
函数可获取数组中的元素数量:
int32 Count = StrArr.Num(); // Count == 6
如需直接访问数组内存,可使用
GetData()
函数返回指向数组中元素的指针。有数组存在且未被执行任何变异操作时,该指针方为有效。只有StrPtr的第一个Num()索引是可解引用的:
FString* StrPtr = StrArr.GetDate(); // StrPtr[0] == "!" // StrPtr[1] == "of" // ... // StrPtr[5] == "Tomorrow" // StrPtr[6] - undefined behavior // 如果容器为常量,则返回的指针也为常量
获取容器内单个元素的大小:
uint32 ElementSize = StrArr.GetTypeSize(); // ElementSize == sizeof(FString)
使用索引操作符
[]
获取元素,并将一个从零开始的索引传递到需要的元素中:
FString Elem1 = StrArr[1]; // Elem1 == "of"
传递小于或大于Num()的无效索引会引起运行错误。可使用
IsValidIndex()
函数判断索引是否有效:
bool bValidM1 = StrArr.IsValidIndex(-1); bool bValid0 = StrArr.IsValid(0); bool bValid6 = StrArr.IsValid(6); // bValidM1 == false // bValid0 == true // bValid6 == false
[]
运算符返回的是一个引用,可用于操作数组中的元素(假定数组不为常量):
StrArr[3] = StrArr[3].ToUpper(); // StrArr == ["!","of","Brave","HELLO","World","Tomorrow"]
和 GetData 函数一样 - 如数组为常量,运算符 [] 将返回一个常量引用。还可使用
Last
函数从数组末端反向进行索引编入。索引默认为零。Top
函数是 Last 的同义词,唯一区别是其不接受索引:
FString ElemEnd = StrArr.Last(); FString ElemEnd0 = StrArr.Last(0); FString ElemEnd1 = StrArr.Last(1); FString ElmeTop = StrArr.Top(); // ElemEnd == "Tomorrow" // ElemEnd0 == "Tomorrow" // ElemEnd1 == "World" // ElemTop == "Tomorrow"
判断一个数组中是否包含特定元素:
bool bHello = StrArr.Contains(TEXT("Hello")); bool bGoodbye = StrArrContains(TEXT("Goodbye")); // bHello == true // bGoodbye == false
判断数组是否包含于特定谓词匹配的元素:
bool bLen5 = StrArr.ContainsByPredicate([](const FString& Str){ return Str.Len() == 5; }); bool bLen6 = StrArr.ContainsByPredicate([]const FString& Str){ return Str.Len() == 6; }); // bLen5 == true // bLen6 ==false
使用
Find
函数家族可找到元素。使用Find
确定元素是否存在并返回其索引:
int32 Index; if (StrArr.Find(TEXT("Hello"), Index) { // Index == 3 } // 会将传入的第二个参数设为匹配到的第一个元素的索引
FindLast
函数将传入的第二个参数设置为最后一个匹配元素的索引:
int32 IndexLast; if (StrArr.FindLast(TEXT("Hello"), IndexLast)) { // IndexLast == 3, 因为数组中只有一个"Hello" }
两个函数均会返回一个布尔值,指出是否已找到元素, 同时在找到的元素索引时将其写入变量。
Find 和 FindLast 也可以直接返回元素索引。如果不将索引作为显式参数传递,这两个函数便会执行此操作。
如果没有找到元素,则返回特殊的INDEX_NONE
值
int32 Index2 = StrArr.Find(TEXT("Hello")); int32 IndexLast2 = StrArr.FindLast(TEXT("Hello")); int32 IndexNone = StrArr.Find(TEXT("None"));
IndexOfByKey
工作方式相似,但允许元素与任意对象进行对比。通过Find函数进行的搜索开始前,参数将被实际转换为元素类型(此例中的FString)。使用IndexOfByKey
,则直接对”键”进行对比,以便在键类型无法直接转换到元素类型时照常进行搜索。
IndexOfByKey
可用于运算符 == (ElementType、KeyType)存在的任意键类型;然后这将被用于执行比较。IndexOfByKey返回首个匹配到的元素的索引;如果没有找到元素,则返回INDEX_NONE
int32 Index = StrArr.IndexOfByKey(TEXT("Hello")); // Index == 3
IndexOfByPerdicate
函数可用于寻找与特定谓词匹配的首个元素的索引;如未找到,则返回特殊的INDEX_NONE值
int32 Index = StrArr.IndexOfByPerdicate([](const FString& Str){ return Str.Contains(TEXT("r")); }); // Index == 2
FindByKey
可以将元素和任意对象对比,并返回首个匹配到的元素的指针,如果未匹配到,则返回nullptr
auto* OfPtr = StrArr.FindByKey(TEXT("of")); auto* ThePtr = StrArr.FindByKey(TEXT("the")); // OfPtr == &StrArr[1] // ThePtr == nullptr
FindByPredicate
的使用方式和IndexOfByPredicate相似,不同的是,它的返回值是指针,而不是索引
auto* Len5Ptr = StrArr.FindByPredicate([](const FString& Str){ return Str.Len() == 5; }); auto* Len6Ptr = StrArr.FindByPerdicate([](const FString& Str){ return Str.Len() == 6; }); // Len5Ptr == &StrArr[2] // Len6Ptr == nullptr
FilterByPredicate
函数将返回匹配特定谓词的元素数组
auto Filter = StrArr.FilterByPredicate([](const FString& Str){ return !Str.IsEmpty() && Str[0] < TEXT('M'); });
移除
可使用
Remove
家族函数从数组中移除元素。Remove
函数将移除与传入元素相等的所有元素
StrArr.Remove(TEXT("Hello")); // StrArr == ["!","of","Brave","World","Tomorrow"] StrArr.Remove(TEXT("goodbye")); // StrArr不会改变,不存在与goodbye匹配的元素
注意:即使我们要求移除“hello”,“HELLO”仍然将被移除。通过元素类型的运算符 == 可对相等性进行测试;记住 FString这是一个不区分大小写的对比
通过
Pop
函数可以出数组的最后一个元素:
StrArr.Pop();
Remove
函数将移除传入参数对比相等的全部实例:
TAarray<int32> ValArr; int32 Temp[] = { 10, 20, 30, 5, 10, 15, 20, 25, 30 }; ValArr.Append(Temp, ARRAY_COUNT(Temp)); // ValArr == [10,20,30,5,10,15,20,25,30] ValArr.Remove(20); // ValArr == [10,30,5,10,15,25,30]
RemoveSingle
移除离数组前部最近的元素。在以下情况尤为实用:数组中可能存在重复,而只希望删除一个;或作为优化,数组只能包含此种元素的一个,因为找到并移除后搜索将停止:
ValArr.RemoveSingle(30); // ValArr == [10,5,10,15,25,30]
RemoveAt
函数移除指定索引处的元素,索引必须存在,否则会出现错误
ValArr.RemoveAt(2); // Removes the element at index 2 // ValArr == [10,5,15,25,30] ValArr.RemoveAt(99); // This will cause a runtime error as there is no element at index 99
RemoveAll
函数即可移除与谓词匹配的元素。例如,移除 3 的倍数的所有数值:
ValArr.RemoveAll([](int32 Val){ return Val % 3 == 0; }); // ValArr == [10,5,25]
在所有这些情况中,当元素被移除时,其后的元素将被下移到更低的指数中,因为数组中不能出现空洞
移动过程存在开销。如不介意剩余元素的排序,可使用
RemoveSwap
、RemoveAtSwap
和RemoveAllSwap
函数减少此开销。这些函数的工作方式与其非交换变种相似,不同之处在于它们不保证剩余元素的排序,因此它们的实现效率更高:
TArray<int32> ValArr2; for (int32 i = 0; i != 10; ++i) { ValArr2.Add(i % 5); } // ValArr2 == [0,1,2,3,4,0,1,2,3,4] ValArr2.RemoveSwap(2); // ValArr2 == [0,1,4,3,4,0,1,3] ValArr2.RemoveAtSwap(1); // ValArr2 == [0,3,4,3,4,0,1] ValArr2.RemoveAllSwap([](int32 Val){ return Val % 3 == 0; }); // ValArr2 == [1,4,4]
Empty()
函数清空数组:
ValArr2.Empty(); // ValArr2 == []
运算符
数组是常规数值类型,可通过标准复制构建函数或运算符被复制。因数组严格拥有其元素,数组拷贝是深拷贝,因此新数组将拥有其自身的元素副本
TArray
ValArr3; ValArr3.Add(1); ValArr3.Add(2); auto ValArr4 = ValArr3; // ValArr4 == [1, 2, 3] ValArr4[0] = 5; // ValArr3 == [1,2,3]; // ValArr4 == [5,2,3]; 可用
+=
运算符替代Append函数进行数组连接
ValArr4 += ValArr3; // ValArr4 == [5,2,3,1,2,3]
使用
MoveTemp
函数可将一个数组中的内容移动到另一个数组中,源数组将被清空:
ValArr3 = MoveTemp(ValArr4); // ValArr3 == [5,2,3,1,2,3] // ValArr4 == []
使用运算符
==
和!=
可对数组进行比较。对比项包含元素数量、元素排序、和元素内容,三者均相等时,才被视为相等。元素间的对比通过其自身的运算符==
进行:
TArray
FlavorArr1; FlavorArr1.Emplace(TEXT("Chocolate")); FlavorArr1.Emplace(TEXT("Vanilla")); // FlavorArr1 == ["Chocolate","Vanilla"] auto FlavorArr2 = FlavorArr1; // FlavorArr2 == ["Chocolate","Vanilla"] bool bComparison1 = FlavorArr1 == FlavorArr2; // bComparison1 == true for ( auto& Str : FlavorArr2 ) { Str = Str.ToUpper(); } // FlavorArr2 == ["CHOCOLATE","VANILLA"] bool bComparison2 = FlavorArr1 == FlavorArr2; // bComparison2 == true,因为FString的对比忽略大小写 Exchange(FlavorArr2[0], FlavorArr2[1]); // FlavorArr2 == ["VANILLA","CHOCOLATE"] bool bComparison3 = FlavorArr1 == FlavorArr2; // bComparison3 == false,因为两个数组内的元素顺序不同 堆
TArray拥有支持二叉堆数据结构的函数。堆是一个二叉树类型。在树中,父节点等于其子节点,或在其所有子节点前排序。作为数组实现时,树的根节点位于元素0,索引 N 处节点左右子节点的指数分别为 2N+1 和 2N+2 。子类彼此之间不存在特定的排序。
调用
Heapify
函数将现有数组转变为堆。这被重载为是否接受谓词,非断言的版本将使用元素类型的操作符<
来确定排序
TArray<int32> HeapArr; for ( int32 Val = 10; Val != 0; --Val ) { HeapArr.Add(Val); // HeapArr == [10,9,8,7,6,5,4,3,2,1] HeapArr.Heapify(); // HeapArr == [1,2,4,3,6,5,8,10,7,9] }
下图是树的直观展示:
通过
HeapPush
函数可将新元素添加到堆,堆其他节点进行整理,对堆进行维护:
HeapArr.HeapPush(4); // HeapArr == [1,2,4,3,4,5,8,10,7,9,6];
HeapPop
和HeapPopDiscard
函数用于移除堆上的顶部节点。两者之间的区别是前者接受对元素类的引用,返回顶部元素的副本;而后者只是简单地移除顶部节点,不进行任何形式的返回。两个函数得出的数组变更一致,重新适当排列其他元素可对堆进行维护:
int32 TopNode; HeapArr.HeapPop(TopNode); // TopNode == 1 // HeapArr == [2,3,4,6,4,5,8,10,7,9]
HeapRemoveAt
将移除数组中给定索引的处的元素,然后重新排列元素,对堆进行维护:
HeapArr.HeapRemoveAt(1); // HeapArr == [2,4,4,6,9,5,8,10,7]
需要注意:只有在结构已经为一个有效堆时(如在 Heapify() 调用、其他堆操作、或手动将数组调为堆之后),才会调用 HeapPush、HeapPop、HeapPopDiscard 和 HeapRemoveAt
HeapTop
函数可查看堆的顶部节点,无需变更数组:
int32 Top = HeapArr.HeapTop(); // Top == 2
Slack
因为数组的尺寸可进行调整,因此它们使用的是可变内存量。为避免每次添加元素时需要重新分配,分配器通常会提供比需求更多的内存,是之后进行的Add调用不会因为重新分配而出现性能损失。同样,删除元素通常不会释放内存。容器中现有的元素数量和下次分配之前可添加的元素数量之差成为Slack
默认构建的数组不分配内存。则slack初始为0。使用
GetSlack
函数即可找出数组中的slack量。另外,通过Max
函数可获取到容器重新分配之前数组可保存的最大元素数量。GetSlack()相当于Max() - Num()
TArray
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 重新分配后,容器中的slack量由分配器决定,不取决于数组使用者
多数情况下不必太在意slack。但如果对此有所理解,即可使用它对数组进行优化,获得益处。例如,如您知道自己即将添加 100 个新元素到数组,您可确保添加前拥有至少为 100 的 slack,使元素添加时不出现分配。上文所述的 Empty 函数接受任选的 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
函数的工作方式与Empty相似,不同的是,如果当前分配已提供所请求的slack,函数将不释放 内存。然而,如果所请求的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
原始内存
本质上而言,TArray只是一些分配内存的包装器。对分配的字节进行直接修改和自行创建元素即可将TArray作为包装器使用,十分实用。TArray 将通过其拥有的信息尽量执行,但有时需要下降一个等级。
需要注意的是:这些函数允许使容器变为无效状态。如果出现失误,将引起未定义的行为。在调用这些函数后,调用其他常规函数之前,可决定是否使容器变回有效状态
AddUninitialized
和InsertUninitialized
函数将为数组添加一些未初始化的空间。它们的工作方式分别于 Add 和 Insert 函数相同,但它们不会调用元素类型的构造函数。对拥有安全性或便利性构造函数的结构体而言,这十分有用,但这将完全重写任意方式的状态(如使用 Memcpy 调用),因此要避免出现构建的损失:
int32 SrcInts[] = { 2, 3, 5, 7 }; TArray<int32> UninitInts; UninitInts.AddUninitialized(4); FMemory::Memcpy(UninitInts.GetData(), SrcInts, 4*sizeof(int32)); // UninitInts == [2,3,5,7]
如果需要或希望自行控制构建过程,它们还可为计划自行显式的对象创建保留部分内存。
TArray
UninitStrs; UninitStrs.Emplace(TEXT("A")); UninitStrs.Emplace(TEXT("D")); UninitStrs.InsertInitialized(1, 2);// 第一个参数指明插入开始位置的索引,第二个参数指明插入几个元素 new ((void*)(UninitStrs.GetData() + 1)) FString(TEXT("B"));// GetData()返回数组头指针 new ((void*)(UninitStrs.GetDate() + 2)) FString(TEXT("C")); // UninitStrs == ["A","B","C","D"]
AddZeroed
和InsertZeroed
的工作方式相似,不同点是它们会把添加/插入的空间字节清零。如需将类型插入有效的按位零状态,这将非常实用
struct S { S(int32 InInt, void* InPtr, float InFlt) :Int(InInt), Ptr(InPtr), Flt(InFlt) {} int32 Int; void* Ptr; float Flt; }; TArray
SArr; SArr.AddZeroed(); // SArr == [{ Int: 0, Ptr: nullptr, Flt: 0.0f }]
SetNumUninitialized
和SetNumZeroed
函数的工作方式与SetNum
相似。不同之处在于,新数字大于当前数字时,新元素的空间将分别为未初始化或按位归零。通过使用 AddUninitialized 和 InsertUninitialized,您应该确保新元素根据需要被正确地构建到新空间中(如有必要):
SArr.SetNumUninitialized(3); new ((void*)(SArr.GetData() + 1)) S(5, (void*)0x12345678, 3.14); new ((void*)(SArr.GetData() + 2)) S(2, (void*)0x87654321, 2.27); // SArr == [ // { Int:0, Ptr: nullptr, Flt:0.0f }, // { Int:5, Ptr:0x12345678, Flt:3.14f }, // { Int:2, Ptr:0x87654321, Flt:2.72f } // ] SArr.SetNumZeroed(5); // SArr == [ // { Int:0, Ptr: nullptr, Flt:0.0f }, // { Int:5, Ptr:0x12345678, Flt:3.14f }, // { Int:2, Ptr:0x87654321, Flt:2.72f }, // { Int:0, Ptr: nullptr, Flt:0.0f }, // { Int:0, Ptr: nullptr, Flt:0.0f } // ]
应谨慎使用未初始化或归零的函数。如对一个元素类型进行修改,以包括需要构建的成员、或不拥有有效按位清零状态的成员,可导致无效数组元素和未定义行为的出现。这些函数在类型数组上最为实用。这些数组(如 FMatrix 或 FVector)几乎不会发生变化。
杂项
BulkSerialize
和GetAllocateSize
函数用于估计数组当前应用的内存量。CountBytes 接受 FArchive,GetAllocatedSize 可被直接调用。它们常用于统计报告
Swap
和SwapMemory
函数均接受两个指数,将对这些指数上元素的数值进行交换。它们相等,不同点是 Swap 会在指数上执行额外的错误检查,并断言指数是否处于范围之外
TMap 主要由两个类型定义:键类型和值类型,作为关联对存储在映射中。将这些对作为映射的元素类型参考十分便利,就像是个体对象一样。元素类型实际上是一个 TPair< KeyType, ElementType >,但它很少需要直接参考 TPair 类型
和 TArray 一样,TMap 是同构容器,因此其所有元素完全为相同类型。TMap 也是值类型,支持常规复制、赋值和析构函数操作,以及其元素较强的所有权。映射被销毁时,其元素也将被销毁。键类型和值类型也必须为值类型
TMap是散列容器,意味着键类型必须支持
GetTypeHash
函数并提供一个运算符==
,对键的相等性进行对比TMap还可通过任选分配器控制内存分配行为。标准 UE4 分配器(如 FHeapAllocator、TInlineAllocator)无法被用作 TMap 的分配器。应使用标准 UE4 分配器进行散列和元素存储,而不使用定义映射使用散列桶数量的集分配器。
最终的TMap模板参数为
KeyFuncs
,它将告知映射如何从元素类获得key、如何对比两个key的相等性、如何散列key。它们默认只返回key的引用,使用运算符 == 对比相等性,使用非成员GetTypeHash
函数进行散列。如果自定义的key类型支持这些函数,它将作为映射键使用,无需提供自定义 KeyFuncs与 TArray 不同,内存中 TMap 元素的相对排序不可被依赖,元素上迭代返回的顺序可能与它们的添加顺序不同。元素在内存中不太可能被持续排列。映射的备份数据结构是稀疏阵列,带有洞。元素从映射移除后,稀疏阵列中将出现洞。之后添加的元素将填充这些洞。然而,即使 TMap 不移动元素填充洞穴,指向映射元素的指针仍然可能被无效化,因为整体存储为满时添加新元素会重新对整体存储进行分配。
创建并填充映射
TMap<int32, FString> FruitMap;
这会创建一个空白的 TMap,把整数映射到字符串。我们指定的并非是分配器或 KeyFuncs,因此映射将执行标准堆分配;使用 == 对键(int32)进行对比,并使用 GetTypeHash 进行散列。此时尚未分配内存。
填入映射的标准方法是使用
Add
函数并提供一个键和值:
FruitMap.Add(5, TEXT("Banana")); FruitMap.Add(2, TEXT("Grapefruit")); FruitMap.Add(7, TEXT("Pineapple")); // FruitMap == [ // { Key:5, Value:"Banana" }, // { Key:2, Value:"Grapefruit" }, // { Key:7, Value:"Pineapple" } // ]
这些元素的排序不存在绝对保证。对于新映射而言,它们可能以插入排序。但映射受支配的插入和移除越多,新元素不出现在末端的可能性越大。
如果添加已经存在的键,会覆盖旧的键映射的内容
FruitMap.Add(2, TEXT("Pear")); // FruitMap == [ // { Key:5, Value:"Banana" }, // { Key:2, Value:"Pear" }, // { Key:7, Value:"Pineapple" } // ]
Add 函数被重载,以接受不带值的键。如只提供了一个键,数值将被默认构建
FruitMap.Add(4); // FruitMap == [ // { Key:5, Value:"Banana" }, // { Key:2, Value:"Pear" }, // { Key:7, Value:"Pineapple" }, // { Key:4, Value:"" } // ]
和 TArray 一样,我们还可使用 Emplace 代替 Add,避免插入映射时创建出临时文件:
FruitMap.Emplace(3, TEXT("Orange")); /* **** * 在此,两个参数分别被直接传到键类型和值类型的构建函数。这对此处的 int32 并无 * 真正效果,但它能避免创建值的临时 FString。和 TArray 不同,只可通过单一参数构 * 建函数将元素安放到映射中。 * ****/
使用
Append
函数进行合并即可插入来自另一个映射的所有元素:
TMap
FString> FruitMap2; FruitMap2.Emplace(4, TEXT("Kiwi")); FruitMap2.Emplace(9, TEXT("Melon")); FruitMap2.Emplace(5, TEXT("Mango")); FruitMap.Append(FruitMap2); // FruitMap == [ // { Key:5, Value:"Mango" }, // { Key:2, Value:"Pear" }, // { Key:7, Value:"Pineapple" }, // { Key:4, Value:"Kiwi" }, // { Key:3, Value:"Orange" }, // { Key:9, Value:"Melon" } // ] 此处生成的映射和使用 Add/Emplace 进行单个添加相等,因此来自源映射的复制键会替代目标映射中的键
迭代
TMap 的迭代与 TArray 相似。可使用 C++ 的 ranged-for 功能,注意元素类型是 TPair:
for ( auto& Elem : FruitMap ) { FPlatfromMisc::LocalPrint( *FString::Printf( TEXT("(%d, \"%s\")\n"), Elem.Key, Elem.Value ) ); } // Output: // (5, "Mango") // (2, "Pear") // (7, "Pineapple") // (4, "Kiwi") // (3, "Orange") // (9, "Melon")
映射还提供其自身的迭代器类型,以便对迭代进行更直接的控制。
CreateIterator
函数提供对元素的读写访问,CreateConstIterator
函数提供只读访问。迭代器对象自身以供Key()
和Value()
函数进行键和值得访问
for ( auto It = FruitMap.CreateConstIterator(); It; ++It ) { FPlatfromMisc::LocalPrint( *FString::Printf( TEXT("(%d, \"%s\")\n"), It.Key(), // same as It->Key *It.Value() // same as *It->Value ) ); }
查询
Num()
函数返回Map中当前保存的元素数量
int32 Count = FruitMap.Num(); // Count == 6
[]
索引运算符,根据传入的key返回对应键值对的引用,在常量Map调用时返回的是const引用;如果给定的键不存在,则出现断言:
FString Val7 = FruitMop[7]; // Val7 == "Pineapple" FString Val8 = FruitMap[8]; // assert !
Contains
函数同于判断给定key是否存在于map中:
bool bHas7 = FruitMap.Contains(7); bool bHas8 = FruitMap.Contains(8); // bHas7 == true // bHas8 == false
Find
函数可进行单一查找,返回指向找到元素数值的指针,而非引用,在常量map上调用时,返回的是const指针;键不存在时,将返回 null
FString* Ptr7 = FruitMap.Find(7); FString* Ptr8 = FruitMap.Find(8); // *Ptr7 == "Pineapple" // Ptr8 == nullptr
FindOrAdd
函数将搜索给定键并返回引用到关联值;如键不存在,则在返回引用前将添加默认构建的值。因可能需要添加,此函数无法在常量映射上被调用
FString& Ref7 = FruitMap.FindOrAdd(7); // Ref7 == "Pineapple" // FruitMap == [ // { Key:5, Value:"Mango" }, // { Key:2, Value:"Pear" }, // { Key:7, Value:"Pineapple" }, // { Key:4, Value:"Kiwi" }, // { Key:3, Value:"Orange" }, // { Key:9, Value:"Melon" } // ] FString& Ref8 = FruitMap.FindOrAdd(8); // Ref8 == "" // FruitMap == [ // { Key:5, Value:"Mango" }, // { Key:2, Value:"Pear" }, // { Key:7, Value:"Pineapple" }, // { Key:4, Value:"Kiwi" }, // { Key:3, Value:"Orange" }, // { Key:9, Value:"Melon" }, // { Key:8, Value:"" } // ]
注意:如已发生重新分配,此处的 Ref7 引用可能已被 FruitMap.FindOrAdd(8) 的调用无效化。
FindRef
函数搜索键返回的是值,而非引用。如果匹配到键,则返回关联值的副本;如果未找到,则返回默认构造值类型。这会导致和 FindOrAdd 相似的行为,但因 FindRef 函数返回的是值而非引用,映射将不会被修改,因此可在常量对象上被调用:
FString Val7 = FruitMap.FindRef(7); FString Val6 = FruitMap.FindRef(6); // Val7 == "Pineapple" // Val6 == "" // FruitMap == [ // { Key:5, Value:"Mango" }, // { Key:2, Value:"Pear" }, // { Key:7, Value:"Pineapple" }, // { Key:4, Value:"Kiwi" }, // { Key:3, Value:"Orange" }, // { Key:9, Value:"Melon" }, // { Key:8, Value:"" } // ]
FindKey
函数允许执行逆向查找(找到键给定值)。使用该函数时要注意,因为值个键不同,不会被散列,因此键查找是线性操作。此外,数值不保证为唯一。因此,如映射包含重复值,键返回的特定值是任意的。
const int32* KeyMangoPtr = FruitMap.FindKey(TEXT("Mango")); const int32* KeyKumquatPtr = FruitMap.FindKey(TEXT("Kumquat")); // *KeyMangoPtr == 5 // KeyKumquatPtr == nullptr
GenerateKeyArray
和GenerateValueArray
函数分别允许以全部键个值得副本对数组进行填充。在两种情况下,被传递的数组在填入前会被清空,因此元素的生成数量将始终等于映射中的元素数量
TArray
FruitKeys; TArray FruitValues; FruitKeys.Add(999); FruitValues.Add(123); FruitMap.GenerateKeyArray(FruitKeys); FruitMap.GenerateValueArray(FruitValues); // FruitKeys == [ 5,2,7,4,3,9,8 ] // FruitValues == [ "Mango","Pear","Pineapple","Kiwi","Orange","Melon","" ] 移除
使用
Remove
函数并提供要删除的元素键即可将元素从Map中移除:
FruitMap.Remove(8); // FruitMap == [ // { Key:5, Value:"Mango" }, // { Key:2, Value:"Pear" }, // { Key:7, Value:"Pineapple" }, // { Key:4, Value:"Kiwi" }, // { Key:3, Value:"Orange" }, // { Key:9, Value:"Melon" } // ] /*移除元素将在数据结构(在 Visual Studio 的观察窗口中可视 化映射时可看到)中留下洞,但为保证清晰性,此处将忽略洞。*/
FindAndRemovedChecked
函数可用于移除元素, 并返回关联值。名称中的checked部分意味着将检查键是否存在,如果不存在,则出现断言:
FString Removed7 = FruitMap.FindAndRemovedChecked(7); // Removed7 == "Pineapple" // FruitMap == [ // { Key:5, Value:"Mango" }, // { Key:2, Value:"Pear" }, // { Key:4, Value:"Kiwi" }, // { Key:3, Value:"Orange" }, // { Key:9, Value:"Melon" } // ] FString Removed8 = FruitMap.FindAndRemovedChecked(8); // assert !
RemoveAndCopyValue
函数作用相似,但会引用将被删除的值,并返回布尔值说明是否已找到。它可结合缺失键使用,不会出现运行错误。如果未找到键,调用将返回false,传递对象和映射保持不变
FString Removed; bool bFound2 FruitMap.RemoveAndCopyValue(2, Removed); // bFound2 == true // Removed == "Pear" // FruitMap == [ // { Key:5, Value:"Mango" }, // { Key:4, Value:"Kiwi" }, // { Key:3, Value:"Orange" }, // { Key:9, Value:"Melon" } // ] bool bFound8 = FruitMap.RemoveAndCopyValue(8, Removed); // bFound8 == false // Removed == "Pear", i.e. unchanged // FruitMap == [ // { Key:5, Value:"Mango" }, // { Key:4, Value:"Kiwi" }, // { Key:3, Value:"Orange" }, // { Key:9, Value:"Melon" } // ]
Empty
函数可清空Map
TMap
FruitMapCopy = FruitMap; // FruitMapCopy == [ // { Key:5, Value:"Mango" }, // { Key:4, Value:"Kiwi" }, // { Key:3, Value:"Orange" }, // { Key:9, Value:"Melon" } // ] FruitMapCopy.Empty(); // FruitMapCopy == [] 和 TArray 一样,Empty 接受任选的 slack 值。以给定数量的元素重新填入映射时,此值可用于优化
排序
可对TMap进行临时排序。映射上的下次迭代将以顺序排序展示元素,之后对映射进行的修改可能导致映射重新排列。排序并不稳定,因此相等元素可能以各种排列方式出现。
KeySort
和ValueSort
函数分别按键和值排序,两个函数均接受二元谓词指定排序顺序
FruitMap.KeySort([](int32 A, int32 B){ return A > B; }); // FruitMap == [ // { Key:9, Value:"Melon" }, // { Key:5, Value:"Mango" }, // { Key:4, Value:"Kiwi" }, // { Key:3, Value:"Orange" } // ] FruitMap.ValueSort([](const FString& A, const FString& B){ return A.Len() > B.Len(); });
运算符
和 TArray 一样,TMap 是常规值类型,可通过标准复制构建函数或赋值运算符进行复制。因映射严格拥有其元素,映射复制为深,因此新映射将拥有其自身的元素副本:
TMap<int32, FString> NewMap = FruitMap; NewMap[5] = "Apple"; NewMap.Remove(3); // FruitMap == [ // { Key:4, Value:"Kiwi" }, // { Key:5, Value:"Mango" }, // { Key:9, Value:"Melon" }, // { Key:3, Value:"Orange" } // ] // NewMap == [ // { Key:4, Value:"Kiwi" }, // { Key:5, Value:"Apple" }, // { Key:9, Value:"Melon" } // ]
MoveTemp
函数将源映射中的内容移动到目标映射中,移动后源映射将被清空:
FruitMap = MoveTemp(NewMap); // FruitMap == [ // { Key:4, Value:"Kiwi" }, // { Key:5, Value:"Apple" }, // { Key:9, Value:"Melon" } // ] // NewMap == []
Slack
TMap 也拥有 slack 的概念,可用于优化映射的填入。
Reset
与Empty()
调用作用相似,但不会释放元素之前使用的内存。
FruitMap.Reset(); // FruitMap == [<invalid>, <invalid>, <invalid>] // 此处映射按照 Empty 相同的方式进行清空,但用于储存的内存不会被释放,仍为 slack。
TMap 不会像 TArray::Max() 一样提供检查预分配元素的数量,但仍支持预分配slack。
Reserve
函数可用于在添加之前预分配特定数量元素的slack
FruitMap.Reserve(10); for (int32 i = 0; i != 10; ++i) { FruitMap.Add(i, FString::Printf(TEXT("Fruit%d"), i)); } // FruitMap == [ // { Key:9, Value:"Fruit9" }, // { Key:8, Value:"Fruit8" }, // ... // { Key:1, Value:"Fruit1" }, // { Key:0, Value:"Fruit0" } // ]
注意:Slack 会导致新元素以倒序被添加。这是为什么不可信赖映射中元素排序的原因。
Shrink
函数和 TArray 中对应函数的相同之处是:它将从容器的末端移除被废弃的slack。然而,因为 TMap 允许其数据结构中存在漏洞,这只会从遗留在结构末端的洞上移除slack
for (int32 i = 0; i != 10; i += 2) { FruitMap.Remove(i); } // FruitMap == [ // { Key:9, Value:"Fruit9" }, //
, // { Key:7, Value:"Fruit7" }, //, // { Key:5, Value:"Fruit5" }, //, // { Key:3, Value:"Fruit3" }, //, // { Key:1, Value:"Fruit1" }, //// ] FruitMap.Shrink(); // FruitMap == [ // { Key:9, Value:"Fruit9" }, // , // { Key:7, Value:"Fruit7" }, //, // { Key:5, Value:"Fruit5" }, //, // { Key:3, Value:"Fruit3" }, //, // { Key:1, Value:"Fruit1" } // ]注意:只有一个无效元素已从 Shrink 调用移除,因为末端只有一个洞。
Compact
函数可用于在缩小前移除所有洞。
FruitMap.Compact(); // FruitMap == [ // { Key:9, Value:"Fruit9" }, // { Key:7, Value:"Fruit7" }, // { Key:5, Value:"Fruit5" }, // { Key:3, Value:"Fruit3" }, // { Key:1, Value:"Fruit1" }, //
, // , // , // // ] FruitMap.Shrink(); // FruitMap == [ // { Key:9, Value:"Fruit9" }, // { Key:7, Value:"Fruit7" }, // { Key:5, Value:"Fruit5" }, // { Key:3, Value:"Fruit3" }, // { Key:1, Value:"Fruit1" } // ] KeyFuncs
只要类型拥有一个运算符
==
和一个非成员GetTypeHash
重载,则可被用作 TMap 的一个 KeyType,无需进行任何修改。然而,不便于重载这些函数时,可将类型作为键使用。这些情况如下,可以提供自定义的 KeyFuncsKeyFuncs 需要2个 typedefs 和 3个静态函数定义:
- KeyInitType - 用于传递键
- ElementInitType - 用于传递元素
- KeyInitType GetSetKey(ElementInitType Element) - 返回元素的键
- bool Matches(KeyInitType A, KeyInitType B) -返回 A 和 B 是否相当等
- uint32 GetKeyHash(KeyInitType Key) - 返回键的散列值
KeyInitType
和ElementInitType
是键类型和元素类型普通传递惯例的 typedefs。它们通常为浅显类型的一个值和非浅显类型的一个常量引用。需牢记:映射的元素类型为 TPair杂项
CountBytes
和GetAllocatedSize
函数用于估计阵列当前应用的内存量。CountBytes 接受 FArchive,GetAllocatedSize 可被直接调用。它们常用于统计报告。
Dump
函数接受 FOutputDevice 并写出关于映射内容的部分实现信息。它通常用于调试。
TSet 保存唯一值的合集,与 std::set 相似。TArray 通过 AddUnique 和 Contains 方法可用作集。然而 TSet 可更快实现这些操作,但无法像 TArray 那样将它们用作 UPROPERTY。TSet 不会像 TArray 那样将元素编入索引。
TSet
ActorSet = GetActorSetFromSomewhere(); int32 Size = ActorSet.Num(); // 如集尚未包含元素,则将其添加到集 AActor* NewActor = GetNewActor(); ActorSet.Add(NewActor); // 检查元素是否已包含在集中 if (ActorSet.Contains(NewActor)) { // ... } // 从集移除元素 ActorSet.Remove(NewActor); // 从集移除所有元素 ActorSet.Empty(); // 创建包含 TSet 元素的 TArray TArray ActorArrayFromSet = ActorSet.Array(); 需注意:TArray 是当前唯一能被标记为 UPROPERTY 的容器类。这意味着无法复制、保存其他容器类,或对其元素进行垃圾回收
容器迭代器
使用迭代器可在容器的每个元素上进行循环。以下是使用 TSet 的迭代器语法范例
void RemoveDeadEnemies(TSet
& EnemySet) { // 从集的开头开始迭代到集的末端 for (auto EnemyIterator = EnemySet.CreateIterator(); EnemyIterator; ++EnemyIterator) { // * 运算符获得当前的元素 AEnemy* Enemy = *EnemyIterator; if (Enemy.Health == 0) { // RemoveCurrent 由 TSets 和 TMaps 支持 EnemyIterator.RemoveCurrent(); } } } 可结合迭代器使用的其他支持操作:
// 将迭代器移回一个元素 --EnemyIterator; // 以一定偏移前移或后移迭代器,此处的偏移为一个整数 EnemyIterator += Offset; EnemyIterator -= Offset; // 获得当前元素的索引 int32 Index = EnemyIterator.GetIndex(); // 将迭代器重设为第一个元素 EnemyIterator.Reset();
For-each 循环
迭代器很实用,但如果只希望在每个元素之间循环一次,则可能会有些累赘。每个容器类还支持 for each 风格的语法在元素上进行循环。TArray 和 TSet 返回每个元素,而 TMap 返回一个键值对。
// TArray TArray
ActorArray = GetArrayFromSomewhere(); for (AActor* OneActor :ActorArray) { // ... } // TSet - 和 TArray 相同 TSet ActorSet = GetSetFromSomewhere(); for (AActor* UniqueActor :ActorSet) { // ... } // TMap - 迭代器返回一个键值对 TMap NameToActorMap = GetMapFromSomewhere(); for (auto& KVP :NameToActorMap) { FName Name = KVP.Key; AActor* Actor = KVP.Value; // ... } 注意:auto 关键词不会自动指定指针/引用,需要自行添加
通过 TSet/TMap(散列函数)使用您自己的类型
TSet 和 TMap 需要在内部使用 散列函数。如要创建在 TSet 中使用或作为 TMap 键使用的自定义类,首先需要创建自定义散列函数。通常会放入这些类型的多数 UE4 类型已定义其自身的散列函数
散列函数接受到您的类型的常量指针/引用,并返回一个 uint64。此返回值即为对象的 散列代码,应该是对该对象唯一虚拟的数值。两个相等的对象固定返回相同的散列代码
class FMyClass { uint32 ExampleProperty1; uint32 ExampleProperty2; // 散列函数 friend uint32 GetTypeHash(const FMyClass& MyClass) { // HashCombine 是将两个散列值组合起来的效用函数 uint32 HashCode = HashCombine(MyClass.ExampleProperty1, MyClass.ExampleProperty2); return HashCode; } // 出于展示目的,两个对象为相等 // 应固定返回相同的散列代码。 bool operator==(const FMyClass& LHS, const FMyClass& RHS) { return LHS.ExampleProperty1 == RHS.ExampleProperty1 && LHS.ExampleProperty2 == RHS.ExampleProperty2; } };
现在, TSet 和 TMap 在散列键时将使用适当的散列函数。如您使用指针作为键(即
TSet
),也将实现uint32 GetTypeHash(const FMyClass* MyClass)
对象迭代器是非常实用的工具,用于在特定 UObject 类型和子类的所有实例上进行迭代
// 将找到当前所有的 UObjects 实例
for (TObjectIterator It; It; ++It)
{
UObject* CurrentObject = *It;
UE_LOG(LogTemp, Log, TEXT("Found UObject named:%s"), *CurrentObject.GetName());
}
为迭代器提供更为明确的类型即可限制搜索范围。假设有一个派生自 UObject,名为 UMyClass 的类,则可以如下迭代:
for (TObjectIterator It; It; ++It)
{
// ...
}
在PIE(Play In Editor)中使用对象迭代器可能出现意外后果。因为编辑器已被加载,除编辑器正在使用的对象外,对象迭代器还将返回为游戏世界实例创建的全部 UObject
Actor 迭代器与 Object 迭代器的工作方式非常相近,但只能用于派生自 AActor 的对象。Actor 迭代器不存在上述问题,只返回当前游戏世界实例使用的对象
创建 Actor 迭代器时,需要为其赋予一个指向 UWorld 实例的指针。许多 UObject (如 APlayerController)会提供 GetWorld
方法。如果不确定,可在 UObject 上检查 ImplementsGetWorld
方法,确认其是否应用 GetWorld 方法
APlayerController* MyPC = GetMyPlayerControllerFromSomewhere();
UWorld* World = MyPC->GetWorld();
// 和Object迭代器一样。可以提供特定类,只获取为该类的对象,或从该类派生的对象
for (TActorIterator It(World); It; ++It)
{
// ...
}