
UE5 里写字符串处理很多人第一反应是去蓝图里拖节点。但项目一旦用到 CFString 的操作就成了家常便饭。我这篇是系列第 53 篇直接把 FString 的常用操作、UKismetStringLibrary 静态函数、还有 leftPad 这类容易让人误解的“补位”函数一次讲透附带解析字符串成数组的完整代码。适合刚转向 UE5 C 的开发者也适合在 C 侧写工具、写数据解析的读者。1. FString 的底层本质为什么 leftPad 改变不了原字符串先聊一个基础但重要的认知。FString 不是 std::string它是 UE 自己实现的动态字符串类底层可以看成TArrayTCHAR的封装。TCHAR在 UE5 里的行为取决于构建配置Windows 下通常对应宽字符UTF-16所以 FString 天然支持中文和 Unicode不需要像传统 C 字符串那样手工处理字节。1.1 值语义和 const 成员函数FString 的绝大多数操作都遵循“不改原字符串返回新字符串”的模式。比如Left、Right、Mid、Replace这些函数往往是const成员函数源码里会创建一个新的 FString 并返回。leftPad 也是同一类// UE 源码风格的 leftPad 简化实现 FString MyLeftPad(const FString InStr, int32 ChCount, TCHAR Pad TEXT( )) { const int32 PadCount ChCount - InStr.Len(); if (PadCount 0) { FString Result; Result.Reserve(ChCount); for (int32 i 0; i PadCount; i) { Result Pad; } Result InStr; return Result; } return InStr; }这个简化版本就把“不改变原字符串长度”这件事说清楚了调用LeftPad后原字符串对象本身没有发生任何变化返回的是一个新对象。如果你的代码写成这样FString Code TEXT(42); FString Padded Code.LeftPad(6, TEXT(0));那么Code依然是42Padded是000042。注意LeftPad 的第二个参数是期望结果的总长度不是要填充多少个字符。如果原字符串长度已经大于等于 ChCount它不会截断而是直接返回原字符串的内容。1.2 不要和 FName、FText 混淆FString 适合做动态文本、拼接、解析、用户输入。FName 是只读的、驻留到全局表里的字符串适合做资源名和标识。FText 是本地化文本适合显示给玩家看的界面文字。三者的转换经常用FName Tag TEXT(PlayerTag); FString TagString Tag.ToString(); FText Label NSLOCTEXT(MyModule, Key, 显示文本); FString LabelString Label.ToString();我见过不少初学者在关键路径上用 FName 反复拼接或者用 FString 做资源检查反而绕路。字符串选型比字符串操作更值得先想清楚。2. 高频字符串操作手册拼接、比较、截取、替换这节是基础但也是最常被问到的。先给一份我在项目里最常用的 FString 操作清单包含输出效果方便对照。2.1 拼接与格式化// 方式一重载运算符适合少量拼接 FString A TEXT(Hello); FString B A TEXT( ) TEXT(World); A TEXT(!!!); // 方式二Printf适合带格式的拼接 int32 Score 98; FString Name TEXT(Tom); FString Msg FString::Printf(TEXT(%s 的分数是 %d), *Name, Score); // 方式三Appendf适合在已有字符串上追加 FString Log TEXT(Result: ); Log.Appendf(TEXT(%d - %s), 100, TEXT(OK));这里有个贯穿 UE 开发的重点%s不能直接传 FString必须传*FString。*运算符获取内部 TCHAR 数组指针这是 FString 和 printf 系函数对接的标准姿势。忘了写*编译器可能不报错输出却全是乱码或空内容这属于 UE 新手的经典踩坑点。2.2 查找、截取、判断前后缀FString Path TEXT(/Game/Maps/Level_01?spawn1); // 查找 int32 DotIndex Path.Find(TEXT(Level)); if (Path.Contains(TEXT(spawn))) { // 至少包含子串时进入 } // 前后缀判断 bool bStart Path.StartsWith(TEXT(/Game)); bool bEnd Path.EndsWith(TEXT(1)); // 截取 FString LeftPart Path.Left(5); // /Game FString RightPart Path.Right(4); // ?spawn1 注意是最后4个字符 FString MidPart Path.Mid(6, 4); // 从索引6开始取4个字符 Maps FString ChopLeft Path.LeftChop(5); // 去掉最后5个字符 FString ChopRight Path.RightChop(10); // 去掉最前10个字符Find默认从左往右找返回第一个匹配的位置索引找不到返回INDEX_NONE也就是 -1。UE5 新版本里还提供了FindLast从右往左找解析路径、日志尾部定位时很好用。2.3 替换与大小写FString Raw TEXT(a-b-c); // Replace 返回新字符串不修改原字符串 FString Replaced Raw.Replace(TEXT(-), TEXT(_)); // Replaced a_b_cRaw 不变 // ReplaceInline 直接修改原字符串 Raw.ReplaceInline(TEXT(b), TEXT(BB)); // Raw a-BB-c // 大小写 FString Lower Raw.ToLower(); FString Upper Raw.ToUpper();选Replace还是ReplaceInline取决于你是否还需要原字符串。需要保留原值时用前者确定不再使用时用后者后者省一次分配和拷贝。对我个人来说写热路径代码时默认用ReplaceInline其他场景无所谓。3. leftPad 与 rightPad两个“补位”函数的使用边界leftPad 看起来简单实际用的时候要理解它“补到指定长度绝不截断”的特性这一节专门展开。3.1 左填充实战FString Number TEXT(7); FString PaddedLeft Number.LeftPad(3, TEXT(0)); // 007 FString PaddedSpace Number.LeftPad(3); // 7典型场景生成玩家序号、关卡编号、存档文件名。例如保存文件时希望文件名排序稳定save1和save10在字典序里会错位用LeftPad(4, TEXT(0))转成save0001就不会出现这种问题。另一种场景是控制台表格打印对齐。多行数据打印时用左填充保持列宽输出 readable 很多TArrayFString Names { TEXT(Amy), TEXT(Bob), TEXT(Christopher) }; for (const FString N : Names) { FString Aligned N.RightPad(15, TEXT( )); UE_LOG(LogTemp, Log, TEXT([%s]%d), *Aligned, N.Len()); }3.2 右填充与边界条件rightPad 和 leftPad 的方向相反其他逻辑一致FString Tag TEXT(HP); FString PaddedRight Tag.RightPad(5, TEXT(.)); // HP...这里再强调一次容易误解的地方。两个函数返回的字符串长度只有在“原长度小于目标长度”时才会变成 ChCount如果原长度已经是 20你传ChCount 10得到的是原字符串内容的拷贝长度还是 20。它不会帮你截断。想要“从中间截取固定长度”请用Mid、Left、Right。另外填充字符必须是TCHAR字面量写TEXT(0)而不是字符串TEXT(0)。这是编译期就能发现的硬要求报错信息会提醒你。3.3 从源码角度理解返回值如果你对 UE 源码比较熟可以去StringTemplate.h或Containers/UnrealString.h看LeftPad的完整实现。它内部会先计算PadCount ChCount - Len()只有PadCount 0才真正构造填充字符串。UE 里还有很多这种“先判断再操作”的函数比如Left在长度不足时也是返回已有字符串。理解这类函数后写业务逻辑时就不容易写出多余的if判断。4. UKismetStringLibraryC 里也能调用蓝图字符串工具箱标题里专门提到 UKismetStringLibrary说明这个静态函数库在蓝图里太常用了。实际上它在 C 侧同样能直接调用有些场景甚至比 FString 成员函数更方便。4.1 为什么 C 里也需要这个库KismetStringLibrary 本质是蓝图的字符串运算节点在 C 层的映射。它的函数大多是 static你只需要 include 对应的头文件#include Kismet/KismetStringLibrary.h调用时不需要创建对象直接用UKismetStringLibrary::函数名就可以。它覆盖了非常全的字符串相关操作拼接、比较、截取、查找、替换、大小写、数组互转、正则匹配等。4.2 常用静态函数清单函数作用典型用法Concat_StrStr拼接两个字符串Concat_StrStr(A, B)EqualEqual_StrStr判断相等可忽略大小写EqualEqual_StrStr(A, B)GetSubstring取子串GetSubstring(Src, 0, 5)Split分割成左右两部分Split(Src, , Left, Right)ParseIntoArray按分隔符拆成数组ParseIntoArray(Src, OutArray, ,)JoinStringArray数组合并成字符串JoinStringArray(Array, ,)Trim去掉首尾空白Trim(Src)Contains子串判断Contains(Src, abc)Len长度Len(Src)C 侧直接调用这些函数时函数参数顺序和蓝图节点的引脚顺序几乎一致很多从蓝图转 C 的开发者会感觉特别亲切。4.3 实际代码示例下面这段代码演示了 C 里用 KismetStringLibrary 做解析把nameTom;score98拆开并重组FString RawData TEXT(nameTom;score98); // 用分号拆成数组 TArrayFString Parts; UKismetStringLibrary::ParseIntoArray(RawData, Parts, TEXT(;), true); // Parts [nameTom, score98] // 继续拆第一段 FString Key, Value; UKismetStringLibrary::Split(Parts[0], TEXT(), Key, Value); // Key name, Value Tom // 数组合并回去 FString Joined UKismetStringLibrary::JoinStringArray(Parts, TEXT(|)); // Joined nameTom|score98这个模式很适合处理命令行参数、配置文本、协议数据。Split 函数最后一个参数控制从左边还是右边开始找分隔符默认FromStart如果你有abc这种数据想取第一个还是最后一个可以通过ESearchDir::FromEnd控制。4.4 什么时候选蓝图库而不是直接调成员函数我的判断是三层纯 C 代码里能用 FString 成员函数就用成员函数代码更短、更直观。涉及“字符串数组和字符串互转”时KismetStringLibrary 最好用。例如JoinStringArray对应手写代码需要循环Append调用库函数一行的可读性远高于手写。后续要让策划或设计在蓝图上复用这段逻辑那应该把核心逻辑封装成 BlueprintCallable 函数内部再调库函数。直接让业务代码依赖蓝图库层并不优雅但作为工具代码调用完全没有问题。注意KismetStringLibrary 的函数往往会带额外的 unicode 处理和临时对象分配在每帧执行的性能敏感代码里要谨慎使用。大多数场景无所谓日志、配置、编辑器工具函数都是典型的安全区。5. 把字符串 parse 成数组从简单分割到正则提取字符串转数组是 FString 用得最多的场景之一UE 提供的好用工具不少我按难度从低到高列一遍。5.1 ParseIntoArray 按分隔符拆解它直接修改传入的TArrayFString返回值是数组下标还是空值不太重要常见用法是FString CSV TEXT(apple,banana,,cherry); TArrayFString Items; CSV.ParseIntoArray(Items, TEXT(,), true); // bCullEmpty true 时空字符串被丢弃 // Items [apple, banana, cherry]第三个参数bCullEmpty是新版 UE 里的标配套路。改成false中间那个空字符串会被保留数组长度变成 4。默认建议开true大部分数据解析不希望出现空项。另外一个姊妹函数是ParseIntoArrayLines专门按换行符拆FString Report TEXT(line1\nline2\nline3); TArrayFString Lines; Report.ParseIntoArrayLines(Lines, true);5.2 Split 分割成左右两半FString::Split适合“一个分隔符拆成两段”的场景比如键值对。FString Entry TEXT(PlayerNameAlice); FString Left, Right; if (Entry.Split(TEXT(), Left, Right, ESearchCase::CaseSensitive)) { // Left PlayerName, Right Alice }注意参数是指针这一点和 KismetStringLibrary 的引用参数形式不同别搞混。5.3 手动遍历字符拆分如果分隔符复杂或者想一次拆出多个字符可以遍历 TCHAR。比如拆掉所有空白字符FString Source TEXT( alpha beta gamma ); TArrayFString Tokens; FString Current; for (const TCHAR C : Source) { if (FChar::IsWhitespace(C)) { if (!Current.IsEmpty()) { Tokens.Add(Current); Current.Empty(); } } else { Current.AppendChar(C); } } if (!Current.IsEmpty()) { Tokens.Add(Current); } // Tokens [alpha, beta, gamma]UE 提供FChar::IsWhitespace这类字符分类函数比手写C TEXT( )靠谱得多因为换行、制表符也会被正确识别。5.4 正则表达式提取当格式不确定、或要从一段日志里抽多个字段时FRegexPattern FRegexMatcher 比手动拆分稳定得多。#include Internationalization/Regex.h FString LogLine TEXT([2024-06-01] PlayerAlice Score99); FRegexPattern Pattern(TEXT(Score(\\d))); FRegexMatcher Matcher(Pattern, LogLine); if (Matcher.FindNext()) { int32 Start Matcher.GetMatchBeginning(1); int32 End Matcher.GetMatchEnd(1); FString Score LogLine.Mid(Start, End - Start); // Score 99 }正则适合做“在一堆文本里搜字段”但如果你的数据是规整的分隔符结构用 ParseIntoArray 更直接正则性能和可读性都不占优。5.5 完整案例解析 URL 风格参数结合上面几种方法把id1001nameTomscore98解析成键值对数组TArrayFString Pairs; UrlPart.ParseIntoArray(Pairs, TEXT(), true); TArrayFString Keys; TArrayFString Values; for (const FString Pair : Pairs) { FString K, V; if (Pair.Split(TEXT(), K, V, ESearchCase::CaseSensitive)) { Keys.Add(K); Values.Add(V); } }这里故意没有用 ParseIntoArray 把也放进去因为每项只要拆一次用 Split 比用 ParseIntoArray 再拼数组更精确少一层循环。6. 实测中容易踩的坑TCHAR、编码、空分隔符与性能最后一块把我在实际项目里见过的 FString 相关坑集中梳理一下每一条都有真实项目背景。6.1 TEXT() 宏不能省写字符串字面量UE 里必须用TEXT(...)。普通 C 的abc在 Windows 下是 ANSI 的char*FString 要的是 TCHAR 指针类型不匹配。很多新手看到FString S hello;能编译通过是因为 UE 提供了从 ANSI 字符串构造 FString 的重载但每构造一次都要做一次编码转换性能是小事字符集不一致的大坑才是大事。中文乱码十次有八次是这里出的。6.2 编码转换场景如果代码要和外部库或 HTTP 响应打交道经常需要拿到 UTF-8 的字节流。FString Utf8String UTF8_TO_TCHAR(Bytes); FTCHARToUTF8 Converter(*SomeString); const char* Utf8Data Converter.Get();UTF8_TO_TCHAR和FTCHARToUTF8是 UE 提供的编码转换便捷类内部处理好堆分配用完即焚不用考虑释放。自己写TCHAR转char时千万别逐字节强转中文会直接变成?。6.3 空字符串和空分隔符的诡异行为ParseIntoArray遇到空源字符串时输出数组是空的这符合直觉。但如果分隔符传空字符串TEXT()UE 会直接断言或者返回异常结果代码里要先判断。FString Delimiter TEXT(,); if (!Delimiter.IsEmpty()) { Data.ParseIntoArray(Out, *Delimiter, true); }顺带一提IsEmpty()和Len() 0等价但语义更清晰IsNumeric()用于判断字符串能否转数字解析用户输入时很实用。6.4 高频拼接时的性能注意以下代码每个循环都会让 FString 重新分配内存FString Result; for (int32 i 0; i 10000; i) { Result FString::Printf(TEXT(%d), i); }更好的做法是先Reserve预估容量或者把内容收集到TArrayFString后统一JoinStringArray。对大多数 UI 和日志场景性能差异感觉不到但在网络打包、序列化、大规模数据导出时预分配能明显减少卡顿。FString Result; Result.Reserve(1024 * 10); for (int32 i 0; i 10000; i) { Result.AppendInt(i); }6.5 排查看似没生效的字符串变化回到最开始的 leftPad。很多人UE_LOG打印原来的变量发现字符串没变就开始怀疑函数没生效。原因就是它返回新字符串返回值必须被接收。这个坑本质上是混淆了“原地修改”和“返回新值”两类 API。判断方法很简单看函数声明里有没有const有没有返回 FString。有const、返回 FString 的就是返回新值。我自己在项目里定的规约是解析类、转换类的函数明确分成两类命名——返回新值的用Convert、Get开头原地修改的用Append、ReplaceInline这种带后缀的函数。后续维护的人看到名字就能知道会不会动原数据少踩很多坑。FString 的东西说深不算深但牵扯到内存、编码、蓝图交互细节翻车率极高。这一篇的代码和坑我都是实际跑过的你直接拷到项目里改一改基本都能用。如果后面遇到更偏门的字符串需求再看一下 KismetStringLibrary 的头文件或者 FString 的源码比自己盲试快得多。