C#中递归嵌套对象列表转CSV输出的实现难题
问题分析与解决方案
你的代码当前无法正确处理**并行嵌套列表(如A的BItems和CItems)**的笛卡尔积合并,同时递归子列表(如B的SubBItems)的展开逻辑不符合预期——前者会导致行不匹配,后者可能生成额外行而非扩展列。
核心问题拆解
- 并行列表未做笛卡尔积:A对象的BItems和CItems是同层级并行列表,需要将每个BItem与每个CItem组合成一行(如示例中Code2的BItem分别和两个CItem配对),但你的代码会分别处理两个列表,导致行数据不对应。
- 递归列表处理逻辑错误:B的SubBItems是递归结构,需要展开为当前行的扩展列(如
B_SubB_Name),而非生成新行,但你的代码会将其当作普通列表生成额外行。
解决方案思路
- 区分两种列表类型:
- 笛卡尔积列表:顶层或同层级的并行列表(如BItems、CItems),需与当前行集合做笛卡尔积,生成多行。
- 展开式递归列表:对象内部的递归子列表(如SubBItems),直接将子对象属性追加为当前行的扩展列,不生成新行。
- 以行集合为处理单位:放弃单一行的处理逻辑,全程维护行集合,每遇到列表属性就将现有行集合与列表生成的行集合做笛卡尔积合并。
- 递归展开子列表属性:处理递归列表时,直接遍历子对象属性,追加到当前行的列中(通过前缀区分层级)。
修改后的代码实现
public static List<Dictionary<string, string>> GenerateRows(object obj, HashSet<string> allHeaders) { var initialRows = new List<Dictionary<string, string>> { new Dictionary<string, string>() }; return ProcessObject(initialRows, obj, "", allHeaders); } private static List<Dictionary<string, string>> ProcessObject(List<Dictionary<string, string>> currentRows, object obj, string prefix, HashSet<string> allHeaders) { if (obj == null) return currentRows; var type = obj.GetType(); var processedRows = new List<Dictionary<string, string>>(currentRows); foreach (var prop in type.GetProperties().Where(p => p.CanRead && p.GetIndexParameters().Length == 0)) { var val = prop.GetValue(obj); var propPrefix = string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}_{prop.Name}"; if (val is string || val is ValueType) { // 更新所有当前行的属性值 foreach (var row in processedRows) { row[propPrefix] = val?.ToString(); allHeaders.Add(propPrefix); } } else if (val is IEnumerable enumerable && prop.PropertyType != typeof(string)) { var listItems = enumerable.Cast<object>().ToList(); if (listItems.Count == 0) continue; // 通过属性名判断是否为递归子列表,也可改用自定义特性标记 bool isRecursiveSubList = prop.Name == nameof(B.SubBItems); if (isRecursiveSubList) { // 递归展开子列表属性到当前行 foreach (var row in processedRows) { foreach (var subItem in listItems) { ProcessObject(new List<Dictionary<string, string>> { row }, subItem, propPrefix, allHeaders); } } } else { // 笛卡尔积合并:现有行 × 列表项生成的行 var newRows = new List<Dictionary<string, string>>(); foreach (var row in processedRows) { foreach (var item in listItems) { var itemRows = ProcessObject(new List<Dictionary<string, string>> { new Dictionary<string, string>(row) }, item, propPrefix, allHeaders); newRows.AddRange(itemRows); } } processedRows = newRows; } } else { // 处理嵌套对象,递归更新所有当前行 processedRows = ProcessObject(processedRows, val, propPrefix, allHeaders); } } return processedRows; }
关键逻辑说明
- 笛卡尔积处理:遇到非递归列表时,将现有每一行与列表中的每个对象组合,生成新的行集合,解决并行列表的合并问题。
- 递归列表展开:对于SubBItems这类递归子列表,直接将子对象属性追加到当前行的列中(添加
B_SubB_前缀),不会生成新行,匹配示例输出要求。 - 行集合维护:全程以行集合为处理单位,避免单行处理导致的并行列表无法合并问题。
使用示例
var allHeaders = new HashSet<string>(); var records = GenerateRows(AItems, allHeaders); // 输出CSV var orderedHeaders = allHeaders.OrderBy(h => h).ToList(); Console.WriteLine(string.Join(", ", orderedHeaders)); foreach (var row in records) { var values = orderedHeaders.Select(h => row.TryGetValue(h, out var val) ? val : "").ToArray(); Console.WriteLine(string.Join(", ", values)); }
扩展建议
- 若需支持多个递归子项(如多个SubBItems),可修改递归列表处理逻辑,为每个子项添加索引前缀(如
B_SubB1_Name、B_SubB2_Name)。 - 可通过自定义特性(如
[CsvCartesianList]、[CsvExpandList])标记列表类型,替代硬编码的属性名判断,提升代码通用性。
内容的提问来源于stack exchange,提问作者K Y
相关产品推荐
相关产品推荐

