扩展属性在函数式编程语言中的应用

函数式编程语言以其强大的功能和优雅的解决方案而著称。其中之一就是扩展方法(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(...)时,Collect将自动调用。

Manual:Collect将从宿主代码中显式调用。

就是这样!这大致就是解决方案的全部内容。源代码可以在文章可下载文件中找到(AttachedProperties_original.cs)。

尽管提出的解决方案在概念上与其他解决方案有相似之处,但与在引言中提到的其他解决方案相比,它有显著的不同:

实现更加精简(~150行代码)。

替代解决方案中的基于计时器的垃圾收集机制对来说似乎太简单了。所以实现了一个事件驱动的收集机制。

提出的解决方案故意不提供任何发现机制。在看来,替代解决方案中的TypeDesciptorProvider并没有带来任何实际价值。所以决定不在这个特性上投资。

修订解决方案

最初的实现是基于WeakReference字典的。然而,在.NET 4.0中,有一个更适合这种用途的集合类型——ConditionalWeakTable。这个类能够完全自动地移除所有不再被引用的实例的引用。因为不再需要任何内存管理,整个解决方案可以压缩到~30行代码。以下是最终修订的解决方案:

public static class AttachedProperties { public static ConditionalWeakTable> ObjectCache = new ConditionalWeakTable>(); public static void SetValue(this T obj, string name, object value) where T : class { Dictionary properties = ObjectCache.GetOrCreateValue(obj); if (properties.ContainsKey(name)) properties[name] = value; else properties.Add(name, value); } public static T GetValue(this object obj, string name) { Dictionary properties; if (ObjectCache.TryGetValue(obj, out properties) && properties.ContainsKey(name)) return (T)properties[name]; else return default(T); } public static object GetValue(this object obj, string name) { return obj.GetValue(name); } }

在第一次实现时,并不知道ConditionalWeakTable,所以使用了一个字典。这个决定要求解决内存管理的挑战。因为原始解决方案展示了一些有趣的技术,决定无论如何都包含在可下载文件中(AttachedProperties_original.cs)。

原始解决方案也可以在CLR的早期版本下使用(除了GC事件)。然而,如果目标平台是.NET 4.0,那么应该使用基于ConditionalWeakTable的解决方案(AttachedProperties.cs)。它更简单,内存管理更好,而且...提到过它更简单吗?

限制

重要的是要意识到所提出解决方案的限制:

基于ConditionalWeakTable的解决方案只能在.NET v4.0上运行。

基于ConditionalWeakTable的解决方案不能扩展以支持任何类型的发现机制。原因是ConditionalWeakTable不支持任何浏览API,就像Dictionary一样。

将值类型作为附加值的实例支持存在问题。这是这类解决方案的常见限制。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485