本文章由cartzhang編寫,轉載請註明出處。 所有權利保留。
文章鏈接: http://blog.csdn.NET/cartzhang/article/details/45367171
作者:cartzhang
說明:這個爲虛幻官方翻譯的一篇文章。
原文地址:https://docs.unrealengine.com/latest/INT/Programming/UnrealArchitecture/TArrays/index.html
一個月前翻譯的,官方很忙,自己先貼這裏。
若有問題,請隨時聯繫!
時間:2015-03-27
翻譯:cartzhang
虛幻引擎的TArray:數組
檢查引擎版本爲虛幻4.7
概述
TArray是重要的指向性容器類。它是一個對類型相同的對象(稱爲元素)進行序列組織和管理的類。作爲一個TArray的類,本身就是一個序列,其元素有很好的順序性和它的方法用於準確的操作這些對象和他們的順序。
TArray
TArray是虛幻引擎中最常見的容器類。它被定義爲快速,安全和有效的內存處理類。TArray類型是由兩個屬性來定義的:主元素類型和可選的分配器。
元素的類型是指將要被存儲在數組中的對象類型。TArray被稱爲同類型的容器:因爲所有的元素類型必須是嚴格的同一類型。不允許存在混合類型的元素。
分配器是經常被忽略的,在大多數的使用時會自動缺省一個分配器。它定義了對象在內存中怎麼放置和數組是怎麼自動適應更多元素的情況。當你覺得缺省的分配器行爲不符合你的要求,你可以使用其他很多不同的分配器,或者你可以自定義分配器。這個將在下面詳細說明。
TArray是一個類型,就意味着它應該跟其他的內置類型,比如int32或float類型一樣來對待。它的設計不是用來繼承,並且在堆上創建/銷燬數組被認爲是一個不尋常的做法。容器擁有元素是值類型的,此類元素在數組銷燬時候被銷燬。從另外一個容器中創建一個TArray變量,將會拷貝容器中所有的元素到一個新的變量中;它們的狀態是不共享。
創建和填充一個數組
創建一個數組,定義如下:
TArray<int32> IntArray;
這是定義了一個空的數組來存放整數變量的隊列。元素的類型可以是任何可拷貝和可析構的符合常規C++值類型,比如:int32,FString,TSharedPtr等等……因爲沒有指定特殊的分配器,所以這個數組使用的就是常規的堆分配器。而此時,並沒有內存被分配。
TArray類型可以被幾種方式來填充。其中一種是通過單個數的多次拷貝來實現填充數組。
通過Add和Emplace兩種方式,在數組的最後來添加新的對象。
隨着元素的添加,數組的內存通過分配器來分配。Add和Emplace做的事情基本類似,但是有些微小的差別:
l Add將會拷貝(或移動)一個元素類型的實例到數組中。
l Emplace將根據你給的參數創建一個新的元素實例。
因此,在我們的TArray中,Add將會從給出的字符串文字中創建一個臨時的FString對象,然後把臨時對象的內容移動到容器中一個新的FString對象裏面;然而Emplace將會直接使用字符串文字來創建一個FString對象。結果是一樣的,但是Emplace避免了創建臨時對象,這個對於像FString這樣有實際構造的類型來說是非常不受歡迎的。Push和Add的使用是一樣的。
通常來說,Emplace是比Add更好一些的,對將要被容器拷貝或移動的對象,儘量的避免不必要的臨時變量創建。根據以往經驗,對於無實際構造的類型使用Add,否則使用Emplace。Emplace並不是總比Add的效率高,有時候Add也許讀效率更好點。
Append允許你一次添加多個元素。不管你是從另外一個TArray、一個指針類型+字節數或常規的C數組,都是可以的。
AddUnique只允許添加一個元素,並且容器中沒有與此元素相同的元素。相等的判定是使用元素類型的操作符==來實現的。
Insert,跟Add、Emplace和Append類型,它允許你在給出的序列號位置上添加單個元素或拷貝一串元素。
SetNum方法直接設置容器中元素個數。若設置的元素個數大於當前容器中元素個數,則新的元素就會通過元素類型的缺省構造函數來創建新元素。
SetNum在新設置的元素個數小於容器中元素個數時,也可以用來刪除元素。更多關於元素刪除細節稍後說明。
迭代
有幾種關於數組元素迭代的方法,但是仍舊推薦使用C++的範圍循環(ranged-for):
常規的基於順序的迭代當然也是可以的:
最後,數組自身的迭代器可以來控制迭代。這裏有兩種方法爲CreateIterator和CreateConstIterator,分別用於元素的讀寫和只讀操作。
排序
數組可以調用排序方法來進行簡單的排序。
這裏,元素的值通過元素類型的的操作符“<”來進行比較。在FString中,是不區分大小寫的字符順序比較。一個二元匹配條件也可提供不同的比較方式。例如:
現在,所有字符串按照其長度進行排序。注意到其中有三個字符串的長度是一樣的-:“Hello”,“Brave”和“World”,只是改變了在數組中之前的相對位置。這是因爲排序算法是不穩定的,相等的元素(這裏這些字符串是相等的,因爲他們有一樣的長度)排列順序是無保障的。排序算法比如:快速排序。
堆排序,無論是否帶二元的匹配條件,都可以實現堆排序。是否使用堆排序取決於你的特定數據和相對其他排序算法堆排序的有效性。跟之前排序算法相似,堆排序是不穩定排序算法。如果你使用堆排序代替之前的排序,以下是排序結果(本例下,結果是一樣的):
總之,穩定排序算法在排序後可以保證相等元素的相對位置。然而,如果使用快排或堆排序代替穩定排序,結果如下:
“Hello”,“Brave”和“World”在根據字母順序排序,排序後仍舊得到它們相同的相對位置。穩定排序算法比如歸併排序。
查詢
我們可以通過Num方法來知道數組中元素個數。
如果你需要直接訪問數組內存,或許爲了互操作c-類型的API,你可以使用GetData()方法來返回數組中的元素指針。指針有效性必須以數組存在和任何其他變異操作數組之前。StrPtr指向數組的指針就是指向Num()取得的第一個索引值的元素。
若容器類型是const,那麼返回的指針將也是const類型的。
你可以獲得容器可包含元素的空間大小:
你可以使用索引的操作符 “[]”來檢索元素,只需給它一個從零開始的你想得到的元素索引:
給一個無效的索引值-比0小或比大於等於Num()值的索引-將會導致運行時錯誤。你可以通過IsValidIndex方法來請求容器某個索引否有有效:
操作符[]返回值是一個引用值,只要你的數組不是const類型的,你就可以改變數組中的元素。
跟GetData方法類型,若數組是const的,操作符[]將返回一個const的引用。你可以使用Last方法來從數組最後來獲得序列元素。索引的缺省值爲0。Top方法與Last作用相反,只是Top不帶索引參數。
還可以檢查數組中是否包含特定元素:
或檢查數組中是否匹配特定的匹配條件:
我們可以通過Find家族函數來查找元素。檢查元素是否存在並返回元素索引,可以用:
這種方法獲得的是從容器中查找到第一個匹配元素索引。如果容器中存在多個複製元素,我們想找到從後倒數第一個匹配元素,我們使用FindLast方法來代替之前:
兩種方法均返回一個布爾值來表示元素是否被找到,並且若找到元素,會將元素索引寫入參數變量中。
Find和FindLast也可以直接返回元素索引。只要你不顯式傳遞參數給函數,就會返回元素索引。這種方法比之前的更簡潔,使用哪種方法取決於你的特定需求或風格。
若沒有找到元素,則返回一個特殊值INDEX_NONE:
IndexOfByKey的使用方法類似,但是它允許元素和任意對象進行比較。在Find方法中,參數實際上在搜素開始時被轉化爲元素類型(本例轉爲FString)。而在IndexOfByKey方法中,“鍵值”是直接比較的,甚至鍵值類型都不需要轉化爲元素類型。
IndexOfByKey可以爲任何類型的鍵值工作,只要你存在操作符“==(ElementType,KeyType)”;這個將會被用於比較。IndexOfByKey將返回第一個被找到的元素索引,或元素沒有找到返回INDEX_NONE。
IndexOfByPredicate方法可以用於查找到與特定匹配條件第一個匹配的元素索引,若沒有找到,依舊返回特定值INDEX_NONE。
除了返回索引,我們也可以返回找到元素的指針。FindByKey與IndexOfByKey也可用於元素和任意對象比較,但是返回是元素指針。沒找元素返回nullptr。
同樣,FindByPredicate也可以像IndexOfByPredicate使用,但它返回值是索引號。
最後,當元素中數組匹配某個特殊匹配條件時候,可使用FilterByPredicate方法:
刪除
你可以使用Remove家族的函數來進行刪除元素的操作。Remove方法將刪除所有與給出的元素相等的元素:
注意到“HELLo”通過請求移“hello”來刪除了。等於的操作測試是通過元素的操作符“==”來進行的。謹記,對FString而已,比較不區分大小寫。
最後一個元素的刪除可以通過 Pop方法來實現:
Remove刪除的是與條件相符的所有實例。例如:
你也可以使用RemoveSingle來刪除數組中從前面數到最近的元素。這個是很有用的,比如:你知道數組中包含多個複製對象,你只想刪除一個的時候,或你的數組只能存在一個這樣的元素作爲對數組的優化時,因爲當它找到並刪除以後,就會停止搜索。
我們也可以使用RemoveAt方法來通過元素索引來刪除元素。索引必須存在,否則會報運行時錯誤。謹記:索引是從0開始的:
我們可以使用RemoveAll來刪除與匹配條件匹配的元素。例如:刪除所有3的倍數的值:
在所有的刪除操作中,當元素被刪除後,此被刪除元素後面的元素索引會調整到低索引,不可能刪除元素後數組中留下一個“洞”。
這種索引調整過程是有開銷的。若你不在乎剩下的元素順序這種開銷可以使用RemoveSwap、RemoveAtSwap和RemoveAllSwap來降低開銷。這些方法的工作類似與非交換區變量,它們不需要保證剩下元素的順序,只保證它們使用的高效性。
最後,所有元素的刪除使用Empty方法:
操作符
數組作爲一個常規值類型,可通過標準拷貝構造函數或賦值運算操作符來實現拷貝。作爲數組完全擁有其元素,在拷貝數組時進行的是深度拷貝,新數組也將完全擁有拷貝的元素。
作爲Append方法的替代,數據使用操作符“+=”來實現連接。
數組的比較使用的是操作符==和操作符!=。兩個數組相等,必須是相同的索引有相同的元素值---元素的順序是很重要的。元素的比較使用的是他們自己的操作符“==”。
堆
TArray有支持二叉堆數據結構的函數。堆是任意父節點等效或排序與所有子節點之前的二叉樹。當作爲數組使用時,樹的根節點元素是0,左右字節點序號N的索引依次爲2N+1和2N+2。子節點之間沒有任何特殊排序。
一個已存在的數組通過調用Heapify可以轉換到堆空間上。這個是可以根據是否匹配條件來重載函數,沒有匹配條件的版本將使用元素類型的操作符“<”來決定順序:
樹的視圖如下:
樹上節點可從左到右,從上到下根據堆數組中元素順序讀取。要說明的是,數組轉換到堆後排序並不是必須的。已排序數組是一個有效堆,其結構定義是足夠鬆散的,並允許多個有效堆有相同元素集。
新元素通過使用HeapPush方法來添加到堆上,重新排序其他節點以更新堆。
HeapPop和HeapPopDiscard方法是用來刪除堆的頂節點。不同之處在於前者傳入參數引用,並返回最上面一個元素的拷貝,而後者簡單的刪除頂部節點,不返回任何值。兩種方法結果對數組來說是一樣的,堆通過自適應排序來更新堆。
HeapRemoveAt方法將刪除指定索引號的元素對象,然後重新排序以更新堆。
值得注意的是,HeapPush、HeapPop、 HeapPopDiscard和HeapRemoveAt均只在堆結構爲有效堆時可被調用,比如:在Heapify()調用之後、其他堆操作或手動把數組轉換之後。
當然,這些操作,包括Heapify(),都可以通過二元匹配條件選項來決定節點在堆中順序。當使用匹配條件時,同一匹配條件要使用到所有的堆操作中以維持正確的樹結構。若無特殊匹配條件,堆操作使用的是元素的操作符“<”來決定順序。
最後,堆中頂端節點可通過HeapTop來取得,但並不改變數組。
預留空間
由於數組是可調整的,它使用的是變長的內存。爲避免每增加一個元素就開闢一次內存,分配器總是提供比請求多的內存,以便在將來添加時重新分配內存的造成性能消耗。同理,刪除元素並不釋放內存。不同的是在作爲預留空間時,容器中多少個元素和有多個的元素能被添加進來,在下次內存分配之前是已知的。
數組的缺省構造並不分配內存,預留空間初始化爲0。你可以使用GetSlack方法來獲取數組中有多少個預留空間。另外,在容器中最多能容納的元素個數可以通過Max方法來獲得。GetSlack() 等於Max() - Num()。
在重新分配內存後有多少個預留空間取決於分配器,所以它不依賴於用戶數組。
大多數情況,你不用關心預留空間。但是當你意識到它時,你可以使用它的優點來對數組進行優化。例如:若你知道,數組中將添加100個新元素,你可以保證你的預留空間至少有100個,這樣當你添加元素時,就不需要再申請空間。上面提到的Empty方法,接受一個可選的預留空間參數。
Reset方法工作方式與Empty類似,稍有不同的是,當請求的預留空間已經由當前分配器提供時,它並不釋放內存空間。然而,當需要更大的預留空間時,它將會分配更多內存空間。
最後,Shrink方法,它會刪除任何浪費的預留空間,重新分配與當前元素序列需要求同樣大小的空間,實際上不刪除這些對象。
Raw內存
TArray只是對一些分配內存的包裝。它可被當做一個可修改分配字節數和創建元素的類型。TArray總是盡其所能的提供它所有信息,但是有時你也需要降低標準。
需要注意的是,若你犯錯誤,這些操作會把容器變成了無效狀態或引起無定義行爲。在這些方法調用之後,其他常規方法被調用之前,你要保證容器處於有效狀態。
AddUninitialized和InsertUninitialized方法允許你添加未初始化的空間到數組中。他們分別類似於Add和Insert的工作方式,但是不調用元素類型的構造函數。這對於有安全或方便構造函數的類型來說是很有用的,但是不管怎樣這你都得完全重寫狀態(例如:調用Memcpy內存拷貝),最好不要自尋煩惱。
若你想或者需要控制構造過程,你可以使用保留一些你將要構造對象的內存空間。
AddZeroed和InsertZeroed 功能類似,還將通過添加/插入內存空間清空。當你有一個類型需要插入一些有效的0狀態的字節位時,這個非常有用。
與SetNum類似,有SetNumUninitialized和SetNumZeroed兩個方法,不同的是,當新的數字大於當前數字時,兩函數對新元素空間分別進行不初始化或按位賦零。在你使用AddUninitialized和InsertUninitialized方法時,有必要保證,新元素在他們被需要時,被正確的構造在新的空間中。
使用任何Uninitialized或 Zeroed方法都需要謹慎對待。如果一個元素類型因添加一個成員而被修改,或它不具有有效的字節位爲零的狀態,都能導致無效的數組元素和未定義的行爲。使用這些方法對於幾乎不改變的數組類型大多數是很有用的,比如:FMatrix或FVector。
雜項
BulkSerialize方法是一個序列化函數,它可以用於替代數組的操作符“<<”序列化操作,用一塊原生字節的序列化來替代逐個元素的序列化。當你元素類型是常見,比如內置類型或普通數據結構類型時,它的表現還是很棒的。
CountBytes和GetAllocatedSize用來估計數組中當前多少內存處於使用中。CountBytes使用的是可直接被調用的FArchive和GetAllocatedSize兩個函數。他們常用於狀態報告。
Swap和SwapMemory均需要兩個索引號,將交換兩個索引的元素值。他們兩個作用是一樣的,唯一區別在於Swap會做一些索引的錯誤檢查,若索引不在範圍內就引起斷言。
--------------THE END -------
中途了修改了幾次版本,這個是最後的版本....
不足之處還請各位多多指教。
若有問題,請隨時聯繫!
最後,期待你在下面點擊一下,頂一下那就是對我莫大的鼓舞。再次感謝!