函数式编程语言以其强大的功能和优雅的解决方案而著称。其中之一就是扩展方法(Extension Methods),它允许以一种非常优雅的方式解决一些非平凡的问题。然而,一直觉得如果没有扩展属性(Extension Properties),类型可扩展性模型就不完整。显然,并不孤单,时不时地,会看到模仿扩展属性的文章/解决方案。最初,认为微软最终会提供一个适当的扩展属性实现,但现在很明显,这似乎不太可能。
为了技术上的准确性,XAML附加属性允许通过类似属性的动态“成员”来扩展类型。然而,附加属性的实现相对笨重,过于特定于XAML,而且使用模式也不是很好。因此,这里提出的解决方案是基于扩展方法语法实现通用附加属性的尝试。
在希望微软能提供一个合适的解决方案的同时,开发并使用了解决方案,它实现了所需的行为。虽然它缺乏只有真正扩展C#语法才能实现的语法糖,但它对非常有用,决定分享它。
这篇文章的触发点是另一篇文章,也是关于同一主题的:。这是一篇非常好的、扎实的文章。作者得到了5分。他很好地解释了他试图实现的目标,并为问题提供了一个健壮的工作解决方案。然而,觉得他的解决方案有点过度工程化,实际上可以用更简单的实现来实现相同的行为。
解决方案是直接的。它围绕着一个字典构建,该字典将对象的实例与一组命名值(键/对)关联起来。这个字典由静态的AttachedProperties类托管,允许通过扩展方法为对象实例设置和获取命名值:
public static class AttachedProperties {
public static Dictionary> ObjectCache;
public static void SetValue(this T obj, string name, object value) {
// ...
}
public static T GetValue(this object obj, string name) {
// ...
}
}
ObjectCache使用WeakReferences引用对象,以避免内存泄漏。类名(AttachedProperties)是故意的,因为它模仿了XAML附加属性。API依赖于扩展方法,所以使用模式可以像下面这样简单:
var animation = (Storyboard)FindResource("Storyboard1");
animation.SetValue("StartTime", DateTime.Now);
animation.Begin();
当然,使用字符串字面量并不是最干净的方法。因此,建议将属性按目标类型分组到包含扩展方法的静态类中。这允许强类型和可读的语法:
public static class StoryboardExtensions {
public static DateTime GetStartTime(this Storyboard obj) {
return obj.GetValue("StartTime");
}
public static void SetStartTime(this Storyboard obj, DateTime value) {
obj.SetValue("StartTime", value);
}
}
使用方式:
animation.SetStartTime(DateTime.Now);
DateTime startTime = animation.GetStartTime();
该解决方案还解决了一个有趣的问题——收集已经释放的对象的弱引用。执行此操作的例程是AttachedProperties.Collect方法。Collect可以根据AttachedProperties.MemoryManagementMode自动或手动调用:
Progressive:每当“弱引用”实例的数量翻倍(自上次Collect调用以来)时,Collect将自动调用。
GCSynchronized:当垃圾收集器收集未引用的资源时,Collect将自动调用。
OnAllocate:每次调用AttachedProperties.SetValue
Manual:Collect将从宿主代码中显式调用。
就是这样!这大致就是解决方案的全部内容。源代码可以在文章可下载文件中找到(AttachedProperties_original.cs)。
尽管提出的解决方案在概念上与其他解决方案有相似之处,但与在引言中提到的其他解决方案相比,它有显著的不同:
实现更加精简(~150行代码)。
替代解决方案中的基于计时器的垃圾收集机制对来说似乎太简单了。所以实现了一个事件驱动的收集机制。
提出的解决方案故意不提供任何发现机制。在看来,替代解决方案中的TypeDesciptorProvider并没有带来任何实际价值。所以决定不在这个特性上投资。
最初的实现是基于WeakReference字典的。然而,在.NET 4.0中,有一个更适合这种用途的集合类型——ConditionalWeakTable。这个类能够完全自动地移除所有不再被引用的实例的引用。因为不再需要任何内存管理,整个解决方案可以压缩到~30行代码。以下是最终修订的解决方案:
public static class AttachedProperties {
public static ConditionalWeakTable
在第一次实现时,并不知道ConditionalWeakTable,所以使用了一个字典。这个决定要求解决内存管理的挑战。因为原始解决方案展示了一些有趣的技术,决定无论如何都包含在可下载文件中(AttachedProperties_original.cs)。
原始解决方案也可以在CLR的早期版本下使用(除了GC事件)。然而,如果目标平台是.NET 4.0,那么应该使用基于ConditionalWeakTable的解决方案(AttachedProperties.cs)。它更简单,内存管理更好,而且...提到过它更简单吗?
重要的是要意识到所提出解决方案的限制:
基于ConditionalWeakTable的解决方案只能在.NET v4.0上运行。
基于ConditionalWeakTable的解决方案不能扩展以支持任何类型的发现机制。原因是ConditionalWeakTable不支持任何浏览API,就像Dictionary一样。
将值类型作为附加值的实例支持存在问题。这是这类解决方案的常见限制。