かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Reactive Extensions再入門 その16「最大、最少、平均を求めるメソッド」

はじめに

今回は、最大値、最小値、平均を求めるメソッドについて説明します。簡単なようで、ちょっとだけ奥深い感じですね。

MaxメソッドとMinメソッドとAverageメソッド

ここでは、IObservableのシーケンスから最大、最小、平均の値を返すMaxメソッド、Minメソッド、Averageメソッドを説明します。この3つのメソッドは、名前の通りそれぞれ最大値、最小値、平均値のように単一の値を返す動作をしますが、これまでに説明したToArrayなどのメソッドと同様に戻り値がIObservableとなっています。そのため、他のメソッドと同様にIObservableの一連のメソッドチェインに対してシームレスに統合することが出来ます。これらのメソッドの使用例を下記に示します。

var s = new Subject<int>();

// 最大値を求めて表示
s.Max().Subscribe(max =>
{
    Console.WriteLine("Max {0}", max);
},
() => Console.WriteLine("Max Completed"));

// 最小値を求めて表示
s.Min().Subscribe(min =>
{
    Console.WriteLine("Min {0}", min);
},
() => Console.WriteLine("Min Completed"));

// 平均を求めて表示
s.Average().Subscribe(avg =>
{
    Console.WriteLine("Average {0}", avg);
},
() => Console.WriteLine("Average Completed"));

// 値の発行〜完了通知
Console.WriteLine("OnNext(1-3)");
s.OnNext(1);
s.OnNext(2);
s.OnNext(3);
Console.WriteLine("OnCompleted()");
s.OnCompleted();

実行結果を下記に示します。

OnNext(1-3)
OnCompleted()
Max 3
Max Completed
Min 1
Min Completed
Average 2
Average Completed

1〜3の値を発行しているので、最大値が3、最小値が1、平均が2という結果になっていることが確認できます。MaxメソッドやMinメソッドやAverageメソッドには数値を表す型と、それらの型のnullable型に対応するオーバーロードが定義されています。MaxとMinは、その型の最大値と最小値、Averageはdouble型で平均値のIObservableを返します。上記のコード例では、IObservableに対してメソッドを呼び出しているのでint型の最大値と最小値、double型の平均値を返しています。
また、MaxとMinメソッドには、独自の比較方法を指定するためのIComparerを受け取るオーバーロードも定義されています。使用例を下記に示します。

// Tuple<int, int>の比較を行うクラス
class TupleIntIntComparer : IComparer<Tuple<int, int>>
{
    // Item1 + Item2の結果で比較を行う
    public int Compare(Tuple<int, int> x, Tuple<int, int> y)
    {
        if (x == y)
        {
            return 0;
        }

        if (x == null)
        {
            return -1;
        }

        if (y == null)
        {
            return 1;
        }

        var xValue = x.Item1 + x.Item2;
        var yValue = y.Item1 + y.Item2;
        return Comparer<int>.Default.Compare(xValue, yValue);
    }
}
// -----------------------------------------
// 最大値を求めて表示
s.Max(new TupleIntIntComparer()).Subscribe(max =>
{
    Console.WriteLine("Max {0}", max);
},
() => Console.WriteLine("Max Completed"));

// 最小値を求めて表示
s.Min(new TupleIntIntComparer()).Subscribe(min =>
{
    Console.WriteLine("Min {0}", min);
},
() => Console.WriteLine("Min Completed"));

// 値の発行〜完了通知
Console.WriteLine("OnNext");
s.OnNext(Tuple.Create(1, 1));
s.OnNext(Tuple.Create(1, 2));
s.OnNext(Tuple.Create(3, 1));
Console.WriteLine("OnCompleted()");
s.OnCompleted();

実行結果を下記に示します。

OnNext
OnCompleted()
Max (3, 1)
Max Completed
Min (1, 1)
Min Completed

このようにして、最大、最小を求める際の比較ロジックをカスタマイズすることが出来ます。

MaxByメソッドとMinByメソッド

ここでは、MaxByメソッドとMinByメソッドについて説明します。このメソッドは最大値、最小値を求めるという意味ではMaxメソッドとMinメソッドと同じですが、引数に最大と最小を求めるためのキーとなる値を取得するためのFunc型のデリゲートを受け取る点が異なります。さらに、MaxByメソッドとMinByメソッドの戻り値はIObservableではなくIObservable>のように複数の結果が返ることを想定したシグネチャになっています。これは、デリゲートで取得したキー値が最小となる値がシーケンス内に複数存在する可能性があるためです。具体例を下記に示します。

var s = new Subject<Tuple<int, int>>();
// 最大値を求めて表示, 比較はタプルのItem1を使用する
s.MaxBy(t => t.Item1).Subscribe(max =>
{
    foreach (var i in max)
    {
        Console.WriteLine("MaxBy {0}", i);
    }
},
() => Console.WriteLine("MaxBy Completed"));

// 最小値を求めて表示, 比較はタプルのItem1を使用する
s.MinBy(t => t.Item1).Subscribe(min =>
{
    foreach (var i in min)
    {
        Console.WriteLine("MinBy {0}", i);
    }
},
() => Console.WriteLine("MinBy Completed"));

// 値の発行〜完了通知
Console.WriteLine("OnNext");
s.OnNext(Tuple.Create(1, 1));
s.OnNext(Tuple.Create(1, 2));
s.OnNext(Tuple.Create(3, 1));
Console.WriteLine("OnCompleted()");
s.OnCompleted();

実行結果を下記に示します。

OnNext
OnCompleted()
MaxBy (3, 1)
MaxBy Completed
MinBy (1, 1)
MinBy (1, 2)
MinBy Completed

MinByメソッドの結果が2つあることが確認できます。これはItem1の値が1になるタプルが(1, 1)と(1, 2)の2種類あるためです。このようなケースに対応するために、IObservable>のようにIList型を返す仕組みになっています。
また、ManByメソッドとMixByメソッドにも、比較方法をカスタマイズできるIComparerインターフェースを受け取るオーバーロードがあります。比較方法をカスタマイズできる点以外は、上記で示したMaxByメソッドとMinByメソッドと同様の動きをするため、コード例と実行結果は省略します。

まとめ

個人的にはMinByやMaxByがIListという型になっていることと、その理由が興味深かった今回の記事でした。