扩展属性






4.89/5 (9投票s)
在.NET中实现扩展属性的另一种解决方案
引言
在C#和.NET中,扩展方法已经存在很长时间了。它允许在不更改类型本身的情况下,用新方法来扩展某个类型。最常见的用法可能是LINQ为集合提供的扩展方法,例如Where
。当我们有了扩展类型的能力后,开发者自然会开始寻求扩展类型并拥有新属性的可能性,例如为对象存储关联数据。
myInstance.MyExtensionId = 123; // Syntax not yet supported
然而,C#或VB目前尚不支持这种语法。尽管如此,已经提出了各种实现扩展属性功能的解决方案,例如在这里Code Project(参见下面的参考文献)。在本文中,我将介绍我自己的解决方案,它具有以下特点:
- 轻量级实现(简单包装
ConditionalWeakTable
) - 不使用
string
作为属性名称的键,从而提高和降低了因拼写错误和重构后出现的运行时错误风险。 - 使用泛型实现类型安全,既包括属性类型,也包括可以扩展的类型。
- 可以存储值类型(但不能扩展值类型)。
- 属性更改通知(可选)。
- 支持只读/延迟初始化的扩展属性。
- 基本单向XAML/WPF绑定支持。
Using the Code
让我们直接从一个声明和使用扩展属性的例子开始。
// Declare the extension property once
static readonly ExtensionProperty<object, int> Id = new ExtensionProperty<object,int>();
// ... and then use it like this:
MyObject testInstance = new MyObject();
testInstance.Set(Id, 123);
int id = testInstance.Get(Id);
上面,我们首先声明了一个强类型扩展属性,名为Id
,类型为int
。在这种情况下,我们指定它可以用于任何派生自Object
的实例。然后,我们使用扩展方法为该对象设置和检索它。如果您将其与附加属性进行比较,您会发现一些相似之处。
与我见过的许多其他扩展属性解决方案相比,我们不使用任何文字字符串来指定扩展属性的名称。这样更便于重构(重命名),并降低了因拼写错误而导致的难以查找问题的风险。
我们无法比这更接近真正的属性语法了。但是,我们实际上可以通过使用索引器语法将其写得更紧凑。
Id[testInstance] = 123; var id = Id[testInstance];
在这里,很明显ExtensionProperty
类似于普通的字典。然而,即使它存储在静态字段中,也只持有条目的弱引用,因此在没有其他引用指向这些条目时,内存可以在垃圾回收期间被回收。
更改通知
如果您想在扩展属性的值发生变化时收到通知,您可以注册一个Changed
事件的处理程序。
Id.Changed += OnIdChanged;
...
private static void OnIdChanged(object instance, EventArgs arg)
{
Debug.Print(String.Format("Id for {0} changed to {1}", instance, Id[instance]);
}
延迟初始化的扩展属性
如果您希望在请求扩展属性时为其生成一个扩展属性,可以使用LazyProperty
。当您希望将 ViewModel、ICommand
或其他内容与另一个对象关联时,这会很有用。
public static readonly LazyProperty<MyModel, MyViewModel> MyViewModelProperty =
new LazyProperty<MyMode, MyViewModel>(model => new MyViewModel(model));
...
// Get viewmodel for a model object (created first time requested,
// and garbage collected when not referred to anymore)
var viewModel = model.Get(ViewModelProperty);
注意:虽然扩展属性看起来很方便,但过度使用可能会损害性能。只要您管理的数据量很小,这可能不成问题。
在WPF/XAML绑定中使用
为了允许在WPF/XAML绑定场景中简单使用,我在附加的源代码中展示了如何扩展ExtensionProperty
以用作值转换器,返回扩展属性的值。这允许我们这样使用它:
<DataTemplate DataType="{x:Type models:MyModel}" >
<ContentControl DataContext=
"{Binding Converter={x:Static viewModels:ViewModels.MyViewModelProperty}}" >
<TextBlock Text={Binding FullName} /> <!-- FullName is a property of the view model -->
</ContentControl>
</DataTemplate>
关注点
简单基础实现
下面的代码片段展示了ExtensionProperty
的基本简单实现。在附加的源代码中,您可以看到我们添加了更多方法以及一个用于继承LazyProperty
的基类,但在许多情况下,此处所示的实现应该足够了。
public class ExtensionProperty<TInstance, TProperty> where TInstance : clas
{
private readonly ConditionalWeakTable<TInstance, object> values =
new ConditionalWeakTable<TInstance, object>();
public TProperty this[TInstance instance] {
get {
object value;
if (values.TryGetValue(instance, out value) == false) {
return default(TProperty);
}
return (TProperty)value;
}
set {
lock (values) { // consider removing lock if you never going to use
// if from multiple concurrent threads.
values.Remove(instance);
values.Add(instance, value);
}
// Change noticification comes here.
}
}
}
内存泄漏?
使用ConditionalWeakTable可以保证,当值或所有者对象(键)不再有任何引用时,存储在扩展属性中的值将被垃圾回收。
正如在一些论坛中指出的那样,即使值和键已被收集,ConditionalWeakTable
仍可能无法回收其内部存储表分配的所有内存。因此,例如,如果您附加了100,000个扩展属性值,然后删除了所有引用,ConditionalWeakTable
似乎仍然保留一个内部存储,至少可以容纳相同数量的条目。当设置更多新的扩展属性值时,此内存会被重用,但直到应用程序(域)卸载之前,所有内存都不会被完全回收。总的来说,这不应该是一个问题。对于那些关心的人,我在扩展属性类中添加了一个ClearAll
方法,该方法可以清除所有属性值并释放对ConditionalWeakTable
的引用,以便可以回收其所有内存。
参考文献
我没有详尽地搜索其他解决方案,但这里有一些其他的扩展属性实现。
- C# Easy Extension Properties 作者:Moises Barba
- "Extension Properties" Revised作者:Oleg Shio
- Connected Properties on CodePlex
历史
- 2015年3月27日 - 发布初始版本