剖析依赖属性

剖析依赖属性 Dependency Property##

【了解属性和字段】

  • 什么是属性
    程序的本质是“数据加算法”
    使用private、public等修饰符来控制字段和方法的可访问性静态成员和非静态成员什么是属性?
  • 字段被封装在实例里,用private修饰则外界不能访问,用非private修饰则能被外界访问。
    这种直接将数据暴露给外界的做法很不安全,很容易将错误的值写入字段,如果每次写入数据的时候先判断值的有效性又会增加冗余的代码并且违反面向对象要求“高内聚”的原则,我们希望对象自己有能力判断被写入的值是否正确。于是仍然把字段标记为private,但使用一对非private的方法包装它,Set方法负责判断数据的有效性并写入数据,Get方法负责将字段的数据读取出来。

经过发展和简化,微软将Get/Set这对方法合并成属性(property),使用属性时格式上很像使用非 private字段,保证了语义的顺畅,同时又不失Get/Set方法的安全性,代码变得更加紧凑,自动提示菜单也短了许多,可谓一举多得。

我们知道,属性是面向对象语言中用来封装字段的外衣,它像是字段对外界的桥梁,我们可以通过属性来验证数据的合法性或控制对外的访问性等等。每个属性的背后都有其对应的一个字段做支撑,就算是自动属性,在编译时系统也会创建其字段,只不过自动属性是微软给我们的语法糖罢了。在C#中,属性最后是会编译成两个方法:get_属性名和set_属性名(如果是只读属性,则没有set方法,反之没有get方法)。

编译成方法,属性就不会占用太多空间,因为方法存在于内存公共的方法区,每个实例的创建不过是多一个指向该方法的指针。但是字段不一样,每个实例创建的创建,都会在内存中开辟对应的空间来存放字段,一个类中的字段越多,它在内存中占用的空间就越大,理解了这个理论,下面我们来正式说明什么是依赖属性,为什么要有依赖属性。

在这里插入图片描述

【什么是依赖属性】

  • 依赖属性是一种可以自己没有值,能通过使用Binding从数据源获得值的属性,拥有依赖属性的对象称为依赖对象。

问题:TextBox有138个属性,假定每个属性包装着一个4字节的字段,如果创建10列100行的TextBox列表,那么这些字段将占用413810*100=552K内存,在100多个属性种,最常用的是Text属性,大多数内存都被浪费了。

一名登山运动员的故事。

  • WPF允许对象在被创建的时候并不包含用于存储数据的空间(即字段所占用的空间),只保留在需要用到数据时能够获得默认值、借用其它对象数据、或实时分配空间的能力(依赖对象)。依赖对象通过依赖属性实时获取数据。WPF的所有UI控件都是依赖对象。

我们使用一个控件,可以看到这个控件有很多的属性,有属性就有字段的内存开销,但实际上对于一个控件,我们大多数只会使用其部分常用属性,比如Button我们最常使用Content,Height等属性,那些不经常使用的属性相当于白白占用着内存。当我们写一个复杂的XAML页面,涉及到很多控件的使用时,这种浪费内存的现象就很严重。

  • 对此,微软在WPF中引入了依赖属性(Dependency Property),依赖属性允许没有自己的字段,可以通过Binding绑定到其它对象的属性或者说数据源上,从而获得值,这种依赖在其它对象上的属性,就是依赖属性,当明确了它的功能,我想大家就不会对依赖二字产生疑惑了,依赖属性可以没有自己的字段,在使用时可以通过Binding从别的对象身上获取,给自己临时创建内存空间,这样不使用就不会有多余内存消耗,但是要注意一点,如果直接给依赖属性赋值,它依然如属性一样,会占用空间,必须使用Binding才能实现“依赖”。

包含依赖属性的对象称为依赖对象(Dependency Object),这种对象需要继承DependencyObject这个基类,实际上,WPF中的控件,都继承了DependencyObject这个类,控件中的大部分属性都是依赖属性,这样我们才能通过Binding去绑定值(不熟悉Binding的同学可以参见前文Binding(一):数据绑定系列),才不会有内存浪费现象的发生。

【从代码中学习依赖属性】

下面我们通过代码来学习一下如何声明并使用依赖属性,请先看我写好的一段代码:

public class Pikachu : DependencyObject
{
    public static readonly DependencyProperty PikachuNameProperty =
        DependencyProperty.Register("PikachuName",typeof(string),typeof(Pikachu));
}

上文说到,使用依赖属性必须要继承DependencyObject类,另外,声明

依赖属性,需要使用public static readonly三个修饰符修饰,实例依赖属性也不是通过new操作符,而是通过DependencyProperty的Register方法来获取。

依赖对象的名字,有个约定,就是以Property为后缀,在C#中有很多命名约定,比如接口用I做前缀,特性用Attribute做后缀等等,这样做都是为了有个良好的命名规范,做到见名知意。

◆ 第一步: 让所在类型继承自 DependencyObject基类,在WPF中,我们仔细观察框架的类图结构,你会发现几乎所有的 WPF 控件都间接继承自DependencyObject类型。
◆ 第二步:使用 public static 声明一个> DependencyProperty的变量,该变量才是真正的依赖属性
,看源码就知道这里其实用了简单的单例模式的原理进行了封装(构造函数私有),只暴露Register方法给外部调用。
◆ 第三步:在静态构造函数中完成依赖属性的元数据注册,并获取对象引用,看代码就知道是把刚才声明的依赖属性放入到一个类似于容器的地方,没有讲实现原理之前,请容许我先这么陈述。

◆> 第四步:在前面的三步中,我们完成了一个依赖属性的注册,那么我们怎样才能对这个依赖属性进行读写呢?答案就是提供一个依赖属性的实例化包装属性,通过这个属性来实现具体的读写操作。

Register方法有三个重载,此处用的是其三个参数的重载,它还有四个参数和五个参数的重载。

  • 第一参数是指定依赖属性的包装器名称是什么(包装器就是用来包装依赖属性的,通过一个属性来包装依赖属性供外部使用,具体下文会讲,此处先做了解)
  • 第二个参数是指定依赖属性要存储的值的类型是什么 第三个参数是指定依赖属性属于哪个类的,或者说是为哪个类定义依赖属性
  • 其它重载中第四个参数是指定依赖属性的源数据,用于提供给调用者此依赖属性的信息 其它重载中第五个参数是自定义的依赖属性生成时的验证回调

声明了依赖属性,但是如何给依赖属性赋值呢,这就要用到DependencyObject基类中的方法了,我们使用其中的SetValue方法和GetValue方法来操作依赖属性的值,请看下面改动后的代码:

public class Pikachu : DependencyObject
{
    public string PikachuName 
    {
        get => (string)GetValue(PikachuNameProperty); 
        set => SetValue(PikachuNameProperty, value); 
    }

    public static readonly DependencyProperty PikachuNameProperty =
        DependencyProperty.Register("PikachuName", typeof(string), typeof(Pikachu));
}

上述代码,就是一个比较完善的声明依赖属性并通过包装器将依赖属性暴露出去的例子,属性PikachuName就是依赖属性的包装器,在get块中通过GetValue方法传入依赖属性的名字获取依赖属性的值,在Set块中通过SetValue方法,给依赖属性赋值,对依赖属性的这层包装,使得我们在外部操作依赖属性变得简单,这也是为什么我们在正常使用中感觉不到依赖属性的存在,因为字段也好,依赖属性也好,我们在外部看到的操作的都是它的属性。

下面通过一个实例展示一下依赖属性的使用:

前台代码是一个名为btn_show的Button控件,后台代码如下:

public MainWindowBase()
{
    InitializeComponent();
    this.DataContext = this;
    Data = "我是皮卡丘";
    Pikachu pikachu = new Pikachu(); 
    //使用Binding操作类将皮卡丘对象的皮卡丘名字依赖属性关联到Data上
    BindingOperations.SetBinding(pikachu,Pikachu.PikachuNameProperty, new Binding("Data") 
    { Source = this });
    //将按钮的Content依赖属性绑定到皮卡丘的皮卡丘名字包装器上
    btn_show.SetBinding(Button.ContentProperty, new Binding(nameof(pikachu.PikachuName)) 
    { Source = pikachu });
}

这个例子的逻辑是有一个名为Data的属性作为数据源,先将皮卡丘对象的依赖属性绑定到Data数据源上,再将Button的Content依赖属性绑定到皮卡丘对象的依赖属性包装器上,这就形成了一个Binding链,运行效果如下:

整个过程中,只有Data属性是有字段在背后支撑的,它存储了“我是皮卡丘”这个数据,皮卡丘对象和Button对象都是依赖属性,不占内存空间,它们之间使用Binding关联,形成数据通道,这样就实现了一块内存,供给多处使用。按照之前的编程模式,需要皮卡丘和Button各自开辟一段空间存储Data来的数据,现在由三块内存节省为一块内存,这就是依赖属性对于节省内存的效果。

【从源码分析依赖属性】
下面我们来分析一下,为什么依赖属性不是用new实例,而是要注册,以及Get/SetValue的操作依赖属性值的原理。

我们先从Register方法看起:
在这里插入图片描述
在这里插入图片描述
Register的三个和四个参数的重载都指向了五个参数的重载,我们主要看一下这五参数重载的方法里边都有什么。方法体里边,前几行实际上是一些验证代码,当参数有误时,会抛出异常。紧接着的是一个返回依赖属性对象的RegisterCommon方法,从名字和返回值来看这就是最核心的方法了,我们接着跟进去看:
在这里插入图片描述
代码内部第一行使用FromNameKey生成了一个key对象,这个FromNameKey是Dependency类的一个内部类,它构造器需要传入的包装器名称和依赖对象所在的类的Type, 这个类及构造器代码如下:
在这里插入图片描述

构造器第三行代码比较重要,我们可以看到,这个类通过传入的参数两者异或生成了一个hashcode,经过这个异或运算,那就保证了同一个类,同样的包装器名称生成的hashcode是一样的。

同时这个类重写了GetHashCode方法,就是把异或生成的hashcode返回出去了。

了解了这个类,我们再回到RegisterCommon类中,接着往下看,下面是一个线程同步块:
在这里插入图片描述

这个代码块里边,出现了一个PropertyFromName参数,看样子是个集合,我们找到这个属性的定义处,发现它是个全局的HashTable:
在这里插入图片描述
那这个代码块的意思就明了了,目的就是判断生成的Key是否已存在,如果存在,就抛异常,从这里就控制了,在类内部定义两个相同包装器名称的依赖属性是不允许的,实际上也必须是这样,同一个类中,属性肯定是不能同名的,依赖属性也是如此,那我们从此处还能获得一个信息,就是PropertyFromName肯定和我们要生成的依赖属性有很大的关系,具体我们继续往下看代码:
在这里插入图片描述
如果没有传入依赖属性的源数据,系统会生成默认的源数据,在往下看是一些校验逻辑,具体内容此处就不分析,有兴趣的可以自己点进去看,紧接着就到代码核心了:
在这里插入图片描述
经过层层把关,依赖属性终于new出来了,new出来后,下面我们又看到PropertyFromName的影子了:
在这里插入图片描述
原来PropertyFromName是存储依赖属性的一个集合,所有new出来的依赖对象都存储在这里,它的hashcode就是之前通过FromNameKey类异或出的。

最后,通过return,返回了这个依赖属性 ,至此,依赖属性的整个创建过程解析完毕。

我们再来了解一下依赖属性的值的读取:

先看GetValue方法:
在这里插入图片描述
前几句代码还是校验,核心代码是最后一句,此处涉及到了依赖属性的GlobalIndex属性,这个属性是系统经过一系列算法得出的,具有唯一性,我们看到,这个GlobalIndex传入了名为LookupEntry方法中,Entry是入口的意思,从方法名上看,我们能得知,是根据GlobalIndex找到了一个访问入口,实际上,这个入口就是依赖属性值的访问入口。

我们进入GetValueEntry方法中查看,会找到一个名为_effectiveValues的属性,这是一个EffectiveValueEntry类型的数组,原来,依赖属性所有的值都存放在这个数组中,根据依赖属性唯一的GlobalIndex,我们就能从这个数组中找到依赖属性的值。

再来看SetValue方法:
其实明白了GetValue,SetValue也就很好理解了,道理都是一样的,根据依赖属性的GlobalIndex值获取到入口,更新上新值,我们进入SetValueCommon方法中看,代码比较繁琐,实际上的流程有三块:

判断值是不是DependencyProperty.UnsetValue,如果是,则清除依赖属性的值,所以我们要想对依赖属性设置空值,不要用null,要用DependencyProperty.UnsetValue
在这里插入图片描述

判断能否找到入口,如果没有入口,则新建一个入口对象,将值放进去,有入口则更新值
最后,通过UpdateEffectiveValue方法对依赖属性的值做一些处理
至此依赖属性的读取流程解析完毕。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

是刘彦宏吖

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

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

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

打赏作者

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

抵扣说明:

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

余额充值