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

来源:互联网 发布:个人域名企业备案 编辑:程序博客网 时间:2024/04/20 04:27

容器

容器也是类,它们的主要功能是存储数据集。常见的类有 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]

AddEmplace 函数可用于在数组末端创建新对象

    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]

在所有这些情况中,当元素被移除时,其后的元素将被下移到更低的指数中,因为数组中不能出现空洞

移动过程存在开销。如不介意剩余元素的排序,可使用 RemoveSwapRemoveAtSwapRemoveAllSwap 函数减少此开销。这些函数的工作方式与其非交换变种相似,不同之处在于它们不保证剩余元素的排序,因此它们的实现效率更高:

    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];

HeapPopHeapPopDiscard 函数用于移除堆上的顶部节点。两者之间的区别是前者接受对元素类的引用,返回顶部元素的副本;而后者只是简单地移除顶部节点,不进行任何形式的返回。两个函数得出的数组变更一致,重新适当排列其他元素可对堆进行维护:

    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 将通过其拥有的信息尽量执行,但有时需要下降一个等级。

需要注意的是:这些函数允许使容器变为无效状态。如果出现失误,将引起未定义的行为。在调用这些函数后,调用其他常规函数之前,可决定是否使容器变回有效状态

AddUninitializedInsertUninitialized 函数将为数组添加一些未初始化的空间。它们的工作方式分别于 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"]

AddZeroedInsertZeroed 的工作方式相似,不同点是它们会把添加/插入的空间字节清零。如需将类型插入有效的按位零状态,这将非常实用

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

SetNumUninitializedSetNumZeroed 函数的工作方式与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)几乎不会发生变化。

杂项

BulkSerializeGetAllocateSize 函数用于估计数组当前应用的内存量。CountBytes 接受 FArchive,GetAllocatedSize 可被直接调用。它们常用于统计报告

SwapSwapMemory 函数均接受两个指数,将对这些指数上元素的数值进行交换。它们相等,不同点是 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

GenerateKeyArrayGenerateValueArray 函数分别允许以全部键个值得副本对数组进行填充。在两种情况下,被传递的数组在填入前会被清空,因此元素的生成数量将始终等于映射中的元素数量

    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进行临时排序。映射上的下次迭代将以顺序排序展示元素,之后对映射进行的修改可能导致映射重新排列。排序并不稳定,因此相等元素可能以各种排列方式出现。

KeySortValueSort 函数分别按键和值排序,两个函数均接受二元谓词指定排序顺序

    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 的概念,可用于优化映射的填入。ResetEmpty() 调用作用相似,但不会释放元素之前使用的内存。

    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) - 返回键的散列值

KeyInitTypeElementInitType 是键类型和元素类型普通传递惯例的 typedefs。它们通常为浅显类型的一个值和非浅显类型的一个常量引用。需牢记:映射的元素类型为 TPair

杂项

CountBytesGetAllocatedSize 函数用于估计阵列当前应用的内存量。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){    // ...}