【问题标题】:INotifyPropertyChanged property name - hardcode vs reflection?INotifyPropertyChanged 属性名称 - 硬编码与反射?
【发布时间】:2008-09-26 19:15:43
【问题描述】:

使用 INotifyPropertyChanged 时指定属性名称的最佳方法是什么?

大多数示例将属性名称硬编码为 PropertyChanged 事件的参数。我正在考虑使用 MethodBase.GetCurrentMethod.Name.Substring(4) 但对反射开销有点不安。

【问题讨论】:

标签: .net wpf reflection


【解决方案1】:

不要忘记一件事:PropertyChanged 事件主要由将使用反射获取命名属性值的组件消耗。

最明显的例子是数据绑定。

当您触发PropertyChanged 事件时,将属性名称作为参数传递,您应该知道此事件的订阅者可能会通过调用例如@987654325 来使用反射 @(至少第一次使用PropertyInfo 的缓存),然后是GetValue。最后一次调用是对属性 getter 方法的动态调用 (MethodInfo.Invoke),其成本高于仅查询元数据的 GetProperty。 (请注意,数据绑定依赖于整个 TypeDescriptor 事物——但默认实现使用反射。)

因此,当然,在触发 PropertyChanged 时使用硬编码属性名称比使用反射动态获取属性名称更有效,但恕我直言,平衡您的想法很重要。在某些情况下,性能开销并不那么重要,您可以从某种强类型事件触发机制中受益。

这是我有时在 C# 3.0 中使用的,当性能不是问题时:

public class Person : INotifyPropertyChanged
{
    private string name;

    public string Name
    {
        get { return this.name; }
        set 
        { 
            this.name = value;
            FirePropertyChanged(p => p.Name);
        }
    }

    private void FirePropertyChanged<TValue>(Expression<Func<Person, TValue>> propertySelector)
    {
        if (PropertyChanged == null)
            return;

        var memberExpression = propertySelector.Body as MemberExpression;
        if (memberExpression == null)
            return;

        PropertyChanged(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

注意使用表达式树来获取属性的名称,以及使用 lambda 表达式作为Expression

FirePropertyChanged(p => p.Name);

【讨论】:

  • 对于属性更改的问题,这似乎有点矫枉过正。虽然它在可重构代码中维护所有内容,但最终需要维护的代码要多得多,而且这些代码的可读性不如 OnPropertyChanged("Name") 或类似方法的原始概念。
  • 非常酷。您认为有什么方法可以完全避免使用字符串?
  • @strongriley:这里介绍的解决方案不使用任何字符串。所以我不确定我明白你的意思......
  • 当你消费我的意思是。除了使用 INotifyPropertyChanged,是否可以创建一个接口,其中 EventArgs 没有在第一个位置使用字符串文字?
  • 请注意,Microsoft 从未展示过反射解决方案的示例。在 .NET 4.5 中,我们现在可以使用新的 [CallerMemberName] 属性 msdn.microsoft.com/en-us/library/…
【解决方案2】:

在 .NET 4.5 (C# 5.0) 中有一个名为 -CallerMemberName 的新属性,如果开发人员决定更改属性名称,它有助于避免硬编码的属性名称防止出现错误,这是一个示例:

public event PropertyChangedEventHandler PropertyChanged = delegate { };

public void OnPropertyChanged([CallerMemberName]string propertyName="")
{
    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}

private string name;
public string Name
{
    get { return name; }
    set 
    { 
        name = value;
        OnPropertyChanged();
    }
}

【讨论】:

    【解决方案3】:

    这里的反射开销非常大,尤其是因为 INotifyPropertyChanged 被大量调用。如果可以的话,最好只对值进行硬编码。

    如果您不关心性能,那么我会查看下面提到的各种方法,然后选择需要最少编码量的方法。如果您可以做一些事情来完全消除对显式调用的需要,那将是最好的(例如 AOP)。

    【讨论】:

      【解决方案4】:

      使用表达式树所涉及的性能损失是由于表达式树的重复解析造成的。

      下面的代码仍然使用表达式树,因此具有重构友好和混淆友好的后续优势,但实际上比通常的技术快了大约 40%(非常粗略的测试) - 包括为新的 PropertyChangedEventArgs 对象每次更改通知。

      它更快并且避免了表达式树的性能损失,因为我们为每个属性缓存了一个静态 PropertyChangedEventArgs 对象。

      我还没有做一件事 - 我打算添加一些代码来检查调试版本,以确保提供的 PropertChangedEventArgs 对象的属性名称与正在使用它的属性匹配 - 目前使用此代码开发者仍然有可能提供错误的对象。

      检查一下:

          public class Observable<T> : INotifyPropertyChanged
          where T : Observable<T>
      {
          public event PropertyChangedEventHandler PropertyChanged;
      
          protected static PropertyChangedEventArgs CreateArgs(
              Expression<Func<T, object>> propertyExpression)
          {
              var lambda = propertyExpression as LambdaExpression;
              MemberExpression memberExpression;
              if (lambda.Body is UnaryExpression)
              {
                  var unaryExpression = lambda.Body as UnaryExpression;
                  memberExpression = unaryExpression.Operand as MemberExpression;
              }
              else
              {
                  memberExpression = lambda.Body as MemberExpression;
              }
      
              var propertyInfo = memberExpression.Member as PropertyInfo;
      
              return new PropertyChangedEventArgs(propertyInfo.Name);
          }
      
          protected void NotifyChange(PropertyChangedEventArgs args)
          {
              if (PropertyChanged != null)
              {
                  PropertyChanged(this, args);
              }
          }
      }
      
      public class Person : Observable<Person>
      {
          // property change event arg objects
          static PropertyChangedEventArgs _firstNameChangeArgs = CreateArgs(x => x.FirstName);
          static PropertyChangedEventArgs _lastNameChangeArgs = CreateArgs(x => x.LastName);
      
          string _firstName;
          string _lastName;
      
          public string FirstName
          {
              get { return _firstName; }
              set
              {
                  _firstName = value;
                  NotifyChange(_firstNameChangeArgs);
              }
          }
      
          public string LastName
          {
              get { return _lastName; }
              set
              {
                  _lastName = value;
                  NotifyChange(_lastNameChangeArgs);
              }
          }
      }
      

      【讨论】:

      • 注意,上面的代码实际上存在一个问题——它将 T 限制为直接子类型(因为直接子类型将 T 指定为自身)。这意味着在处理比 Person 层次结构更靠下的类中的属性时,您不能使用 CreateArgs。一个简单的解决方法是使 CreateArgs 方法通用,即 CreateArgs 而不是 Observable 类通用。使用起来有点麻烦,因为您必须明确指定类型,例如: static PropertyChangedEventArgs _firstNameChangeArgs = CreateArgs(x => x.FirstName);
      • 任何迫使我改变我的基类的事情都是大忌。对于已经必须具有不同的基类或具有自动生成的基类的东西,我没有完全选择更改基类的选项。
      • Orion... 如果是这种情况,将上述内容从基类 Observable 转换为可以显式调用的辅助类(例如“ObservableHelper”)应该很容易。如果您有兴趣,我可能会发布一个更新,该更新将涵盖您无法控制使用哪个基类或不想引入基类的用例。不过,在您控制基类的地方,我确实认为使用基类有优势;使用简单是主要的。
      【解决方案5】:

      罗马:

      我想说你甚至不需要“Person”参数 - 因此,像下面这样一个完全通用的 sn-p 应该这样做:

      private int age;
      public int Age
      {
        get { return age; }
        set
        {
          age = value;
          OnPropertyChanged(() => Age);
        }
      }
      
      
      private void OnPropertyChanged<T>(Expression<Func<T>> exp)
      {
        //the cast will always succeed
        MemberExpression memberExpression = (MemberExpression) exp.Body;
        string propertyName = memberExpression.Member.Name;
      
        if (PropertyChanged != null)
        {
          PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
      }
      

      ...但是,我更喜欢在调试版本中使用带有条件验证的字符串参数。 Josh Smith 对此发布了一个很好的示例:

      A base class which implements INotifyPropertyChanged

      干杯:) 菲利普

      【讨论】:

      • 我赞成带有条件验证的字符串。这很简单,而且很有效。归根结底,这些反射解决方案都没有解决我想要解决的问题 - 编译时检查属性更改通知。
      【解决方案6】:

      是的,我看到了您建议的功能的使用和简单性,但是当考虑到反射导致的运行成本时,是的,这是一个坏主意,我在这种情况下使用的是 代码 sn-p 正确添加以利用时间和错误编写属性并触发所有 Notifyproperty 事件。

      【讨论】:

        【解决方案7】:

        我能想到的另一种非常好的方法是

        使用 Aspects 自动实现 INotifyPropertyChanged
        AOP:面向方面的编程

        关于 codeproject 的好文章:AOP Implementation of INotifyPropertyChanged

        【讨论】:

        • 我也认为 AOP 是解决这类问题的最佳方案。但是您知道大多数 C# 开发人员并不真正了解 AOP 是什么。
        【解决方案8】:

        你可能对这个关于

        的讨论感兴趣

        "Best Practices: How to implement INotifyPropertyChanged right?"

        也是。

        【讨论】:

          【解决方案9】:

          由于 C# 6.0 有一个 nameof() 关键字,它将在编译时进行评估,因此它将具有硬编码值的性能,并防止与通知属性不匹配。

          public event PropertyChangedEventHandler PropertyChanged;
          
          protected void NotifyPropertyChanged(string info)
          {       
              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(info));
          }
          public string SelectedItem
          {
              get
              {
                  return _selectedItem;
              }
              set
              {
                  if (_selectedItem != value)
                  {
                      _selectedItem = value;
                      NotifyPropertyChanged(nameof(SelectedItem));
                  }
              }
          }
          private string _selectedItem;
          

          【讨论】:

            【解决方案10】:

            不要紧,在硬编码和反射之间,我的选择是:notifypropertyweaver

            此 Visual Studio 包允许您获得反射的好处(可维护性、可读性......)而不必失去性能。

            实际上,您只需实现 INotifyPropertyChanged 并在编译时添加所有“通知内容”。

            如果您想完全优化您的代码,这也是完全可参数化的。

            例如,使用 notifypropertyweaver,您将在编辑器中拥有以下代码:

            public class Person : INotifyPropertyChanged
            {
                public event PropertyChangedEventHandler PropertyChanged;
            
                public string GivenNames { get; set; }
                public string FamilyName { get; set; }
            
                public string FullName
                {
                    get
                    {
                        return string.Format("{0} {1}", GivenNames, FamilyName);
                    }
                }
            }
            

            而不是:

            public class Person : INotifyPropertyChanged
            {
            
                public event PropertyChangedEventHandler PropertyChanged;
            
                private string givenNames;
                public string GivenNames
                {
                    get { return givenNames; }
                    set
                    {
                        if (value != givenNames)
                        {
                            givenNames = value;
                            OnPropertyChanged("GivenNames");
                            OnPropertyChanged("FullName");
                        }
                    }
                }
            
                private string familyName;
                public string FamilyName
                {
                    get { return familyName; }
                    set
                    {
                        if (value != familyName)
                        {
                            familyName = value;
                            OnPropertyChanged("FamilyName");
                            OnPropertyChanged("FullName");
                        }
                    }
                }
            
                public string FullName
                {
                    get
                    {
                        return string.Format("{0} {1}", GivenNames, FamilyName);
                    }
                }
            
                public virtual void OnPropertyChanged(string propertyName)
                {
                    var propertyChanged = PropertyChanged;
                    if (propertyChanged != null)
                    {
                        propertyChanged(this, new PropertyChangedEventArgs(propertyName));
                    }
                }
            }
            

            法语使用者:Améliorez la lisibilité de votre code et simplifiez vous la vie avec notifypropertyweaver

            【讨论】:

              【解决方案11】:

              此外,我们发现在 Debug 与 Release 版本中获取方法名称的方式不同:

              http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/244d3f24-4cc4-4925-aebe-85f55b39ec92

              (我们使用的代码与您建议的方式并不完全一致,但它让我们确信硬编码属性名称是最快、最可靠的解决方案。)

              【讨论】:

                【解决方案12】:

                我曾经做过一次类似的实验,从内存中它工作正常,并且不需要硬编码字符串中的所有属性名称。如果您在桌面上构建大容量服务器应用程序,性能可能会出现问题,您可能永远不会注意到差异。

                protected void OnPropertyChanged()
                {
                    OnPropertyChanged(PropertyName);
                }
                
                protected string PropertyName
                {
                    get
                    {
                        MethodBase mb = new StackFrame(1).GetMethod();
                        string name = mb.Name;
                        if(mb.Name.IndexOf("get_") > -1)
                            name = mb.Name.Replace("get_", "");
                
                        if(mb.Name.IndexOf("set_") > -1)
                            name = mb.Name.Replace("set_", "");
                
                        return name;
                    }
                }
                

                【讨论】:

                • 我喜欢这种创造性的方式。它可能不应该在实践中使用。但这个概念很有趣。
                【解决方案13】:

                基于反射的方法的问题在于它相当昂贵,而且速度不是很快。当然,它更加灵活,并且对重构更不脆弱。

                但是,它确实会损害性能,尤其是在频繁调用事物时。 stackframe 方法(我相信)在 CAS 中也存在问题(例如受限信任级别,例如 XBAP)。最好硬编码。

                如果您在 WPF 中寻找快速、灵活的属性通知,有一个解决方案 -- 使用 DependencyObject :) 这就是它的设计目的。如果您不想依赖依赖,或者担心线程关联问题,请将属性名称移动到常量中,然后繁荣!你的好。

                【讨论】:

                  【解决方案14】:

                  您可能希望完全避免使用 INotifyPropertyChanged。它为您的项目添加了不必要的簿记代码。考虑改用Update Controls .NET

                  【讨论】:

                    【解决方案15】:
                    【解决方案16】:
                    猜你喜欢
                    • 2012-02-05
                    • 1970-01-01
                    • 2014-02-25
                    • 2011-10-02
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2014-12-12
                    相关资源
                    最近更新 更多