Skip to content

WPF 冷知识 定义依赖属性的最大数量是 65534 个

Updated: at 08:22,Created: at 13:16

远古的 WPF 框架开发的大佬们认为没有任何业务的开发者需要用到超过 65534 个依赖属性和附加属性,为了节省内存空间就限制了所有的依赖属性和附加属性的定义总和加起来不能大于等于 65535 个

似乎大家可能对 65535 个依赖属性的定义量没有概念,这么说,即使只是将这些依赖属性定义出来,那代码的 cs 文件的大小也差不多有 20MB 这么大。敲黑板,这里的 65535 个依赖属性的定义量,指的是在代码里面定义 65535 个依赖属性或附加属性,指的是编写的代码,而和应用运行过程中创建多少个对象毫无关系

接下来咱来写一点有趣的代码来测试 WPF 的这个行为,先新建两个项目,一个是名为 LunallherbeanalLerejucahallyeler 的 WPF 项目,另一个是名为 KeeheekairbiQahairnairdacem 的控制台项目。将由控制台项目 KeeheekairbiQahairnairdacem 生成超过 65535 个依赖属性的定义的代码,用来给 LunallherbeanalLerejucahallyeler 项目引用

由于如此多的定义在一个类型里面,将会触发 CLR 层的异常,如果生成的代码都放在 MainWindow 类型里面,运行过程中大家将会看到如下异常

Type 'LunallherbeanalLerejucahallyeler.MainWindow' from assembly 'LunallherbeanalLerejucahallyeler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' contains more methods than the current implementation allows.

为了能够让这个逗比代码能够跑起来,于是接下来我拆分为 10 个类型,每个类型里面放入 7000 个依赖属性

而由于分了类型了,众所周知,依赖属性的定义默认放的是静态的属性。而静态的属性是由静态构造函数初始化的,静态构造函数又是需要在逻辑碰到静态字段等情况下才会执行的,这就意味着还需要给这 10 个类型投点毒,让这些类型的静态构造函数能够正确执行,从而创建出足够的依赖属性定义的静态字段

foreach (var temp in new IBase[]
{
new Type0(),
new Type1(),
new Type2(),
new Type3(),
new Type4(),
new Type5(),
new Type6(),
new Type7(),
new Type8(),
new Type9(),
})
{
temp.Add();
}
public interface IBase
{
void Add();
}
public partial class Type1 : DependencyObject, IBase
{
private static int _count = 0;
public void Add()
{
// 写入静态字段只是为了触发静态构造函数
_count++;
}
}
public partial class Type2 : DependencyObject, IBase
{
private static int _count = 0;
public void Add()
{
// 写入静态字段只是为了触发静态构造函数
_count++;
}
}
public partial class Type3 : DependencyObject, IBase
{
private static int _count = 0;
public void Add()
{
// 写入静态字段只是为了触发静态构造函数
_count++;
}
}
public partial class Type0 : DependencyObject, IBase
{
private static int _count = 0;
public void Add()
{
// 写入静态字段只是为了触发静态构造函数
_count++;
}
}
public partial class Type5 : DependencyObject, IBase
{
private static int _count = 0;
public void Add()
{
// 写入静态字段只是为了触发静态构造函数
_count++;
}
}
public partial class Type4 : DependencyObject, IBase
{
private static int _count = 0;
public void Add()
{
// 写入静态字段只是为了触发静态构造函数
_count++;
}
}
public partial class Type6 : DependencyObject, IBase
{
private static int _count = 0;
public void Add()
{
// 写入静态字段只是为了触发静态构造函数
_count++;
}
}
public partial class Type7 : DependencyObject, IBase
{
private static int _count = 0;
public void Add()
{
// 写入静态字段只是为了触发静态构造函数
_count++;
}
}
public partial class Type8 : DependencyObject, IBase
{
private static int _count = 0;
public void Add()
{
// 写入静态字段只是为了触发静态构造函数
_count++;
}
}
public partial class Type9 : DependencyObject, IBase
{
private static int _count = 0;
public void Add()
{
// 写入静态字段只是为了触发静态构造函数
_count++;
}
}

接着为了显示出当前 WPF 框架里面注册的依赖属性数量,我还使用反射在界面显示当前的注册的依赖属性数量,如下面代码

var propertyFromNameField = typeof(DependencyProperty).GetField("PropertyFromName", BindingFlags.Static | BindingFlags.NonPublic);
var propertyFromName = (Hashtable) propertyFromNameField.GetValue(null);
TextBlock.Text = $"依赖属性定义数量:{propertyFromName.Count}";

以上代码的 TextBlock 是定义在 MainWindow.xaml.cs 的控件,界面代码如下

<Grid>
<TextBlock x:Name="TextBlock" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>

接着编写 KeeheekairbiQahairnairdacem 控制台项目的代码,生成足够数量的依赖属性的定义,这部分代码没有什么难度,我就不贴在博客里面,大家可以在本文末尾找到全部代码的下载方法

这时候运行 WPF 项目,即可看到大概如下的异常

System.InvalidOperationException: 注册“Type9.Foo2176”时已超出 DependencyProperty 限制。依赖项属性通常是使用静态字段初始值设定项或静态构造函数注册的静态类成员。在这种情况下,可能会在实例构造函数中意外地初始化依赖项属性,从而导致超出最大限制。
at System.Windows.DependencyProperty.GetUniqueGlobalIndex(Type ownerType, String name)
at System.Windows.DependencyProperty..ctor(String name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback)
at System.Windows.DependencyProperty.RegisterCommon(String name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback)
at System.Windows.DependencyProperty.Register(String name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback)
at System.Windows.DependencyProperty.Register(String name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata)

这就是因为定义的依赖属性超过了最大数量的限制,在 WPF 里面的 DependencyProperty 限制了最大的依赖属性和附加属性加起来的总数量,代码如下

public sealed class DependencyProperty
{
internal static int GetUniqueGlobalIndex(Type ownerType, string name)
{
// Prevent GlobalIndex from overflow. DependencyProperties are meant to be static members and are to be registered
// only via static constructors. However there is no cheap way of ensuring this, without having to do a stack walk. Hence
// concievably people could register DependencyProperties via instance methods and therefore cause the GlobalIndex to
// overflow. This check will explicitly catch this error, instead of silently malfuntioning.
if (GlobalIndexCount >= (int)Flags.GlobalIndexMask)
{
if (ownerType != null)
{
throw new InvalidOperationException(SR.Format(SR.TooManyDependencyProperties, ownerType.Name + "." + name));
}
else
{
throw new InvalidOperationException(SR.Format(SR.TooManyDependencyProperties, "ConstantProperty"));
}
}
// Covered by Synchronized by caller
return GlobalIndexCount++;
}
private static int GlobalIndexCount;
}

以上的 GlobalIndexCount 静态字段是用来表示当前定义的依赖属性或附加属性是第几个加入到 WPF 框架里面的,如果超过了 Flags.GlobalIndexMask 数量个,那将会抛出异常。这里的 GlobalIndexMask 就是 65535 个

大家都知道,在 WPF 里面的依赖属性和附加属性都是存放在类型里面的字典里面,而字典的查找是依赖于哈希算法的。为了能够让依赖属性既有足够快的查找速度且又对人类友好,于是定义了依赖属性包含了属性名字符串,还包含了从 GlobalIndexCount 静态字段算出的 GlobalIndex 索引值。通过 GlobalIndexCount 确保每个依赖属性定义都有独立且不重复的 GlobalIndex 索引值,如此即可实现依赖属性字典通过 int 作为 key 提升其性能

更具体一点,讲 WPF 的依赖属性和附加属性在底层使用字典存放是片面的,属于思想正确但具体实现不正确,具体的实现是在 WPF 底层存放了一个有序数组,这个数组通过上文说讲的依赖属性的 GlobalIndex 作为排序依据,如此即可通过折半查找算法快速找到命中的依赖属性对应的值

本文以上的代码放在githubgitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 5c8a31243b7f2e1ad87f49b319dbab39d5d18f0e

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 5c8a31243b7f2e1ad87f49b319dbab39d5d18f0e

获取代码之后,进入 LunallherbeanalLerejucahallyeler 文件夹

先运行 KeeheekairbiQahairnairdacem 项目一次,让其创建 LunallherbeanalLerejucahallyeler 所使用的代码,接着再运行 LunallherbeanalLerejucahallyeler 项目,即可看到本文的效果


知识共享许可协议

原文链接: http://blog.lindexi.com/post/WPF-%E5%86%B7%E7%9F%A5%E8%AF%86-%E5%AE%9A%E4%B9%89%E4%BE%9D%E8%B5%96%E5%B1%9E%E6%80%A7%E7%9A%84%E6%9C%80%E5%A4%A7%E6%95%B0%E9%87%8F%E6%98%AF-65534-%E4%B8%AA

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。 欢迎转载、使用、重新发布,但务必保留文章署名 林德熙 (包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我 联系