1. 程式人生 > 實用技巧 >WPF學習筆記二 依賴屬性實現原理及效能分析

WPF學習筆記二 依賴屬性實現原理及效能分析

 在這裡討論依賴屬性實現原理,目的只是學習WPF是怎麼設計依賴屬性的,同時更好的使用依賴屬性。

  首先我們來思考一個簡單的問題:我們希望能驗證屬性的值是否有效,屬性變更時進行自己的處理。回顧一下.net的處理方式

Public Class MyClass{
private int index;
Public int Index{
get{
return index;
}
set{
if(屬性變更時){
//有效性檢查
//處理或激發事件通知外部處理
}
}
}
}

現在,我們希望設計一套屬性系統,能驗證屬性的值是否有效,屬性變更時能進行處理(WPF屬性系統肯定不是為這個設計的,但它支援這種功能)。我希望讀者在這裡思考一下,你會怎麼做,最後看WPF怎麼做。

我先提出第一種設計:設計一個基類,用來實現以上需求。當你定義一個屬性,希望該屬效能驗證屬性的值是否有效,屬性變更時能進行處理時,讓該屬性從這個基類繼承,就可以達到目的了。基類定義如下:

Public Class PropertyBase {
  protected bool virtual IsValidValue(object value){
    return true;
  }
  protected void virtualValueChangedHandler(Object sender, PropertyChangedEventArgs e){

  }

}

但顯然,WPF不會這麼做。倒不是這種方法實現不了WPF屬性系統的功能,而是這樣做對WPF開發者來說真的是災難。想想如果你定義一個簡單的double型的依賴屬性FontSize,卻要去寫一個類。從系統性能,記憶體亂費來說也是不能接受的。既然不能採用這種繼承方式,那就定義一個類,所有依賴屬性均宣告為這個類的物件,讓這個類來完成以上功能。WPF中這個類的名字叫DependencyProperty,依賴屬性的宣告如下:

public static readonly DependencyProperty FontSizeProperty;

我們知道.net屬性一般有訪問器,WPF也不例外,上面程式碼完善一下:

public class Control {
  public static readonly DependencyProperty FontSizeProperty;
  publicdouble FontSize{
    get{...};
    set{...};
  }
}

  從內部來說,FontSizeProperty才是真正的依賴屬性,FontSize只是外部訪問FontSizeProperty的介面。很顯然,上面的get/set必須和FontSizeProperty關聯,所以WPF加入了一對訪問函式SetValue/GetValue.至於怎麼關聯,那是SetValue/GetValue的實現問題。由於每一個依賴屬性的訪問要通過SetValue/GetValue,因此WPF定義了一個DependencyObject,來實現SetValue/GetValue,進一步完善以上程式碼:

public class Control :DependencyObject{
  public static readonly DependencyProperty FontSizeProperty;
  public double FontSize{
get {
     return (double)GetValue(FontSizeProperty);
  }
set {
    SetValue(FontSizeProperty, value);
   }
  }
}

還有一個問題,FontSizeProperty為什麼定義為publicstatic 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屬性系統。就這樣,我們完成了定義一個依賴屬性的完整定義。

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));
}
}

  這裡也有人會問了:為什麼使用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的入口。下面程式碼不全,但已能說明問題。

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;
 }
}

  由此可見,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匹配。

因此依賴屬性的效能由屬性表的檢索效能決定。不能說使用預設值比使用使用者值快,但一個例項裡,使用者設定值太多肯定影響依賴屬性訪問速度。