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

Anders Hejlsberg 的性能与想法 INotifyPropertyChanged

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (25投票s)

2012 年 2 月 16 日

CPOL

7分钟阅读

viewsIcon

89053

downloadIcon

316

Anders Hejlsberg 的性能与想法 INotifyPropertyChanged

概述

我从来都不怎么喜欢编写实现 INotifyPropertyChanged 接口的代码。部分原因在于,在触发 PropertyChanged 事件时使用 string 感觉不对,而且还需要编写大量的代码。最初,所有属性都必须有一个后备字段,然后微软通过不再要求定义后备字段,大大简化了属性。但对于 WPF 来说,这似乎回退了两步(甚至更多)。任何能够简化 WPF/Silverlight 绑定的简单属性的做法都是向前迈出的一步。最好的选择是调用实现所需代码的属性中的方法,但这需要一些技巧。

INotifyPropertyChanged 的实现

MSDN 推荐的 INotifyPropertyChanged 实现(如何:实现 INotifyPropertyChanged 接口)如下所示:

  class MsdnVersionExample : INotifyPropertyChanged
  {
    private int _variable;
 
    public int Variable
    {
      get { return this._variable; }
      set
      {
        if (value != this._variable)
        {
          this._variable = value;
          NotifyPropertyChanged("Variable");
        }
      }
    }
    private void NotifyPropertyChanged(String propertyName)
    {
      if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
  }

这段代码实现了用于绑定的属性中大部分应有的内容,但代码量很大,而且必须为每个属性都这样做,并且实际上缺少了一个本应有的功能。

在 Anders Hejlsberg 的演讲“C# 和 Visual Basic 的未来方向”中,我注意到他做了一些非常不同的事情。

  class AndersVersionExample : INotifyPropertyChanged
  {
    private int _variable;
 
    public int Variable
    {
      get { return this._variable; }
      set
      {
          SetProperty(ref _variable, value, "Variable");
      }
    }
 
    private void SetProperty<T>(ref T field, T value, string propertyName)
    {
      if (!EqualityComparer<T>.Default.Equals(field, value))
      {
        field = value;
        var handler = PropertyChanged;
        if (handler != null)
        {
          handler(this, new PropertyChangedEventArgs(propertyName));
        }
      }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
  }

这极大地简化了实际的属性本身,意味着一切都更清晰了。之所以能这样工作,是因为 EquityComparer<T>.Default 暴露了 Equals 方法。还要注意事件的处理方式,其中使用了一个“handler”来保存事件信息,然后再触发事件(这可以防止竞态条件),这是微软内部的首选方法。

现在对比 Anders 的实现和 MSDN 的实现,可以立即看出 MSDN 缺少在触发事件前将 PropertyChanged 赋值给一个变量的操作。这是为了防止竞态条件,也是微软公司内部首选的做法。许多人甚至会使用比 MSDN 更简单的实现,即不检查值是否已更改就直接更改值并触发 PropertyChanged 事件。因此,为每个用于绑定的简单属性应该实现的这部分代码相当多,并且重复编写代码会成为一个维护问题,所有这些额外的代码都会模糊对应用程序正在做什么的理解。

在我们的小组中,我们一直在使用一种不同的方法,这种方法具有消除属性名称的“魔术 string”的优点。

  class OurVersionExample : INotifyPropertyChanged
  {
    private int _variable;
 
    public int Variable
    {
      get { return this._variable; }
      set
      {
        if (value != this._variable)
        {
          this._variable = value;
          NotifyPropertyChanged(GetPropertyName(() => Variable));
        }
      }
    }
 
    public static string GetPropertyName<T>(Expression<Func<T>> property)
    {
        var memberExpression = (MemberExpression)((lambda.Body is UnaryExpression)
               ? ((UnaryExpression)lambda.Body).Operand : lambda.Body);
      Debug.Assert(memberExpression != null);
      return memberExpression.Member.Name;
    }
 
    private void NotifyPropertyChanged(String propertyName)
    {
      if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
  }

这非常好,因为当使用重构更改变量名时,属性更改中使用的值也会随之改变。但是,这会带来性能影响,我稍后会详细介绍。(我确实对最初使用的代码做了一些修改,将检查改为了 Debug.Assert ,因为这不会出现在生产代码中,除非属性代码中存在相当明显的错误,否则 memberExpression 不应为 null 。我还清理了一些其他问题。)

如果“Expression<Func<T>>”让您感到困惑,我必须承认它确实令人困惑。Expression 的作用是提供对匿名方法的访问。之所以使用它,是因为它提供了有关 lambda 表达式的信息,如果参数中只有一个 lambda 表达式,则无法获得这些信息。可以通过 compile 语句创建 lambda 表达式(在本例中将编译为 Func<T>)。

    Func<T> _lambda = `_expression.Compile ();

然后就可以执行这个函数(Func<T>)了。

    var value = _lambda();

我相信“UnaryExpression”是为了处理程序员在变量周围放置括号的情况,因此可以省略。这不是我写的代码,我知道过去也曾出现过其他问题。

我现在喜欢 Anders 的做法,也喜欢我们小组的做法。我确实知道,在事件处理中,使用“handler”来保存事件信息,然后再触发事件(这可以防止竞态条件)是首选方法,为什么不将两者结合起来呢?

  class ModifiedAndersVersionExample : INotifyPropertyChanged
  {
    private int _variable;
 
    public int Variable
    {
      get { return this._variable; }
      set
      {
        SetProperty(ref _variable, value, () => Variable);
      }
    }
 
    private void SetProperty<T>(ref T field, T value, Expression<Func<T>> property)
    {
      if (!EqualityComparer<T>.Default.Equals(field, value))
      {
        field = value;
        NotifyPropertyChanged (property);
      }
    }
 
    public void NotifyPropertyChanged<T>(Expression<Func<T>> property)
    {
      var handler = PropertyChanged;
      if (handler != null)
      {
        var memberExpression = (MemberExpression)((lambda.Body is UnaryExpression)
               ? ((UnaryExpression)lambda.Body).Operand : lambda.Body);
        Debug.Assert(memberExpression != null);
        handler(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
      }
    }

    public event PropertyChangedEventHandler PropertyChanged;
  }

您会注意到我仍然保留了 NotifyPropertyChanged 方法,以及 Anders 的 SetProperty 方法,但 SetProperty 方法现在调用 NotifyPropertyChanged 方法。我使用两个方法是因为有许多时候需要触发 PropertyChanged 事件,但值已经正确(可能是通过某些方程或 Linq 语句确定的正确值)。

最终测试还包括对 MethodBase.GetCurrentMethod() 的调用,以获取属性名,从而可以使用纯粹的 Anders 方法(这个测试是由于 Hanzie53 的评论而添加的)。与 lambda 方法相比,性能看起来确实不错,但结果中看不到的是初始化成本。它有两个缺点:它使调用更复杂,并且 MethodBase.GetCurrentMethod() 不能在属性之外使用。实际测试有点“杂糅”,因为我没有从属性中调用 MethodBase.GetCurrentMethod()

另一条评论促使我测试将名称保存在字典中,但检查和查找的成本似乎与此类似,我认为不值得在测试中保留。

结果

显然,通过调用方法来简化属性具有代码重用的优势,但代价是什么?Anders 的方法似乎没有太大的性能损失,但这到底有多大?以及传递 lambda 表达式来确定属性名称的开销有多大?

本文附带的应用程序基本上是对 INotifyPropertyChanged 的四种实现的测试。

  1. 基本 MSDN 实现
  2. 我小组一直在使用的实现
  3. Anders Hejlsberg 演讲中的实现
  4. 结合了 Anders 版本和我们一直在使用的版本
  5. 结合了 Anders 版本和 MethodBase.GetCurrentMethod() 的实现

我进行了几次运行,包括:

  1. 首次运行,将编译所有内容并加载到内存中(**注意**:运行 1000 次迭代与 10,000 次迭代之间存在显著差异)
  2. 第二次运行,此时所有内容都应该已编译并加载到内存中
  3. 运行一个空事件处理程序被触发的场景
  4. 在 WPF 表单上,将 TextBlock Text 属性进行绑定的运行。

一次示例运行产生了以下结果:

First Run

Count of number of iterations: 10000

Time for MSDN Implementation = 581 ticks
Time for our current implementation = 119532 ticks
Time for Anders implementation = 815 ticks
Time for combined implementation = 117971 ticks
Time for combined implementation = 119639 ticks
Time using reflection GetCurrentMethod = 61407 ticks


Second Run

Count of number of iterations: 10000

Time for MSDN Implementation = 560 ticks
Time for our current implementation = 116366 ticks
Time for Anders implementation = 1334 ticks
Time for combined implementation = 120099 ticks
Time for combined implementation = 119186 ticks
Time using reflection GetCurrentMethod = 61814 ticks


Run with Property Changed event

Count of number of iterations: 10000

Time for MSDN Implementation = 888 ticks
Time for our current implementation = 117229 ticks
Time for Anders implementation = 1211 ticks
Time for combined implementation = 129511 ticks
Time for combined implementation = 123790 ticks
Time using reflection GetCurrentMethod = 61666 ticks


Run with Binding

Count of number of iterations: 10000

Time for MSDN Implementation = 183643 ticks
Time for our current implementation = 427613 ticks
Time for Anders implementation = 190121 ticks
Time for combined implementation = 427392 ticks
Time for combined implementation = 425799 ticks
Time using reflection GetCurrentMethod = 346575 ticks

从结果可以看出,使用 Anders 演讲中的实现的性能影响很小;比处理空事件的开销小得多。使用 lambda 表达式确定名称具有非常高的开销,其成本与绑定相当。使用 MethodBase.GetCurrentMethod() 获取名称相比使用 lambda 表达式提供了显著的性能提升。(**注意**:这不是一个现实的场景,而且实际的绑定成本可能更高,因为框架可能会消除冗余事件)。下图是运行本文示例的截图,只运行了 1000 次迭代而不是 10,000 次。可以看到,1000 次迭代所需的时间略多于 10,000 次迭代的 1/10。

331274/image001.png

实现

我们小组实际上并没有将 INotifyPropertyChanged 代码放在每个 ViewModel 中,而是有一个包含 INotifyPropertyChanged 代码的 abstract 类,所有 ViewModels 都继承自这个 abstract 类。

C# 5.0

smolesen 指出 C# 5.0 可能会提供另一种方法,即使用 Caller Info Attributes。这将允许 Anders 的实现修改如下:

  class AndersVersionExample : INotifyPropertyChanged
  {
    private int _variable;
 
    public int Variable
    {
      get { return this._variable; }
      set
      {
          SetProperty(ref _variable, value);
      }
    }
 
    private void SetProperty<T>(ref T field, T value, 
        [CallerMemberName] string propertyName = "")
    {
      if (!EqualityComparer<t />.Default.Equals(field, value))
      {
        field = value;
        var handler = PropertyChanged;
        if (handler != null)
        {
          handler(this, new PropertyChangedEventArgs(propertyName));
        }
      }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
  }

结论

经过我的调查,我们肯定会将 Anders 的想法应用到我们的代码中,我们还将研究消除 lambda 表达式中的属性名称确定是否会给我们带来可观的性能提升——我的初步印象是,使用 lambda 表达式的维护性改进值得性能上的牺牲。但是,在一个有很多控件的网格中,可能会有可观的性能提升。因此,我们可能会使用 Anders 的实现,同时使用和不使用 lambda 表达式。

也许通过 Roslyn ,用于绑定的属性可以用一个简单的属性来简化,在此期间,我们可以从 Anders 演讲中的实现中学习。

请务必查看我从其他用户那里得到的一些回复,因为它们可能包含您比我文章中介绍的更喜欢的方法。我可能没有在文章中包含它们,因为我认为它们在可维护性方面没有我介绍的方法有显著改进,但意见不同,这些方法确实有其优点。

历史

  • 2012 年 2 月 15 日:初版
  • 2012 年 2 月 21 日:更新内容包括其他成员的评论以及一些代码修复。
© . All rights reserved.