既存のDependencyPropertyの変更を検知する
自前でDependencyPropertyを実装する場合は、Register時にPropertyMetadataを渡すことで、値の変更時にコールバックを呼び出すことが可能となります。
しかし既存のDependencyPropertyの場合はすでにRegisterされているため、変更を検知するには別のアプローチが必要となります。
今回は2つのアプローチを実装してみたいと思います。
なお実用上は、既存のコントロールには対応するChangedイベントが用意されているため(ActualWidthならSizeChangedなど)、DependencyPropertyの変更の検知が必要なケースはほとんどないと思います。
DependencyPropertyDescriptor
RectangleとTextBlockがあり、RectangleのActualWidthがTextBlockに表示される例を考えます。
FromPropertyでDependencyPropertyDescriptorを取得し、コールバックをAddValueChangedで登録しています。
var desc = DependencyPropertyDescriptor.FromProperty(Rectangle.ActualWidthProperty, typeof(Rectangle));
desc.AddValueChanged(rectangle, (s, e) => textblock.Text = rectangle.ActualHeight.ToString());
この方法の場合、RemoveValueChangedを呼び出さない限りコントロールの参照が持ち続けられるので、コントロールを動的に削除したりする時には注意が必要です。
DependencyObjectにBindingして検知
ターゲットのDependencyPropertyにBindingして変更を検知する仲介クラス、DependencyPropertyChangedを定義してみました。
仲介用のValueプロパティを持っており、値が変化したときに登録したコールバックを呼ぶようになっています。
またConditionalWeakTableにターゲットのコントロールと共に保持することにより、ターゲットと同じ寿命になるようにしてあります。
public class DependencyPropertyChanged<T> : DependencyObject { public T Value { get { return (T)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(T), typeof(DependencyPropertyChanged<T>), new PropertyMetadata(default(T), (obj, e) => (obj as DependencyPropertyChanged<T>).OnValueChanged())); static ConditionalWeakTable<DependencyObject, List<DependencyObject>> _instances = new ConditionalWeakTable<DependencyObject, List<DependencyObject>>(); Action<T> _changed; public DependencyPropertyChanged(DependencyObject target, string path, Action<T> changed) { _changed = changed; _instances.GetOrCreateValue(target).Add(this); BindingOperations.SetBinding(this, DependencyPropertyChanged<double>.ValueProperty, new Binding(path) { Source = target, Mode = BindingMode.OneWay }); } void OnValueChanged() => _changed(Value); }
new DependencyPropertyChanged<double>(rectangle, nameof(rectangle.ActualWidth), x => textboxWidth.Text = x.ToString());
この方法であれば、コントロールを動的に削除しても参照が残り続けることはなくなります。
スライダー等からの大量リクエストの、最新値のみを別スレッド処理する
下記のような、スライダーの存在するアプリケーションを作成し、スライダーの値が変わる度に何かの処理をするとします。
もしこの処理が重たい場合、普通に実装しただけでは、UIのレスポンスが悪くなってしまいます。
slider.ValueChanged += (s, e) => Thread.Sleep(1000); // 仮想的な重い処理
そこで処理を別スレッドに移動するのですが、下記のように実装してしまうと、大量のスレッドが走ってしまうこととなります。
slider.ValueChanged += (s, e) =>
Task.Run(() => Thread.Sleep(1000));
スライダーの最新の値の処理結果しか利用しない場合、全リクエストを処理するのは無駄になります。
今回はこのような場合に利用できるクラスを実装してみたいと思います。
LatestTaskWorker
処理を行うスレッドが一つ存在し、スレッドが利用可能になったときに、PostされたActionのうち最新のものを処理するよう実装してあります。
BlockingCollectionを利用すると、lockを自前で書く必要がなく、要素が存在しない間はRead側がブロックされるので便利です。
Post時に既存のActionを削除することにより、最新のActionのみ処理されるようにしてあります。
class LatestTaskWorker { BlockingCollection<Action> _actions = new BlockingCollection<Action>(); public LatestTaskWorker() => Task.Run(() => { // 要素が存在しない場合はブロックされる foreach (var action in _actions.GetConsumingEnumerable()) action(); }); public void Post(Action action) { // 既存のActionを削除(実際はTryTake一回で問題ないと思います) while (_actions.TryTake(out var a)) { } _actions.Add(action); } }
利用例
下記の様に実装すると、スレッドが利用可能になった時点で最後にPostされたActionを処理するようになります。
これにより、UIをフリーズさせることなく、可能な限り処理を行いつつ、最新値は必ず処理されるようになります。
var worker = new LatestTaskWorker(); slider.ValueChanged += (s, e) => { worker.Post(() => { Thread.Sleep(1000); Console.WriteLine($"{e.NewValue}"); }); };
安全なスレッド終了対応
このクラスを利用した状態でアプリケーションを閉じると、処理中のActionは強制終了させられてしまいます。
きちんと処理の終了を待ちたい時用にFinish関数を追加しました。
アプリケーション終了前にFinishを呼べば、処理が終了するのを待つことが可能です。
class LatestTaskWorker { BlockingCollection<Action> _actions = new BlockingCollection<Action>(); Task _task; public LatestTaskWorker() => _task = Task.Run(() => { foreach (var action in _actions.GetConsumingEnumerable()) action(); }); public void Post(Action action) { while (_actions.TryTake(out var a)) { } _actions.Add(action); } public void Finish() { _actions.CompleteAdding(); _task.Wait(); } }
Functional Reactive Programming + MVVMでのUserControl --- C# WPF
下記記事にてFRPライブラリのSodiumとWPFを連携させてみました。
Functional Reactive Programming + MVVM --- C# WPF - 何でもプログラミング
今回はUserControlでも利用できるよう実装してみたいと思います。
アプリケーションコード
いつも通り、カウンタアプリケーションを実装します。
Main側は純粋にValueの保存と更新を行うのみの実装になっています。
引き続きCounterコントロールを実装していきます。
<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApp" Title="MainWindow" Height="100" Width="200"> <Grid> <local:Counter Value="{Binding Value}" ValueChanged="{Binding SetValue}" Margin="10" Height="23" /> </Grid> </Window>
class ViewModel { public DiscreteCell<int> Value { get; } public ViewModel(Stream<int> setValue) => Value = setValue.Hold(0); }
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new DataContext<ViewModel>(); } }
Counterコントロール
通常のロジックに対し、DependencyPropertyからのDiscreteCellの入力への追加、ICommandへのイベント通知の追加を行います。
それに伴う、DataContextクラスの拡張、DependencyPropertyクラスの作成、Listen拡張メソッドの作成は、後述いたします。
<UserControl x:Class="WpfApp.Counter" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="23" d:DesignWidth="200"> <Grid Height="23"> <Grid.ColumnDefinitions> <ColumnDefinition Width="23"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="23"/> </Grid.ColumnDefinitions> <Button Command="{Binding Decrement}" Content="◀" Grid.Column="0" /> <TextBox Text="{Binding Count}" Grid.Column="1" IsReadOnly="True" TextAlignment="Center" VerticalContentAlignment="Center" /> <Button Command="{Binding Increment}" Content="▶" Grid.Column="2" /> </Grid> </UserControl>
class CounterVM { List<IListener> listeners = new List<IListener>(); public DiscreteCell<int> Count { get; } public CounterVM(UserControl1 uc, Stream<Unit> increment, Stream<Unit> decrement) { Count = _value.Cell(uc); increment.MapTo(1).OrElse(decrement.MapTo(-1)).Snapshot(Count, (dx, x) => x + dx) .Listen(listeners, () => uc.ValueChanged); } }
public partial class Counter: UserControl { static DependencyProperty<Counter, int> _value = new DependencyProperty<Counter, int>(); public static readonly DependencyProperty ValueProperty = _value.Register(); public int Value { get => _value.Get(this); set => _value.Set(this, value); } static DependencyProperty<Counter, ICommand> _valueChanged = new DependencyProperty<Counter, ICommand>(); public static readonly DependencyProperty ValueChangedProperty = _valueChanged.Register(); public ICommand ValueChanged { get => _valueChanged.Get(this); set => _valueChanged.Set(this, value); } public Counter() { InitializeComponent(); ((FrameworkElement)Content).DataContext = new DataContext<CounterVM>(this); } }
DependencyProperty
通常のRegister、Get、Setのほかに、DescreteCellが取得できるようになっています。
DescreteCellがきちんと解放されるよう、インスタンスはConditionalWeakTableで管理しています。
今回はViewModelからの通知を、DescreteCellSinkにPostSendしています。(ViewModel側もSodiumを利用している場合、Listenの中でSendを呼ぶ可能性があり、その場合例外で止まるので、それを避けるため。)
もしFRPの回路を途切らせたくない場合は、型をDescreteCell
public class DependencyProperty<TClass, TValue> where TClass : DependencyObject { DependencyProperty _property; ConditionalWeakTable<TClass, DiscreteCellSink<TValue>> _sinks = new ConditionalWeakTable<TClass, DiscreteCellSink<TValue>>(); public DependencyProperty Register(TValue defaultValue = default(TValue), [CallerMemberName]string name = "") { name = name.Substring(0, name.Length - "Property".Length); var metadata = new PropertyMetadata(defaultValue, (obj, args) => { if (_sinks.TryGetValue((TClass)obj, out var sink)) Transaction.Post(() => sink.Send((TValue)args.NewValue)); }); _property = DependencyProperty.Register(name, typeof(TValue), typeof(TClass), metadata); return _property; } public TValue Get(TClass obj) => (TValue)obj.GetValue(_property); public void Set(TClass obj, TValue value) => obj.SetValue(_property, value); public DiscreteCell<TValue> Cell(TClass obj) => _sinks.GetValue(obj, x => new DiscreteCellSink<TValue>(Get(x))); }
Listen拡張メソッド
StreamとICommandを結びつける関数です。
ViewModel側がSodiumを利用している可能性があるため、Transaction.PostでExecuteしています。(理由はDependencyPropertyクラスの時と同じです。)
FRP回路を途切らせたくない場合はICommandではなく、型をDiscreteCell
public static void Listen<T>(this Stream<T> source, List<IListener> listeners, Func<ICommand> getCommand) => listeners.Add(source.Listen(x => Transaction.Post(() => getCommand()?.Execute(x))));
DataContext
引数に好きな値を設定できるように修正しています。
public DataContext(params object[] preArgs) { var constructor = typeof(T).GetConstructors().Single(); var args = preArgs.Concat(constructor.GetParameters().Skip(preArgs.Length).Select(parameter => ...
Functional Reactive Programming + MVVM --- C# WPF
下記記事にて、Functional Reactive ProgrammingライブラリであるSodiumとWPFを連携させました(F#)
SodiumでFunctional Reactive Programming (F#) --- WPF連携 - 何でもプログラミング
今回は、C#にてSodiumとWPFを連携させてみたいと思います。
アプリケーションコード
いつも通り、カウンタを実装してみたいと思います。
Streamを引数とするコンストラクタを持ち、DiscreteCellプロパティを公開するクラスを定義します。
引数のStreamがICommandに変換され、DiscreteCellの変更がINotifyPropertyChangedとして通知されるDataContextを利用します。(実装は後述します。)
(Sodiumは最近、CellとDiscreteCellを区別するようになりました。基本はDiscreteCellを使うことになると思います。)
<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="100" Width="200"> <Grid Height="23" Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="23"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="23"/> </Grid.ColumnDefinitions> <Button Command="{Binding Decrement}" Content="◀" Grid.Column="0" /> <TextBox Text="{Binding Count}" Grid.Column="1" IsReadOnly="True" TextAlignment="Center" VerticalContentAlignment="Center" /> <Button Command="{Binding Increment}" Content="▶" Grid.Column="2" /> </Grid> </Window>
class Counter { public DiscreteCell<int> Count { get; } public Counter(Stream<Unit> increment, Stream<Unit> decrement) { Count = Transaction.Run(() => { DiscreteCellLoop<int> count = new DiscreteCellLoop<int>(); var stream = increment.MapTo(1).OrElse(decrement.MapTo(-1)).Snapshot(count, (dx, x) => x + dx); count.Loop(stream.Hold(0)); return count; }); } }
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new DataContext<Counter>(); } }
DataContext
今回もDynamicObjectを利用して実装します。
まずコンストラクタの引数(複数のStream)を作成し、コンストラクタを呼び出してインスタンスを作成します。
作成したStreamのSinkはCommandとして公開されます。
続いてインスタンスのDiscreteCellプロパティをListenしてPropertyChangedに関連付けます。
public class DataContext<T> : DynamicObject, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; object _instance; Dictionary<string, ICommand> _commands = new Dictionary<string, ICommand>(StringComparer.OrdinalIgnoreCase); Dictionary<string, object> _propertyValues = new Dictionary<string, object>(); List<IListener> _listeners = new List<IListener>(); public DataContext() { // コンストラクタの引数(Streams)を作成し、インスタンス作成 var constructor = typeof(T).GetConstructors().Single(); var args = constructor.GetParameters().Select(parameter => { var type = parameter.ParameterType.GetGenericArguments()[0]; return GetType().GetMethod(nameof(CreateStream), BindingFlags.Instance | BindingFlags.NonPublic) .MakeGenericMethod(type).Invoke(this, new object[] { parameter.Name }); }).ToArray(); _instance = constructor.Invoke(args); // インスタンスのDiscreteCellプロパティとPropertyChangedを関連付け foreach (var property in typeof(T).GetProperties()) { var cell = property.GetValue(_instance); var type = property.PropertyType.GetGenericArguments()[0]; GetType().GetMethod(nameof(Listen), BindingFlags.Instance | BindingFlags.NonPublic) .MakeGenericMethod(type).Invoke(this, new object[] { property.Name, cell }); } } // DiscreteCellをListenし、PropertyChangedを呼び出す void Listen<U>(string name, DiscreteCell<U> cell) => _listeners.Add(cell.Calm().Listen(x => { _propertyValues[name] = x; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); })); // Streamを発火するCommandを作成・保存し、Streamを返す Stream<U> CreateStream<U>(string name) { var sink = new StreamSink<U>(); _commands[name] = new Command<U>(sink.Send); return sink; } public override bool TryGetMember(GetMemberBinder binder, out object result) { result = _propertyInfos.ContainsKey(binder.Name) ? _propertyInfos[binder.Name].GetValue(_model) : _commands[binder.Name]; return true; } }
Elm Architectureを利用したMVVMでのUserControl --- C# WPF
下記記事にて、C#でElm Architectureを利用してWPFアプリケーションを作成しました。
Elm Architectureを利用したMVVM --- C# WPF - 何でもプログラミング
今回はElm ArchitectureでUserControlを実装してみたいと思います。
F#版での実装は下記記事を参照してください。
F#でWPF --- Elm Architectureで実装されたUserControl - 何でもプログラミング
アプリケーションコード
Counterコントロールを配置しただけのViewと、値を保存・変更するだけのModel・Updaterになります。
引き続き、このCounterコントロールを作成していきます。
<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApp" Title="MainWindow" Height="100" Width="200"> <Grid> <local:Counter Value="{Binding Value}" ValueChanged="{Binding SetValue}" Margin="10" Height="23" /> </Grid> </Window>
class Model { public int Value { get; } public Model(int value) => Value = value; }
class Updater { public Model SetValue(Model model, int value) => new Model(value); }
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new DataContext(new Model(0), new Updater()); } }
Counterコントロール
Increment及びDecrementが押された場合にMain側に通知し(ValueChanged)、Main側の値(Value)が変わった場合にカウンタの値を更新するように実装してあります。
C#で利用しているDependencyPropertyManagerは下記記事を参照してください。
DependencyProperty定義の記述量削減 - 何でもプログラミング
また、RegisterWithUpdateName、DataContextクラスは後程実装いたします。
<UserControl x:Class="WpfApp.Counter" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="23" d:DesignWidth="200"> <Grid Height="23"> <Grid.ColumnDefinitions> <ColumnDefinition Width="23"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="23"/> </Grid.ColumnDefinitions> <Button Command="{Binding Decrement}" Content="◀" Grid.Column="0" /> <TextBox Text="{Binding Count}" Grid.Column="1" IsReadOnly="True" TextAlignment="Center" VerticalContentAlignment="Center" /> <Button Command="{Binding Increment}" Content="▶" Grid.Column="2" /> </Grid> </UserControl>
public partial class Counter : UserControl { static DependencyPropertyManager<Counter> _dp = new DependencyPropertyManager<Counter>(); // 値が変わった場合にUpdaterのSetCountを呼ぶように登録 public static readonly DependencyProperty ValueProperty = _dp.RegisterWithUpdateName<int>(nameof(Updater.SetCount)); public int Value { get => (int)_dp.Get(this); set => _dp.Set(this, value); } public static readonly DependencyProperty ValueChangedProperty = _dp.Register<ICommand>(); public ICommand ValueChanged { get => (ICommand)_dp.Get(this); set => _dp.Set(this, value); } class Model { public int Count { get; } public Model(int count) => Count = count; } class Updater { Counter _uc; public Updater(Counter uc) => _uc = uc; public PostCommand Increment(Model model) => new PostCommand(_uc.ValueChanged, model.Count + 1); public PostCommand Decrement(Model model) => new PostCommand(_uc.ValueChanged, model.Count - 1); public Model SetCount(Model model, int count) => new Model(count); } public Counter() { InitializeComponent(); // UserControlのDataContextはMain側で利用されるので、Content(今回はGrid)のDataContextにセット ((FrameworkElement)Content).DataContext = new DataContext(new Model(0), new Updater(this)); } }
RegisterWithUpdateName
値変更のコールバック時に、UserControlのContentのDataContextのExecute(後述します。)を呼び出します。
public DependencyProperty RegisterWithUpdateName<TValue>(string updateName, TValue defaultValue = default(TValue), [CallerMemberName]string name = "") { name = name.Substring(0, name.Length - "Property".Length); var metadata = new PropertyMetadata(defaultValue, (obj, args) => (((obj as UserControl).Content as FrameworkElement).DataContext as DataContext).Execute(updateName, args.NewValue)); return DependencyProperty.Register(name, typeof(TValue), typeof(TClass), metadata); }
DataContext
元々のDataContextではModelを返す関数しか対応していませんでしたが、Model更新後にCommandを実行できるよう、PostCommandクラスを返す関数にも対応させます。
またRegisterWithUpdateNameからCommandを呼び出せるよう、Execute関数も実装します。
public class PostCommand { public Action Execute { get; } public object Model { get; } public PostCommand(ICommand command, object parameter, object model = null) { Model = model; Execute = () => command?.Execute(parameter); } }
public class DataContext : DynamicObject, INotifyPropertyChanged { object _model; Dictionary<string, PropertyInfo> _propertyInfos; Dictionary<string, ICommand> _commands; public event PropertyChangedEventHandler PropertyChanged; public DataContext(object initialModel, object updater) { _model = initialModel; _propertyInfos = initialModel.GetType().GetProperties().ToDictionary(x => x.Name); _commands = updater.GetType().GetMethods().ToDictionary( method => method.Name, method => (ICommand)new Command<object>(parameter => { object prevModel = _model; object[] parameters = parameter == null ? new[] { prevModel } : new[] { prevModel, parameter }; object result = method.Invoke(updater, parameters); if (result.GetType() == _model.GetType()) _model = result; // PostCommandのModelをセット(nullの場合は変更なし) else if (result is PostCommand) _model = ((PostCommand)result).Model ?? _model; if (prevModel != _model) foreach (var property in _propertyInfos.Values) if (property.GetValue(prevModel) != property.GetValue(_model)) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name)); // PostCommandの場合、Command実行 (result as PostCommand)?.Execute(); })); } public override bool TryGetMember(GetMemberBinder binder, out object result) { result = _propertyInfos.ContainsKey(binder.Name) ? _propertyInfos[binder.Name].GetValue(_model) : _commands[binder.Name]; return true; } // 外部からコマンドを呼び出す用 public void Execute(string name, object parameter) => _commands[name].Execute(parameter); }
Elm Architectureを利用したMVVM --- C# WPF
下記記事にてF#でElm ArchitectureをWPFに導入してみました。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング
今回はC#で近いものを実装してみたいと思います。
F#の方ではサポートしたModel→ViewModelの変換は省略していますので、必要な場合は追加で実装してください。
アプリケーションコード
今回もカウンタアプリケーションを実装してみたいと思います。
後述しますDataContextクラスを利用すると、下記のようなアプリケーションコードになります。
F#の時と異なり、Modelの更新は判別共用体を利用するのではなく、メソッドを定義することでCommandとして呼び出されるようにしています。
Xaml
<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="100" Width="200"> <Grid Height="23" Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="23"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="23"/> </Grid.ColumnDefinitions> <Button Command="{Binding Decrement}" Content="◀" Grid.Column="0" /> <TextBox Text="{Binding Count}" Grid.Column="1" IsReadOnly="True" TextAlignment="Center" VerticalContentAlignment="Center" /> <Button Command="{Binding Increment}" Content="▶" Grid.Column="2" /> </Grid> </Window>
class Model { public int Count { get; } public Model(int count) => Count = count; } class Updater { public Model Increment(Model model) => new Model(model.Count + 1); public Model Decrement(Model model) => new Model(model.Count - 1); } public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new DataContext(new Model(0), new Updater()); } }
DataContextクラス
今回もDynamicObjectを利用して実装します。
Modelのプロパティと、Updaterで定義されたメソッドを呼び出すCommandを公開しています。
Commandクラスは単にExecuteをラムダで登録できるようにしたクラスです。
public class DataContext : DynamicObject, INotifyPropertyChanged { object _model; Dictionary<string, PropertyInfo> _propertyInfos; Dictionary<string, ICommand> _commands; public event PropertyChangedEventHandler PropertyChanged; public DataContext(object initialModel, object updater) { _model = initialModel; _propertyInfos = initialModel.GetType().GetProperties().ToDictionary(x => x.Name); _commands = updater.GetType().GetMethods().ToDictionary( method => method.Name, method => (ICommand)new Command<object>(parameter => { object prevModel = _model; object[] parameters = parameter == null ? new[] { prevModel } : new[] { prevModel, parameter }; _model = method.Invoke(updater, parameters); // 変更箇所通知 if (prevModel != _model) foreach (var property in _propertyInfos.Values) if (property.GetValue(prevModel) != property.GetValue(_model)) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name)); })); } public override bool TryGetMember(GetMemberBinder binder, out object result) { result = _propertyInfos.ContainsKey(binder.Name) ? _propertyInfos[binder.Name].GetValue(_model) : _commands[binder.Name]; return true; } }
C# Immutableオブジェクト 導出項目のキャッシュ機能
例として下記のようなVector2クラスを考えます。
Lengthプロパティは、呼び出される毎に計算を行っています。
処理が軽い場合は特に問題ありませんが、導出に時間がかかるプロパティは結果をキャッシュしてあるほうが好ましいです。
今回はキャッシュを行う機能を実装してみたいと思います。
public class Vector2 { public int X { get; } public int Y { get; } public double Length => Math.Sqrt(X * X + Y * Y); public Vector2(int x, int y) { X = x; Y = y; } }
Cache構造体
簡単のため、入力はValueTupleを利用します。(ValueTupleを利用しない場合は、TArgsを増やした複数のCache構造体を定義してください。)
入力に変化がない場合は、前回の値を返すように実装しています。
構造体である理由は、Immutableクラスが変更・複製されたときに、キャッシュを共有してしまわないようにするためです。
struct Cache<TArgs, TResult> { bool _initialized; TArgs _args; TResult _result; Func<TArgs, TResult> _f; public Cache(Func<TArgs, TResult> f) { _initialized = false; _args = default(TArgs); _result = default(TResult); _f = f; } public TResult Get(TArgs args) { if (_initialized && args.Equals(_args)) return _result; _initialized = true; _args = args; _result = _f(args); return _result; } }
Vector2書換え
新たにキャッシュのフィールドを追加し、LengthプロパティではGetを呼び出しています。
public class Vector2 { public int X { get; } public int Y { get; } Cache<(int, int), double> _length = new Cache<(int x, int y), double>(args => Math.Sqrt(args.x * args.x + args.y * args.y)); public double Length => _length.Get((X, Y)); public Vector2(int x, int y) { X = x; Y = y; } }
動作確認
同じオブジェクトに対し、二回目以降のLength呼出ではキャッシュが返るようになります。
Withで複製した場合でも問題なく動作していることも確認できます。(Withに関しましては、下記記事を参照してください。)
ネストしたImmutableオブジェクトの更新(C#) - 何でもプログラミング
Vector2 v = new Vector2(1, 1); v.Length; v.Length; // キャッシュが返る Vector2 v2 = v.With(x => x.Y, 2); v.Length; // キャッシュが返る v2.Length; v2.Length; // キャッシュが返る