ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UE4 C++实战:Json数组解析从入门到USTRUCT自动映射

UE4 C++实战:Json数组解析从入门到USTRUCT自动映射

做UE4开发的朋友应该都有这种感觉:Json这东西,平时看起来不起眼,可一到项目里就绕不开。配表、存档、网络请求、工具链数据交换……随便找个商业项目翻一遍,多少都能翻出几处要读Json的地方,而且凡是涉及列表数据、批量数据,基本都压在“Json数组”这个点上。C++侧解析Json数组并不是一个多玄乎的操作,但里面涉及模块配置、类型系统、序列化约定、容错处理一堆细节,网上资料又比较散,新手经常卡在“照着写了却跑不通”的阶段。

这篇内容我把整套流程从头到尾拆开:从工程配置怎么写、测试数据怎么构造,到用UE4内置Json模块读文件、反序列化、遍历数组,再到嵌套结构解析和USTRUCT自动映射,最后附上我在实际项目里踩过的一堆坑。适合刚上手UE4 C++、准备做配表读取或存档系统的人,也适合想把手动解析改成结构体映射的老手。

1. 先把需求看清楚:Json数组到底用在哪些地方

1.1 游戏项目里最常见的四种Json数组场景

Json数组,说穿了就是[ {...}, {...}, {...} ]这样的数据集合。在UE4项目里,它最常出现在四个地方。

第一个是配表驱动。很多项目拿Json当轻量级配置格式,比如武器列表、怪物刷新点、关卡波次配置,一个数组就是一个对象的集合。策划改配置不用重新编代码,程序启动时读一次就行。这类需求的典型特征是:数据量大、结构稳定、字段名是约定好的。

第二个是存档系统。玩家背包、任务进度、已解锁成就,本质上都是一堆数组。存档时把数组序列化成Json字符串,读档时再反序列化回来。这类需求的特点是对容错要求高,存档文件可能被玩家手动改坏、可能版本升级后字段对不上,所以解析代码必须扛得住“脏数据”。

第三个是网络接口数据。排行榜、邮件列表、商城商品、好友列表,服务端下发的数据几乎都是Json数组。这类需求对时序和稳定性要求高,网络返回的数据结构由后端定,客户端只能去适配,嵌套层级往往比本地配置复杂得多。

第四个是编辑器工具链。比如从外部工具导出关卡数据、批量导入角色动画配置,Json数组是中间交换格式的常客。

无论哪种场景,核心动作是一致的:拿到一个Json字符串,把它变成C++能遍历的数据结构,然后逐个字段取出来用。UE4没提供那种“一行代码搞定一切”的魔法接口,但内置模块已经把底层脏活干完了,我们要做的是把流程串对。

1.2 方案取舍:为什么我用UE4自带Json模块而不是第三方库

网上聊Json解析,总有人推荐RapidJson、nlohmann/json,甚至声明UE4自带的Json模块性能不够看。我的观点很明确:游戏项目里,优先用UE4内置Json和JsonUtilities模块,别一上来就引第三方。

理由不复杂。第一,内置模块是引擎团队维护的,随引擎走,平台兼容性有保证。第三方库在Windows上跑得好好的,到了主机平台或移动端可能就出编译问题,那时候你只能干瞪眼。第二,内置Json模块和UE4的类型体系无缝打通,尤其是FJsonObjectConverter可以直接把Json映射到USTRUCT,第三方库想做这件事还得自己写桥接层。第三,减少工程依赖等于减少风险,你的Build.cs里少一行,后面就少操一份心。

RapidJson这种库在极致性能场景下确实有优势,比如高频网络消息解析、超大文件流式解析。但游戏客户端99%的Json解析需求是一次性读取、一次性解析,数据量也不会大到毫秒级都等不了。内置模块的性能完全够用,真正影响体验的往往是文件读取和UI刷新,不是Json解析本身。

1.3 构造一个贯穿全文的示例数据

为了后面讲起来不空对空,我先定义一个模拟项目里常见的配置需求:读取一个“角色配置表”,每个角色有名字、等级、属性列表和一个“技能数组”,技能本身又包含技能ID、技能名和冷却时间。

对应的Json长这样:

[ { "name": "战士", "level": 10, "stats": { "hp": 1200, "attack": 85 }, "skills": [ { "id": 101, "skillName": "猛击", "cooldown": 4.5 }, { "id": 102, "skillName": "旋风斩", "cooldown": 8.0 } ] }, { "name": "法师", "level": 12, "stats": { "hp": 800, "attack": 60 }, "skills": [ { "id": 201, "skillName": "火球术", "cooldown": 3.0 } ] } ]

这个结构覆盖了三种情况:根节点是数组、数组元素是对象、对象里嵌套对象和嵌套数组。把这一个例子吃透,项目里大多数Json数组解析需求都能顺手拿下。

注意:Java和C#里管这种叫List或ArrayList,UE4里对应的就是TArray。FJsonValueArray底层存的不是TArray<FJsonValue>,而是TArray<TSharedPtr<FJsonValue>>,每个指针指向一个多态的FJsonValue对象。理解这一点,后面遍历逻辑就不容易懵。

2. 工程配置与数据准备:别让编译器和文件路径卡住你

2.1 Build.cs里少了Json模块,一切白搭

很多新手第一步就栽在编译上:代码写好了,编译直接报错,说找不到FJsonSerializer或者FFileHelper符号无法解析。十有八九是模块依赖没加上。

需要在项目的.Build.cs文件里,把Json和JsonUtilities加到依赖列表。以某个模拟项目X的X.Build.cs为例:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Json", "JsonUtilities" });

Json模块提供FJsonSerializer、FJsonObject、FJsonValue这些核心类。JsonUtilities模块提供FJsonObjectConverter,这个后面讲自动映射的时候要重点用。

如果你不确定改完有没有生效,最直接的验证方法是把#include "Json.h"和#include "JsonObjectConverter.h"写在.cpp文件顶部,然后编译。编译通过,说明模块链接成功了。如果还报错,检查一下你是不是不小心把模块名写成了JsonUtilities而不是Json,前者依赖后者,但光加后者不够。

2.2 Json文件放哪:路径问题比你想的容易翻车

测试用的Json文件,存放位置直接影响LoadFileToString能不能读成功。UE4里常见的坑是:直接写相对路径,运行出来是空字符串,然后怀疑人生。

相对路径的基准不是工程根目录,也不是项目文件夹根目录,而是引擎的执行环境。调试运行PIE模式下,工作目录通常是Root下几层,不同启动方式还不一样。别赌运气,直接拼绝对路径最稳。

我常用的方案是放在Content/Config下,然后用下面这种方式构造完整路径:

FString FilePath = FPaths::ProjectContentDir() + TEXT("Config/CharacterConfig.json");

FPaths::ProjectContentDir()返回的是当前项目的Content目录完整绝对路径。想多一层保险,可以用FPaths::ConvertRelativePathToFull把相对路径转成绝对路径。

另外注意Windows路径分隔符,Json文件路径里统一用/,C++字符串里写\\也合法,但混用容易看花眼。我建议全部用/,跨Windows和Mac都不用改。

2.3 三个核心类五分钟速成

开始写解析逻辑前,先花五分钟认识三个类,后面代码你看一眼就懂。

FJsonSerializer是入口,提供Deserialize函数,把Json字符串变成TSharedPtr<FJsonValue>。它还管序列化反向操作,但本文核心是读取解析,所以只用Deserialize方向。

FJsonValue是Json值基类,本身是多态的。一个Json节点可能是对象、数组、字符串、数字、布尔值。它提供AsObject()、AsArray()、AsString()、AsNumber()等转换方法。注意,这些方法不会管类型对不对,它只是“尝试”把内部数据转换到你想要的类型。类型不匹配时返回空指针或默认值,而某些方法在编辑器和运行时里会有assert行为,后面坑里细说。

FJsonObject是Json对象类型,可以理解成一组键值对集合。它用FString作为键,值仍然是TSharedPtr<FJsonValue>。FJsonObject提供GetIntegerField("level")这类便捷方法,也提供HasField("name")来判断字段是否存在,还有GetArrayField("skills")直接取出数组字段。

还有一个容易混淆的是TSharedRef和TSharedPtr。简单理解:TSharedRef不允许空引用,创建时必须赋值;TSharedPtr可空,要用.IsValid()判断。Json解析里,TJsonReaderFactory::Create返回TSharedRef<TJsonReader<TCHAR>>,而Deserialize出来的根节点是TSharedPtr<FJsonValue>,所以后面判空是必要的。

3. 解析Json数组的完整实操流程

3.1 从文件读取Json字符串:先保证字符串是完整的

解析的第一步是把Json文件内容读进一个FString。用FFileHelper的LoadFileToString方法,它把整个文件一次性读成字符串:

FString JsonString; bool bLoadSuccess = FFileHelper::LoadFileToString( JsonString, *FilePath, FFileHelper::EHashOptions::None, 0 // 读取标志位,用默认 ); if (!bLoadSuccess) { UE_LOG(LogTemp, Error, TEXT("Failed to load Json file: %s"), *FilePath); return; } if (JsonString.IsEmpty()) { UE_LOG(LogTemp, Error, TEXT("Json file is empty: %s"), *FilePath); return; }

这里有个值得注意的细节:LoadFileToString默认按UTF-8编码读取。如果你的Json文件是带BOM的UTF-8,读出来的字符串第一个字符可能是不可见的0xFEFF。大多数情况下解析器能忽略它,但如果你做的操作是字符串比较或字段名匹配,这个隐藏字符能让你排查半天。

我的习惯是读取之后做一个BOM清理:

if (JsonString.Len() > 0 && JsonString[0] == 0xFEFF) { JsonString.RemoveAt(0); }

这段代码放进去,BOM的问题永远不需要再操心。

3.2 反序列化:把字符串变成可遍历的对象树

拿到字符串后,调用FJsonSerializer::Deserialize。它的输入是TJsonReader,输出是TSharedPtr<FJsonValue>。

TSharedRef<TJsonReader<TCHAR>> Reader = TJsonReaderFactory<TCHAR>::Create(JsonString); TSharedPtr<FJsonValue> RootValue; if (!FJsonSerializer::Deserialize(Reader, RootValue) || !RootValue.IsValid()) { UE_LOG(LogTemp, Error, TEXT("Deserialize Json failed.")); return; }

Deserialize失败的原因通常有三种:Json格式不合法(缺括号、多逗号、引号不匹配)、字符串被截断、文件内容根本就不是Json格式。检查Deserialize的返回值是必须的,别省略。

反序列化成功后,RootValue就是一个指向整棵Json树的根节点。你需要根据Json顶部结构决定走哪条分支:如果顶部是数组,就用AsArray();如果顶部是对象,就用AsObject()。

我们的示例数据顶部是数组,所以:

TArray<TSharedPtr<FJsonValue>> CharacterArray = RootValue->AsArray(); UE_LOG(LogTemp, Log, TEXT("Character count: %d"), CharacterArray.Num());

如果RootValue是数组而调用了AsArray(),返回的就是正常数组;如果根节点是对象而误用AsArray(),得到的数组可能是空或产生断言,这也要注意。

3.3 遍历数组并逐个取值:核心循环的写法

拿到数组以后,一个for循环就是全部核心逻辑:

for (int32 i = 0; i < CharacterArray.Num(); i++) { const TSharedPtr<FJsonValue>& ItemValue = CharacterArray[i]; if (!ItemValue.IsValid()) { continue; } const TSharedPtr<FJsonObject>& CharacterObj = ItemValue->AsObject(); if (!CharacterObj.IsValid()) { UE_LOG(LogTemp, Warning, TEXT("Index %d is not a Json object."), i); continue; } FString Name = CharacterObj->GetStringField(TEXT("name")); int32 Level = CharacterObj->GetIntegerField(TEXT("level")); UE_LOG(LogTemp, Log, TEXT("Character[%d]: %s, Level: %d"), i, *Name, Level); }

这里有个习惯我强烈建议你养成:AsObject()返回的TSharedPtr要判空。为什么?因为Json数组里的元素可能是字符串、数字、布尔值,如果某个元素不是对象,AsObject()会返回空指针,不小心解引用就直接崩了。加了IsValid()判断后,哪怕有一个元素类型和预期不符,也只是打条日志跳过,不至于整个解析流程崩掉。

同样,取字段的时候要了解GetStringField和TryGetStringField的差异。GetStringField的前提是字段必须存在且类型正确,否则在编辑器里会直接触发check失败导致崩溃。TryGetStringField找到字段才返回true并写值,找不到就返回false,不抛异常不崩溃。实话说,配表数据是约定好的,用GetStringField问题不大;但存档、网络这种数据来源不可控的,尽量用TryGet系列更安全。

3.4 嵌套结构的层层拆解:对象里的数组,数组里的对象

到了示例中“技能数组”这种嵌套层级,思路就变成一层一层往下剥。

先取角色对象里的skills数组字段:

const TArray<TSharedPtr<FJsonValue>>* SkillArray = nullptr; if (CharacterObj->TryGetArrayField(TEXT("skills"), SkillArray)) { for (const TSharedPtr<FJsonValue>& SkillValue : *SkillArray) { const TSharedPtr<FJsonObject>& SkillObj = SkillValue->AsObject(); if (!SkillObj.IsValid()) { continue; } int32 SkillId = SkillObj->GetIntegerField(TEXT("id")); FString SkillName = SkillObj->GetStringField(TEXT("skillName")); float Cooldown = SkillObj->GetNumberField(TEXT("cooldown")); UE_LOG(LogTemp, Log, TEXT("Skill: id=%d, name=%s, cooldown=%.1f"), SkillId, *SkillName, Cooldown); } }

TryGetArrayField返回的是TArray<TSharedPtr<FJsonValue>>的指针,不是拷贝,用指针接收能省一次数组拷贝。代价是你在循环里不能修改原数组,但解析场景下根本不需要改。

至于“stats”这个嵌套对象,取法也一样,问题不大,就是先取对象再取字段:

const TSharedPtr<FJsonObject>& StatsObj = CharacterObj->GetObjectField(TEXT("stats")); int32 Hp = StatsObj->GetIntegerField(TEXT("hp")); int32 Attack = StatsObj->GetIntegerField(TEXT("attack"));

实操里遇到真正恶心的嵌套是什么?“数组套数组”,就是Json数组里的元素本身还是一个数组。这种情况通常出现在二维数据中,比如一个坐标点集合、一个矩阵。处理方式和外层一样:外层遍历元素,每个元素再AsArray(),然后第二层继续遍历。逻辑没有新东西,但小心索引别写拧巴了。

嵌套解析的代码,层级多了以后很容易写得又长又啰嗦。我的经验是:超过三层嵌套,就赶紧把它拆成独立函数。比如ParseCharacter、ParseSkill,每个函数只负责解析自己那一层。这既是可读性问题,也是排查效率问题。出错时你能直接定位到是哪一层解析失败,而不用在几百行的嵌套循环里翻来翻去。

4. 进阶玩法:用FJsonObjectConverter把Json直接映射成USTRUCT

4.1 为什么建议把你的数据装进USTRUCT

新手可能觉得手工一个个字段取,写完就完事了。但项目里一旦Json结构复杂、字段又多,手工解析的代码量会膨胀得吓人。举个例子,一个角色配表里有20个字段,你就要写20行取值代码,每个字段还要考虑类型转换是否正确,既不优雅也容易出错。

这时候就该上FJsonObjectConverter自动映射。你需要做的是定义一个USTRUCT,字段名和Json字段名一一对应,然后一行代码把整个Json字符串映射到结构体数组里,UE4自动帮你完成字段匹配和类型转换。好处不只是省代码,更在于:结构体是强类型的,字段就算写错,编译期能提示;结构体还能直接在蓝图中暴露,数据给蓝图用也非常方便。

4.2 定义结构体并完成自动解析

以示例数据为例,先定义两个结构体:

USTRUCT(BlueprintType) struct FSkillData { GENERATED_BODY() UPROPERTY(BlueprintReadWrite, Category = "Data") int32 Id = 0; UPROPERTY(BlueprintReadWrite, Category = "Data") FString SkillName; UPROPERTY(BlueprintReadWrite, Category = "Data") float Cooldown = 0.0f; }; USTRUCT(BlueprintType) struct FCharacterData { GENERATED_BODY() UPROPERTY(BlueprintReadWrite, Category = "Data") FString Name; UPROPERTY(BlueprintReadWrite, Category = "Data") int32 Level = 0; UPROPERTY(BlueprintReadWrite, Category = "Data") TArray<FSkillData> Skills; };

然后一行代码解析:

#include "JsonObjectConverter.h" TArray<FCharacterData> Characters; bool bParseSuccess = FJsonObjectConverter::JsonArrayStringToUStruct( JsonString, &Characters, 0, 0 ); if (bParseSuccess) { for (const FCharacterData& Char : Characters) { UE_LOG(LogTemp, Log, TEXT("Auto parsed: %s, Level=%d, Skills=%d"), *Char.Name, Char.Level, Char.Skills.Num()); } }

JsonArrayStringToUStruct这个函数就是为“Json字符串是数组、目标类型是结构体数组”这种场景设计的。它内部先解析字符串,再遍历数组元素,每个元素自动映射到一个结构体实例。Json里的skills数组会自动填充到TArray<FSkillData>里,Json里没有的字段,结构体保留默认值。匹配规则是大小写敏感的精确匹配,字段名对不上就默认为空或默认值,不会报错。

如果Json顶层是对象而不是数组,换JsonObjectStringToUStruct,第三个和第四个参数分别对应CheckFlags和SkipFlags,控制哪些属性参与映射,一般保持传0就行。

4.3 什么时候用自动转换,什么时候用手工解析

自动映射这么好用,是不是所有场景都该用?也不是。我在实际项目里总结出一个选择标准:结构稳定、字段名明确的Json,优先用自动映射;结构多变、需要在解析过程中根据不同字段值做分支处理的Json,用手工解析。

自动映射有一个天然弱点:它对字段缺失、多余字段非常宽容,都是静默忽略。这在很多场景里反而是优点,但在存档校验场景里就是隐患——你可能期望某个字段必须存在,但JsonObjectStringToUStruct直接给默认值,你根本不知道数据缺损了。

另外,自动映射对性能损耗比手工解析大。一个结构体字段多、数组元素多时,这个差距会明显一点。如果这个Json要在性能敏感路径上反复解析,我建议手工解析或者把自动映射结果缓存下来。

还有个细节:FJsonObjectConverter对字段名匹配是精确匹配,不像Unity的Newtonsoft有特性可以“指定别名”。如果你的Json字段名是skill_name而结构体字段名是SkillName,这就尴尬了。我自己的习惯是,在项目初期就把Json字段命名规范和代码命名规范统一好,都采用一致的风格,避免这种问题发生。如果换不了字段名,那只能手动取几个特殊字段再把结构体填完,或者写转换函数。

5. 实战项目里那些防不胜防的问题与排查记录

5.1 编译链接报错:Unresolved External Symbol

刚把Json相关代码写进工程,编译提示unresolved external symbol,指向FJsonSerializer::Deserialize这类函数。这种问题排查范围很小:几乎百分之百是.Build.cs模块依赖没加全。

我排查的顺序是:先打开.Build.cs确认Json和JsonUtilities是否在PublicDependencyModuleNames里;再确认改完.Build.cs后编译是否重新生成了工程文件,在代码编辑器里改了.Build.cs有时候需要重新生成项目工程(比如在IDE里右键项目重新Generate)。然后确认#include头文件路径有没有问题,"Json.h"没引就会是类型未定义编译错误,JsonObjectConverter.h同理。

如果是符号找不到,九成是模块缺了。补上模块,清理缓存重新编译,问题必解。

5.2 文件读不到:路径、权限、相对路径三连坑

排查文件读取问题时,先在加载失败的分支里打出你拼好的那个FilePath。很多时候你以为是路径不对,一打印发现是某个目录函数返回空。最典型的是把FPaths::ProjectContentDir()用在了不合适的模块里,返回空字符串。

还有个容易忽略的点是Android等移动平台的打包路径。打包后Content目录被压缩进pak文件,FFileHelper::LoadFileToString不一定能直接访问到Content/Config路径下的文件。需要走FPlatformFileManager或者FFileManagerGeneric的流式读取,或者把配置数据放进Config目录但标记为NonPak单独拷贝。这一块早期开发阶段不会暴露,打包出来才会炸。我的建议是,本地调试用直接文件读取,进打包阶段前统一换平台兼容逻辑。

权限问题相对少见,但在Mac或Linux的沙箱环境里可能出现“文件存在但读不了”。这种情况一般是LoadFileToString返回false,但你手动打开路径发现文件好好的。处理办法是检查运行目录和文件权限,当开发环境出现这种问题,找运维或环境配置的人,而不是在代码里硬调。

5.3 类型断言崩溃:GetStringField和类型不匹配

经典崩溃场景:Json里某个字段本该是字符串,但数据源里误写成数字或对象,CharacterObj->GetStringField(TEXT("name"))崩了。崩溃原因深入一点说,是FJsonValue内部利用类型信息调用错误的访问器,触发check或ensure。

排查方法分三步。第一步,在崩溃堆栈里找到是哪一行调用的GetStringField。第二步,打印对应FJsonObject里该字段的真实类型。第三步,看数据源为什么会生成类型不一致的Json。

避免这类崩溃的思路,我推荐一个通用写法:

FString Name; if (CharacterObj->TryGetStringField(TEXT("name"), Name)) { // 用Name } else { UE_LOG(LogTemp, Warning, TEXT("Field 'name' missing or not a string in character index: %d"), i); }

TryGet系列函数不会在任何情况下触发断言,它只返回是否成功。解析外部数据的代码,我全部改成TryGet风格。解析自己项目生成的Json,为了效率我可以偶尔用GetStringField,但前提是对数据结构有绝对信心。没这个信心就别省那点事。

补充一个隐藏知识点:GetNumberField拿float时,如果Json里存的数值超过double精度范围,精度会有损失。涉及大量金额、ID精度要求高的数据,考虑用字符串存储这些字段或者用int64类型对应取值,不要让它变成浮点。

5.4 中文编码与多行字符串:肉眼排查法

用UE4内置Json模块解析中文,基本都能正常出结果。真正出问题的是文件编码——如果是GBK格式,UE4按UTF-8读出来就会乱码。最快判断方式:用代码编辑器打开Json文件,看右下角编码是不是UTF-8无BOM或者UTF-8带BOM。如果不是,请编辑器另存为UTF-8再试。

另一种乱码是走了JsonObjectStringToUStruct,结构体的FString字段变成一串菱形加问号,而手工解析用GetStringField反而正常。这个现象多半是文件用的是UTF-16或者本地代码页编码,读取时编码推断错误。正常情况下UTF-8不会出这种问题,如果出了,且文件内容是外部工具生成的,你要让对方工具把导出编码改成UTF-8。

调试Json解析还有一个实用技巧:打印之前先打印整个Json字符串。这不是为了看内容,而是为了检查字符串是不是被截断了。常见场景是LoadFileToString加载时因为读取标志位设置导致文件内容被截断到第一个空字符位置。多打一条日志,问题一眼就看出来。

5.5 蓝图侧看不到结构体数据:UPROPERTY和USTRUCT标记

用USTRUCT解析完数据,想在蓝图里看或修改,却发现蓝图面板里搜不到这个结构体,或者节点引不到字段。排查点其实是这几处。

第一,结构体是否有USTRUCT(BlueprintType)标记。BitmapType没写,蓝图引擎根本不会公开它。第二,字段是否有UPROPERTY(BlueprintReadWrite)标记。没有UPROPERTY标记的字段不会暴露到蓝图,BlueprintReadWrite决定读写权限,只读用BlueprintReadOnly。第三,TArray<FSkillData>这种数组字段,蓝图里是可以直接访问数组的,但前提是整体结构体也要是BlueprintType。

还有一个老生常谈的问题:在蓝图里接收解析的结果,需要在C++侧把数据赋值给一个UPROPERTY变量,然后蓝图引这个变量,而不是想着直接在C++函数返回整个数组给蓝图。TArray<FCharacterData>按值返回蓝图在低版本UE里是正常能用的,但跨函数调用容易有拷贝问题。我建议在某个UCLASS对象上缓存结果变量,蓝图去读变量,性能更可控,逻辑也更清晰。

最后的实操心得

这篇文章里所有代码我都按最常见的UE4项目写法整理过,但有一点要说清楚:任何项目里Json的最终结构都是由数据源决定的,配表配置文件是策划定的,网络消息是后端定的,存档是之前的自己定的。你把解析逻辑写得再漂亮,结构一换,所有代码照样废掉。所以我把“结构性容错”放在最前面——先判断字段是否存在,再判断类型是否匹配,最后才取数据。看似多写了几行,调试时能帮你省几十倍的时间。

另外有个小建议,如果把数字、字符串、对象、数组都混在一个Json结构里,数据结构设计本身就不健康。能拆对象就拆对象,平铺字段、尽量让一个数组元素的内部结构保持一致,解析代码写起来顺畅,数据维护起来也轻松。我从一个老朋友那里学来的习惯:每次设计Json数据结构的当天,顺手把字段文档和示例文件写好,这个过程重复十次左右,你对Json解析的理解会完全不一样。

数据驱动开发这条路,一旦跨过“读取解析”这道坎,后面就是一片开阔地。配表随便拆、存档随便存、自动生成测试数据、批量接后端接口,你会发现很多以前靠硬编码的玩法全都能换一种更灵活的方式做了。我先写到这里,各位动手去试试吧。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进