"扩展属性"修订版






4.91/5 (17投票s)
通用"扩展属性"的极其简单的实现
背景
C# 提供了许多函数式语言的特性。其中之一是扩展方法。这个出色的特性可以用非常优雅的方式解决一些非平凡的问题。
然而,我一直觉得类型扩展模型不完整,缺少扩展属性。显然,我不是一个人。不时地,我看到有关模拟扩展属性的文章/解决方案。起初,我以为微软最终会提供一个真正的扩展属性实现,但现在看来可能性非常小。
从技术上讲,XAML 附加属性允许用类似属性的动态“成员”来扩展类型。但是,附加属性的实现相对笨重,过于 XAML 特有,而且使用模式也不是很好。
因此,本文提出的解决方案是尝试实现基于扩展方法语法的通用附加属性。
在期待微软提供官方解决方案的同时,我开发并使用了自己的解决方案,它实现了期望的行为。虽然它缺少只有通过真正扩展 C# 语法才能实现的语法糖,但它对我来说非常有用,因此我决定分享它。
促成这篇文章的起因是另一篇关于同一主题的文章。
这是一篇非常优秀且扎实的文章。作者得到了我的 5 星好评。他很好地解释了他试图实现的目标,并为这个问题提供了一个健壮的可行解决方案。然而,我觉得他的解决方案有点过度设计,实际上可以用一个简单得多的实现来实现相同的行为。
解决方案
工作原理
该解决方案非常直接。它围绕dictionary
构建,该字典将对象实例与一组命名值(键/值对)相关联。该字典由AttachedProperies
static
类托管,该类允许通过扩展方法为对象实例设置和获取命名值。
static class AttachedProperies
{
public static Dictionary<WeakReference, Dictionary<string, WeakReference>> ObjectCache;
public static void SetValue<T>(this T obj, string name, object value)...
public static T GetValue<T>(this object obj, string name)...
ObjectCache
使用WeakReferences
引用对象,以避免内存泄漏。类名(AttachedProperties
)是故意的,因为它模仿了XAML 附加属性。API 依赖于扩展方法,因此使用模式可以很简单,如下所示:
var animation = (Storyboard)FindResource("Storyboard1");
animation.SetValue("StartTime", DateTime.Now);
animation.Begin();
.....
void OnStoryboard_Complete(object sender,....)
{
var animation = (Storyboard)sender;
DateTime startTime = animation.GetValue<DateTime>("StartTime");
当然,使用string
字面量不是最干净的方法。因此,我建议在包含扩展方法的static
类中按目标类型对属性进行分组。这允许强类型和可读的语法。
static class StoryboardExtensions
{
public static DateTime GetStartTime(this Storyboard obj)
{
return obj.GetValue<DateTime>("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
调用以来)翻倍时自动调用。GC 同步 (GCSynchronized)
当垃圾收集器收集未引用的资源时,将自动调用Collect
。按分配 (OnAllocate)
每次调用AttachedProperties.SetValue<T>(...)
时,将自动调用Collect
。手动
Collect
将从宿主代码中显式调用。
就是这样!这大致就是解决方案的全部内容。源代码可以在文章下载(AttachedProperties_original.cs)中找到。
尽管存在一些概念上的相似之处,但本文提出的解决方案与我在引言中提到的其他解决方案相比,存在显著差异。
- 实现更加精简(约 150 行代码)。
- 我认为备选解决方案中存在的基于计时器的垃圾回收机制过于简单。因此,我实现了事件驱动的收集机制。
- 本文提出的解决方案故意不提供任何发现机制。在我看来,在备选解决方案中使用
TypeDesciptorProvider
并没有带来任何实际价值。因此,我决定不投入这方面的开发。
修订解决方案
我最初的实现基于WeakReference
字典。但在 .NET 4.0 中,有一个更适合此目的的集合类型——ConditionalWeakTable
。这个类能够完全自动地移除不再被引用的实例的所有引用。由于不再需要任何内存管理,整个解决方案可以精简到约 30 行代码。以下是最终修订的解决方案:
public static class AttachedProperies
{
public static ConditionalWeakTable<object,
Dictionary<string, object>> ObjectCache = new ConditionalWeakTable<object,
Dictionary<string, object>>();
public static void SetValue<T>(this T obj, string name, object value) where T : class
{
Dictionary<string, object> properties = ObjectCache.GetOrCreateValue(obj);
if (properties.ContainsKey(name))
properties[name] = value;
else
properties.Add(name, value);
}
public static T GetValue<T>(this object obj, string name)
{
Dictionary<string, object> 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<object>(name);
}
}
在首次实现时,我并不知道ConditionalWeakTable
,所以使用了dictionary
。这个决定让我不得不解决内存管理方面的挑战。由于原始解决方案展示了一些有趣的技术,我决定仍然将其包含在下载文件中(AttachedProperties_original.cs)。
此外,原始解决方案可以在早期版本的 CLR 下使用(GC 事件除外)。但是,如果您的目标平台是 .NET 4.0,那么您应该使用基于ConditionalWeakTable
的解决方案(AttachedProperties.cs)。它更简单,内存管理更好,而且……我提过它更简单吗?
限制
了解所提出解决方案的局限性很重要。
- 基于
ConditionalWeakTable
的解决方案只能在 .NET v4.0 上运行。 - 基于
ConditionalWeakTable
的解决方案无法扩展以支持任何形式的发现机制。原因是ConditionalWeakTable
不支持像Dictionary
那样的任何浏览 API。 - 支持值类型作为可以附加值的实例存在问题。这是所有此类解决方案的常见限制。
关注点
通用的附加属性是实现松耦合架构的出色“辅助功能”。它们还可以在需要时为扩展方法提供的原本无状态的扩展模型带来状态特性。这反过来又将扩展方法提升到我称之为 C# 中“安全多重继承”的水平。尽管这是另一个话题了……
历史
- 首次发布