Anders Hejlsberg 的性能与想法 INotifyPropertyChanged






4.94/5 (25投票s)
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
的四种实现的测试。
- 基本 MSDN 实现
- 我小组一直在使用的实现
- Anders Hejlsberg 演讲中的实现
- 结合了 Anders 版本和我们一直在使用的版本
- 结合了 Anders 版本和
MethodBase.GetCurrentMethod()
的实现
我进行了几次运行,包括:
- 首次运行,将编译所有内容并加载到内存中(**注意**:运行 1000 次迭代与 10,000 次迭代之间存在显著差异)
- 第二次运行,此时所有内容都应该已编译并加载到内存中
- 运行一个空事件处理程序被触发的场景
- 在 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。
实现
我们小组实际上并没有将 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 日:更新内容包括其他成员的评论以及一些代码修复。