お急ぎのあなたのために、まずは結論。FNameが保持する文字列が必要な時はToString()で生成したFStringを使うべし。GetPlainANSIString()
とGetPlainWIDEString()
を使うとハマるから止めといた方がいい。
Unreal Engine 4の軽量文字列クラスFName
(公式リファレンス)のGetPlainANSIString
関数/GetPlainWIDEString
関数で取得できる文字列ポインタには、そのFNameインスタンスが本来持っている文字列の一部しか入っていない事がある。恐らく仕様。以下が実証コード。
TArray<FName> Names; Names.Add(FName(TEXT("Hoge_"))); Names.Add(FName(TEXT("Hoge_0000"))); Names.Add(FName(TEXT("Hoge_0001"))); Names.Add(FName(TEXT("Hoge_10"))); Names.Add(FName(TEXT("Hoge_1200"))); Names.Add(FName(TEXT("Hoge_1300"))); Names.Add(FName(TEXT("Hoge_9999"))); Names.Add(FName(TEXT("Hoge9999"))); for (const auto& Name : Names) { UE_LOG(LogWindows, Log, TEXT("◆%s"), *Name.ToString()); UE_LOG(LogWindows, Log, TEXT(" PlainAnsiString=%s (%p)"), *FString(Name.GetPlainANSIString()), Name.GetPlainANSIString()); UE_LOG(LogWindows, Log, TEXT(" [FNameEntry]")); UE_LOG(LogWindows, Log, TEXT(" ComparisonIndex=%d"), Name.GetComparisonIndex()); UE_LOG(LogWindows, Log, TEXT(" [ComparisonEntry]")); UE_LOG(LogWindows, Log, TEXT(" Address=%p"), Name.GetComparisonNameEntry()); UE_LOG(LogWindows, Log, TEXT(" isWide=%d"), Name.GetComparisonNameEntry()->IsWide()); UE_LOG(LogWindows, Log, TEXT(" DisplayIndex=%d"), Name.GetDisplayIndex()); UE_LOG(LogWindows, Log, TEXT(" [DisplayEntry]")); UE_LOG(LogWindows, Log, TEXT(" Address=%p"), Name.GetDisplayNameEntry()); UE_LOG(LogWindows, Log, TEXT(" isWide=%d"), Name.GetDisplayNameEntry()->IsWide()); UE_LOG(LogWindows, Log, TEXT(" Number=%d"), Name.GetNumber()); }
実行結果を見ると、Hoge_10, Hoge_1200, Hoge_1300, Hoge_999で見事に同一のPlainAnsiString
が返ってきているのが分かる(★の部分)
LogWindows: ◆Hoge_ LogWindows: PlainAnsiString=Hoge_ (000000021299B988) LogWindows: [FNameEntry] LogWindows: ComparisonIndex=1029720 LogWindows: [ComparisonEntry] LogWindows: Address=000000021299B978 LogWindows: isWide=0 LogWindows: DisplayIndex=1029720 LogWindows: [DisplayEntry] LogWindows: Address=000000021299B978 LogWindows: isWide=0 LogWindows: Number=0 LogWindows: ◆Hoge_0000 LogWindows: PlainAnsiString=Hoge_0000 (000000021299B9A0) LogWindows: [FNameEntry] LogWindows: ComparisonIndex=1029721 LogWindows: [ComparisonEntry] LogWindows: Address=000000021299B990 LogWindows: isWide=0 LogWindows: DisplayIndex=1029721 LogWindows: [DisplayEntry] LogWindows: Address=000000021299B990 LogWindows: isWide=0 LogWindows: Number=0 LogWindows: ◆Hoge_0001 LogWindows: PlainAnsiString=Hoge_0001 (000000021299B9C0) LogWindows: [FNameEntry] LogWindows: ComparisonIndex=1029722 LogWindows: [ComparisonEntry] LogWindows: Address=000000021299B9B0 LogWindows: isWide=0 LogWindows: DisplayIndex=1029722 LogWindows: [DisplayEntry] LogWindows: Address=000000021299B9B0 LogWindows: isWide=0 LogWindows: Number=0 LogWindows: ◆Hoge_10 LogWindows: PlainAnsiString=Hoge (000000021299B9E0)…★ LogWindows: [FNameEntry] LogWindows: ComparisonIndex=1029723 LogWindows: [ComparisonEntry] LogWindows: Address=000000021299B9D0 LogWindows: isWide=0 LogWindows: DisplayIndex=1029723 LogWindows: [DisplayEntry] LogWindows: Address=000000021299B9D0 LogWindows: isWide=0 LogWindows: Number=11 LogWindows: ◆Hoge_1200 LogWindows: PlainAnsiString=Hoge (000000021299B9E0)…★ LogWindows: [FNameEntry] LogWindows: ComparisonIndex=1029723 LogWindows: [ComparisonEntry] LogWindows: Address=000000021299B9D0 LogWindows: isWide=0 LogWindows: DisplayIndex=1029723 LogWindows: [DisplayEntry] LogWindows: Address=000000021299B9D0 LogWindows: isWide=0 LogWindows: Number=1201 LogWindows: ◆Hoge_1300 LogWindows: PlainAnsiString=Hoge (000000021299B9E0)…★ LogWindows: [FNameEntry] LogWindows: ComparisonIndex=1029723 LogWindows: [ComparisonEntry] LogWindows: Address=000000021299B9D0 LogWindows: isWide=0 LogWindows: DisplayIndex=1029723 LogWindows: [DisplayEntry] LogWindows: Address=000000021299B9D0 LogWindows: isWide=0 LogWindows: Number=1301 LogWindows: ◆Hoge_9999 LogWindows: PlainAnsiString=Hoge (000000021299B9E0)…★ LogWindows: [FNameEntry] LogWindows: ComparisonIndex=1029723 LogWindows: [ComparisonEntry] LogWindows: Address=000000021299B9D0 LogWindows: isWide=0 LogWindows: DisplayIndex=1029723 LogWindows: [DisplayEntry] LogWindows: Address=000000021299B9D0 LogWindows: isWide=0 LogWindows: Number=10000 LogWindows: ◆Hoge9999 LogWindows: PlainAnsiString=Hoge9999 (000000021299B9F8) LogWindows: [FNameEntry] LogWindows: ComparisonIndex=1029724 LogWindows: [ComparisonEntry] LogWindows: Address=000000021299B9E8 LogWindows: isWide=0 LogWindows: DisplayIndex=1029724 LogWindows: [DisplayEntry] LogWindows: Address=000000021299B9E8 LogWindows: isWide=0 LogWindows: Number=0
FNameはハッシュ付き文字列として実装されている。文字列はFNameの共用領域に格納され、各FNameインスタンスはその文字列格納領域へのインデックス=ハッシュを保持してる。FNameの同士の比較は互いのハッシュの比較、つまり整数の比較に還元されるため、通常の文字列比較より速いって仕掛けなんですな。
ただ、このハッシュ生成方法がちょっと曲者で、文字列がアンダースコア+ゼロ詰めされていない数値で終わっていたら、その部分を除いた文字列を共用領域に格納し、数値は各FNameインスタンスで保持するという方法なのだ。割と最近のバージョンアンプで変わったらしい(知人曰く4.12あたりで変わった気がすると)。なかなか破壊的な変更をしてくださりやがるな!言葉だと分かりにくいので図を作ってみた。
図を踏まえつつ改めて実行結果を見てみると、条件に合致するHoge_10, Hoge_1200, Hoge_1300, Hoge_999の中身が、その通りになっている事がお分かりいただけよう。Number
が実際の数値+1されているのは、これまた仕様で、数値分割条件を満たさないFName
インスタンス(Number == 0)との区別の為っぽい。
UE4では生成されたオブジェクトのインスタンスに対し、オブジェクト名+連番の名前を自動付与するため、大規模な開発になるとFName文字列ストアの肥大化が無視できなくなり、保持方法を変更したのだと思われる。
こんな格納の仕方で大丈夫なの!?と思うが、ふつーにFName
を使う分には何の問題もない。や、正確には大丈夫じゃなかったからこの記事書いてる訳だけど、文字列の生ポインタ取ってこねくり回すような事をしなければ大丈夫。FName文字列に対して低レベルな操作を行いたい時は、ToString()
関数で生成したFString
に対して行う事をオススメする。エンジンのコードを見てもらうと分かるが、ComparisonIndex
の文字列にアンダースコアと数値をくっつけて元の文字列を復元してやがるので(笑)
ちなみに、実行結果内のDisplayIndex
というのは、名前の通り表示用の文字列へのインデックス。簡単に言えば、FName生成時に与えられた文字列のインデックスである。FNameは基本的にcase-preserving(内部的には大文字小文字を区別するが対外的には区別せずに扱う)なので、比較用と表示用で別々の文字列を持つ必要がある訳だ。知らずに使ってると、これも地味にハマりポイントかも。
FName abc(TEXT("abc")); FName ABC(TEXT("ABC")); UE_LOG(LogWindows, Log, TEXT("%s %s %s"), *abc.ToString(), (abc == ABC ? TEXT("==") : TEXT("!=")), *ABC.ToString());
念のため上記コードで確認したら、LogWindows: abc == ABC
となった。
掘ってみるとFNameには結構罠があるので注意が必要だ。