`
totoxian
  • 浏览: 1075780 次
  • 性别: Icon_minigender_2
  • 来自: 西安
文章分类
社区版块
存档分类
最新评论

不使用反射进行C#属性的运行时动态访问

 
阅读更多

摘要

单纯的反射带来灵活性的同时,也大大降低了应用程序的效率。本文将利用C#的各种技术,就如何实现动态的方法调用或属性访问做一些初步的研究。希望可以给同样需要提高反射性能的朋友一些帮助。

问题的抽象

反射可以用在很多的情景中,但是抽象来看就是用来访问编译时无法确定的成员。这成员可以是方法,也可以是属性。为了简化问题,我们把问题限定在属性的访问上。那么反射这个功能就可以抽象成下面这个接口。

/// <summary>
/// Abstraction of the function of accessing member of a object at runtime.
/// </summary>
public interface IMemberAccessor
{
    /// <summary>
    /// Get the member value of an object.
    /// </summary>
    /// <param name="instance">The object to get the member value from.</param>
    /// <param name="memberName">The member name, could be the name of a property of field. Must be public member.</param>
    /// <returns>The member value</returns>
    object GetValue(object instance, string memberName);

    /// <summary>
    /// Set the member value of an object.
    /// </summary>
    /// <param name="instance">The object to get the member value from.</param>
    /// <param name="memberName">The member name, could be the name of a property of field. Must be public member.</param>
    /// <param name="newValue">The new value of the property for the object instance.</param>
    void SetValue(object instance, string memberName, object newValue);
}

 

下面我们就来探讨这个接口怎么实现才能达到最高效率。

没有优化的反射

使用反射是实现上面接口的最直观最简单的方式。代码如下:

public class ReflectionMemberAccessor : IMemberAccessor
{
    public object GetValue(object instance, string memberName)
    {
        var propertyInfo = instance.GetType().GetProperty(memberName);
        if (propertyInfo != null)
        {
            return propertyInfo.GetValue(instance, null);
        }

        return null;
    }

    public void SetValue(object instance, string memberName, object newValue)
    {
        var propertyInfo = instance.GetType().GetProperty(memberName);
        if (propertyInfo != null)
        {
            propertyInfo.SetValue(instance, newValue, null);
        }
    }
}

 

但是这种方式的效率让人望而却步。经过分析我们可以发现最慢的部分就是GetValue和SetValue这两个调用。

使用Delegate优化的反射

将PropertyInfo的XetValue代理起来是最简单的提高性能方法。而且也已经有很多人介绍了这种方式,

1. Fast Dynamic Property Field Accessors

2. 晚绑定场景下对象属性赋值和取值可以不需要PropertyInfo

如果仅仅是看到他们的测试结果,会以为晚绑定就可以让属性的动态访问的速度达到和直接取值一样的速度,会觉得这生活多么美好啊。但是如果你真的把这个技术用在个什么地方会发现根本不是这么回事儿。真实的生活会如老赵写的Fast Reflection Library中给出的测试结果一般。你会发现即使是晚绑定了或是Emit了,速度也是要比直接访问慢5-20倍。是老赵的实现方式有问题吗?当然不是。

公平的竞赛

这里明确一下我们要实现的功能是什么?我们要实现的功能是,用一组方法或是模式,动态地访问任何一个对象上的任何一个属性。而前面那些看些美好的测试,都只是在测试晚绑定后的委托调用的性能,而那测试用的晚绑定委托调用都是针对某个类的某个属性的。这不是明摆着欺负反射吗?虽然测试用的反射Invoke也是针对一个属性,但是反射的通用版本的性能也是差不多的,Invoke才是消耗的大头。这也是数据统计蒙人的最常见的手法,用自己最好的一部分和对方的最差的一部分去比较。但是我们真正关心的是整体。

用晚绑定这个特性去实现类似反射能实现的功能,是需要把每个类的每个属性都缓存起来,并且在使用的时候,根据当前对象的类型和所取的属性名查找对应的缓存好的晚绑定委托。这些功能在那些美好的测试结果中都完全没有体现出来。而老赵的Fast Reflection Libary实现了这些功能,所以测试结果看上去要差很多。但是这才是真实的数据。

公平的实现方式

为了文章的完整起见,Delegate反射的实现方式如下。(我这里为了简单起见,没有过多优化,如果你要用这个方法,还是有很大的优化空间的。)

方法有两种,一种是使用Delegate.CreateDelegate函数。一种是使用Expression Tree。

使用Delegate的核心代码分别如下所示:

internal class PropertyAccessor<T, P> : INamedMemberAccessor
{
    private Func<T, P> GetValueDelegate;
    private Action<T, P> SetValueDelegate;

    public PropertyAccessor(Type type, string propertyName)
    {
        var propertyInfo = type.GetProperty(propertyName);
        if (propertyInfo != null)
        {
            GetValueDelegate = (Func<T, P>)Delegate.CreateDelegate(typeof(Func<T, P>), propertyInfo.GetGetMethod());
            SetValueDelegate = (Action<T, P>)Delegate.CreateDelegate(typeof(Action<T, P>), propertyInfo.GetSetMethod());
        }
    }

    public object GetValue(object instance)
    {
        return GetValueDelegate((T)instance);
    }

    public void SetValue(object instance, object newValue)
    {
        SetValueDelegate((T)instance, (P)newValue);
    }
}

Delegate.CreateDelegate在使用上有一个要求,其生成的Delegate的签名必须与Method的声明一致。所以就有了上面使用泛型的方式。每个PropertyAccessor是针对特定属性的,要真正用起来还要用Dictionary做下Mapping。如下所示:

public class DelegatedReflectionMemberAccessor : IMemberAccessor
{
    private static Dictionary<string, INamedMemberAccessor> accessorCache = new Dictionary<string, INamedMemberAccessor>();

    public object GetValue(object instance, string memberName)
    {
        return  FindAccessor(instance, memberName).GetValue(instance);
    }

    public void SetValue(object instance, string memberName, object newValue)
    {
        FindAccessor(instance, memberName).SetValue(instance, newValue);
    }

    private INamedMemberAccessor FindAccessor(object instance, string memberName)
    {
        var type = instance.GetType();
        var key = type.FullName + memberName;
        INamedMemberAccessor accessor;
        accessorCache.TryGetValue(key, out accessor);
        if (accessor == null)
        {
            var propertyInfo = type.GetProperty(memberName);
            accessor = Activator.CreateInstance(typeof(PropertyAccessor<,>).MakeGenericType(type, propertyInfo.PropertyType), type, memberName) as INamedMemberAccessor;
            accessorCache.Add(key, accessor);
        }

        return accessor;
    }
}

 

用ExpressionTree的生成委托的时候,也会遇到类型的问题,但是我们可以在ExpressionTree中对参数和返回值的类型进行处理,这样就不需要泛型的实现方式了。代码如下:

public class DelegatedExpressionMemberAccessor : IMemberAccessor
{
    private Dictionary<string, Func<object, object>> getValueDelegates = new Dictionary<string, Func<object, object>>();
    private Dictionary<string, Action<object, object>> setValueDelegates = new Dictionary<string, Action<object, object>>();

    public object GetValue(object instance, string memberName)
    {
        var type = instance.GetType();
        var key = type.FullName + memberName;
        Func<object, object> getValueDelegate;
        getValueDelegates.TryGetValue(key, out getValueDelegate);
        if (getValueDelegate == null)
        {
            var info = type.GetProperty(memberName);
            var target = Expression.Parameter(typeof(object), "target");

            var getter = Expression.Lambda(typeof(Func<object, object>),
                Expression.Convert(Expression.Property(Expression.Convert(target, type), info), typeof(object)),
                target
                );

            getValueDelegate = (Func<object, object>)getter.Compile();
            getValueDelegates.Add(key, getValueDelegate);
        }

        return getValueDelegate(instance);
    }
}

 

一个优化方式是,把这个类做成泛型类,那么key就可以只是memberName,这样就少去了type.FullName及一次字符串拼接操作。性能可以提高不少。但是这种委托式的访问就是性能上的极限了吗?如果是我就不用来写这篇文章了。

虽然山寨却更直接的方法

我们的目标是动态的访问一个对象的一个属性,一谈到动态总是会自然而然地想到反射。其实还有一个比较质朴的方式。就是让这个类自己去处理。还记得一开始定义的IMemberAccessor接口吗?如果我们所有的类的都实现了这个接口,那么就直接调用这个方法就是了。方式如下。

public class Man : IMemberAccessor
{
    public string Name { get; set; }

    public int Age { get; set; }

    public DateTime Birthday { get; set; }

    public double Weight { get; set; }

    public double Height { get; set; }

    public decimal Salary { get; set; }

    public bool Married { get; set; }

    public object GetValue(object instance, string memberName)
    {
        var man = instance as Man;
        if (man != null)
        {
            switch (memberName)
            {
                case "Name": return man.Name;
                case "Age": return man.Age;
                case "Birthday": return man.Birthday;
                case "Weight": return man.Weight;
                case "Height": return man.Height;
                case "Salary": return man.Salary;
                case "Married": return man.Married;
                default:
                    return null;
            }
        }
        else
            throw new InvalidProgramException();
    }

    public void SetValue(object instance, string memberName, object newValue)
    {
        var man = instance as Man;
        if (man != null)
        {
            switch (memberName)
            {
                case "Name": man.Name = newValue as string; break;
                case "Age": man.Age = Convert.ToInt32(newValue); break;
                case "Birthday": man.Birthday = Convert.ToDateTime(newValue); break;
                case "Weight": man.Weight = Convert.ToDouble(newValue); break;
                case "Height": man.Height = Convert.ToDouble(newValue); break;
                case "Salary": man.Salary = Convert.ToDecimal(newValue); break;
                case "Married": man.Married = Convert.ToBoolean(newValue); break;
            }
        }
        else
            throw new InvalidProgramException();
    }
}

 

有人可能会担心用这种方式,属性多了之后性能会下降。如果你用Reflector之类的工具反编译一下生成的DLL,你就不会有这种顾虑了。C#对于 switch语句有相当力度的优化。简略地讲,当属性少时会将switch生成为一堆if else。对于字段类型为string,也会自动地转成dictionary + int。

经过测试这种方式比上面的缓存晚绑定的方式要快一倍。但是劣势也很明显,就是代码量太大了,而且不是一个好的设计,也不优雅。

用动态生成的工具函数让动态属性访问更快一些

上面的方法速度上其实是最有优势的,但是缺乏可操作性。但是如果我们能为每个类动态地生成两个Get/Set方法,那么这个方法就实际可用了。注意,这时的动态调用并不是反射调用了。生成的方式就是使用Expression Tree编译出函数。

又因为这个方式是每个类一个函数,不像之前的方式都是一个属性一个访问对象。我们就可以利用C#的另一个特性来避免Dictionary的使用——泛型类中的静态成员:如果GenericClass<T>中定义的静态成员staticMember,那么GenericClass<A>中的staticMember和GenericClass<B>中的staticMember是不共享的。虽然查找泛型类也需要额外的运行时工作,但是代价比Dictionary查询要低。

在这个方法中,既没有用到反射,也没有用到缓存Dictionary。能更好地保证与手工代码性能的一致度。

实现的代码如下,鉴于代码量,只列出了get方法的代码:

public class DynamicMethod<T> : IMemberAccessor
{
    internal static Func<object, string, object> GetValueDelegate;

    public object GetValue(object instance, string memberName)
    {
        return GetValueDelegate(instance, memberName);
    }

    static DynamicMethod()
    {
        GetValueDelegate = GenerateGetValue();
    }

    private static Func<object, string, object> GenerateGetValue()
    {
        var type = typeof(T);
        var instance = Expression.Parameter(typeof(object), "instance");
        var memberName = Expression.Parameter(typeof(string), "memberName");
        var nameHash = Expression.Variable(typeof(int), "nameHash");
        var calHash = Expression.Assign(nameHash, Expression.Call(memberName, typeof(object).GetMethod("GetHashCode")));
        var cases = new List<SwitchCase>();
        foreach (var propertyInfo in type.GetProperties())
        {
            var property = Expression.Property(Expression.Convert(instance, typeof(T)), propertyInfo.Name);
            var propertyHash = Expression.Constant(propertyInfo.Name.GetHashCode(), typeof(int));

            cases.Add(Expression.SwitchCase(Expression.Convert(property, typeof(object)), propertyHash));
        }
        var switchEx = Expression.Switch(nameHash, Expression.Constant(null), cases.ToArray());
        var methodBody = Expression.Block(typeof(object), new[] { nameHash }, calHash, switchEx);

        return Expression.Lambda<Func<object, string, object>>(methodBody, instance, memberName).Compile();
    }
}

 

但是,好吧,问题来了。泛型类就意味着需要在写代码的时候,或者说编译时知道对象的类型。这样也不符合我们一开始定义的目标。当然解决方案也是有的,就是再把那个Dictionary缓存请回来。具体方式参考上面的给Delegate做缓存的代码。

还有一个问题就是,这种Switch代码的性能会随着Property数量的增长而呈现大致为线性的下降。会最终差于Delegate缓存方式的调用。但是好在这个临界点比较高,大致在40个到60个属性左右。

性能测试

我们先把所有的方式列一下。

  1. 直接的对象属性读写
  2. 单纯的反射
  3. 使用Delegate.CreateDelegate生成委托并缓存
  4. 使用Expression Tree生成属性访问委托并缓存
  5. 让对象自己实现IMemberAccessor接口,使用Switch Case。
  6. 为每个类生成IMemberAcessor接口所定义的函数。(非泛型方式调用)
  7. 为每个类生成IMemberAcessor接口所定义的函数。(泛型方式调用)

我们来看一下这6种实现对应的7种使用方式的性能。

Debug:执行1000万次

方法 第一次结果 第二次结果
直接调用 208ms 227ms
反射调用 21376ms 21802ms
Expression委托 4341ms 4176ms
CreateDelegate委托 4204ms 4111ms
对象自身Switch 1653ms 1338ms
动态生成函数 2123ms 2051ms
(泛型)动态生成函数 1167ms 1157ms

Release:执行1000万次

方法 第一次结果 第二次结果
直接调用 73ms 77ms
反射调用 20693ms 21229ms
Expression委托 3852ms 3853ms
CreateDelegate委托 3704ms 3748ms
对象自身Switch 1105ms 1116ms
动态生成函数 1678ms 1722ms
(泛型)动态生成函数 843ms 862ms

动态生成的函数比手写的Switch还要快的原因是手写的Switch要使用到Dictionary来将String类型的字段名映射到int值。而我们生成的Switch是直接使用属性的hashcode值进行的。所以会更快。

0
0
分享到:
评论

相关推荐

    使用反射动态设定组件属性(C#)

    ### 使用反射动态设定组件属性(C#) #### 核心知识点概述 本文主要探讨了如何在C#中利用反射机制动态地设置组件属性,并通过一个具体的案例——Ini文件的访问类来展示反射技术的应用场景。文章还提到了.NET ...

    C#,利用反射动态创建对象

    综上所述,C#的反射机制为开发者提供了在运行时动态操作代码的能力,尤其在处理不确定类型的对象或者需要实现动态行为时,反射成为了一种不可或缺的工具。然而,需要注意的是,由于其内在的性能成本,应当合理地在...

    C#net反射实现访问类中的私有变量或者方法

    反射是.NET Framework中的一个重要特性,它允许程序在运行时检查自身,并且可以调用类型的字段、属性、方法等成员。这为动态执行代码提供了强大的工具。 #### 1.2 反射的应用场景 反射通常用于以下几种情况: - ...

    C#反射基础学习

    C#反射是.NET框架提供的一种强大机制,它允许在运行时检查和操作程序集、类型、接口、方法、属性等元数据。通过反射,开发者可以在程序执行过程中动态地获取类型信息,并实例化对象、调用方法或访问字段。这篇学习...

    C#动态加载DLL主要说明如何通过反射实现动态加载DLL

    确保只加载可信任的源代码,或者在运行时进行适当的权限检查。 6. 性能影响:反射通常比直接调用方法慢,因为涉及到运行时类型查找和动态方法调用。在性能敏感的场景下,应谨慎使用反射。 7. 错误处理:在实践中,...

    C# 利用反射动态创建对象

    在C#编程中,反射是一种强大的机制,允许在运行时检查和操作程序集、类型、方法、属性等元数据。利用反射,我们可以动态地创建对象,这意味着在代码执行时,我们能够实例化未知类型的对象,这在处理插件系统、动态...

    C# 反射举例 反射实例

    在C#编程语言中,反射是一个强大的特性,它允许运行时的代码动态地获取类型信息并操作对象。本文将深入探讨C#中的反射,并通过具体的实例来帮助大家更好地理解和掌握这个概念。 首先,我们需要理解什么是反射。反射...

    c# 反射应用几乎最全面的实例

    "C# 反射应用几乎最全面的实例"可能包含各种示例,如动态类型创建、方法调用、属性访问、泛型使用以及安全性和性能优化等。学习和理解这些实例有助于开发者更有效地利用反射来编写更加灵活和强大的C#应用程序。

    c#中使用动态类的样例程序

    C# 4.0引入了`dynamic`关键字,它允许我们在编译时不进行任何类型检查,而是在运行时进行所有的类型检查和绑定。这使得能够编写更灵活的代码,尤其是在与非C#组件交互时,如JavaScript库、IronPython等。 2. **...

    c#反射机制的一种使用

    总结,C#的反射机制使得我们能够在运行时动态地访问和操作类型及其成员,这对于处理动态数据绑定、插件架构、元编程等场景非常有用。在本文中,我们学习了如何利用反射将数据库字段与UI控件进行绑定,从而减少手动...

    C#泛型、反射实例、自动生成sql语句

    反射是.NET Framework提供的一个强大工具,它允许运行时的代码分析、访问和修改程序集、类型、方法、属性等元数据。通过反射,我们可以动态地创建对象,调用方法,获取或设置属性值。例如,我们可以通过类型名称获取...

    c# 变量反射使用赋值

    在C#编程中,反射是一种强大的工具,它允许我们在运行时检查类型、接口、属性、方法等信息,并能动态地创建对象和调用方法。在本案例中,我们将探讨如何利用反射来实现变量的动态赋值,特别是在WPF(Windows ...

    C#反射.zip

    1. 性能开销:反射在运行时进行类型查找和成员调用,相比于直接编译时访问,性能会有所下降。 2. 安全性:反射可以访问私有和受保护的成员,可能导致安全风险,需谨慎使用。 3. 错误处理:反射操作可能出现异常,如...

    c# 基于反射、自定义特性、Web Services、xml序列化的应用实例 !

    1. 反射:反射是C#中的一种强大机制,它允许运行时的代码获取类型信息并动态地创建对象、调用方法或访问字段。例如,在“DBWebService”中,我们可能有一个数据库服务,反射可以用来动态加载服务接口并在运行时发现...

    C#反射动态创建菜单

    反射允许程序在运行时获取类型信息,并能够创建对象实例,调用方法,访问字段和属性。在C#中,`System.Reflection`命名空间提供了所有与反射相关的类。例如,`Type`类代表一个类型,`MethodInfo`类表示方法的信息,`...

    c# 反射测试demo

    在.NET编程环境中,C#是一种强大的面向对象的编程语言,而反射则是C#中的一个核心特性,它允许程序在运行时动态地获取类型信息并操作这些类型。在“c# 反射测试demo”中,我们将深入探讨这个关键概念以及如何在实际...

    C#实现反射实例和软件

    总之,C#的反射机制为开发者提供了强大的灵活性,使得代码能够在运行时探索和操作自身,从而实现更加动态和灵活的软件设计。在实践中,虽然反射能解决很多难题,但应谨慎使用,因为过度使用反射可能会影响性能,增加...

    C#的反射

    在.NET框架中,C#的反射是一个强大的工具,它允许程序在运行时检查自身并动态地操作类型和对象。这个特性使得代码具有高度的灵活性和可扩展性,尤其是在设计插件系统、元数据处理和创建动态类型时。下面将详细解释...

    .net c#动态创建程序集、类、属性、方法等

    在.NET框架中,C#提供了一种强大的能力,即能够在运行时动态地创建程序集、类、属性和方法。这种技术通常被称为元编程或者反射,它允许开发者在代码执行过程中生成和修改代码,大大增强了软件的灵活性和可扩展性。在...

Global site tag (gtag.js) - Google Analytics