WPF学习笔记二 依赖属性实现原理及性能分析

时间:2022-11-12 23:01:12

 在这里讨论依赖属性实现原理,目的只是学习WPF是怎么设计依赖属性的,同时更好的使用依赖属性。

  首先我们来思考一个简单的问题:我们希望能验证属性的值是否有效,属性变更时进行自己的处理。回顾一下.net的处理方式

WPF学习笔记二 依赖属性实现原理及性能分析
Public Class MyClass{
private int index;
Public int Index{
get{
return index;
}
set{
if(属性变更时){
//有效性检查
//处理或激发事件通知外部处理
}
}
}
}
WPF学习笔记二 依赖属性实现原理及性能分析

现在,我们希望设计一套属性系统,能验证属性的值是否有效,属性变更时能进行处理(WPF属性系统肯定不是为这个设计的,但它支持这种功能)。我希望读者在这里思考一下,你会怎么做,最后看WPF怎么做。

我先提出第一种设计:设计一个基类,用来实现以上需求。当你定义一个属性,希望该属性能验证属性的值是否有效,属性变更时能进行处理时,让该属性从这个基类继承,就可以达到目的了。基类定义如下:

WPF学习笔记二 依赖属性实现原理及性能分析
Public Class PropertyBase {
  protected bool virtual IsValidValue(object value){
    return true;
  }
  protected void virtualValueChangedHandler(Object sender, PropertyChangedEventArgs e){

  }

}
WPF学习笔记二 依赖属性实现原理及性能分析

但显然,WPF不会这么做。倒不是这种方法实现不了WPF属性系统的功能,而是这样做对WPF开发者来说真的是灾难。想想如果你定义一个简单的double型的依赖属性FontSize,却要去写一个类。从系统性能,内存乱费来说也是不能接受的。既然不能采用这种继承方式,那就定义一个类,所有依赖属性均声明为这个类的对象,让这个类来完成以上功能。WPF中这个类的名字叫DependencyProperty,依赖属性的声明如下:

public static readonly DependencyProperty FontSizeProperty;

我们知道.net属性一般有访问器,WPF也不例外,上面代码完善一下:

WPF学习笔记二 依赖属性实现原理及性能分析
public class Control {
  public static readonly DependencyProperty FontSizeProperty;
  publicdouble FontSize{
    get{...};
    set{...};
   }
}
WPF学习笔记二 依赖属性实现原理及性能分析

  从内部来说,FontSizeProperty才是真正的依赖属性,FontSize只是外部访问FontSizeProperty的接口。很显然,上面的get/set必须和FontSizeProperty关联,所以WPF加入了一对访问函数SetValue/GetValue.至于怎么关联,那是SetValue/GetValue的实现问题。由于每一个依赖属性的访问要通过SetValue/GetValue,因此WPF定义了一个DependencyObject,来实现SetValue/GetValue,进一步完善以上代码:

WPF学习笔记二 依赖属性实现原理及性能分析
public class Control :DependencyObject{
  public static readonly DependencyProperty FontSizeProperty;
  public double FontSize{
 get {
     return (double)GetValue(FontSizeProperty);
  }
     set {
    SetValue(FontSizeProperty, value);
   }
  }
}
WPF学习笔记二 依赖属性实现原理及性能分析

还有一个问题,FontSizeProperty为什么定义为public static readonly ?
定义为public 是有原因的,WPF有一种特殊属性,叫附加属性,需要直接访问FontSizeProperty的方法才能实现,所以FontSizeProperty是public 的。至于static,和依赖属性的实现有关,也就是说,一个类,不管同一个依赖属性有多少个实例,均对应同一个DependencyProperty 。比如你创建100个Control ,每个Control 都有一个FontSize属性,但这100个FontSize均对应同一个FontSizeProperty实例。

接下来想知道的是:DependencyProperty怎么实现?
我们知道一个依赖属性可以是简单类型(如bool,int),也可以是复杂类型(如List,自定义类型),大家一定想到了一个东西,那就是泛类型技术,.net中就大量使用了泛类型技术,我们使用泛类型来定义依赖属性:

public class DependencyProperty<T> {
}

但事实上WPF的DependencyProperty不是泛类型!为什么?
原因很简单,WPF属性系统想知道的不仅仅依赖属性的类型,还有依赖属性名,所有者的类型,元数据,回调代理等,泛类型并不能解决这些问题,所以WPF使用了一个Register()函数,由该函数将所有信息提供给WPF属性系统。就这样,我们完成了定义一个依赖属性的完整定义。

WPF学习笔记二 依赖属性实现原理及性能分析
public class Control :DependencyObject{
  public static readonly DependencyProperty FontSizeProperty;
  public double FontSize{
 get {
     return (double)GetValue(FontSizeProperty);
  }
     set {
    SetValue(FontSizeProperty, value);
   }
  }

  static Control () {

FontSizeProperty= DependencyProperty.Register(
                "FontSize", typeof(double), typeof(Control),
                new FrameworkPropertyMetadata(),null));   
        }
}

WPF学习笔记二 依赖属性实现原理及性能分析

  这里也有人会问了:为什么使用Register()函数来传递数据,而不用构造函数来传递?如果在创建一个依赖属性时忘了调用Register()怎么办?此问题由于涉及到DependencyProperty的具体实现,稍后再说。

  上面提到,100个Control实例会有100个FontSize,均对应同一个FontSizeProperty实例。读者一定会想,哦,那DependencyProperty中一定有一张表,来保存每个FontSize的值。开始我也这么想,但事实上不太一样。不过,DependencyProperty中确实有一张表,并且还是静态的!!

public sealed class DependencyProperty {    
  //全局的IDictionary用来储存所有的DependencyProperty
internal static IDictionary<int, DependencyProperty> properties =
          new Dictionary<int, DependencyProperty>();

那这张表里保存什么呢?就让Register()函数来回答吧,这是创建DependencyProperty的入口。下面代码不全,但已能说明问题。

WPF学习笔记二 依赖属性实现原理及性能分析
public sealed class DependencyProperty {
//全局的IDictionary用来储存所有的DependencyProperty
internal static IDictionary<int, DependencyProperty> properties = new Dictionary<int, DependencyProperty>();
private static int globalIndex = 0;
private int _index;

//构造函数私有,保证外界不会对它进行实例化
private DependencyProperty(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata) {
...
}
public int Index {
get{
     return_index;
   }
   set{
    _index =value;
   }
 }
//注册的公用方法,把这个依赖属性加入到IDictionary的键值集合中,GetHashCode为name和owner_type的GetHashCode取异,Value就是我们注册的DependencyProperty
public static DependencyProperty Register(stringname, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata) {
  DependencyProperty property = new DependencyProperty(name, propertyType, ownerType, defaultMetadata);
globalIndex++;
property.Index =globalIndex;
if(properties.ContainsKey(property.GetHashCode())) {
   throw new InvalidOperationException("A property with the same name already exists");
}
//把刚实例化的DependencyProperty添加到这个全局的IDictionary
   properties.Add(property.GetHashCode(), property);
   returnproperty;
 }
}
WPF学习笔记二 依赖属性实现原理及性能分析

  由此可见,DependencyProperty的properties中保存的是所有依赖属性创建时的数据,也就是为什么WPF每个依赖属性都可以恢复到默认值,即使你改变了该依赖属性的值很多次。
  这段代码也解释了另外一个问题:为什么使用Register()函数来传递数据,而不用构造函数来传递。因为DependencyProperty的构造函数是私有的。当然你也可以和WPF不一样,去掉Register()函数,把构造函数改为Public,并把Register()中的其他功能移到构造函数中来。至于其中利弊你自己去衡量吧。

  DependencyProperty的属性当然不止上面代码中的几个,我特意保留了一个Index,因为Index将关系到依赖属性值的真正访问。分析上面代码,得出结论:Index的值是和每一个依赖属性一一对应的,不管你现在开发的系统有多少个类,每个类有多少个依赖属性。同时,一个依赖属性不管有多少个实例,都只有一个Index值。上面提到的100个FontSize对应的也是同一个Index。

  用户设定的依赖属性值到底保存在哪里?别忘了SetValue/GetValue,它们是DependencyObject的方法。到这里读者大概想到了用户设定的依赖属性值到底保存在哪里。没错,就在DependencyObject的_effectiveValues中。

public abstract class DependencyObject :  IDisposable{
  private List<EffectiveValueEntry> _effectiveValues = new List<EffectiveValueEntry>();
  public object GetValue(DependencyProperty dp){...}
  public void SetValue(DependencyProperty dp, objectvalue){...}

  由于DependencyObject是依赖属性拥有者的基类,因此,每创建一个实例,就会创建一个List<EffectiveValueEntry>,以List的方式保存该实例的用户设定的依赖属性值。
绕了一圈,从终点又回到原点,WPF中属性的用户值和.net中一样,都保存在该实例中。只不过.net区分不了用户值和默认值,只有当前值,而WPF把默认值保存到了DependencyProperty中。

  留一个问题给读者思考:依赖属性FontSize对应一个DependencyProperty的Index值,是FontSize在DependencyObject.List<EffectiveValueEntry>中的位置索引吗?

关于依赖属性的性能问题,就简单说一下:

  1.所有依赖属性的默认值保存在DependencyProperty的属性表中,读取(不写)时通过属性的HashCode检索

2.每个实例也有一张属性表,保存该实例当前依赖属性的用户值,通过DependencyProperty的Index匹配。

因此依赖属性的性能由属性表的检索性能决定。不能说使用默认值比使用用户值快,但一个实例里,用户设定值太多肯定影响依赖属性访问速度。