
做UE5 C开发的朋友应该都绕不开FString这玩意儿跟std::string看着像但用起来坑是真的不少。尤其是你上手第三天想格式化个字符串或者把一串带逗号的配置字符串拆成数组一搜资料全是蓝图节点截图想找C的完整写法反而东拼西凑。这篇我把FString的日常操作全捋了一遍顺便把UKismetStringLibrary这个蓝图字符串库在C里的调用方式、leftPad左填充的返回值问题、字符串Parse成数组的完整代码一次性讲清楚。适合刚接触UE5 C的新手也适合写过一阵子但每次碰到字符串就查文档的人。1. 先把FString这玩意儿看透1.1 TCHAR和FString的底层关系FString本质上不是字符串它是TArray的亲戚——它内部维护一个动态数组用来存字符所以它具备数组的屁股可以按下标访问单个字符可以动态扩容也可以像数组一样reserve内存。它存储的基本单位是TCHAR这个TCHAR在Windows上默认是UTF-16编码下的2字节宽度在Linux上是UTF-8编码下的1字节宽度。这一点跟std::string的char是两码事。所以你在写FString相关代码时写字符串字面量前面要带TEXT()宏比如TEXT(Hello)。这个宏的作用是把普通字符串字面量转成TCHAR类型的数组。忘了带TEXT()在Windows下编译可能没问题放到主机平台或者换到Linux打包就很容易莫名其妙乱码甚至编译报错。这不是玄学是编码宽度不同导致的。FString内部还有个小设计它保证字符串末尾一定带个TCHAR的终止符所以你可以直接把它当成C字符串传给底层API用*FString取出指向缓冲区的TCHAR指针。这个操作出现频率极高因为很多引擎接口只接受传入字符指针而不是FString。1.2 FString和std::string最大的区别是啥如果从C转过来的你会特别不习惯FString的可变性和std::string不太一样。std::string本身就支持拼接、替换FString也支持目的都差不多。区别在于FString关注的是显示和多语言本地化所以很多函数有LEFT、RIGHT、MID这种和字符位置直接挂钩的命名跟蓝图节点一一对应。std::string的操作结果永远是你自己管理FString则直接套了一层引擎内存分配器字符操作上有一些引擎级的优化。FString的Len()方法计算的是TCHAR个数不是字节数。之前有人拿它去算中文长度每两个字节一个汉字窗口平台下Len()返回的是汉字个数但你在控制台用printf输出时发现占的字节数两倍这是因为TCHAR宽度和char宽度不是一回事。说白了你不需要纠结TCHAR在哪个平台有多宽只要记住两件事第一永远用TEXT()包字面量。 第二Len()是字符数是TCHAR的个数不是字节数。这两条记牢能避开接下来一半的坑。2. FString日常高频操作一网打尽2.1 拼接和格式化Append、Printf、Format字符串拼接是最基础的。FString直接用就能拼也可以调用Append方法。但项目里最常用的其实是格式化毕竟拼接多个变量容易乱FString PlayerName TEXT(Tom); int32 Score 1024; // 方式一Printf FString Result1 FString::Printf(TEXT(%s:%d), *PlayerName, Score); // 方式二Format FString Result2 FString::Format(TEXT({PlayerName}:{Score}), TMapFString, FString{{TEXT(PlayerName), PlayerName}, {TEXT(Score), FString::FromInt(Score)}}); // 方式三Append 逐个拼 FString Result3 TEXT(); Result3.Append(PlayerName); Result3.Append(TEXT(:)); Result3.AppendInt(Score);这里有个非常值得注意的点Printf和Format接收到字符串参数时要传入*PlayerName而不是直接传PlayerName。原因就是前面说的接口需要TCHAR指针这个*就是取出FString内部的字符缓冲区地址。新手第一次写十有八九会在这里报错报错信息还特别抽象搜半天也搜不到原因其实就差个*。Append系列除了追加文本还有个AppendInt用来接整数AppendFloat接浮点数免得你去手动转字符串再拼。不过实际写代码的时候我反而更推荐Printf因为它直观格式和占位符一眼就能看懂维护成本低。2.2 查找和判断Find、Contains、StartsWith、EndsWith查找子串的场景很多比如判断文件后缀、检查URL前缀、判断输入有没有非法字符。FString这几个函数够用FString Path TEXT(/Game/Character/BP_Player.uasset); // 判断是否包含某个子串 bool bHasPlayer Path.Contains(TEXT(Player)); // 查找子串位置返回值是第一个匹配的起始下标 int32 Index Path.Find(TEXT(Character)); // 从右往左找 int32 LastIndex Path.Find(TEXT(/, ESearchCase::IgnoreCase, ESearchDir::FromEnd)); // 判断开头和结尾 bool bStart Path.StartsWith(TEXT(/Game)); bool bEnd Path.EndsWith(TEXT(.uasset));Find最常见的使用场景不是单纯判断存在性而是配合Mid做取文件名的操作。比如找出最后一个斜杠的位置然后从这个位置往后截取就能拿到资源名。搜索类型默认是CaseSensitive区分大小写如果要忽略大小写记得传ESearchCase::IgnoreCase。这个API参数位置容易记混写多了自然就熟了。还有一个容易被忽略的是Contains内部也走的是Find的底层逻辑所以如果只需要判断存在性用Contains更简洁没必要先Find再判断Index0。2.3 截取和替换Left、Right、Mid、Replace截取几个函数虽然简单但配合起来能解决一堆实际问题FString FilePath TEXT(/Game/UI/MainMenuMap.umap); // 从左边取N个字符 FString LeftPart FilePath.Left(5); // /Game // 从右边取N个字符 FString RightPart FilePath.Right(5); // .umap // 从中间截取 int32 NameStart FilePath.Find(TEXT(/), ESearchCase::IgnoreCase, ESearchDir::FromEnd) 1; FString MapName FilePath.Mid(NameStart); // MainMenuMap.umap // 去掉后缀从右边找到点然后取左侧 FString NameOnly FilePath.Left(FilePath.Find(TEXT(.), ESearchCase::IgnoreCase, ESearchDir::FromEnd)); // 结果 MainMenuMap替换这块我更常用ReplaceInline因为它直接改原字符串少写一步赋值FString ErrorText TEXT(Error: [ERROR_CODE] happened); ErrorText.ReplaceInline(TEXT([ERROR_CODE]), TEXT(404)); // 结果 Error: 404 happened FString CleanText ErrorText.Replace(TEXT(404), TEXT(403)); // 返回新字符串原字符串不变顺带说一句Replace的分学名是大小写敏感想忽略大小写的话最稳妥的做法是先用ToLower()统一转小写再替换。但要注意如果替换的是中文字符串ToLower和ToUpper对中文没有影响放心用。2.4 比较、转换和大小写Equals、Atoi、ToUpper/LowerFString的比较可以直接用也可以用Equals。区别是Equals支持忽略大小写FString A TEXT(Hello); FString B TEXT(HELLO); bool bSame1 (A B); // false bool bSame2 A.Equals(B); // false bool bSame3 A.Equals(B, ESearchCase::IgnoreCase); // true字符串和数字的互转在生产代码里几乎是每天都要用FString NumberStr TEXT(10086); int32 Number FCString::Atoi(*NumberStr); float FloatVal FCString::Atof(*TEXT(3.14)); FString FromInt FString::FromInt(Number); FString FromFloat FString::SanitizeFloat(FloatVal);大小写转换不复杂FString Mixed TEXT(aBcDeF); FString Upper Mixed.ToUpper(); FString Lower Mixed.ToLower();有个很隐秘的坑FCString::Atoi对格式不正确的字符串不会报错它会返回0或者解析前面的合法数字。比如abc123返回0123abc返回123。如果你的数据来源不可靠建议用LexTryParseString这种严格的转换方式或者自己加正则判断再决定要不要强转。3. UKismetStringLibrary蓝图库在C里的正确打开方式3.1 这个库到底存了哪些好用函数UKismetStringLibrary是UE引擎内置给蓝图用的字符串函数库开发者调蓝图节点时看到的那一堆Get Length、To Upper、Find、Left Pad其实背后全部是这个类的静态函数。也就是说它不只是蓝图专用C代码里可以直接调它的静态方法。平时我能用上的有这些UKismetStringLibrary::Concat拼接两个字符串UKismetStringLibrary::Len取长度UKismetStringLibrary::ToUpper/ToLower大小写UKismetStringLibrary::LeftPad/RightPad左右填充UKismetStringLibrary::Mid/Left/Right截取UKismetStringLibrary::ParseIntoArray按分隔符切片UKismetStringLibrary::Split按分隔符拆成两个部分UKismetStringLibrary::Conv_StringToInt字符串转整数UKismetStringLibrary::MakeLiteralString字面量字符串蓝图输出节点UKismetStringLibrary::StartsWith/EndsWith3.2 C代码内部怎么调用这些静态函数使用前需要引入头文件#include Kismet/KismetStringLibrary.h然后直接类名加作用域调用即可FString RawString TEXT(Tom:100); // 调用蓝图库里的按分隔符拆分成数组 TArrayFString PairArray; UKismetStringLibrary::ParseIntoArray(RawString, PairArray, TEXT(:), true); // 结果 [Tom, 100] // 调用 Split 取两边 FString LeftPart; FString RightPart; UKismetStringLibrary::Split(RawString, TEXT(:), LeftPart, RightPart); // LeftPartTom, RightPart100 // 调用 LeftPad 左填充 FString Padded UKismetStringLibrary::LeftPad(TEXT(7), 3, TEXT(0)); // 结果 007这里有两个容易踩的细节第一ParseIntoArray这个静态函数的参数顺序是源字符串输出数组分隔符是否去掉空项注意不是源字符串分隔符输出数组我一开始就是参数顺序记错编译过了但运行结果一团糟。第二LeftPad的填充字符参数类型是TCHAR所以里要用单引号包字符TEXT(0)不是双引号。写双引号也能编译但那是字符串字面量转字符运行效果可能跟你想的完全不一样。还有一点Split如果原字符串里没有分隔符LeftPart会拿到整个原字符串RightPart为空。这个行为要在使用前想清楚不然很容易出现业务逻辑错乱却找不着的原因。3.3 为什么我不建议把所有字符串操作全甩给它明明FString成员函数就能做到的事为什么还要绕一圈调UKismetStringLibrary答案很简单当你在写C的时候直接用FString自己的成员函数更直观性能上也少一层静态调用开销。蓝图库里那些静态函数是给蓝图节点用的C里没必要舍近求远。但也有例外。比如你希望某个函数能被蓝图调用或者你正在写BP节点库、自定义Kismet节点那UKismetStringLibrary作为现成的字符串处理库完全可以被你的函数内部调用省得自己再写一遍底层逻辑。再比如你希望同一套操作逻辑既能被C调用又被蓝图调用那你把函数封装成BlueprintCallable里面适当用几个UKismetStringLibrary静态方法反而代码更少。所以我的观点是C项目代码里能用FString成员函数解决的就不要绕到静态库去但静态库的存在确实节约了不少自造轮子的时间尤其是左右填充、ParseIntoArray这俩FString成员函数不提供直接版本我就直接用静态库。4. leftPad左填充一个函数踩出两个坑4.1 leftPad到底干了啥leftPad是左填充的意思在字符串的左边用指定字符补齐直到字符串长度达到指定值。这是典型的文本对齐场景用的比如排行榜上的分数要显示成固定位数或者时间显示要用09:05而不是9:05。// 目标效果把9变成009 FString ScoreText TEXT(9); FString Padded UKismetStringLibrary::LeftPad(ScoreText, 3, TEXT(0)); // Padded 009填充的逻辑说起来非常简单如果源字符串长度小于目标长度计算差值在左边垫上足够数量的填充字符如果源字符串长度已经大于等于目标长度则不补原样返回。4.2 原字符串真的没变值传递和返回值的问题这个标题里强调的并不会改变原字符串的长度其实说的是一个非常容易让人懵掉的行为你调用LeftPad之后传进去的源字符串并不会被修改。FString Origin TEXT(9); FString Padded UKismetStringLibrary::LeftPad(Origin, 3, TEXT(0)); // Origin 依旧是 9长度1 // Padded 才是 009长度3很多人第一次用的时候以为调完LeftPadOrigin自己就变成009了结果打印Origin还是9于是怀疑函数没生效。原因就是FString这种字符串是按值传递返回新结果的不会原地修改你传进去的变量。如果确实想覆盖原变量必须显式赋值FString Origin TEXT(9); Origin UKismetStringLibrary::LeftPad(Origin, 3, TEXT(0)); // 这次 Origin 变成了 009另外还有一个更隐蔽的点当源字符串的长度大于目标长度时LeftPad的返回值长度等于源字符串长度也就是说填充目标长度不是硬性保证它只是一个下限约束FString Text TEXT(HelloWorld); FString Padded UKismetStringLibrary::LeftPad(Text, 4, TEXT(0)); // Text长度为10目标长度4不会截断 // Padded还是HelloWorld长度10这个行为一旦不理解你在做长度校验逻辑时就会判断出错。所以用leftPad的时候心里要默念两句话一是它只加不减二是它不改原串返回值才是填充结果。4.3 重新实现一个自己的leftPad虽然引擎已经提供了LeftPad但自己实现一遍能帮你彻底理解它的细节。其实代码非常短#include Kismet/KismetStringLibrary.h FString MyLeftPad(const FString Source, int32 TargetLength, TCHAR PadChar) { int32 PadCount TargetLength - Source.Len(); if (PadCount 0) { return Source; } FString Result; Result.Reserve(TargetLength); for (int32 i 0; i PadCount; i) { Result.AppendChar(PadChar); } Result.Append(Source); return Result; }注意我加了Reserve(TargetLength)。这个是为了避免循环里反复扩容分配内存。虽然字符串本身不大但养成习惯是有好处的你提前告诉底层我预计要存储这么多空间它可以一次性把内存分配好循环里只管往里塞字符。这个思路在FString频繁拼接大段内容时作用特别明显。顺带说一句如果你想要的不是左填充到指定长度而是居中填充或者右填充RightPad在UKismetStringLibrary里也有现成的。有需要直接查文档就行不需要自己拧。5. 字符串拆成数组ParseIntoArray和相关解析姿势5.1 ParseIntoArray的完整用法很多配置文件、消息协议、CSV导出数据本质上都是用分隔符把多个字段串在一起。比如Tom,Jerry,Spike你希望拿到的是[Tom, Jerry, Spike]这样的数组。FString自己就带了这个能力就是我前面提到过的成员函数FString CSVLine TEXT(Tom,Jerry,Spike); TArrayFString Names; CSVLine.ParseIntoArray(Names, TEXT(,), true); // Names [Tom, Jerry, Spike]注意这是FString的成员函数直接在原字符串上调用不是静态函数。而UKismetStringLibrary里也有对应的静态版本参数顺序不一样TArrayFString Names; UKismetStringLibrary::ParseIntoArray(TEXT(Tom,Jerry,Spike), Names, TEXT(,), true);两个版本的代码我是都遇到过的项目里混用的情况也常有。我的建议是写C时优先用FString的成员函数读起来更自然也少一次静态调用如果这个功能同时要让蓝图用、要写进自定义蓝图库再走UKismetStringLibrary。第三个参数InCullEmpty表示是否过滤空项。比如Tom,,Jerry如果传true空字符串会被丢掉传false数组中会保留一个空字符串元素。按我经验解析路径和配置时基本都传true否则拆分结果里会莫名奇妙多出空串再加上你后续用.、/这种做下标访问一个不存在的元素就可能拿到空数据或者触发断言。5.2 从FString数组到TArray 真正能用的解析字符串拆完之后大多数人真正的需求是转成数值数组。比如拿到服务器下发的分数列表100,89,75,34你最终想得到的是TArrayint32。FString ScoreList TEXT(100,89,75,34); TArrayFString ScoreStrs; ScoreList.ParseIntoArray(ScoreStrs, TEXT(,), true); TArrayint32 Scores; for (const FString ScoreStr : ScoreStrs) { Scores.Add(FCString::Atoi(*ScoreStr)); } // Scores [100, 89, 75, 34]如果分数是浮点数把FCString::Atoi换成FCString::Atof数组类型换成TArrayfloat就行。配合FString的Trim()和TrimTrailing()可以处理用户配置文件里常见的多余空格FString RawLine TEXT( Tom , Jerry ); TArrayFString Parts; RawLine.ParseIntoArray(Parts, TEXT(,), true); for (FString Part : Parts) { Part Part.Trim(); Part Part.TrimTrailing(); }这里有个心得Trim()只去掉字符串开头的空白TrimTrailing()只去掉结尾的空白。如果你两者都调等于是全去掉。UE里的字符串空白不仅仅指空格还包括制表符\t所以处理配置文件时这个操作是必不可少的。5.3 解析路径、URL、CSV时的几个坑第一种坑分隔符选错。解析Windows路径时经常有人用\做分隔符结果FString里反斜杠还需要转义很容易写错。更推荐用\\或者干脆统一用/。UE在这方面比较友好资源路径本身就强制用/但来自外部配置的路径不保证。第二种坑忽略空项过滤。a,,b拆完如果不去空你会得到3个元素其中第二个是空字符串。如果你拿这个数组去跟别人约定好的配置格式对齐一个空项整条逻辑全错。所以如果不是明确需要保留位置信息统一传true。第三种坑分隔符是多个字符。比如Tom;;Jerry;;Spike有些同学直接ParseIntoArray(Array, TEXT(;), true)结果拆出了空串。这时候要么先ReplaceInline(TEXT(;;), TEXT(;))再拆要么干脆写自己的拆解逻辑。多字符分隔符的解析FString的ParseIntoArray只支持单字符这是很多人的盲区。还有一种更隐蔽的情况你的字符串里同时包含分隔符和换行符。比如CSV文件读取后一行里带\r\n结尾你用ParseIntoArray按逗号拆最后一个字段可能会残留\r或者\n打印出来表面看不出来一用Equals比较就永远失败。解决方案是在拆完之后对每个元素做一次Trim()和TrimTrailing()把不可见字符清理掉。5.4 不用数组的拆法Split和二段拆分如果你的需求只是从字符串里取左右两部分Split更合适。很多保存玩家名和分数的逻辑都是这个形式FString SaveData TEXT(PlayerNameTom|Score100); FString LeftPart; FString RightPart; SaveData.Split(TEXT(|), LeftPart, RightPart); // LeftPartPlayerNameTom, RightPartScore100 FString Key; FString Value; LeftPart.Split(TEXT(), Key, Value); // KeyPlayerName, ValueTom一个很典型的场景玩家存档里有一行PlayerNameTom|Score100|Level5你可以先用|拆成数组再逐个用拆分键值。这种二段拆分法是配置文件类的通用解决套路远比直接用正则库方便。6. 全部代码一套字符串操作实战模板6.1 头文件怎么声明下面这套代码我把前面讲到的操作集中封装到一个类里方便直接复制到项目中使用。头文件声明如下// StringTools.h #pragma once #include CoreMinimal.h #include StringTools.generated.h UCLASS() class YOURMODULE_API UStringTools : public UBlueprintFunctionLibrary { GENERATED_BODY() public: // 拼接、格式化示例 UFUNCTION(BlueprintCallable, Category StringTools) static FString FormatPlayerInfo(const FString PlayerName, int32 Score); // 截取文件名去掉目录和后缀 UFUNCTION(BlueprintCallable, Category StringTools) static FString GetFileNameWithoutExtension(const FString FullPath); // 左填充默认用 0 填充到 Width 长度 UFUNCTION(BlueprintCallable, Category StringTools) static FString PadLeft(const FString Source, int32 Width, TCHAR PadChar TEXT(0)); // 按分隔符解析成整数数组 UFUNCTION(BlueprintCallable, Category StringTools) static TArrayint32 ParseIntArray(const FString Source, const FString Delimiter TEXT(,)); // 按分隔符解析成字符串数组 UFUNCTION(BlueprintCallable, Category StringTools) static TArrayFString ParseStringArray(const FString Source, const FString Delimiter TEXT(,)); };我把它做成UBlueprintFunctionLibrary的子类而不是普通类用意是你在蓝图里也能直接搜到这些函数。如果你只是C内部使用完全可以去掉UCLASS和UFUNCTION写成纯静态类减少不必要的反射开销。6.2 实现文件里直接抄的代码// StringTools.cpp #include StringTools.h #include Kismet/KismetStringLibrary.h FString UStringTools::FormatPlayerInfo(const FString PlayerName, int32 Score) { // 直接使用 Printf格式一目了然 return FString::Printf(TEXT(Player: %s | Score: %d), *PlayerName, Score); } FString UStringTools::GetFileNameWithoutExtension(const FString FullPath) { // 1. 找最后一个斜杠的位置 int32 LastSlashIndex FullPath.Find(TEXT(/), ESearchCase::IgnoreCase, ESearchDir::FromEnd); // 2. 从最后一个斜杠后开始截取拿到含后缀的文件名 FString FileName (LastSlashIndex ! INDEX_NONE) ? FullPath.Mid(LastSlashIndex 1) : FullPath; // 3. 去掉后缀 int32 DotIndex FileName.Find(TEXT(.), ESearchCase::IgnoreCase, ESearchDir::FromEnd); if (DotIndex ! INDEX_NONE) { FileName FileName.Left(DotIndex); } return FileName; } FString UStringTools::PadLeft(const FString Source, int32 Width, TCHAR PadChar) { // 直接复用 UKismetStringLibrary 的静态版本 return UKismetStringLibrary::LeftPad(Source, Width, PadChar); } TArrayint32 UStringTools::ParseIntArray(const FString Source, const FString Delimiter) { TArrayint32 Result; // 先拆成字符串数组 TArrayFString StrArray; Source.ParseIntoArray(StrArray, *Delimiter, true); // 再逐个转整数 for (const FString Str : StrArray) { Result.Add(FCString::Atoi(*Str)); } return Result; } TArrayFString UStringTools::ParseStringArray(const FString Source, const FString Delimiter) { TArrayFString Result; Source.ParseIntoArray(Result, *Delimiter, true); // 清理首尾空白防止未知来源数据带空格 for (FString Str : Result) { Str Str.Trim(); Str Str.TrimTrailing(); } return Result; }这段代码里的ParseIntoArray第二个参数是const TCHAR*所以传*Delimiter把FString转成TCHAR指针。这两处转换是C调用时最容易被编译器提示卡住的地方提前写明白能省不少时间。6.3 跑起来看日志输出在任意函数里调试一下FString Info UStringTools::FormatPlayerInfo(TEXT(Tom), 100); UE_LOG(LogTemp, Log, TEXT(Info: %s), *Info); // 输出: Info: Player: Tom | Score: 100 FString Name UStringTools::GetFileNameWithoutExtension(TEXT(/Game/UI/MainMenuMap.umap)); UE_LOG(LogTemp, Log, TEXT(Name: %s), *Name); // 输出: Name: MainMenuMap FString Padded UStringTools::PadLeft(TEXT(9), 3, TEXT(0)); UE_LOG(LogTemp, Log, TEXT(Padded: %s), *Padded); // 输出: Padded: 009 TArrayint32 Nums UStringTools::ParseIntArray(TEXT(100,89,75,34), TEXT(,)); for (int32 Num : Nums) { UE_LOG(LogTemp, Log, TEXT(Num: %d), Num); } // 输出: Num: 100, Num: 89, Num: 75, Num: 34这些日志里的*Info、*Padded同样是一再强调的TCHAR指针取址操作忘了写*UE_LOG编译能过吗某些重载下能过但打出来是空串或者乱码。建议形成肌肉记忆凡是UE_LOG、Printf这类函数输出FString都要加*。7. 常见问题排查速查表7.1 中文乱码编码表象与本质FString在Windows平台默认UTF-16所以中文字符能正常存。乱码往往出现在你把FString转成std::string或者通过TCHAR_TO_UTF8、TCHAR_TO_ANSI这类宏转码的时候。FString Chinese TEXT(你好); std::string Utf8String TCHAR_TO_UTF8(*Chinese); // 转UTF-8 FString Back UTF8_TO_TCHAR(Utf8String.c_str()); // 转回不太建议直接ToString底层转ANSI因为ANSI编码跟系统区域设置有关窗口系统上遇到非简体中文用户转出来的乱码很头疼。统一用UTF-8中转是比较稳妥的方案。7.2 FString、FName、FText到底选谁这个问题真的是老生常谈但还是要讲。FName不可变底层哈希化适合做标识、Tag、资产名比较和查找非常快但不适合拼接和修改。FText面向显示的本地化文本重载多语言时用它但不适合做底层逻辑处理。FString可变适合运行时动态构建、解析、传递。大多数逻辑处理场景直接FString没问题。只有当你需要一个固定不变的名称、并且大量重复比较时才考虑FName。做UI显示且需要多语言用FText。三者的转换也很常见FName NameObj FName(*MyString); // FString - FName FString Str1 NameObj.ToString(); // FName - FString FText TextObj FText::FromString(MyString); // FString - FText FString Str2 TextObj.ToString(); // FText - FString7.3 频繁拼接伤性能Reserve来救FString底层是动态数组反复调用Append每次到达容量上限都会重新分配内存。如果你要在一个循环里拼接几百个字符FString Result; Result.Reserve(1024); // 预期总长度先预留出来 for (int32 i 0; i 100; i) { Result.AppendInt(i); Result.Append(TEXT(,)); }提前预留容量避免循环过程中反复触发分配和拷贝。这个优化在大量日志拼接、CSV生成、网络数据编码时效果很明显。7.4 从字符串到数值的稳健转换FCString::Atoi遇到非法输入会返回0这在很多场景下不够严谨。推荐用LexTryParseString或者LexFromStringint32 Value 0; bool bParsed LexTryParseString(Value, *SomeString); if (bParsed) { // 成功解析 } else { // 解析失败需要处理异常 }这个方式的好处是能明确区分内容就是0和解析失败在做玩家输入校验、外部配置文件读取时这个区别至关重要。7.5 用StringTools这套封装前要搞清楚的两点我这套封装里ParseIntArray内部用了Atoi所以如果传入abc,100,def结果会是[0, 100, 0]而不是报错。如果你要的是严格校验模式需要改成LexTryParseString失败就跳过或者返回错误代码。同理ParseStringArray里自动清理首尾空白对某些格式严格要求保留空格的数据反而是副作用。在你真正把代码贴进重要项目之前把这两条读明白免得业务逻辑偏离预期。从我自己的项目经验看FString的操作本身不难难的是搞清楚每个函数的返回语义和边界行为。尤其是leftPad这类看似简单但处处要留神的函数最容易在不经意间做出错误假设。字符串的常规操作建议多写、多打日志验证把输出结果打印出来看一眼比盯着代码猜快得多。