您的位置:首页 > 产品设计 > UI/UE

UE4入门-常见基本数据类型-容器

2017-11-03 17:16 1116 查看

容器

容器也是类,它们的主要功能是存储数据集。常见的类有 TArray、TMap 和 TSet。它们的大小均为动态,因此可变为所需的任意大小

TArray:虚幻引擎中的数组

主要使用的容器类为 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<FString> 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<int32> 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<FString> 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<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


重新分配后,容器中的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<FString> 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<S> 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

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<int32, 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<int32> FruitKeys;
TArray<FString> 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<int32, FString> 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" },
//  <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 调用移除,因为末端只有一个洞。
Compact
函数可用于在缩小前移除所有洞。

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" }
// ]


KeyFuncs

只要类型拥有一个运算符
==
和一个非成员
GetTypeHash
重载,则可被用作 TMap 的一个 KeyType,无需进行任何修改。然而,不便于重载这些函数时,可将类型作为键使用。这些情况如下,可以提供自定义的 KeyFuncs

KeyFuncs 需要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

TSet 保存唯一值的合集,与 std::set 相似。TArray 通过 AddUnique 和 Contains 方法可用作集。然而 TSet 可更快实现这些操作,但无法像 TArray 那样将它们用作 UPROPERTY。TSet 不会像 TArray 那样将元素编入索引。

TSet<AActor*> ActorSet = GetActorSetFromSomewhere();

int32 Size = ActorSet.Num();

// 如集尚未包含元素,则将其添加到集
AActor* NewActor = GetNewActor();
ActorSet.Add(NewActor);

// 检查元素是否已包含在集中
if (ActorSet.Contains(NewActor))
{
// ...
}

// 从集移除元素
ActorSet.Remove(NewActor);

// 从集移除所有元素
ActorSet.Empty();

// 创建包含 TSet 元素的 TArray
TArray<AActor*> ActorArrayFromSet = ActorSet.Array();


需注意:TArray 是当前唯一能被标记为 UPROPERTY 的容器类。这意味着无法复制、保存其他容器类,或对其元素进行垃圾回收

容器迭代器

使用迭代器可在容器的每个元素上进行循环。以下是使用 TSet 的迭代器语法范例

void RemoveDeadEnemies(TSet<AEnemy*>& 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<AActor*> ActorArray = GetArrayFromSomewhere();
for (AActor* OneActor :ActorArray)
{
// ...
}

// TSet - 和 TArray 相同
TSet<AActor*> ActorSet = GetSetFromSomewhere();
for (AActor* UniqueActor :ActorSet)
{
// ...
}

// TMap - 迭代器返回一个键值对
TMap<FName, AActor*> 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<FMyClass*>
),也将实现
uint32 GetTypeHash(const FMyClass* MyClass)


Object / Actor 迭代器

对象迭代器是非常实用的工具,用于在特定 UObject 类型和子类的所有实例上进行迭代

// 将找到当前所有的 UObjects 实例
for (TObjectIterator<UObject> It; It; ++It)
{
UObject* CurrentObject = *It;
UE_LOG(LogTemp, Log, TEXT("Found UObject named:%s"), *CurrentObject.GetName());
}


为迭代器提供更为明确的类型即可限制搜索范围。假设有一个派生自 UObject,名为 UMyClass 的类,则可以如下迭代:

for (TObjectIterator<UMyClass> 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<AEnemy> It(World); It; ++It)
{
// ...
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: