TSet
官方文档:虚幻引擎中的Set容器 | 虚幻引擎 5.5 文档 | Epic Developer Community (epicgames.com)
TSet
是通过对元素求值的可覆盖函数,使用数据值本身作为键,而不是将数据值与独立的键相关联。
默认情况下,TSet
不支持重复的键,但使用模板参数可激活此行为。
TSet
是一种快速容器类,用于在排序不重要的情况下存储唯一元素。TSet
可以非常快速地添加、查找和删除元素(恒定时间)。在大多数情况下,只需要一种参数——元素类型。但是,TSet
可配置各种模板参数来改变其行为,使其更全面。除了可指定从 DefaultKeyFuncs
的派生结构体来提供散列功能,还可允许集合中的多个键拥有相同的值。它和其它容器类一样,可设置自定义内存分配器来存储数据。
和 TArray
一样,TSet
是同质容器。TSet
也是值类型,支持常规复制、赋值和析构函数操作,以及其元素较强的所有权。TSet
被销毁时,其元素也将被销毁。键类型也必须是值类型。
TSet
使用散列,即如果给出了 KeyFuncs
模板参数,该参数会告知集合如何从某个元素确定键,如何比较两个键是否相等,如何对键进行散列,以及是否允许重复键。它们默认只返回对键的引用,使用 运算符==
对比相等性,使用非成员函数 GetTypeHash
进行散列。默认情况下,集合中不允许有重复的键。如果您的键类型支持这些函数,则可以将其用作集合键,无需提供自定义 KeyFuncs
。要写入自定义 KeyFuncs
,可扩展 DefaultKeyFuncs
结构体。
最后,TSet
可通过任选分配器控制内存分配行为。标准虚幻引擎4(UE4)分配器(如 FHeapAllocator
和 TInlineAllocator
)不能用作 TSet
的分配器。实际上,TSet
使用集合分配器,该分配器可定义集合中使用的散列桶数量以及用于存储元素的标准UE4分配器。
与 TArray
不同的是,内存中 TSet
元素的相对排序既不可靠也不稳定,对这些元素进行迭代很可能会使它们返回的顺序和它们添加的顺序有所不同。这些元素也不太可能在内存中连续排列。集合中的后台数据结构是稀疏数组(在数组中有空位。从集合中移除元素时,稀疏数组中会出现空位)。将新的元素添加到阵列可填补这些空位。
但是,即便 TSet
不会打乱元素来填补空位,指向集元素的指针仍然可能失效,因为如果存储器被填满,又添加了新的元素,整个存储可能会重新分配。
1.创建和填充集合
创建一个MapActor类并继承于Actor,其与TArray创建方法一样就不一一详细介绍;
Actor类头文件增添代码:
public:
UFUNCTION(BlueprintCallable)
void InitSet();
源文件增添代码:
void ASetActor::InitSet()
{
TSet<FString> FruitSet;
//此处的元素按插入顺序排列,但不保证这些元素在内存中实际保留此排序。
//如果是新集合,可能会保留插入排序,但插入和删除的次数越多,新元素不出现在末尾的可能性越大。
FruitSet.Add(TEXT("Banana"));
FruitSet.Add(TEXT("Grapefruit"));
FruitSet.Add(TEXT("Pineapple"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple" ]
FruitSet.Add(TEXT("Pear"));
FruitSet.Add(TEXT("Banana"));//此处与上面的键重复因此覆盖了,但此处会触发扩容
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear" ]
// Note:Only one banana entry.
//此处,参数直接传递给键类型的构造函数。这可以避免为该值创建临时 FString。
//与 TArray 不同的是,只能使用单一参数构造函数将元素放到集合中。
FruitSet.Emplace(TEXT("Orange"));//追加元素,用Emplace函数代替Add避免插入集合时创造临时文件
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear", "Orange" ]
TSet<FString> FruitSet2;
FruitSet2.Emplace(TEXT("Kiwi"));
FruitSet2.Emplace(TEXT("Melon"));
FruitSet2.Emplace(TEXT("Mango"));
FruitSet2.Emplace(TEXT("Orange"));
FruitSet.Append(FruitSet2);
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear", "Orange", "Kiwi", "Melon", "Mango" ]
}
注意元素以及Num的变化,从而去理解TSet键的原理 (使用数据值本身作为键)
2.编辑UPROPERTY TSet
如果用 UPROPERTY
宏和一个可编辑的关键词(EditAnywhere
、EditDefaultsOnly
或 EditInstanceOnly
)标记 TSet
,则可在虚幻编辑器中添加和编辑元素。
Actor类头文件增添代码:
public:
UPROPERTY(BlueprintReadWrite,EditAnyWhere,Category = SetExample)
TSet<FString> MyFruitSet;
在蓝图可编辑操作
此处为FString,但其也可以放结构体
3.迭代
Actor类头文件增添代码:
public:
UFUNCTION(BlueprintCallable)
void LoopSet();
源文件增添代码:
void ASetActor::LoopSet()
{
TSet<FString> FruitSet;
FruitSet.Add(TEXT("Banana"));
FruitSet.Add(TEXT("Grapefruit"));
FruitSet.Add(TEXT("Pineapple"));
//依次输出元素
for (auto& Elem :FruitSet)//此处的auto可换为FString,其本质时自动推导类型
{
FPlatformMisc::LocalPrint(*FString::Printf(TEXT(" \"%s\"\n"),*Elem));
}
//创建迭代器(如为CreateConstIterators 函数则为常量迭代器)
for (auto It = FruitSet.CreateIterator(); It; ++It)
{
//注意后面*(*It),第一个*是把FString类型变成TCHAR,第二个是指针,因为迭代器It是地址
FPlatformMisc::LocalPrint(*FString::Printf(TEXT("(%s)\n"),*(*It)));
}
for (auto It = FruitSet.CreateConstIterator(); It; ++It)
{
FPlatformMisc::LocalPrint(*FString::Printf(TEXT("(%s)\n"),*(*It)));
}
}
4.查询
Actor类头文件增添代码:
public:
UFUNCTION(BlueprintCallable)
void QuerySet();
源文件增添代码:
void ASetActor::QuerySet()
{
TSet<FString> FruitSet;
bool bHave=false;
FruitSet.Add(TEXT("Banana"),&bHave);//此处bHave为false
FruitSet.Add(TEXT("Grapefruit"));
FruitSet.Add(TEXT("Pineapple"));
FruitSet.Add(TEXT("Banana"),&bHave);//此处使得bHave为true
int32 Count = FruitSet.Num();
// Count == 3
//要确定集合是否包含特定元素,可按如下所示调用 Contains 函数
bool bHasBanana = FruitSet.Contains(TEXT("Banana"));
bool bHasLemon = FruitSet.Contains(TEXT("Lemon"));
// bHasBanana == true
// bHasLemon == false
//使用 FSetElementId 结构体可查找集合中某个键的索引。
FSetElementId SetElementId=Fruit.Add(TEXT("Water"));//FSetElementId为标识符,此处SetElementId={Index=3}
FruitSet[SetElementId]+=TEXT("Modify");//此处将Index为3的元素进行修改为"WaterModify"
//使用 Find 函数查找一次即可完成这些行为。
//如果集合中包含该键,Find 将返回指向元素数值的指针。如果映射不包含该键,则返回null。对常量集合调用Find,返回的指针也将为常量。
FString* PtrBanana = FruitSet.Find(TEXT("Banana"));
FString* PtrLemon = FruitSet.Find(TEXT("Lemon"));
// *PtrBanana == "Banana"
// PtrLemon == nullptr
//Array 函数会返回一个 TArray,其中填充(覆盖,即使有元素在内)了 TSet 中每个元素的一份副本。因此元素的生成数量将始终等于集合中的元素数量
TArray<FString> FruitArray = FruitSet.Array();//用Array函数转换为TArray数组
}
5.移除
Actor类头文件增添代码:
public:
UFUNCTION(BlueprintCallable)
void RemoveSet();
源文件增添代码:
void ASetActor::RemoveSet()
{
TSet<FString> FruitSet;
FruitSet.Reserve(4);
FruitSet.Add(TEXT("Banana"));
FruitSet.Add(TEXT("Grapefruit"));
FruitSet.Add(TEXT("Pineapple"));
FruitSet.Add(TEXT("GG"));
FruitSet.Add(TEXT("HH"));
FruitSet.Add(TEXT("JJ"));
//Remove函数有两种使用参数方法
FruitSet.Remove(FSetElementId::FromInterger(0));//通过索引移除第一个元素
//Remove函数会返回已删除元素的数量。
int32 GGNum=FruitSet.remove(TEXT("GG"));//GGNum=1
int32 MMNum=FruitSet.remove(TEXT("MM"));//MMNum=0
TSet<FString> FruitSet1=FruitSet;
FruitSet1.Reset();//将集合中的所有元素移除,但内存空间还在
TSet<FString> FruitSet2=FruitSet;
FruitSet2.Empty(0);//此操作是将元素及其内存空间都删除
}
6.排序&运算符&Slack
6.1排序
TSet
可以排序。排序后,迭代集合会以排序的顺序显示元素,但下次修改集合时,排序可能会发生变化。由于排序不稳定,可能按任何顺序显示集合中支持重复键的等效元素。
Actor类头文件增添代码:
public:
UFUNCTION(BlueprintCallable)
void SortSet();
源文件增添代码:
void ASetActor::SortSet()
{
TSet<FString>FruitSet={ "Orange","Pear", "Melon", "Grapefruit", "Mango", "Kiwi"};
FruitSet.Sort([](const FString& A, const FString& B) {
return A > B; // sort by reverse-alphabetical order
});
// FruitSet == [ "Pear", "Orange", "Melon", "Mango", "Kiwi", "Grapefruit" ] (order is temporarily guaranteed)
//Sort 函数使用指定排序顺序的二进制谓词
FruitSet.Sort([](const FString& A, const FString& B) {
return A.Len() < B.Len(); // sort strings by length, shortest to longest
});
// FruitSet == [ "Pear", "Kiwi", "Melon", "Mango", "Orange", "Grapefruit" ] (order is temporarily guaranteed)
}
6.2运算符
其和 TArray
一样,TSet
是常规值类型,可通过标准复制构造函数或赋值运算符进行复制。因为集合严格拥有其元素,复制集合的操作是深层的,所以新集合将拥有其自身的元素副本。
Actor类头文件增添代码:
public:
UFUNCTION(BlueprintCallable)
void OpeatorSet();
源文件增添代码:
void ASetActor::SortSet()
{
TSet<FString>FruitSet={ "Orange","Pear", "Melon", "Grapefruit", "Mango", "Kiwi"};
TSet<FString> NewSet = FruitSet;
//在新的进行增删改查
NewSet.Add(TEXT("Apple"));
NewSet.Remove(TEXT("Pear"));
}
//在移除目录处也有提及
6.3Slack
其用法与TMap容器大致相似
Actor类头文件增添代码:
public:
UFUNCTION(BlueprintCallable)
void SlackSet();
源文件增添代码:
void ASetActor::SortSet()
{
TSet<FString>FruitSet={ "Orange","Pear", "Melon", "Grapefruit", "Mango", "Kiwi"};
//Reset函数可在不取消任何内存的情况下移除集合中的所有元素,从而产生slack
FruitSet.Reset();
FruitSet.Reserve(10);//在原基础上追加预分配10个内存
for (int32 i = 0; i < 10; ++i)
{
FruitSet.Add(FString::Printf(TEXT("Fruit%d"), i));
}
// FruitSet == [ "Fruit9", "Fruit8", "Fruit7" ..."Fruit2", "Fruit1", "Fruit0" ]
// Remove every other element from the set.
for (int32 i = 0; i < 10; i += 2)
{
FruitSet.Remove(FSetElementId::FromInteger(i));
}
// FruitSet == ["Fruit8", <invalid>, "Fruit6", <invalid>, "Fruit4", <invalid>, "Fruit2", <invalid>, "Fruit0", <invalid> ]
//Shrink裁剪元素
FruitSet.Shrink();
//注意此处数组Max值为10
// FruitSet == ["Fruit8", <invalid>, "Fruit6", <invalid>, "Fruit4", <invalid>, "Fruit2", <invalid>, "Fruit0" ]
//CompactStable函数压缩元素,Compact函数则可能改变排序
FruitSet.CompactStable();
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0", <invalid>, <invalid>, <invalid>, <invalid> ]
FruitSet.Shrink();
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0" ]
}