C#递归遍历复杂对象结构时如何处理可枚举集合类型
递归遍历复杂对象属性提取值的实现方案
核心问题解答
- 可枚举/集合类型识别逻辑
不要用「命名空间是否以System.开头」作为递归判断条件,这个逻辑鲁棒性极差。集合类型的标准判断方式:- 优先排除
string类型:string本质实现了IEnumerable<char>,但业务场景下不需要遍历字符串的单个字符 - 判断目标类型是否实现
System.Collections.IEnumerable接口,符合条件的即为可遍历集合类型 - 泛型集合(如
List<T>、ICollection<T>)取泛型参数T作为集合元素类型递归,非泛型集合(如ArrayList)元素类型按object处理,遍历每个元素时取运行时实际类型递归。
- 优先排除
- 集合遍历未命中的回退逻辑
不需要单独编写回退逻辑。采用深度优先递归规则即可:遍历到集合时逐个遍历内部元素,对每个元素递归执行属性查找;如果当前集合所有元素的属性树遍历完成仍未找到目标属性,递归方法会自动返回到父级节点,继续遍历父级剩余的其他属性。建议给递归方法设置返回值,找到目标值后立刻逐层返回终止遍历,减少无意义的性能损耗。 - 需额外兼容的复杂类型
原逻辑仅判断IsClass会遗漏大量场景,需要补充兼容:- 自定义结构体(struct):值类型不满足
IsClass判断,但结构体内部同样可能嵌套自定义属性,需要纳入递归范围 - 可空值类型(
Nullable<T>):需要先通过Nullable.GetUnderlyingType(type)取到底层实际类型,再判断是否需要继续递归 - 字典类型(
IDictionary/IDictionary<TKey,TValue>):常规业务场景下只需要遍历字典Value部分的属性,不需要遍历Key - 数组类型:本质属于可枚举类型,可被集合判断逻辑覆盖,注意区分交错数组和多维数组的遍历差异即可
- 必须增加循环引用防护:业务对象经常存在双向引用(如用户对象关联部门属性,部门对象包含用户列表属性),不做防护会触发无限递归导致栈溢出,需要通过哈希集合记录已经遍历过的对象实例引用,遇到重复引用直接跳过。
同时需要明确递归终止边界:遇到基元类型(int、bool、long等)、DateTime、decimal、Guid、byte[]这类无需拆解的系统内置类型,直接终止递归即可。
- 自定义结构体(struct):值类型不满足
修正后的可直接运行的实现代码
原实现仅传入Type参数只能遍历类型定义结构,无法获取运行时属性值、也无法遍历集合内实际元素,以下实现直接传入对象实例,支持提取实际属性值:
using System.Collections; using System.Reflection; using System.Runtime.CompilerServices; public static object FindPropertyValue(object source, string targetPropertyName, HashSet<object> visitedInstances = null) { if (source == null) return null; // 初始化循环引用防护集合 visitedInstances ??= new HashSet<object>(new ReferenceEqualityComparer()); if (!visitedInstances.Add(source)) return null; // 已遍历过的实例直接跳过,避免死循环 Type sourceType = source.GetType(); // 遇到不需要拆解的基础类型,直接终止递归 if (IsTerminateType(sourceType)) return null; // 优先处理字典类型 if (source is IDictionary dictionary) { foreach (DictionaryEntry entry in dictionary) { var findInValue = FindPropertyValue(entry.Value, targetPropertyName, visitedInstances); if (findInValue != null) return findInValue; } return null; } // 处理其他集合类型(排除字符串) if (source is IEnumerable enumerable && source is not string) { foreach (var item in enumerable) { var findInItem = FindPropertyValue(item, targetPropertyName, visitedInstances); if (findInItem != null) return findInItem; } return null; } // 遍历当前对象所有公开实例属性 PropertyInfo[] properties = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (PropertyInfo prop in properties) { // 命中目标属性直接返回值,属性名匹配忽略大小写 if (prop.Name.Equals(targetPropertyName, StringComparison.OrdinalIgnoreCase)) { return prop.GetValue(source); } // 递归遍历当前属性的值 var propValue = prop.GetValue(source); var findInProp = FindPropertyValue(propValue, targetPropertyName, visitedInstances); if (findInProp != null) return findInProp; } return null; } // 判断是否为不需要继续递归的终止类型 private static bool IsTerminateType(Type type) { if (type.IsPrimitive || type == typeof(string) || type == typeof(DateTime) || type == typeof(decimal) || type == typeof(Guid) || type == typeof(DateTimeOffset) || type == typeof(byte[])) { return true; } // 可空值类型如果底层是基元类型也终止递归 Type underlyingType = Nullable.GetUnderlyingType(type); if (underlyingType != null) { return IsTerminateType(underlyingType); } return false; } // 旧版本.NET 自定义引用相等比较器,.NET 5+可直接用内置ReferenceEqualityComparer替换 private class ReferenceEqualityComparer : IEqualityComparer<object> { public new bool Equals(object x, object y) => ReferenceEquals(x, y); public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); }
调用说明:直接传入根对象实例和目标属性名即可,方法会自动递归所有嵌套层级、集合、字典、结构体类型,找到第一个匹配的属性值就返回,未找到返回null。如果需要获取所有匹配的属性值,把返回值改为
List<object>,找到值时加入列表不提前返回即可。
内容的提问来源于stack exchange,提问作者BatmanPriddy
相关产品推荐
相关产品推荐

