类树遍历






4.48/5 (8投票s)
实用程序类,用于检查对象的树,使用反射查找符合特定条件的成员。
引言
在过去的几年中,许多技术倾向于使用类来描述数据元素,以简化数据结构的构建和维护。 在这种框架中,用户以清晰的声明方式提供自己的类,并期望框架构建、维护并提供数据对象所需的必要功能。
Microsoft Entity Framework 就是这种框架的一个很好的例子。
受此想法的启发,我正在尝试构建一个框架,用于在共享环境中在远程服务器上存储和检索图形资源(纹理、3D 模型、字体等)。 该框架是抽象的,让用户可以自由地存储自己的类以及相关资源; 框架负责检查用户的类,检测资源成员并操作它们。
任务
我们需要递归地检查根类和所有后代类,查找符合特定条件的公共属性(在本例中实现 IResource),并缓存我们可以从根类访问这些属性的方式。 稍后,我们可以使用这些缓存的方法来检索根类的任何实例的资源属性。
代码
循环遍历对象的公共属性
以下代码块展示了我们可以多么容易地遍历对象的公共属性。
var properties = propertyType.GetProperties(BindingFlags.Instance | BindingFlags.Public); foreach (PropertyInfo property in properties) { //Check each property }
直接访问后代类
使用 PropertyInfo
,我们可以很容易地从其父对象评估后代对象
_getMethod = propertyInfo.GetGetMethod(); var obj = _getMethod.Invoke(parentObj, new object[] { });
在我的代码中,我将 PropertyInfo
包装到一个新类 (PropertyAccessor
) 中,该类缓存属性的 get 方法,以及一些其他信息以确定该属性是简单对象还是对象列表
访问更深层的后代类
通过链接 PropertyAccessor
,我们可以定义如何访问更深层类中的属性。 链接意味着让每个 PropertyAccessor
知道下一个 PropertyAccessor
在类树中转到下一个更深层时要调用的 PropertyAccessor
以下代码显示了 PropertyAccessor
的简化版本
public class PropertyAccessor
{
public PropertyInfo Property { get; private set; }
public PropertyAccessor NextAccessor { get; private set; }
private MethodInfo _getMethod;
public PropertyAccessor(PropertyInfo property, PropertyAccessor nextAccessor)
{
Property = property;
NextAccessor = nextAccessor;
_getMethod = Property.GetGetMethod();
}
public void GetObjects<T>(object parentObj, List<T> results)
{
var obj = _getMethod.Invoke(parentObj, new object[] { });
if (obj is T)
{
results.Add((T)obj);
}
if (NextAccessor != null)
{
NextAccessor.GetObjects(obj, results);
}
}
}
每个 PropertyAccessor
组都包装到一个新类 (TreeAccessor
) 中,该类显示了通过类树到达符合我们条件的属性的方式。
以下代码显示了 TreeAccessor 的简化版本
public class TreeAccessor
{
public List<PropertyAccessor> Accessors { get; set; }
public TreeAccessor()
{
Accessors = new List<PropertyAccessor>();
}
public TreeAccessor(TreeAccessor parentTreeAccessors, PropertyAccessor propAccessor)
{
Accessors = new List<PropertyAccessor>(parentTreeAccessors.Accessors.Count + 1);
Accessors.AddRange(parentTreeAccessors.Accessors);
Accessors.Add(propAccessor);
}
public void GetObjects<T>(object obj, List<T> results)
{
if (Accessors.Count > 0)
{
Accessors[0].GetObjects(obj, results);
}
}
}
递归遍历类树
遍历是通过为每个属性类型递归调用一个函数来完成的,步骤如下
- 我们简单地循环遍历底层类型的所有公共属性。
- 如果属性的类型符合我们的条件,则存储其树访问器。
- 将属性类型作为参数发送到步骤 1。
核心功能由类 ClassTreeTraversal
表示,以下是它的简化版本
public class ClassTreeTraversal
{
private readonly Type _classType;
private readonly Predicate<Type> _typeFilter;
public List<TreeAccessor> MatchedAccessors { get; set; }
public ClassTreeTraversal(Type classType, Predicate<Type> typeFilter)
{
_classType = classType;
_typeFilter = typeFilter;
MatchedAccessors = new List<TreeAccessor>();
var rootAccessor = new TreeAccessor();
FindMembers(rootAccessor, _classType);
}
private void FindMembers(TreeAccessor parentPropertyTreeAccessor, Type parentPropertyType)
{
var childProperties = parentPropertyType.GetProperties(BindingFlags.Instance
| BindingFlags.Public);
//Loop through all public properties of the class in the current level
foreach (var childProperty in childProperties)
{
//Here we can early exclude a complete branch of the tree
//if it not likelly to contain any interesting results
if (!childProperty.PropertyType.IsClass)
continue;
//Create property accessor of the child property
var childPropertyAccessor = new PropertyAccessor(childProperty);
//Create tree accessor of the child property
var childPropertyTreeAccessor = new TreeAccessor(parentPropertyTreeAccessor
, childPropertyAccessor);
//If the property matchs our criteria, store it
if (_typeFilter.Invoke(childProperty.PropertyType))
{
MatchedAccessors.Add(childPropertyTreeAccessor);
}
//Here, we go down to the next level in the tree to check
//all properties of the class represented by the current property
FindMembers(childPropertyTreeAccessor, childPropertyAccessor.Property.PropertyType);
}
}
public void GetObjects<T>(object obj, List<T> results)
{
foreach (var accessor in MatchedAccessors)
{
accessor.GetObjects(obj, results);
}
}
public List<T> GetObjects<T>(object obj)
{
var results = new List<T>();
GetObjects(obj, results);
return results;
}
}
您可能会注意到,该条件以 Predict<Type>
的形式提供,这是一种灵活的方式来自定义树中的搜索。
使用代码
假设我们有一个 Scene3D 类型的对象,我们需要收集在该 Scene3D 类中找到的所有实现 IResource 接口的对象,以下代码显示了实现该目的所需的最少代码
var myScene = new Scene3D();
.
.
.
var classTreeTraversal = new ClassTreeTraversal(typeof(Scene3D ))
.UsingClassFilter((type) => type.IsClass && type.GetInterfaces().Contains(typeof(IResource)))
.Prepare();
var resources = classTreeTraversal.GetObjects<IResource>(myScene);
使用更多选项的另一种方法
var myScene = new Scene3D();
.
.
.
var classTreeTraversal = new ClassTreeTraversal(typeof(Scene3D ))
.UsingClassFilter((type) => type.IsClass && type.GetInterfaces().Contains(typeof(IResource)))
.ExcludingTypes(typeof(DownloadElementResourcesJob), typeof(Exception))
.WithOption(ClassTreeTraversalOptions.SkipValues | ClassTreeTraversalOptions.SkipInterfaces)
.Prepare();
var resources = classTreeTraversal.GetObjects<IResource>(myScene);
使用注意事项
- 调用
.UsingClassFilte
r 是强制性的,至少在我们有另一种在树中定位对象的方法(即.UsingAttribute
)之前是这样。 - 调用
.Prepare()
是可选的,但这会将树遍历推迟到第一次调用.GetObjects
时。 - 完整的代码提供了一组方法来排除遍历过程中的分支,即
.ExcludingTypes
、SkipValue
类型选项、SkipInterfaces
选项。 - 众所周知,反射速度很慢,但每次遍历树的开销每个类型只执行一次,因此最好为同一类型的对象保存 ClassTreeTraversal 实例以供将来使用。
- 如果类在后代类中被引用,则可能会发生无限循环(循环分支)。 在当前版本中,我通过确保在同一个序列中没有两个 PropertyAccessor 共享相同的类型来避免这种情况。
最后一点说明,这个实用程序类对于我的特定场景运行良好,但这并不意味着它没有错误,欢迎任何改进此工作的建议。
我很喜欢编写这段代码; 我希望您也喜欢使用它。
关注点
我太专注于编程了,以至于忘记了我的兴趣,哦,我想起来了,政治和叙利亚灾难……没什么有趣的。
历史
目前还没有历史记录,敬请期待...