[UE C++] TMap
继 TArray
之后,虚幻引擎4(UE4)中最常用的容器是 TMap
。TMap
与 TSet
类似,它们的结构均基于对键进行散列运算。但与 TSet
不同的是,此容器将数据存储为键值对(TPair<KeyType, ValueType>),只将Key用于存储和获取。
Map有两种类型:TMap
和 TMultiMap
。两者之间的不同点是,TMap
中的Key是 唯一 的,而TMultiMap
可存储多个相同的Key。在 TMap
中添加新的Key值时,若所用的Key与原有的键值对相同,新的键值对将 替换 原有的键值对。在 TMultiMap
中,容器可以同时 存储新键值对和原有的键值对。
- 元素类型为 键值对(TPair<KeyType, ValueType>),但很少需要直接引用
TPair
类型 - 同质容器,所有元素的类型都应完全相同
- 值类型,支持通常的复制、赋值和析构函数运算,以及它的元素的强所有权。在
TMap
被销毁时,它的元素都会被销毁。键和值也必须为值类型 - 散列容器,这意味着Key类型必须支持
GetTypeHash
函数,并提供 运算符==
来比较各个键是否等值。 - 支持分配器来控制内存分配行为,但不同于
TArray
,这些是集合分配器,而不是FHeapAllocator
和TInlineAllocator
之类的标准UE4分配器。集合分配器(TSetAllocator
类)定义映射应使用的散列桶数量,以及应使用哪个标准UE4分配器来存储散列和元素。 - KeyFuncs 是最后一个
TMap
模板参数,该参数告知映射如何从元素类型获取键,如何比较两个键是否相等,以及如何对键进行散列计算。这些参数有默认值,它们只会返回对键的引用,使用 运算符==
确定相等性,并调用非成员GetTypeHash
函数进行散列计算。如果您的键类型支持这些函数,可使用它作为映射键,不需要提供自定义 KeyFuncs - 与
TArray
不同的是,内存中TMap
元素的相对排序 既不可靠也不稳定,对这些元素进行迭代很可能会使它们返回的顺序和它们添加的顺序有所不同 TMap
元素不太可能在内存中连续排列,TMap
的支持数据结构是 稀疏数组,这种数组可有效支持元素之间的空位。当元素从映射中被移除时,稀疏数组中就会出现空位。将新的元素添加到数组可填补这些空位。但是,即便TMap
不会打乱元素来填补空位,指向映射元素的指针仍然可能失效,因为如果存储器被填满,又添加了新的元素,整个存储可能会重新分配。
1. 创建和初始化
// 默认构造
TMap<int32, FString> TestMap;
// 拷贝构造
TMap<int32, FString> TempMap{ TestMap };
// 移动拷贝构造
TMap<int32, FString> TempMap{ MoveTemp(TestMap) };
// 初始化列表
TMap<int32, FString> TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
// TTuple/TPair 初始化列表
TMap<int32, FString> TestMap = { TTuple<int32,FString> { 1,TEXT("PairOne") }, TPair<int32,FString>{2,TEXT("Two")} ,{3,TEXT("Three")} };
2. 增
2.1 Add
添加一个元素到TMap
中,共有 8 种重载,支持移动语义,可分为三类。返回值为TMap中的Value的引用,但需要注意该引用仅在下次更改TMap中的任何键之前有效(添加了新的元素,整个存储可能会重新分配)。
// 输入Key 和 Value
ValueType& Add(const KeyType& InKey, const ValueType& InValue);
// 仅输入Key,Value会调用默认构造函数,FString的为""
ValueType& Add(const KeyType& InKey);
// 输入TTuple
ValueType& Add(const TTuple<KeyType, ValueType>& InKeyValue);
TestMap.Add(1, TEXT("One"));
TestMap.Add(2);
TestMap.Add(MakeTuple<int32, FString>(3, TEXT("Three")));
// TestMap == [
// { Key:1, Value:"One" },
// { Key:2, Value:"" },
// { Key:3, Value:"Three" }
// ]
此处的元素按插入顺序排列,但不保证这些元素在内存中实际保留此排序。如果是新的映射,可能会保留插入排序,但插入和删除的次数越多,新元素不出现在末尾的可能性就越大。
这不是 TMultiMap
,所以各个键都必定是唯一。如果尝试添加重复键,将发生以下情况:
TestMap.Add(2, TEXT("Two"));
// TestMap == [
// { Key:1, Value:"One" },
// { Key:2, Value:"Two" },
// { Key:3, Value:"Three" }
// ]
映射仍然包含3个元素,但之前键值为2的""已被"Two"替代。
2.2 Emplace
和 TArray
一样,还可使用 Emplace
代替 Add
,防止插入 TMap
时创建临时文件。但与 TArray
不同的是,只能通过 单一参数构造函数 将元素插入到到TMap中。返回值和 Add
一样
// 输入单参数构造Key,Value默认构造
template <typename InitKeyType>
ValueType& Emplace(InitKeyType&& InKey);
// 输入两个参数分别构造Key,与Value
template <typename InitKeyType, typename InitValueType>
ValueType& Emplace(InitKeyType&& InKey, InitValueType&& InValue);
TestMap.Emplace(1);
TestMap.Emplace(2, TEXT("Two"));
// TestMap == [
// { Key:1, Value:"" },
// { Key:2, Value:"Two" },
// ]
此处直接将键和值传递给了各自的构造函数。这对 int32 键实际上没有影响,但避免了为该值创建临时 FString
2.3 Append
将另一个TMap元素添加到TMap,支持移动语义。
TestMap.Emplace(1);
TestMap.Emplace(2, TEXT("Two"));
TMap<int32, FString> TempMap{ {1,TEXT("One")},{3,TEXT("Three")} };
TestMap.Append(TempMap);
// TestMap == [
// { Key:1, Value:"One" },
// { Key:2, Value:"Two" },
// { Key:3, Value:"Three" }
// ]
3. 删
3.1 Remove
根据Key值删除指定的键值对,返回值是被移除元素的数量,如果TMap不包含与Key匹配的元素,则返回值可为零。
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
TestMap.Remove(1);
// TestMap == [
// { Key:2, Value:"Two" },
// { Key:3, Value:"Three" }
// ]
3.2 FindAndRemoveChecked
从TMap移除元素并返回其Value。名称的"Checked"部分表示若Key不存在,TMap将调用 check
(UE4中等同于 assert
)。
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
TestMap.FindAndRemoveChecked(1);
// Assert!
TestMap.FindAndRemoveChecked(5);
3.3 RemoveAndCopyValue
作用与 Remove
相似,不同点是会将已移除元素的值复制到引用参数。如果TMap中不存在指定的键,则输出参数将保持不变,函数将返回 false
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
FString TempString;
// TempString == "One",Return == true
TestMap.RemoveAndCopyValue(1, TempString);
// TempString == "One" 没变化,Return == false
TestMap.RemoveAndCopyValue(5, TempString);
3.4 Empty
清空TMap,输入参数为TMap的Slack量,Default = 0
void Empty(int32 ExpectedNumElements = 0);
3.5 Reset
清空TMap,但不会改变TMap的内存分配。区别于TArray,无输入参数
void Reset();
4. 迭代
4.1 ranged-for
for (auto& Item : PrintMap)
{
Item.Key;
Item.Value;
}
4.2 元素迭代器
TMap.CreateIterator()
可读可写与 TMap.CreateConstIterator()
只读迭代器
for (auto It = TestMap.CreateIterator(); It; ++It)
{
// Key
It->Key;
It.Key();
(*It).Key;
// Value
It->Value;
It.Value();
(*It).Value;
}
4.3 Key迭代器
CreateKeyIterator()
可读可写与 CreateConstKeyIterator()
只读迭代器
在映射中与指定Key相关联的Value上创建迭代器,TMap中无用,因为一个Key只对应一个Value,用于 TMultiMap
TMultiMap<int32, FString> TestMulMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} ,{1,TEXT("OneOne")} };
for (auto Item = TestMulMap.CreateKeyIterator(1); Item; ++Item)
{
UE_LOG(LogTemp, Warning, TEXT("Key:%d\tValue:%s"), Item.Key(), *Item.Value());
}
// Key:1 Value:OneOne
// Key:1 Value:One
5. 查
5.1 Num
查询保存元素的数量
int32 Count = TestMap.Num();
5.2 Contains
根据输入的Key值,判断是否存在键值对,返回bool
bool Contains(KeyConstPointerType Key) const;
5.3 Find
根据输入的Key值,判断是否存在键值对,存在则返回Key对应Value的指针,反之返回nullptr
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
FString* TempString = TestMap.Find(1);
5.4 FindOrAdd
FindOrAdd
将返回对与给定Key关联的Value的 引用 。如果TMap中不存在该Key,FindOrAdd
将返回新创建的元素(使用给定Key和默认构建Value),该元素也会被添加到TMap。FindOrAdd
可修改 TMap
,因此 仅 适用于 非const TMap。
ValueType& FindOrAdd(const KeyType& Key);
// 可能会Add,所以存在移动语义
ValueType& FindOrAdd( KeyType&& Key);
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
FString TempString = TestMap.FindOrAdd(1);
// TempString == "One"
// TestMap == [
// { Key:1, Value:"One" },
// { Key:2, Value:"Two" },
// { Key:3, Value:"Three" }
// ]
TempString = TestMap.FindOrAdd(4);
// TempString == ""
// TestMap == [
// { Key:1, Value:"One" },
// { Key:2, Value:"Two" },
// { Key:3, Value:"Three" },
// { Key:4, Value:"" }
// ]
5.5 FindRef
FindRef
会返回与给定Key关联的Value副本;若 TMap
中 未找到 给定Key,则返回默认构建值。FindRef
不会 创建新元素,因此既可用于 const TMap,也可用于 非const TMap。
ValueType FindRef(KeyConstPointerType Key) const;
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
FString TempString = TestMap.FindRef(1);
// TempString == "One"
// TestMap == [
// { Key:1, Value:"One" },
// { Key:2, Value:"Two" },
// { Key:3, Value:"Three" }
// ]
TempString = TestMap.FindRef(4);
// TempString == ""
// TestMap == [
// { Key:1, Value:"One" },
// { Key:2, Value:"Two" },
// { Key:3, Value:"Three" }
// ]
即使在 TMap
中找不到Key,FindOrAdd
和 FindRef
也会成功运行,因此无需执行常规的安全规程(如提前检查 Contains 或对返回值进行Valid检查)就可安全地调用。
5.6 FindKey
执行逆向查找,这意味着提供的Value与Key匹配,并返回指向与所提供Value配对的 第一个 Key的指针。搜索TMap中不存在的Value将返回nullptr。
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
// *TempInt == 1
const int32* TempInt = TestMap.FindKey(TEXT("One"));
请注意返回 第一个 无法保证返回的是首先添加的,只是返回首先查询到的
5.7 GenerateKeyArray
用 所有 Key的 副本 来填充TArray。会将TArray置空,因此产生的元素数量始终等于 Map 中的元素数量。
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
TArray<int32> TempArray = TestMap.GenerateKeyArray(TempArray);
// TempArray == [1,2,3];
5.8 GenerateValueArray
用 所有 Value的 副本 来填充TArray。会将TArray置空,因此产生的元素数量始终等于 Map 中的元素数量。
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
TArray<FString> TempArray = TestMap.GenerateKeyArray(TempArray);
// TempArray == ["One","Two","Three"];
5.9 GetKeys
获取此映射中包含的唯一键。在TMap中作用和 GenerateKeyArray
相同,但是在TMultiMap
中,会获得筛选过后的TArray/TSet。返回值为唯一键的数量(Num)。在文章中都是以 TMap
叙述,但其实 TMultiMap
也存在这些API,它们都继承于 TMapBase
。
int32 GetKeys(TArray<KeyType, Allocator>& OutKeys) const;
int32 GetKeys(TSet<KeyType, Allocator>& OutKeys) const;
5.10 FilterByPredicate
通过谓词对 TMap
进行筛选,返回筛选过后的 TMap
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
TMap<int32, FString> TempMap = TestMap.FilterByPredicate([](TPair<int32, FString> Item) { return Item.Key <= 2; });
6. 排序
TMap 可以进行排序。排序后,迭代映射会以排序的顺序显示元素,但下次修改映射时,排序可能会发生变化。
6.1 KeySort
Key排序,不稳定,等值元素在MultiMap中可能以任何顺序出现。输入参数为二元谓词
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
TestMap.KeySort([](int32 A, int32 B) { return A > B; });
// TestMap == [
// { Key:3, Value:"Three" },
// { Key:2, Value:"Two" },
// { Key:1, Value:"One" }
// ]
TMultiMap<int32, FString> TestMulMap = { {1,TEXT("One")}, {1,TEXT("OneOne")},{2,TEXT("Two")} ,{3,TEXT("Three")} };
TestMulMap.KeySort([](int32 A, int32 B) { return A > B; });
// TestMulMap == [
// { Key:3, Value:"Three" },
// { Key:2, Value:"Two" },
// { Key:1, Value:"OneOne" },
// { Key:1, Value:"One" }
// ]
6.2 KeyStableSort
Key排序,稳定。等值元素在MultiMap中会保持现有顺序
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
TestMap.KeySort([](int32 A, int32 B) { return A > B; });
// TestMap == [
// { Key:3, Value:"Three" },
// { Key:2, Value:"Two" },
// { Key:1, Value:"One" }
// ]
TMultiMap<int32, FString> TestMulMap = { {1,TEXT("One")}, {1,TEXT("OneOne")},{2,TEXT("Two")} ,{3,TEXT("Three")} };
TestMulMap.KeySort([](int32 A, int32 B) { return A > B; });
// TestMulMap == [
// { Key:3, Value:"Three" },
// { Key:2, Value:"Two" },
// { Key:1, Value:"One" },
// { Key:1, Value:"OneOne" }
// ]
6.3 ValueSort/ValueStableSort
Value排序,不稳定/稳定,用法和Key一致。
7. 运算符
7.1 =
赋值运算符,和构造函数差不多,此处不赘述
7.2 []
通过Key得到对应Value的引用,内部调用了 FindChecked
,Key不存在会触发Assert
TestMap = { {1,TEXT("One")}, {2,TEXT("Two")} ,{3,TEXT("Three")} };
TestMap[1] = TEXT("OneOne");
// TestMap == [
// { Key:1, Value:"OneOne" },
// { Key:2, Value:"Two" },
// { Key:3, Value:"Three" }
// ]
8. Slack
Slack是不包含元素的已分配内存。调用 Reserve
可分配内存,无需添加元素; 调用 Reset
或 通过非零slack参数调用 Empty
可移除元素,无需将其使用的内存取消分配。Slack优化了将新元素添加到映射的过程,因为可以使用预先分配的内存,而不必分配新内存。它在移除元素时也十分实用,因为系统不需要将内存取消分配。在清空希望用相同或更少的元素立即重新填充的映射时,此方法尤其有效。
TMap 不像 TArray 中的 Max 函数那样可以检查预分配元素的数量。
8.1 Reserve
预先分配内存,若输入的Number大于元素个数,则会产生Slack
TestMap.Reserve(10);
8.2 Shrink
从容器的末端移除所有slack,但这会在中间或开始处留下空白元素。若想移除所有slack,需要先调用 Compact
TMap<int32,FString> FruitMap;
for (int32 i = 0; i < 10; i += 2)
{
FruitMap.Remove(i);
}
// FruitMap == [
// { Key:9, Value:"Fruit9" },
// <invalid>,
// { Key:7, Value:"Fruit7" },
// <invalid>,
// { Key:5, Value:"Fruit5" },
// <invalid>,
// { Key:3, Value:"Fruit3" },
// <invalid>,
// { Key:1, Value:"Fruit1" },
// <invalid>
// ]
FruitMap.Shrink();
// FruitMap == [
// { Key:9, Value:"Fruit9" },
// <invalid>,
// { Key:7, Value:"Fruit7" },
// <invalid>,
// { Key:5, Value:"Fruit5" },
// <invalid>,
// { Key:3, Value:"Fruit3" },
// <invalid>,
// { Key:1, Value:"Fruit1" }
// ]
在上述代码中,Shrink 只删除了一个无效元素,因为末端只有一个空元素。
8.3 Compact
将空白元素组合到一起放在末尾。下述例子元素顺序不一定正确,Compact
可能会改变元素之间的顺序。若不想改变顺序,可以使用 CompactStable
FruitMap.Compact();
// FruitMap == [
// { Key:9, Value:"Fruit9" },
// { Key:7, Value:"Fruit7" },
// { Key:5, Value:"Fruit5" },
// { Key:3, Value:"Fruit3" },
// { Key:1, Value:"Fruit1" },
// <invalid>,
// <invalid>,
// <invalid>,
// <invalid>
// ]
FruitMap.Shrink();
// FruitMap == [
// { Key:9, Value:"Fruit9" },
// { Key:7, Value:"Fruit7" },
// { Key:5, Value:"Fruit5" },
// { Key:3, Value:"Fruit3" },
// { Key:1, Value:"Fruit1" }
// ]
9. KeyFuncs
只要类型具有 运算符 ==
和非成员 GetTypeHash
重载,就可用作 TMap 的键类型,不需要任何更改。但是,您可能需要将类型用作键,而不重载这些函数。在这些情况下,可对 KeyFuncs 进行自定义。
为键类型创建 KeyFuncs,必须定义两个typedef和三个静态函数,如下所示:
KeyInitType
—— 用于传递Key的类型。ElementInitType
—— 用于传递元素的类型。KeyInitType GetSetKey(ElementInitType Element)
—— 返回元素的Key。bool Matches(KeyInitType A, KeyInitType B)
—— 如果 A 和 B 等值将返回 true,否则返回 false。uint32 GetKeyHash(KeyInitType Key)
—— 返回 Key 的散列值。
KeyInitType
和 ElementInitType
是Key类型和值类型的常规传递约定的typedef。它们通常为浅显类型的一个值,和非浅显类型的一个常量引用。请记住,映射的元素类型是 TPair。
作为Key的Struct:
struct FMyStruct
{
// String which identifies our key
FString UniqueID;
// Some state which doesn't affect struct identity
float SomeFloat;
explicit FMyStruct(float InFloat)
:UniqueID (FGuid::NewGuid().ToString())
, SomeFloat(InFloat)
{
}
};
自定义 KeyFuncs:
struct FMyStruct
{
// String which identifies our key
FString UniqueID;
// Some state which doesn't affect struct identity
float SomeFloat;
explicit FMyStruct(float InFloat)
:UniqueID (FGuid::NewGuid().ToString())
, SomeFloat(InFloat)
{
}
};
template <typename ValueType>
struct TMyStructMapKeyFuncs :
BaseKeyFuncs<
TPair<FMyStruct, ValueType>,
FString
>
{
private:
typedef BaseKeyFuncs<
TPair<FMyStruct, ValueType>,
FString
> Super;
public:
typedef typename Super::ElementInitType ElementInitType;
typedef typename Super::KeyInitType KeyInitType;
static KeyInitType GetSetKey(ElementInitType Element)
{
return Element.Key.UniqueID;
}
static bool Matches(KeyInitType A, KeyInitType B)
{
return A.Compare(B, ESearchCase::CaseSensitive) == 0;
}
static uint32 GetKeyHash(KeyInitType Key)
{
return FCrc::StrCrc32(*Key);
}
};
FMyStruct 具有唯一标识符(UniqueID
),以及一些与身份无关的其他数据(SomeFloat
)。GetTypeHash
和 运算符==
不适用于此,因为 运算符==
为实现通用目的不应忽略任何类型的数据,但同时又需要如此(忽略除UniqueID
之外的其他数据)才能与 GetTypeHash
的行为保持一致,后者(GetTypeHash
)只关注 UniqueID
字段。以下步骤有助于为 FMyStruct 创建自定义 KeyFuncs:
- 首先,继承
BaseKeyFuncs
,因为它可以帮助定义某些类型,包括KeyInitType
和ElementInitType
。
BaseKeyFuncs
使用两个模板参数:映射的元素类型和Key类型。- 和所有映射一样,元素类型是 TPair,使用 FMyStruct 作为其 KeyType,TMyStructMapKeyFuncs 的模板参数作为其 ValueType。将TMyStructMapKeyFuncs声明为模板类,并将模板参数ValueType使用于TPair,这样可以避免每次使用FMyStruct作为TMap的键时都要创建新的KeyFuncs
- 第二个 BaseKeyFuncs 参数是Key类型,不要与元素存储的键(TPair 的 KeyType)混淆。因为此映射应使用
UniqueID
(来自 FMyStruct)作为键,所以此处使用 FString。
- 然后,定义三个必需的 KeyFuncs 静态函数:
- 第一个是
GetSetKey
,该函数返回给定元素类型的Key。由于元素类型是 TPair,而键是UniqueID
,所以该函数可直接返回 UniqueID。 - 第二个静态函数是
Matches
,该函数接受两个元素的键(由GetSetKey
获取),然后比较它们是否相等。在 FString 中,标准的等效测试(运算符==)不区分大小写;要替换为区分大小写的搜索,请用相应的大小写对比选项使用 Compare 函数。 - 最后,
GetKeyHash
静态函数接受提取的键并返回其散列值。由于Matches
函数区分大小写,GetKeyHash
也必须区分大小写。区分大小写的FCrc
函数将计算键字符串的散列值。
- 第一个是
现在结构已满足 TMap
要求的行为,可创建它的实例。
TMap<
FMyStruct,
int32,
FDefaultSetAllocator,
TMyStructMapKeyFuncs<int32>
> MyMapToInt32;
// Add some elements
MyMapToInt32.Add(FMyStruct(3.14f), 5);
MyMapToInt32.Add(FMyStruct(1.23f), 2);
// MyMapToInt32 == [
// {
// Key:{
// UniqueID:"D06AABBA466CAA4EB62D2F97936274E4",
// SomeFloat:3.14f
// },
// Value:5
// },
// {
// Key:{
// UniqueID:"0661218447650259FD4E33AD6C9C5DCB",
// SomeFloat:1.23f
// },
// Value:5
// }
// ]
TMap
假设两个项目使用 Matches
比较的结果相等,则它们会从 GetKeyHash
返回相同的值。此外,如果对现有映射元素的键进行的修改将会改变来自这两个函数中任一个的结果,那么系统会将这种修改视作未定义的行为,因为这会使映射的内部散列失效。这些规则也适用于使用默认 KeyFuncs 时 运算符==
和 GetKeyHash
10. 其它
10.1 GetAllocatedSize
返回此容器分配的内存量,只返回容器直接分配的⼤⼩,⽽不返回元素本⾝。
10.2 CountBytes
计数序列化此数组所需的字节数。输入为FArchive对象。
最后
本文结合官方文档总结了TMap的一些常用API,若有错误,还请指正。