C# 反射优化

概述

反射是 Microsoft .NET 框架中非常强大的功能之一。除了在运行时动态检索元数据信息(如方法、属性和特性)外,它还在命名空间 System.Reflection 下提供了一套丰富的 API,用于加载和处理程序集和对象。

使用反射确实使开发人员的工作变得简单,但应尽量少用,换句话说,只有在需要时才使用,否则会在很大程度上影响性能。有不同的技术可以帮助高效地使用反射。

很多时候,我们在使用反射时并没有意识到它所带来的代价。本文将介绍一些优化使用反射的技术。

解决方案

本文将讨论一些完全避免使用反射或至少避免重复调用反射的技巧。我们将探讨一些可以有效使用反射的场景。

场景1 - 动态调用方法

如果在运行时创建的对象上要调用的方法/成员的名称在开发时是已知的,那么不要使用反射来动态调用它们,而是使用接口并静态调用它们。

完全动态调用的示例

public void DynamicExecution()
{
    Type objType = Type.GetType("DynamicExecution.Test.DynamicClass");

    object obj = Activator.CreateInstance(objType);
    MethodInfo mInfo = objType.GetMethod("AddNumbers",
	new Type[] { typeof(int), typeof(int) });

    // 在 obj 上动态执行 AddNumbers 方法
    mInfo.Invoke(obj, new object[] { 1, 5 });

    PropertyInfo pInfo = objType.GetProperty("ID");

    // 在 obj 上动态设置 ID 属性
    pInfo.SetValue(obj, "TEST_ID", null );
}

使用上述代码的缺点是: 

  1. 对方法或属性的每次动态调用都比对同一方法或属性的静态调用开销几倍。
  2. 在编译期间不会检查方法参数和属性值的类型安全性,如果提供的参数类型不合适,则代码可能会在运行时中断。
  3. 代码比优化的代码更长,因此可维护性较差。

优化代码示例

Public void OptimizedDynamicExecution()
{
    Type objType = Type.GetType("DynamicExecution.Test.DynamicClass");

    IDynamicClass obj = Activator.CreateInstance(objType) as IDynamicClass;

    if (null != obj)
    {
        // 在 obj 上静态执行 AddNumbers 方法
        int result = obj.AddNumbers(1, 5);

        // 在 obj 上静态设置 ID 属性
	// The code will not compile if value is assigned as "TEST_ID"
        obj.ID = 10; 
    }
}

// 优化动态执行 API 使用的接口
public interface IDynamicClass
{
    int ID { get; set; }

    int AddNumbers(int a, int b);
}

使用上述代码的好处是: 

  1. 由于该方法是静态执行的,因此执行速度快。
  2. 通过使用接口实现类型安全。
  3. 可以根据需要在其他地方使用相同的接口。
  4. 用于进行方法调用等的较短代码。

场景2 - 从类中读取自定义属性

自定义特性(Attributes)用于提供有关程序集、类或类成员的附加信息,因此,如果一个类修饰了每次创建该类的实例时都需要的特性,则建议将该值缓存在静态变量中,以便反射仅用于第一个实例,并且可以避免用于后续实例。

下面是一个实体类的示例:

[Table(Name="Employees")]
public class Employee : Entity
{
    [PrimaryKey]
    public int Id { get; set; }
    
    public string Name { get; set; }
    public string Address { get; set; }
    public DateTime DOB { get; set; }
}

上面的代码是一个实体类的示例,它对应于数据库中名为“Employees”的表。如果您仔细观察它,属性Id被PrimaryKey特性标记。所有成员,包括类本身,都用属性装饰,都具有一些特殊的含义,这是此类的使用者在处理其每个实例时所必需的。

为了检索此信息,我们必须应用反射来读取此类的特性(Attributes)。如果对此类的每个实例重复此操作,则成本将非常高。从性能的角度来看,一个更好的想法是仅在第一次读取它,并为所有后续实例缓存它。

动态检索每个实例的属性信息的示例

public class Entity
{
    public Entity()
    {
        Type curType = this.GetType();

        // 以下反射代码在创建此类的每个实例时都会被执行。
        object[] tableAttributes =
		curType.GetCustomAttributes(typeof(TableAttribute), true);

        if(null != tableAttributes && tableAttributes.Count() > 0)
        {
            // 读取特性(attribute)信息
        }

        // 在这里使用特性(attribute)信息
    }
}

使用上述代码的缺点是

  1. 每次创建此类的对象时,都会使用反射动态检索属性信息。
  2. 使用反射检索属性信息是一项非常昂贵的操作。

以下示例演示了缓存属性信息以实现更好性能的概念:

// 用于保存表信息的结构
public struct TableInfo
{
    public string TableName;
    public string PrimaryKey;
}

在下面的此示例代码中,构造函数使用当前实体类的类型作为键来查找静态字典对象中是否存在表信息。它不会在第一次被发现,这时将使用反射检索属性信息,并保留在集合中以供后续检索。这就是我们节省反射成本的方式。

public class Entity
{
    private static Dictionary<Type, TableInfo> tableInfoList = 
                                         new Dictionary<Type, TableInfo>();

    public Entity()
    {
        Type curType = this.GetType();
        TableInfo curTableInfo;
        
        if (!tableInfoList.TryGetValue(curType, out curTableInfo))
        {
            lock (this)
            {
                // 仔细检查以确保在this锁之前没有创建实例
                if (!tableInfoList.TryGetValue(curType, out curTableInfo))
                {
                    object[] tableAttributes = curType.GetCustomAttributes(typeof(TableAttribute), true);

                    if(null != tableAttributes && tableAttributes.Count() > 0)
                    {
                        curTableInfo = new TableInfo();
                        curTableInfo.TableName = ((TableAttribute) tableAttributes[0]).Name;
                    }
                }
            }
        }

        // 在这里使用 curTableInfo 
    }
}

上面的示例代码演示了如何从自定义属性中检索一次信息,并将其用于同一类的后续实例,而无需每次都运行反射代码。

使用此技术的好处是:

  1. 反射仅在第一次应用,并且所有后续时间都使用信息的缓存副本。
  2. 通过使用缓存技术,可以最大限度地降低反射成本。

场景3 - 依赖注入

很多时候,我们需要在一些代码片段中注入依赖项,以便将特定功能的实现逻辑与其消费者的逻辑解耦,以便实现可以在以后的时间点由不同的提供者提供。为了处理这种情况,我们通常允许用户在配置文件中配置程序集和要注入的类。

在这种情况下,应用程序将需要加载程序集,并在每次需要时使用反射动态创建依赖项类的实例;就处理而言,此操作非常昂贵,并且在应用程序的这一部分流量过大的情况下,将导致性能不佳。

Microsoft .NET 轻量级代码生成有助于解决在设计时创建未知的任何对象的实例的问题。它在System.Reflection.Emit命名空间下提供了一组 API,用于以编程方式创建程序集、类和方法等。

此技术可用于减少每次使用反射创建对象的开销,方法是创建静态创建所需类的实例的动态方法。然后,可以将此方法的委托缓存在字典中,以便后续调用。

以下示例演示了此概念 

public interface ISecurityProvider
{
    bool ValidateUser(string userId, string password);
    List<User> GetUsersList();
}

public interface DefaultSecurityProvider : ISecurityProvider
{
    public bool ValidateUser(string userId, string password)
    {
        ...
    }
    public List<User> GetUsersIist()
    {
        ...
    }
}

完全动态调用的示例

// 动态创建 DefaultSecuirtyProvider 类实例的方法
private void CreateInstance()
{
    Type classType = Type.GetType("DefaultSecurityProvider, AssemblyName");
    
    // 以下代码每次都使用反射动态创建依赖类的实例。
    ISecurityProvider employeeInstance = Activator.CreateInstance(classType) as ISecurityProvider;
} 

使用上述代码的缺点是: 

  1. 每次需要依赖类的实例时,都将使用反射动态创建该实例。
  2. 使用反射动态创建对象是一项非常昂贵的操作。

以下代码演示了如何避免反射成本: 

// 动态创建 DefaultSecuirtyProvider 类实例的方法
private void CreateInstance()
{
    Type classType = Type.GetType("DefaultSecurityProvider, AssemblyName");

    // 以下函数静态地返回用于实例化所需对象的委托。
    CreateInstanceDelegate createInstance = ObjectInstantiater(classType);

    // 执行用于创建依赖类实例的委托
    ISecurityProvider employeeInstance = createInstance() as ISecurityProvider;
}

以下委托将用于映射使用 Microsoft .NET 轻量级代码生成技术动态创建的方法 

// 用于保存对象实例化器方法的委托
public delegate object CreateInstanceDelegate();

以下字典对象将保存动态创建的方法的代表,这些方法是为了实例化依赖类对象

// 用于保存对象实例化委托的字典
private static Dictionary<Type, CreateInstanceDelegate> _createInstanceDelegateList = new Dictionary<Type, CreateInstanceDelegate>();

以下函数在运行时以编程方式创建一个方法,用于实例化给定类型的对象。

// 动态创建给定类型实例的方法
public static CreateInstanceDelegate ObjectInstantiater(Type objectType)
{
    CreateInstanceDelegate createInstanceDelegate;

    if (!_createInstanceDelegateList.TryGetValue(objectType, out createInstanceDelegate))
    {
        lock (objectType)
        {
            if (!_createInstanceDelegateList.TryGetValue(objectType, out createInstanceDelegate))
            {
                // 创建一个新方法        
                DynamicMethod dynamicMethod = new DynamicMethod("Create_" + objectType.Name, objectType, new Type[0]);

                // 获取插入类型的默认构造函数
                ConstructorInfo ctor = objectType.GetConstructor(new Type[0]);

                // 生成中间语言      
                ILGenerator ilgen = dynamicMethod.GetILGenerator();
                ilgen.Emit(OpCodes.Newobj, ctor);
                ilgen.Emit(OpCodes.Ret);

                // 创建一个新委托并存储到字典中
                createInstanceDelegate = (CreateInstanceDelegate)dynamicMethod.CreateDelegate(typeof(CreateInstanceDelegate));
                _createInstanceDelegateList[objectType] = createInstanceDelegate;
            }
        }
    }
    return createInstanceDelegate; // 返回对象实例化器委托
}

上面的代码演示了如何静态创建任何类的对象,该对象在设计时是未知的,只能由其接口(合约)识别。

在上面的示例中,我们使用System.Reflection.Emit命名空间中提供的 API 动态创建了一个代理方法,该 API 静态创建依赖项类的实例。一旦创建了这个方法,我们就把它存储在字典中,而不是依赖类的类型作为键,这样在以后的所有时间里,我们都可以从字典中提取这个委托并使用它。

使用此技术的好处是:

  1. 依赖项类的对象是使用Object实例化方法静态创建的。
  2. 这种技术有助于避免对象实例化的反射。

场景4 - 动态设置ORM实体的属性

在所有 ORM 框架中,都有与数据库中的表相对应的实体类。该框架应用反射来读取属性(列)的值,并从实体类的每个实例设置属性(列)的值。这种操作非常昂贵,并且可能会在很大程度上降低性能。这可以通过以与方案 3 中演示的相同方式生成动态方法来处理,但在此示例中,我们将使用 .NET Framework 3.5 中引入的称为表达式树的新功能来实现此目的。

以下示例演示了此概念 

// Employees实体类
public class Employees
{
    public int EmployeeID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public DateTime BirthDate { get; set; }
}

// Employees列表用于从数据读取器中检索数据进行填充
List<Employees> empList = new List<Employees>();

以下代码填充数据读取器中的Employees列表

List<Employees> empList = new List<Employees>();

using(SqlConnection con = new SqlConnection(@"Data Source=localhost\SQLEXPRESS;
   Initial Catalog=Northwind;Integrated Security=True"))
{
    SqlCommand cmd = new SqlCommand("Select * from employees");
    cmd.Connection = con;
    con.Open();

    using (SqlDataReader reader = cmd.ExecuteReader())
    {
        empList = ReadList<Employees>(reader);
    }
}

使用反射动态设置实体类的属性值的示例

public List<T> ReadList<T>(SqlDataReader reader) where T : new()
{
    var list = new List<T>();

    while (reader.Read())
    {
        T entity = new T();
        Type entityType = typeof(T);
        foreach(var entityProperty in entityType.GetProperties())
        {
            entityProperty.SetValue(entity, reader[entityProperty.Name], null);
        }

        list.Add(entity);
    }
    return list;
}

使用上述代码的缺点是:

  1. 使用反射设置对象的属性是一项非常繁重的操作。
  2. 开发商在设定房产价值时必须注意类型安全

下面的示例演示了如何从实体的SqlDataReader对象中读取值并将其添加到实体列表中,而无需编写事件静态代码,也无需重复应用反射。

Following 函数循环访问数据读取器,并使用相关值填充实体列表

public List<T> ReadList<T>(SqlDataReader reader)
{
    var list = new List<T>();  
    Func<SqlDataReader, T> readRow = GetReader<T>(); 

    while (reader.Read())
    {
        list.Add(readRow(reader));
    }
    return list;
}

以下字典包含动态生成的方法的委托,用于将数据表值填充到实体列表中 

ConcurrentDictionary<Type, Delegate> ExpressionCache = new ConcurrentDictionary<Type, Delegate>();

以下方法使用 Microsoft .NET 的表达式树功能以编程方式创建函数。动态生成的函数将作为委托保存在字典中,以便以后检索

// 动态创建实体属性
public Func<SqlDataReader, T> GetReader<T>()
{
    Delegate resDelegate;
    if (!ExpressionCache.TryGetValue(typeof(T), out resDelegate))
    {
        // 获取 SqlDataReader 的索引器属性 
        var indexerProperty = typeof(SqlDataReader).GetProperty("Item", new[] { typeof(string) });
        var statements = new List<Expression>();
        ParameterExpression instanceParam = Expression.Variable(typeof(T));
        ParameterExpression readerParam = Expression.Parameter(typeof(SqlDataReader));

        BinaryExpression createInstance = Expression.Assign(instanceParam, Expression.New(typeof(T)));
        statements.Add(createInstance);

        foreach (var property in typeof(T).GetProperties())
        {
            MemberExpression getProperty = Expression.Property(instanceParam, property);
            IndexExpression readValue =
                Expression.MakeIndex(readerParam, indexerProperty,
                new[] { Expression.Constant(property.Name) });

            BinaryExpression assignProperty = Expression.Assign(getProperty, Expression.Convert(readValue, property.PropertyType));

            statements.Add(assignProperty);
        }
        var returnStatement = instanceParam;
        statements.Add(returnStatement);

        var body = Expression.Block(instanceParam.Type,
            new[] { instanceParam }, statements.ToArray());

        var lambda = Expression.Lambda<Func<SqlDataReader, T>>(body, readerParam);
        resDelegate = lambda.Compile();

        ExpressionCache[typeof(T)] = resDelegate;
    }
    return (Func<SqlDataReader, T>)resDelegate;
}

在上面的代码中,我们使用 .NET 表达式树功能动态创建了一个函数,该函数在从作为参数传递给它的SqlDataReader对象中读取实体对象的值后设置它们的值。此方法被编译并缓存在字典中,以便可以在所有后续时间使用它,而无需重新创建它。

使用此技术的好处是:

  1. 开发人员无需编写用于设置任何实体值的代码。
  2. 为该方法创建的表达式树可以序列化并跨进程边界传输。
  3. 使用 .NET API 创建表达式树比编写 IL 代码更容易。

结论

我们研究了四种使用场景,在这些场景中,反射技术以不适当的方式使用,导致性能不佳,我们还研究了可以通过使用反射的替代方案或使用缓存机制来避免重复使用反射来提高性能的技术。

本文中使用的缓存技术可确保仅使用反射检索一次信息,并多次使用。

使用轻量级代码生成技术和表达式树的动态代码生成有助于创建静态代理方法,以避免在实例化对象和设置/获取属性值等场景中的反射。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Unity打怪升级

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值