65.9K
CodeProject 正在变化。 阅读更多。
Home

类树遍历

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.48/5 (8投票s)

2015 年 10 月 12 日

CPOL

3分钟阅读

viewsIcon

12398

downloadIcon

3

实用程序类,用于检查对象的树,使用反射查找符合特定条件的成员。

引言

在过去的几年中,许多技术倾向于使用类来描述数据元素,以简化数据结构的构建和维护。 在这种框架中,用户以清晰的声明方式提供自己的类,并期望框架构建、维护并提供数据对象所需的必要功能。

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. 我们简单地循环遍历底层类型的所有公共属性。
  2. 如果属性的类型符合我们的条件,则存储其树访问器。
  3. 将属性类型作为参数发送到步骤 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);

使用注意事项

  • 调用 .UsingClassFilter 是强制性的,至少在我们有另一种在树中定位对象的方法(即 .UsingAttribute)之前是这样。
  • 调用 .Prepare() 是可选的,但这会将树遍历推迟到第一次调用 .GetObjects 时。
  • 完整的代码提供了一组方法来排除遍历过程中的分支,即 .ExcludingTypesSkipValue 类型选项、SkipInterfaces 选项。
  • 众所周知,反射速度很慢,但每次遍历树的开销每个类型只执行一次,因此最好为同一类型的对象保存 ClassTreeTraversal 实例以供将来使用。
  • 如果类在后代类中被引用,则可能会发生无限循环(循环分支)。 在当前版本中,我通过确保在同一个序列中没有两个 PropertyAccessor 共享相同的类型来避免这种情况。

最后一点说明,这个实用程序类对于我的特定场景运行良好,但这并不意味着它没有错误,欢迎任何改进此工作的建议。

我很喜欢编写这段代码; 我希望您也喜欢使用它。

关注点

我太专注于编程了,以至于忘记了我的兴趣,哦,我想起来了,政治和叙利亚灾难……没什么有趣的。

历史

目前还没有历史记录,敬请期待...

© . All rights reserved.