从‘硬编码’到‘动态配置’用C#反射JSON打造通用数据存储与读取模块附Unity示例在游戏开发中数据持久化是一个绕不开的话题。无论是玩家存档、配置设置还是关卡进度都需要可靠地将对象状态保存到磁盘并在需要时准确还原。传统做法往往是为每个需要保存的类编写特定的序列化代码——这种硬编码方式不仅重复劳动更会在类结构变更时带来维护噩梦。想象一下每次新增一个需要保存的字段都要手动修改序列化逻辑这种开发体验显然不够优雅。反射Reflection作为C#的高级特性为解决这类问题提供了全新思路。通过运行时分析类型信息我们可以构建一个完全通用的数据存储模块——它不关心具体传入什么类只关注类的公共字段结构结合JSON的灵活性实现真正的一次编写到处使用。本文将带你从零实现这样一个系统并分享在Unity中的实战技巧。1. 为什么需要通用存储模块在中小型项目中直接使用Unity自带的PlayerPrefs或许能满足基本需求。但当数据复杂度上升时这种简单键值存储的局限性就会暴露类型支持有限仅支持int、float、string等基础类型缺乏结构化复杂对象需要手动拆解存储难以维护字段增减时需要同步修改存储逻辑更专业的做法是采用JSON或二进制序列化。但即便是Newtonsoft.Json这样的强大库也需要为每个类单独调用序列化方法。当你有几十个需要保存的类时这种重复代码会迅速膨胀。反射的妙处在于它允许我们在运行时获取任意类型的字段信息动态读取/设置字段值结合Attribute实现精细控制这使得构建一个统一的数据存取接口成为可能开发者只需关注数据模型本身无需再为序列化逻辑分心。2. 反射基础运行时类型探索理解反射是构建通用模块的关键。让我们通过几个核心API快速掌握基本用法// 获取类型信息 Type type typeof(PlayerData); // 获取所有公共字段 FieldInfo[] fields type.GetFields(BindingFlags.Public | BindingFlags.Instance); // 读取字段值 object value fieldInfo.GetValue(targetObject); // 设置字段值 fieldInfo.SetValue(targetObject, newValue);这些基础操作已经足够实现字段的自动遍历。但优秀的反射代码还需要考虑性能优化缓存Type和FieldInfo对象异常处理处理字段访问权限等问题类型安全确保赋值时的类型兼容性下面是一个简单的字段遍历示例public static void DumpFields(object obj) { Type type obj.GetType(); foreach (FieldInfo field in type.GetFields()) { Debug.Log(${field.Name}: {field.GetValue(obj)}); } }3. 构建通用存储系统结合反射与JSON序列化我们可以设计出如下架构[数据类] → [反射分析] → [JSON转换] → [文件IO]具体实现分为三个核心部分3.1 数据保存流程public static void SaveToFileT(T data, string filePath) { // 1. 通过反射获取所有公共字段 var fields typeof(T).GetFields(); // 2. 构建动态字典 var dict new Dictionarystring, object(); foreach (var field in fields) { dict[field.Name] field.GetValue(data); } // 3. 序列化为JSON string json JsonConvert.SerializeObject(dict); // 4. 写入文件 File.WriteAllText(filePath, json); }3.2 数据加载流程public static T LoadFromFileT(string filePath) where T : new() { // 1. 读取JSON文本 string json File.ReadAllText(filePath); // 2. 反序列化为字典 var dict JsonConvert.DeserializeObjectDictionarystring, object(json); // 3. 创建新实例 T instance new T(); // 4. 通过反射设置字段值 foreach (var pair in dict) { FieldInfo field typeof(T).GetField(pair.Key); if (field ! null) { object value Convert.ChangeType(pair.Value, field.FieldType); field.SetValue(instance, value); } } return instance; }3.3 字段过滤机制有时我们不想保存所有公共字段可以通过自定义Attribute实现精细控制[AttributeUsage(AttributeTargets.Field)] public class NonSerializedField : Attribute { } // 使用示例 public class PlayerData { public string playerName; public int level; [NonSerializedField] public Vector3 position; // 该字段不会被保存 }修改保存逻辑时跳过标记字段foreach (var field in fields) { if (!field.IsDefined(typeof(NonSerializedField), false)) { dict[field.Name] field.GetValue(data); } }4. Unity实战优化在Unity中使用这套系统时还需要考虑一些引擎特有的问题4.1 处理Unity特有类型Unity的Vector3、Color等类型需要特殊序列化处理。可以扩展JSON转换器public class Vector3Converter : JsonConverterVector3 { public override Vector3 ReadJson(...) { // 反序列化实现 } public override void WriteJson(...) { // 序列化实现 } }使用时注册转换器JsonConvert.DefaultSettings () new JsonSerializerSettings { Converters new ListJsonConverter { new Vector3Converter() } };4.2 性能优化策略反射虽然强大但过度使用会影响性能。以下是几个关键优化点缓存反射结果将Type和FieldInfo存储在静态字典中使用快速反射库如FastMember避免频繁调用批量处理数据而非单条处理private static DictionaryType, FieldInfo[] _fieldCache new(); public static FieldInfo[] GetCachedFields(Type type) { if (!_fieldCache.TryGetValue(type, out var fields)) { fields type.GetFields(); _fieldCache[type] fields; } return fields; }4.3 多存档管理实际游戏往往需要管理多个存档槽位。我们可以扩展系统支持public class SaveSlotManager { private const string SAVE_PREFIX save_; public void SaveGameT(T data, int slot) { string path ${SAVE_PREFIX}{slot}.json; SaveToFile(data, path); } public T LoadGameT(int slot) where T : new() { string path ${SAVE_PREFIX}{slot}.json; return File.Exists(path) ? LoadFromFileT(path) : new T(); } }5. 进阶技巧与边界情况要让系统真正健壮还需要处理一些特殊场景5.1 版本兼容性当数据结构变更时需要考虑旧存档的兼容问题。可以引入版本号机制[Serializable] public class SaveMetadata { public int version; public DateTime saveTime; } public static void SaveWithVersionT(T data, string path, int version) { var wrapper new { metadata new SaveMetadata { version version }, data data }; SaveToFile(wrapper, path); }加载时检查版本并做相应转换public static T LoadWithVersionT(string path, int currentVersion) where T : new() { var wrapper LoadFromFiledynamic(path); if (wrapper.metadata.version currentVersion) { return MigrateData(wrapper.data, wrapper.metadata.version); } return wrapper.data; }5.2 加密与压缩敏感数据应该加密存储大存档可以考虑压缩public static void SaveEncryptedT(T data, string path, string key) { string json JsonConvert.SerializeObject(data); byte[] encrypted EncryptString(json, key); File.WriteAllBytes(path, encrypted); } private static byte[] EncryptString(string text, string key) { // 使用AES等加密算法实现 }5.3 异步操作为避免卡顿文件IO应该异步执行public async Task SaveAsyncT(T data, string path) { string json await Task.Run(() JsonConvert.SerializeObject(data)); await File.WriteAllTextAsync(path, json); }在Unity中可以使用UniTask等库获得更好的性能。6. 替代方案对比虽然反射方案很强大但也要了解其他可选方案的特点方案优点缺点硬编码序列化性能最佳维护成本高反射JSON通用性强反射有性能开销代码生成平衡性能与通用性需要生成步骤ECS架构天然适合序列化架构改造成本大对于大多数Unity项目反射JSON提供了最佳的平衡点。但在性能敏感场景可以考虑使用代码生成工具如MessagePack或Protocol Buffers。实现这个系统后最直观的感受就是开发效率的提升。最近一个中型项目中使用这套方案将数据存储相关的代码量减少了约70%而且再也不用担心忘记更新序列化逻辑了。特别是在快速迭代阶段能够随时增减字段而无需修改存储代码这种流畅感是传统方式无法比拟的。