C#中含不同类型公共属性的子类设计方案
这个场景太常见了——JSON里同一个字段一会儿是单个对象,一会儿是数组,在C#里确实得花点心思处理。你提到的用子类+抽象类/接口的思路完全可行,甚至是强类型场景下的最优解之一,下面我给你详细拆解几种方案,重点说说你关心的多态实现方式:
方案一:动态类型(快速但不够安全)
如果只是临时读取数据,不想定义太多类,可以用dynamic或者Newtonsoft.Json里的JObject来快速处理,但缺点是编译时没有类型检查,后期维护容易出问题:
using Newtonsoft.Json.Linq; var json = "你的JSON字符串"; var root = JObject.Parse(json); foreach (var node in root["myNodes"]) { var param3 = node["param3"]; if (param3.Type == JTokenType.Object) { // 处理单个对象 var paramValue = param3["myParam3param"].Value<int>(); Console.WriteLine($"单个对象值:{paramValue}"); } else if (param3.Type == JTokenType.Array) { // 处理数组 foreach (var item in param3) { var itemA = item["myItemA"].Value<string>(); Console.WriteLine($"数组项A:{itemA}"); } } }
方案二:抽象类/接口+自定义JsonConverter(强类型+多态)
这正是你考虑的子类思路,完全可行!抽象类和接口都能实现,下面分别说清楚逻辑:
1. 定义基类/接口与子类
先定义一个抽象基类(或者接口)作为param3的统一类型,再分别实现两种场景的子类:
// 抽象基类,也可以换成接口IParam3 public abstract class Param3Base { } // 对应单个对象的场景 public class SingleParam3 : Param3Base { public int myParam3param { get; set; } } // 数组里的单个项 public class Param3Item { public string myItemA { get; set; } public string myItemB { get; set; } public string myItemC { get; set; } } // 对应数组的场景 public class ArrayParam3 : Param3Base { public List<Param3Item> Items { get; set; } }
2. 定义根节点与MyNode类
把param3的类型指定为我们的抽象基类,并标记使用自定义转换器:
public class RootObject { public List<MyNode> myNodes { get; set; } } public class MyNode { public int param1 { get; set; } public string param2 { get; set; } [JsonConverter(typeof(Param3Converter))] // 指定自定义转换器 public Param3Base param3 { get; set; } }
3. 实现自定义JsonConverter
核心逻辑在这里:判断JSON里的param3是对象还是数组,然后实例化对应的子类:
using Newtonsoft.Json; using Newtonsoft.Json.Linq; public class Param3Converter : JsonConverter<Param3Base> { public override Param3Base ReadJson(JsonReader reader, Type objectType, Param3Base existingValue, bool hasExistingValue, JsonSerializer serializer) { var token = JToken.Load(reader); switch (token.Type) { case JTokenType.Object: // 检查是否有myParam3param字段,确认是SingleParam3类型 if (token["myParam3param"] != null) { return token.ToObject<SingleParam3>(); } break; case JTokenType.Array: // 数组类型转成ArrayParam3,把元素存入Items列表 var items = token.ToObject<List<Param3Item>>(); return new ArrayParam3 { Items = items }; } throw new JsonSerializationException("无法识别的param3类型"); } public override void WriteJson(JsonWriter writer, Param3Base value, JsonSerializer serializer) { // 如果需要序列化回去,这里也要对应处理不同子类 if (value is SingleParam3 single) { serializer.Serialize(writer, single); } else if (value is ArrayParam3 array) { serializer.Serialize(writer, array.Items); } else { throw new JsonSerializationException("无法序列化的Param3Base子类"); } } }
4. 使用示例
现在就可以正常反序列化并处理不同类型的param3了:
var json = "{ \"myNodes\": [ { \"param1\": 1, \"param2\": \"myValue2a\", \"param3\": { \"myParam3param\": 0 } }, { \"param1\": 1, \"param2\": \"myValue2b\", \"param3\": [ { \"myItemA\": \"abc\", \"myItemB\": \"def\", \"myItemC\": \"0\" }] }, { \"param1\": 1, \"param2\": \"myValue2c\", \"param3\": [ { \"myItemA\": \"ghi\", \"myItemB\": \"jkl\", \"myItemC\": \"0\" }] }] }"; var root = JsonConvert.DeserializeObject<RootObject>(json); foreach (var node in root.myNodes) { Console.WriteLine($"param2: {node.param2}"); if (node.param3 is SingleParam3 single) { Console.WriteLine($"单个param3值:{single.myParam3param}"); } else if (node.param3 is ArrayParam3 array) { Console.WriteLine($"数组param3包含{array.Items.Count}个项:"); foreach (var item in array.Items) { Console.WriteLine($" {item.myItemA} | {item.myItemB} | {item.myItemC}"); } } }
抽象类 vs 接口?
你纠结用抽象类还是接口,其实都可以,看你的需求:
- 如果
Param3Base需要有共享的属性或默认方法(比如一个IsArray()的判断方法),用抽象类更合适,因为它可以提供默认实现。 - 如果只是想定义一个契约,不需要共享实现逻辑,用接口(比如
IParam3)更灵活,因为C#类可以实现多个接口。
两种方式的转换器逻辑基本一致,只是基类的定义形式不同。
额外提示:System.Text.Json的情况
如果你用的是.NET Core/.NET 5+自带的System.Text.Json,思路完全一样,只是自定义转换器的写法略有不同——需要继承JsonConverter<Param3Base>,用Utf8JsonReader和Utf8JsonWriter来处理JSON token,核心的类型判断逻辑不变。
内容的提问来源于stack exchange,提问作者WEFX
相关产品推荐
相关产品推荐

