かずきのBlog@hatena

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

Reactive Extensions再入門 その23「重複を排除するメソッド」

過去記事インデックス

はじめに

ここでは、IObservableのシーケンスから重複要素を排除するメソッドについて紹介します。

Distinctメソッド

Distinctメソッドは、IObservableのシーケンスから、一度通過した値を二度と通さないメソッドです。コード例を下記に示します。

// Distinctで重複を排除して購読
s.Distinct()
    .Subscribe(
    // 値を出力
        i => Console.WriteLine("OnNext({0})", i),
    // OnCompletedしたことを出力
        () => Console.WriteLine("OnCompleted()"));

// 1〜3の値を発行
Console.WriteLine("OnNext 1〜3");
s.OnNext(1);
s.OnNext(2);
s.OnNext(3);

// 繰り返し1〜3の値を発行
Console.WriteLine("OnNext 1〜3");
s.OnNext(1);
s.OnNext(2);
s.OnNext(3);

// 2〜4の値を発行
Console.WriteLine("OnNext 2〜4");
s.OnNext(2);
s.OnNext(3);
s.OnNext(4);

Console.WriteLine("OnCompleted call.");
s.OnCompleted();

このコードでは、順番に1, 2, 3, 1, 2, 3, 2, 3, 4という値を発行するIObservableのシーケンスに対してDistinctを適用してSubscribeしています。実行結果を下記に示します。

OnNext 1〜3
OnNext(1)
OnNext(2)
OnNext(3)
OnNext 1〜3
OnNext 2〜4
OnNext(4)
OnCompleted call.
OnCompleted()

実行結果からわかるように、2回目の1, 2, 3の値を発行している所では、SubscribeのOnNextが呼ばれていません。これはDistinctメソッドで一度出現した値をブロックしているためです。次の2〜4の値を発行している箇所では、既に発行されている2と3はブロックされていますが、4はSubscribeのOnNextに渡っていることが確認できます。

Distinctメソッドのオーバーロード

Distinctメソッドには、FuncというTから重複の判定を行う値を選択するためのデリゲートを渡すオーバーロードと、IEqualityComparerインターフェースを実装したクラスで、値が等しいかどうかの判定方法を指定するオーバーロードと、Func型のデリゲートとIEqualityComparerインターフェースを実装したクラスの両方を指定するオーバーロードがあります。各オーバーロードのシグネチャを下記に示します。

// 比較方法を指定するオーバーロード
public static IObservable<T> Distinct<T>(
	this IObservable<T> source,
	IEqualityComparer<T> comparer
)

// 比較する値を選択するデリゲートを指定するオーバーロード
public static IObservable<T> Distinct<T, TKey>(
	this IObservable<T> source,
	Func<T, TKey> keySelector
)

// 比較する値を選択するデリゲートと、値の比較方法を指定するオーバーロード
public static IObservable<T> Distinct<T, TKey>(
	this IObservable<T> source,
	Func<T, TKey> keySelector,
	IEqualityComparer<TKey> comparer
)

この各種オーバーロードを使うことで、柔軟に重複値のフィルタリングを行えます。ここでは、最後のFunc型のデリゲートとIEqualityComparerインターフェースの実装クラスの両方を指定するオーバーロードのコード例を示します。Func型のデリゲートのみを指定するオーバーロードと、IEqualityComparerインターフェースの実装クラスのみを指定するオーバーロードのサンプルコードについては、単純に引数が少ないだけなので、ここでは割愛します。
まず、サンプルコードで使用するクラスの定義を下記に示します。ここでは、名前と年齢をもった人を表すクラスと、int型を1の位を除いた状態で比較するクラスの2つのコードを示します。

/// <summary>
/// 人
/// </summary>
class Person
{
    public int Age { get; set; }
    public string Name { get; set; }
    public override string ToString()
    {
        return string.Format("{0}: {1}歳", this.Name, this.Age);
    }
}

/// <summary>
/// 1の位を省いた状態で比較を行う。
/// </summary>
public class GenerationEqualityComparer : IEqualityComparer<int>
{
    /// <summary>
    /// 1の位を除いた数が等しければtrueを返す
    /// </summary>
    public bool Equals(int x, int y)
    {
        return (x / 10) == (y / 10);
    }

    public int GetHashCode(int obj)
    {
        return (obj / 10).GetHashCode();
    }
}

上記の2つのクラスを使った、Distinctメソッドの使用例のコードを下記に示します。

var s = new Subject<Person>();
s.Distinct(
    // PersonクラスのAgeプロパティの値で比較する
    p => p.Age,
    // 比較方法はGenerationEqualityComparerを使用する
    new GenerationEqualityComparer())
    // 購読
    .Subscribe(
        // 値を出力
        p => Console.WriteLine(p),
        // OnCompletedしたことを出力
        () => Console.WriteLine("OnCompleted()"));

// 10代, 20代, 30代の人を発行する
Console.WriteLine("OnNext 10代〜30代");
s.OnNext(new Person { Name = "田中 一郎", Age = 15 });
s.OnNext(new Person { Name = "田中 二郎", Age = 22 });
s.OnNext(new Person { Name = "田中 三郎", Age = 38 });

// 別の名前, 年齢の10代, 20代, 30代の人を発行する
Console.WriteLine("OnNext 10代〜30代");
s.OnNext(new Person { Name = "木村 一郎", Age = 12 });
s.OnNext(new Person { Name = "木村 二郎", Age = 28 });
s.OnNext(new Person { Name = "木村 三郎", Age = 31 });

// 40代の人を発行する
Console.WriteLine("OnNext 40代");
s.OnNext(new Person { Name = "井上 エリザベス", Age = 49 });

Console.WriteLine("OnCompleted call.");
s.OnCompleted();

上記のコードの実行結果を下記に示します。

OnNext 10代〜30代
田中 一郎: 15歳
田中 二郎: 22歳
田中 三郎: 38歳
OnNext 10代〜30代
OnNext 40代
井上 エリザベス: 49歳
OnCompleted call.
OnCompleted()

上記の結果からわかるとおり、PersonクラスのAgeプロパティの10の位の値で重複チェックが行われていることが確認できます。

DistinctUntilChangedメソッド

DistinctUntilChangedメソッドは、直前の値と異なる値が出現するまで値をブロックするメソッドになります。Distinctメソッドが、今まで発行された値すべてに対して重複チェックを行っていたのに対してDistinctUntilChangedメソッドは直前の値のみをチェック対象にする点が異なります。コード例を下記に示します。

var s = new Subject<int>();
// Distinctで重複を排除して購読
s.DistinctUntilChanged()
    .Subscribe(
        // 値を出力
        i => Console.WriteLine("OnNext({0})", i),
        // OnCompletedしたことを出力
        () => Console.WriteLine("OnCompleted()"));

// 1〜3の値を2回ずつ発行
Console.WriteLine("OnNext 1 -> 1 -> 2 -> 2 -> 3 -> 3");
s.OnNext(1);
s.OnNext(1);
s.OnNext(2);
s.OnNext(2);
s.OnNext(3);
s.OnNext(3);

// 1〜3の値を発行
Console.WriteLine("OnNext 1〜3");
s.OnNext(1);
s.OnNext(2);
s.OnNext(3);

Console.WriteLine("OnCompleted call.");
s.OnCompleted();

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

OnNext 1 -> 1 -> 2 -> 2 -> 3 -> 3
OnNext(1)
OnNext(2)
OnNext(3)
OnNext 1〜3
OnNext(1)
OnNext(2)
OnNext(3)
OnCompleted call.
OnCompleted()

最初に1, 1, 2, 2, 3, 3という値を発行している箇所では、SubscribeのOnNextに1, 2, 3という値しか渡っていないことから、値の変化が無い場合にブロックされていることが確認できます。また、そのあとに1, 2, 3の値を発行している箇所でSubscribeのOnNextに1, 2, 3の値が渡っていることから、Distinctメソッドとの動作の違いである、直前の値しか比較の対象にしないという点が確認できます。
DistinctUntilChangedメソッドにも、比較対象の値を選択するFunc型のデリゲートを受け取るオーバーロードと、IEqualityComparerインターフェースを実装したクラスを受け取るオーバーロードがありますが、Distinctメソッドと基本的に同じためサンプルコードは割愛します。