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

基于 DefaultValue 属性的属性初始化方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (33投票s)

2010年3月18日

CPOL

7分钟阅读

viewsIcon

73367

downloadIcon

909

基于 DefaultValue 属性实现属性初始化的各种方法

引言

Microsoft Corporation 在 .NET 1.1 版本中引入了 DefaultValueAttribute 类。其文档中的歧义导致了一些关于该类如何使用和如何工作的混淆。为了消除混淆,Microsoft 发布了知识库文章 311339,其中指出:“DefaultValue 属性不会导致初始值使用该属性的值进行初始化。”因此,这种行为的实现留给了我们开发者。

本文介绍如何创建轻量级且高效的算法,以从 DefaultValue 属性初始化类属性。文章实现了、测试并分析了不同的方法。它还演示了如何将其实现为 AOP(面向切面编程)扩展,可以直接用于任何 C# 类。

背景

DefaultValueAttribute 用默认值注解属性。此信息随类型的元数据一起保存,可用于向运行时或设计时工具和环境进行描述。有关 DefaultValueAttribute 类的详细信息可在 MSDN 库中找到。该属性的典型用法示例如下:

public class TestClass
{
    [DefaultValue(1)]
    public int IntProperty { get; set; }

    [DefaultValue(1)]
    public long LongProperty { get; set; }

    [DefaultValue(true)]
    public bool BoolProptrty { get; set; }
}

*本文中的示例代码为简化起见,仅作演示。完整的实现请下载源代码。

各种方法和实现

原生初始化

最简单、最直接的方法是在构造函数中手动初始化所需的属性,如下所示:

public Constructor()
{
    IntProperty = 1;
    LongProperty = 1;
    BoolProptrty = true;
}

public Constructor(int i)
{
    IntProperty = 1;
    LongProperty = 1;
    BoolProptrty = true;
}

此解决方案的主要优点是简单,代码紧凑且速度快。此方法的缺点在于它是“手动”的。它需要手动同步 DefaultValueAttribute 中设置的值与此类型的所有构造函数,并且容易出现所有可能的人为错误。

使用 ComponentModel 服务进行初始化

在寻找此问题的解决方案时,我偶然发现了 Daniel Stutzbach 撰写的一篇文章,他在文章中提出了一种简单而有效的算法,该算法会遍历类的属性列表,并根据与其成员关联的 DefaultValue 属性设置默认值。

public Constructor()
{
    // Iterate through each property
    foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(this))
    {
        // Set default value if DefaultValueAttribute is present
        DefaultValueAttribute attr = prop.Attributes[typeof(DefaultValueAttribute)]
                                                         as DefaultValueAttribute; 
        if (attr != null)
            prop.SetValue(this, attr.Value);
    }

    ...
}

在类的构造过程中,此算法会遍历每个属性,并检查该属性是否具有关联的 DefaultValueAttribute。如果存在这样的属性,则使用该属性中的值初始化该属性。

此算法的好处在于它消除了容易出错的手动同步,并允许值初始化与各种设计工具更加一致。
坏处是效率不高。该算法使用反射服务,并且在每次创建新对象时都会遍历所有属性。

使用 PropertyDescriptor.ResetValue(...) 进行初始化

下一个方法与 ComponentModel 初始化方法非常相似,只是它使用了 PropertyDescriptor 类的 ResetValue(…) 方法。根据 MSDN 文档,此方法按以下优先顺序重置属性到确定的值:

  1. 该属性有一个被覆盖的属性。
  2. 该属性有一个 DefaultValueAttribute
  3. 已实现一个“ResetMyProperty”方法,其中“MyProperty”是被重置的属性的名称。

因此,此实现的用法如下:

public Constructor()
{
    // Iterate through each property and call ResetValue()
    foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(this))
        property.ResetValue(this);

    ...
}

此方法比直接查询 DefaultValue 属性创建了更灵活的解决方案。它提供了重置属性值的替代方法,但并未提高其性能。

使用委托进行初始化

前两个算法相对较慢。很多 CPU 周期都花在了遍历集合和搜索要调用的正确方法上。如果所有 ResetValue 方法只解析一次,并且它们的引用存储在缓存中供以后使用,那么它们的性能可以得到提高。

private static Action<object><this> setter;   // Reset multicast delegate

public Constructor()
{
    // Attempt to get it from cache
    if (null == setter)
    {
        // If no initializers are added do nothing
        setter = (o) => { };

        // Go through each property and add method calls to multicast delegate
        foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(this))
        {
            // Add only these which values can be reset
            if (prop.CanResetValue(this))
                setter += prop.ResetValue;
        }
    }

    // Initialize member properties
    setter(this);
}

此算法与前两个类似,但它不是在每个属性上调用 ResetValue 方法,而是将该方法的引用添加到多播委托中。一旦所有属性都被迭代,调用此委托就会将它们重置为适当的值。每次连续实例化该类时,只需调用此多播委托,而无需再次重新构建它。

此方法创建了最灵活、最完整的重置属性为其默认值的机制。与其他方法相比,它通过收集属性描述符和属性的迭代来消除不必要的迭代,从而提高了它们的性能。不幸的是,在性能方面,它仍然无法与直接初始化方法相比。

使用预编译的 Setter 进行初始化

带有委托缓存的解决方案消除了对 ResetValue 方法的重复查询。不幸的是,它仍然在每次调用时搜索每个属性的默认值。如果既没有使用覆盖属性,也没有使用“ResetMyProperty”方法来初始化默认值,那么就可以消除这些搜索。每个属性的初始常量值只能从 DefaultValue 属性中检索一次,并与适当的 SetValue 委托一起存储在缓存中以备将来使用。

通过使用 lambda 表达式,我们可以在运行时生成自定义代码,并创建以下方法:

(object o){ o.PropertySetter(constant); }

其中 o 是对象的实例,PropertySetter 是指向适当的 set 方法的委托,而 constant 是从 DefaultValue 属性检索到的值。

这些表达式的列表可以被编译并添加到多播委托中以供将来使用。

// Default Value multicast delegate
private static Action<object><this> setter;  
     
public CompiledComponentModelInitialization()
{
    // Attempt to get it from cache
    if (null == setter)
    {
        ParameterExpression objectTypeParam = Expression.Parameter(typeof(object), 
                                                                   "this");
                                                                   
        // If no initializers are added do nothing
        setter = (o) => { };
        
        // Iterate through each property
        foreach (PropertyInfo prop in this.GetType().GetProperties(
            BindingFlags.Public | BindingFlags.Instance))
        {
            // Skip read only properties
            if (!prop.CanWrite)
                continue;
            
            // There are no more then one attribute of this type
            DefaultValueAttribute[] attr = prop.GetCustomAttributes( 
                     typeof(DefaultValueAttribute), false) as DefaultValueAttribute[];
                     
            // Skip properties with no DefaultValueAttribute
            if ((null == attr) || (null == attr[0]))
                continue;
                
            // Build the Lambda expression
            
            // Create constant expression with value from DefaultValue attribute 
            // and convert it into appropriate type
            Expression dva = Expression.Convert(Expression.Constant(attr[0].Value), 
                                                                  prop.PropertyType);
                                                                  
            // Create expression describing call to appropriate setter and 
            // passing it instance and value parameters
            Expression setExpression = Expression.Call(Expression.TypeAs(
                                     objectTypeParam, this.GetType()), 
                                     prop.GetSetMethod(), dva);
            
            // Create lambda expression describing proxy method which receives 
            // instance parameter and calls instance.setter( constant )
            Expression<Action<object><action><this>> setLambda = 
		Expression.Lambda<action><this><Action<object><action><this>>(
                                                     setExpression, objectTypeParam);
                                                     
            // Compile and add this action to multicast delegate
            setter += setLambda.Compile();
        }
    }
    
    // Initialize member properties
    setter(this);
}

首次创建该类的实例时,将遍历所有 public 可写属性,生成 Lambda 表达式,编译并添加到多播委托。下次实例化该类型的对象时,只需通过该委托调用这些预编译的方法。

此方法在首次实例初始化时需要大量资源和时间,但在所有其他实例初始化过程中,几乎消除了与使用 DefaultValue 属性相关的所有开销。在性能方面,一旦代理方法被构建和编译,这是唯一能够与直接初始化方法媲美的算法。

测试结果

在多次执行此测试应用程序以消除随机结果后,可以得出以下平均值:

执行周期数 1 2 3 4 5 6 7 8 9 10 100 1000 10000 100000 1000000
直接初始化 973 8 2 2 2 2 2 2 2 2 2 2 2 2 2
使用 ComponentModel 11211 88 40 38 38 38 38 38 38 37 30 27 26 26 26
使用 Reset Property 4423 57 35 34 34 33 33 33 34 33 26 26 26 26 26
使用多播委托进行初始化 8194 44 24 23 23 23 23 23 23 23 16 16 16 17 16
使用预编译的 Setter 进行初始化 18999 14 3 2 2 2 2 2 2 2 2 2 2 2 2

从这些数字可以看出,第一次实例化类需要很长时间,即使初始化时没有任何特殊操作。在类至少实例化一次之后,性能会提高。

以下图表展示了本文所述所有方法的相对性能图。

DefaultValueInitialization
*此图未包含第一个周期,以提高缩放比例。

AOP 实现

面向切面编程 (AOP) 是一种编程范式,其中将次要或支持性函数与主程序业务逻辑隔离。这些初始化方法提供的功能非常适合此类。

C# 提供了一种特殊的机制,允许为任何类类型扩展自定义行为。这种机制称为扩展方法。有关此技术的详细信息,请访问MSDN

此实现提供了两种算法:“委托缓存的 PropertyDescriptor.ReserValue() 调用”和“预编译的 setter”。这些方法分别称为 ResetDefaultValues()ApplyDefaultValues()。为了使用这些方法,请执行以下步骤:

  1. 下载 AOP.zip,解压缩并将其文件 AOP.cs 添加到您的项目中。
  2. 通过在类源文件中添加行:using AOP; 将 AOP 命名空间引入作用域。
  3. 在需要使用默认值初始化参数时,在代码中添加语句 this.ResetDefaultValues();this.ApplyDefaultValues();
using System; 
using System.Linq; 
using AOP; 

namespace YourNamespace 
{
    public class YourClass 
    { 
        public Constructor() 
        {
            this.ApplyDefaultValues(); 
        }
       ...
    } 
}

历史

  • 2010 年 3 月 16 日 - 文章首次发布
  • 2010 年 3 月 18 日 - 在 Steve Hansen 的精彩建议下,缓存查找中的 trycatchfinally 序列已被 Dictionary.TryGetValue 替换。
  • 2010 年 3 月 19 日 - 在 Miss Julia Lutchenko 的帮助下,修复了一些语法和风格问题。
  • 2010 年 3 月 25 日 - 修复了一个小 bug 并添加了多线程支持。

欢迎任何批评、评论、 bug 修复或改进。

© . All rights reserved.