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

tmori3y2のブログ

主にWindowsのプログラムなど

初心者が迷った流すべきものを流さない時はラムダ式に入れないとイケナイという話

C# MVVM ReactiveProperty WPF LINQ

tmori3y2.hatenablog.com

取りあえず、ラムダ式に埋め込むところまでは、やりました。

なお、Observableじゃない拡張メソッドに渡しているReactivePropertyは、ラムダ式の中にある限り、最新の値が使用されます。

ラムダ式の中にある限り・・・」

意外にこれが曲者です。しかし、その話はまた後で・・・

この話があったので今回のネタはこれで・・・

実は、メタデータを固定値から変更しようとした時に、最初の石に躓きました。

LINQというよりかは、ラムダ式の初心者がやらかす初歩的ミスだと思うんですが、なにぶんC#2.0をかじった後、ソフトウェア技術者になってからはMFCをずっと弄っていたので・・・

もちろん、すぐに動かないと分かるパターンですが、検証なしの初期バージョンが、これ・・・

var provider = CultureInfo.CurrentUICulture;
var format = string.Format(CultureInfo.InstalledUICulture, "N{0}", model.Decimals.Value);
var style = NumberStyles.Number & ~NumberStyles.AllowTrailingSign;

X =
    // Observes the dependent property.
    model.X
    .CombineLatest(CanUpdateBinding, (d, condition) => d)
    .Where(_ =>
        CanUpdateBinding.Value)
    // Converts from decimal to string.
    .Select(d =>
        d.ToString(format, provider))    // NG
    // Initializes the property.
    .ToReactiveProperty()
    // Disposes this property if unused.
    .AddTo(disposables);

X
    // Updates this property by the view.
    .Do(s =>
        Debug.WriteLine("CalculatorViewModel.X: {0}({1})", s, "Changed"))
    .Where(_ =>
        CanUpdateBinding.Value && !X.HasErrors)
    // Converts from string to decimal.
    .Select(s =>
        decimal.Parse(s, style, provider))
    .Select(d =>
        Math.Round(d, model.Decimals.Value, MidpointRounding.AwayFromZero))
    .Do(d =>
        Debug.WriteLine("CalculatorViewModel.X: {0}({1})", d, "Converted back"))
    // Subscribes to source and reformats target if source not changed. 
    .Subscribe(d =>
        model.X.Value = d)
    // Disposes the delegate if unused.
    .AddTo(disposables);

UserControlを初期化した時に、ラムダ式にはformatの値が埋め込まれるから、その後にmodel.Decimalsを弄っても、小数点の桁数は変わらない・・・

NGの行にブレークポイントを置いたらステップ実行できるが、あくまでもReactivePropertyのprivateメンバーであるSourceDisposableに繋ぎ止められている

public static IObservable<TResult> Select<TSource, TResult>(this IObservable<TSource> source, Func<TSource, int, TResult> selector);

のselectorにアタッチされているデリゲートが呼び出されている時に、たまたまXの初期化式のラムダ式の定義で、ブレークするだけであって、初期化式が再実行されている訳でもないし、ましてやformatが再評価されるわけでもないということを、C#2.0な頭から切り替えるのに、ちょっと時間がかかった。

ラムダ式とか式木とかは、こちらで勉強させていただきました・・・

ufcpp.net

しかし・・・

「歴史は繰り返す」

X =
    // Observes the dependent property.
    model.X
    .CombineLatest(CanUpdateBinding, (d, condition) => d)
    .Where(_ =>
        CanUpdateBinding.Value)
    // Converts from decimal to string.
    .Convert(model.Decimals)
    // Initializes the property.
    .ToReactiveProperty()
    // Disposes this property if unused.
    .AddTo(disposables);

とかやりたくて、

public static IObservable<string> Convert(this IObservable<decimal> self, int decimals)
{
    if (self == null)
    {
        throw new ArgumentNullException("self");
    }

    return self.Select(d => d.Convert(decimals));
}

public static IObservable<string> Convert(this IObservable<decimal> self, IReadOnlyReactiveProperty<int> decimals)
{
    if (self == null)
    {
        throw new ArgumentNullException("self");
    }

    return self.Convert(decimals != null ? decimals.Value : -1)); // NG 
}

とかすると、これは拡張メソッドの中で、以下のようにやっているのと同じなので、正しく動くわけがない・・・

var decimals = decimals != null ? decimals.Value : -1);
return self.Select(d => d.Convert(decimals));

正しく動くのは、こちら。

public static IObservable<string> Convert(this IObservable<decimal> self, IReadOnlyReactiveProperty<int> decimals)
{
    if (self == null)
    {
        throw new ArgumentNullException("self");
    }

    return self.Select(d => d.Convert(decimals != null ? decimals.Value : -1));
}

拡張メソッドLINQやり始めてから使っているので、なんとなく

「拡張メソッドの中 (引数)に入ってたらOK」

みたいな錯覚が中々抜けきれない・・・

まとめ

  • CombineLatestで繋げて絶対にStreamに流さないとイケナイObservableなものは流すことを先ず考える
  • めったに変わらないものや、一番流したいものを流したときに変わらないことが保証されているものは、ラムダ式に埋め込む
  • 拡張メソッドに騙されるな!!