近来新到一个使用Unreal Engine 3的项目,本着熟悉代码的目的,看了一些代码,简单记录一下。
本文主要分析Unreal Engine 3中对于字符串封装后的结构FName,内容主要包含以下3点:
1.FName的作用;
2.FName的具体实现;
3.FName的一些特殊处理
1.FName的作用
2.FName的具体实现
FName中的关键数据结构
struct FNameEntry{FNameEntry是一个全局的结构,后面会说到的存储结构中都是以FNameEntry为元素存储的。
INT Index;
FNameEntry* HashNext;
}
class FName{ INT Index; INT Number; static TArrayNoInit<FNameEntry*> Names; static FNameEntry* NameHash[65536];}
FName就是今天要分析的重点,这里是几个关键的变量及存储字符串的容器。
Index是Names数组中的索引,用于快速的查找字符串的字符部分,Number是字符串中的数字部分。
接着,来看FName的Init()函数,Init()函数会在FName的构造函数中调用,一般情况下,使用FName只需传入第一个参数,其他参数在FName的构造函数中填入默认值。FName的Init()函数传入了4个参数,InName字符串中的字符部分,InNumber字符串中的数字部分,FindType表示操作类型,bSplitName分割标示符。传入的字符串在InName中,分割后会将字符保留在InName内,而将数字部分保存在InNumber中。
void FName::Init(const TCHAR* InName, INT InNumber, EFindName FindType, UBOOL bSplitName)
{
StaticInit();
if (InNumber == NAME_NO_NUMBER_INTERNAL && bSplitName == TRUE)
{
if (SplitNameWithCheck(InName,...))
{}
}
INT iHash;
iHash = appStrhash( InName ) & ( ARRAY_COUNT(NameHash)-1 );
for (FNameEntry* Hash = NameHash[iHash]; Hash; Hash=Hash->HashNext)
{
if( Hash->IsEqual(InName))
{
Index = Hash->GetIndex();
...
}
}
Index = Names.Add();
Names(Index) = NameHash[iHash] = AllocateNameEntry( NewName, Index, NameHash[iHash], bIsPureAnsi );
}
FName::Init()函数主要做了以下的工作:
根据FindType的不同,Fname::Init()函数有两个作用,第一个作用是作为FName的初始化函数,第二个作用是作为FName的查找函数。
Init()中具体的查找步骤为通过计算字符串的hash值,在hash table中定位到到对应链表的头结点,接着遍历链表依次比较当前元素与传入的InName。
Enum EFindName
{
FNAME_Find,
FNAME_Add,
FNAME_Replace
}
当Init中的FindName变量值为FNAME_Find时,当没有找到对应的字符串会将Index = NAME_None,找到对应的字符串会将Index置为hash table中的hash值。
当FindName为FName_Add时,没有找到对应的字符串就会将该字符串经过hash加入到hash table对应的位置。
当FindName为FName_Replace时,会将查找到的字符串替换为传入的字符串。
FORCEINLINE UBOOL operator==(const FNAME& other) const
{
return Index == Other.Index && Number == Other.Number;
}
对FName对象的比较有两种方式,第一种是通过重载关系运算符==,首先比较索引(这个索引是保存在Names数组中的索引值),第二种实现了一个compare函数,不同之处在于两个FName对象不等时compare函数的返回值将根据字母表的升序来返回小于0或者大于0。
INT FName::Compare(const FName& other) const
{
if (GetIndex() == Other.GetIndex())
{
return GetNumber() – Other.GetNumber();
}
else
…
}
可以看到比较两个FName对象时,先比较二者的Index,如果不等则退化为调用传统的字符比较函数(正常情况下经由FName的构造函数调用FName::Init()都会生成对应的Index)。
static TArrayNoInit<FNameEntry*> Names;
static FNameEntry* NameHash[65536];
在前面FName的定义中,看到除了定义NameHash数组之外,还定义了一个Names数组,这是为什么?两个数组在功能上有什么不同之处?
其原因在于hash table可以快速查找,却不能随机存取一个元素,当需要根据Index来获取一个FNameEntry对象时,在Names数组中通过下标直接存取效率会更高,这样做弥补了hash table无法随机存取的缺点。
在FName::Init()中,有提到最后一个参数bSplitName是分割标示符,而在实际的使用中,假设我们将其置为TRUE,传入“test1”,发现调用分割函数FName::SplitNameWithCheck()并没有成功。查看FName::SplitNameWithCheck()的代码后,发现分割函数只是针对类似“test_1”这样格式的字符串进行分割,经过与同事的交流得知,Unreal编辑生成的资源文件多是以这样格式命名(或者在内部处理时将对象的命名统一格式)以加快处理速度。