使用YamlDotNet实现嵌套字符串到类的动态反序列化方案咨询
问题解答
1. 是否必须使用反射?
不需要手动编写反射逻辑,YamlDotNet本身已经封装了底层反射操作,你只需要自定义类型解析规则即可,无需自己实现反射遍历属性、实例化类的代码。
2. YamlDotNet有没有对应内置支持?
有专门的内置扩展点覆盖你的需求:
INodeTypeResolver接口:支持根据yaml内的自定义字段(比如你的Type字段)动态匹配对应C#类IYamlTypeConverter接口:支持自定义属性的反序列化逻辑,可实现跨配置文件的实例引用
3. 简洁实现方案
第一步:定义基础实体结构
先把所有可配置对象、组件统一基类,方便类型匹配:
// 所有可配置游戏对象的基类 public abstract class GameObjectBase { public string Name { get; set; } public string DisplayName { get; set; } public string Sprite { get; set; } } // 组件基类 public abstract class ComponentBase { } // 炮塔类 public class Turret : GameObjectBase { public int MaxHP { get; set; } public int Damage { get; set; } public float TurretTurnSpeed { get; set; } // 反序列化后会自动赋值为对应子弹实例 public Bullet Projectile { get; set; } // 原始配置的名称仅作为反序列化中间映射字段 public string ProjectileName { get; set; } } // 子弹类 public class Bullet : GameObjectBase { public int Damage { get; set; } public List<ComponentBase> Components { get; set; } = new(); } // 爆炸组件类 public class ExplosionComp : ComponentBase { public string ExplosionType { get; set; } public int Damage { get; set; } public float Radius { get; set; } }
第二步:实现动态类型解析器
实现根据Type字段自动匹配对应C#类的逻辑:
public class GameObjectTypeResolver : INodeTypeResolver { // 程序启动时扫描一次所有游戏对象、组件类型做缓存,避免重复反射损耗 private static readonly Dictionary<string, Type> _typeCache = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(asm => asm.GetTypes()) .Where(t => t.IsSubclassOf(typeof(GameObjectBase)) || t.IsSubclassOf(typeof(ComponentBase))) .ToDictionary(t => t.Name, t => t); public bool Resolve(NodeEvent nodeEvent, ref Type currentType) { // 解析到Type字段时,用字段值匹配对应类型 if (nodeEvent is Scalar scalar && scalar.Value == "Type") { parser.MoveNext(); var typeName = (parser.Current as Scalar)?.Value; if (!string.IsNullOrEmpty(typeName) && _typeCache.TryGetValue(typeName, out var targetType)) { currentType = targetType; return true; } } // 解析Components下的组件时自动匹配对应类型 if (nodeEvent is MappingStart mappingStart && _typeCache.TryGetValue(mappingStart.Anchor?.Value, out var compType)) { currentType = compType; return true; } return false; } }
第三步:实现跨文件引用转换器
把ProjectileName字符串自动映射为已经加载好的子弹实例:
public class ProjectileReferenceConverter : IYamlTypeConverter { private readonly IReadOnlyDictionary<string, GameObjectBase> _configCache; // 传入所有预加载的配置缓存 public ProjectileReferenceConverter(IReadOnlyDictionary<string, GameObjectBase> configCache) { _configCache = configCache; } public bool Accepts(Type type) => type == typeof(Bullet); public object ReadYaml(IParser parser, Type type) { var bulletName = parser.Consume<Scalar>().Value; return _configCache[bulletName]; } public void WriteYaml(IEmitter emitter, object value, Type type) { emitter.Emit(new Scalar(((Bullet)value).Name)); } }
第四步:组合反序列化流程
// 1. 预加载所有依赖的配置(先加载子弹、组件配置,再加载炮塔配置) var configCache = new Dictionary<string, GameObjectBase>(); var allConfigFiles = Directory.GetFiles("你的配置文件目录", "*.yaml"); // 先加载非引用类配置(子弹、组件等) foreach (var file in allConfigFiles.Where(f => !f.Contains("Turret"))) { using var reader = new StreamReader(file); var deserializer = new DeserializerBuilder() .WithNodeTypeResolver(new GameObjectTypeResolver()) .IgnoreUnmatchedProperties() .Build(); var configObj = deserializer.Deserialize<GameObjectBase>(reader); configCache.Add(configObj.Name, configObj); } // 2. 加载炮塔配置,自动完成子弹实例注入 using var turretReader = new StreamReader("Turret.yaml"); var turretDeserializer = new DeserializerBuilder() .WithNodeTypeResolver(new GameObjectTypeResolver()) .WithTypeConverter(new ProjectileReferenceConverter(configCache)) .IgnoreUnmatchedProperties() .Build(); var finalTurret = turretDeserializer.Deserialize<Turret>(turretReader);
补充说明
- 组件的嵌套反序列化无需额外编码,
GameObjectTypeResolver已经支持自动把Components下的ExplosionComp映射为对应类实例 - 如果需要支持循环引用,可以在反序列化构造时添加
.WithAliases()配置 - 类型缓存仅需程序启动时扫描一次,无额外性能损耗
内容的提问来源于stack exchange,提问作者velkyalrie
相关产品推荐
相关产品推荐

