読者です 読者をやめる 読者になる 読者になる

ちょっとイケてるINotifyPropertyChangedの実装

ネタ元→いけてるINotifyPropertyChangedの実装は、結構遅かった - かずきのBlog@Hatena

sender 用インスタンスを取得しているのが、遅くなる一番の要因でしょうな。

// ConstraintExpressionじゃないと駄目
if (senderExpression == null) throw new ArgumentException();

// 式を評価してsender用のインスタンスを得る
var sender = Expression.Lambda(senderExpression).Compile().DynamicInvoke();
INotifyPropertyChangedのいけてる実装 - かずきのBlog@Hatena

sender は毎回ラムダ式から取得しなくてもいいんじゃないかな。拡張メソッドの引数で渡しても、まだじゅうぶんイケてる。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;

namespace PropertyChangedSample
{
    class Program
    {
        private const int COUNT = 10000;

        static void Main(string[] args)
        {
            {
                イケてる実装 p = new イケてる実装();
                p.PropertyChanged += (s, e) => { };
                Stopwatch sw = Stopwatch.StartNew();
                foreach (var i in Enumerable.Range(0, COUNT))
                {
                    p.Name = "田中" + i;
                }
                sw.Stop();
                Console.WriteLine("イケてる実装:{0}ms", sw.ElapsedMilliseconds);
            }

            {
                ちょっとイケてる実装 p = new ちょっとイケてる実装();
                p.PropertyChanged += (s, e) => { };
                Stopwatch sw = Stopwatch.StartNew();
                foreach (var i in Enumerable.Range(0, COUNT))
                {
                    p.Name = "田中" + i;
                }
                sw.Stop();
                Console.WriteLine("ちょっとイケてる実装:{0}ms", sw.ElapsedMilliseconds);
            }

            {
                普通の実装 p = new 普通の実装();
                p.PropertyChanged += (s, e) => { };
                Stopwatch sw = Stopwatch.StartNew();
                foreach (var i in Enumerable.Range(0, COUNT))
                {
                    p.Name = "田中" + i;
                }
                sw.Stop();
                Console.WriteLine("普通の実装:{0}ms", sw.ElapsedMilliseconds);
            }

            Console.ReadLine();
        }
    }

    public class イケてる実装 : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string _name;

        public string Name
        {
            get { return _name; }
            set
            {
                if (_name == value)
                {
                    return;
                }
                _name = value;
                PropertyChanged.Raise(() => Name);
            }
        }
    }

    public class ちょっとイケてる実装 : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string _name;

        public string Name
        {
            get { return _name; }
            set
            {
                if (_name == value)
                {
                    return;
                }
                _name = value;
                PropertyChanged.Raise2(this, () => Name);
            }
        }
    }

    public class 普通の実装 : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string _name;

        public string Name
        {
            get { return _name; }
            set
            {
                if (_name == value)
                {
                    return;
                }
                _name = value;
                OnPropertyChanged("Name");
            }
        }

        private void OnPropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

    public static class PropertyChangedEventHandleExtensions
    {
        private readonly static Dictionary<Expression, string> _dic = new Dictionary<Expression, string>();

        // イケてる実装
        public static void Raise<TResult>(this PropertyChangedEventHandler self, Expression<Func<TResult>> property)
        {
            if (self == null)
            {
                return;
            }

            var memberExp = property.Body as MemberExpression;
            if (memberExp == null)
            {
                throw new ArgumentException();
            }

            var senderExp = memberExp.Expression as ConstantExpression;
            if (senderExp == null)
            {
                throw new ArgumentException();
            }

            var sender = Expression.Lambda(senderExp).Compile().DynamicInvoke();
            self(sender, new PropertyChangedEventArgs(memberExp.Member.Name));
        }

        // ちょっとイケてる実装
        public static void Raise2<TResult>(this PropertyChangedEventHandler self, object sender, Expression<Func<TResult>> property)
        {
            if (self == null)
            {
                return;
            }

            var memberExp = property.Body as MemberExpression;
            if (memberExp == null)
            {
                throw new ArgumentException();
            }

            self(sender, new PropertyChangedEventArgs(memberExp.Member.Name));
        }
    }
}

イケてる実装はネタ元を引用&ちょっと改変。

実行結果は次の通り。

イケてる実装:6009ms
ちょっとイケてる実装:140ms
普通の実装:9ms

40倍くらい速くなった。普通の実装よりは15倍ほど遅いけど。でも、妥協できるレベルじゃないかな?

取得したプロパティ名をキャッシュして使いまわせれば、もっと速くなると思うけど、スマートな方法がすぐには閃かなかった…。すごくシンプルな実装になっているから、今のままでいいか。