65.9K
CodeProject 正在变化。 阅读更多。
Home

扩展属性

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (9投票s)

2015年3月27日

CPOL

4分钟阅读

viewsIcon

22203

downloadIcon

278

在.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的引用,以便可以回收其所有内存。

参考文献

我没有详尽地搜索其他解决方案,但这里有一些其他的扩展属性实现。

  1. C# Easy Extension Properties 作者:Moises Barba
  2. "Extension Properties" Revised作者:Oleg Shio
  3. Connected Properties on CodePlex

历史

  • 2015年3月27日 - 发布初始版本
     
© . All rights reserved.