Interactive Extensionsを自分で実装する – 7.Do

InteractiveExtensions(Ix)を自分で実装するシリーズ、第7回はDoです。

DoのシグネチャはReactiveExtensionsのSubscribeに似ています。たくさんオーバーロードがあります。

public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext);
public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action onCompleted);
public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action<Exception> onError);
public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action<Exception> onError, System.Action onCompleted);
public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.IObserver<TSource> observer)

使用例1

static IEnumerable<int> GetDoTestSequence()
{
    yield return 2;
    yield return 4;
    yield return 3;
    yield return 1;
}
static void DoTest()
{
    var sequence = GetDoTestSequence();
    var doAttachedSequence = sequence.Do(
            i => Console.WriteLine("do:{0}", i),
            e => Console.WriteLine("error:{0}", e.Message),
            () => Console.WriteLine("completed!")
    );
    try{
        foreach (var item in doAttachedSequence){
            Console.WriteLine("foreach:{0}", item);
        }
    }
    catch (Exception e){
        Console.WriteLine("Exception Handled in Main: {0}", e.Message);
    }
}

do_1
Doはシーケンスの間に挟む処理をonNextで指定します。また、例外発生時にはonError, 列挙完了時にはonCompletedが呼ばれます。たくさんオーバーロードがあったのは適宜省略しやすいように用意してくれているだけですね。注意する点としては、初回のonNextは1つめのアイテム列挙に呼ばれること。

続いて、onErrorを使った使用例2

static IEnumerable<int> GetDoTestSequenceWithException()
{
    yield return 2;
    yield return 4;
    yield return 3;
    throw new Exception("Test Exception");
    yield return 1;
}
static void DoTestWithException()
{
    var sequence = GetDoTestSequenceWithException();
    var doAttachedSequence = sequence.Do(
            i => Console.WriteLine("do:{0}", i),
            e => Console.WriteLine("error:{0}", e.Message),
            () => Console.WriteLine("completed!")
    );
    try{
        foreach (var item in doAttachedSequence){
            Console.WriteLine("foreach:{0}", item);
        }
    }
    catch (Exception e){
        Console.WriteLine("Exception Handled in Main: {0}", e.Message);
    }
}

do_2
予想通りではないかと思います。onErrorで例外を一旦キャッチしますが、例外は列挙元にそのまま渡されます。また、ReactiveExtensionsのSubsribeと違う点として、onError発生時のonCompletedの呼ばれ方が挙げられます。ReactiveExtensionsのSubscribeはonErrorが発生してもonCompletedが呼ばれますが、InteractiveExtensionsのDoではonErrorが呼ばれる場合onCompletedが呼ばれません。

話は変わりますが、マイコンを使った組込み機器では処理が正常に動作していることを確認するためWDT(Watch Dog Timer)というペリフェラルがあります。これは定期的に番犬をなだめてやる処理を事前に設定しておくことで、万一一定期間番犬をなだめる処理が入らなかったらシステムが意図通り動作できていないことだと検出し緊急処理(システム停止とか)するために用いられるものです。この番犬をなだめる処理はKickdogと言い、システム依存ですが1~100msに1回実行したりします。今回のDoメソッドはそのKickdogにも使えそうです。C#が使えれば…

さぁ、では実装です。(Let’s Implement It!!)

Do…対象シーケンスの列挙中、また例外発生・完了時にも処理を割り込ませたシーケンスを返す。

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext){
            return source.Do(onNext, () => {});
        }
        public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action onCompleted){
            return source.Do(onNext, e => {}, onCompleted);
        }
        public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action<Exception> onError){
            return source.Do(onNext, onError, () => {});
        }
        public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.IObserver<TSource> observer){
            if (observer == null){
                throw new ArgumentNullException();
            }
            return source.Do(
                new Action<TSource>(observer.OnNext),
                new Action<Exception>(observer.OnError),
                new Action(observer.OnCompleted)
            );
        }
        public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action<Exception> onError, System.Action onCompleted){
            if (source == null || onNext == null || onCompleted == null){
                throw new ArgumentNullException();
            }

            var enumerator = source.GetEnumerator();
            bool exist = false;
            do
            {
                try
                {
                    exist = enumerator.MoveNext();
                }
                catch (Exception e)
                {
                    onError(e);
                    throw;
                }
                if (exist)
                {
                    onNext(enumerator.Current);
                    yield return enumerator.Current;
                }
            } while (exist);
            onCompleted();
        }
    }
}

たくさんオーバーライドがありますが、結局最後の1つで全て代用できます。やっていることは、対象のシーケンス列挙中に1アイテム列挙前にonNextを呼び、例外の発生確認をしつつ発生したらonErrorを呼んでキャッチ&再スローしつつ、列挙完了時にonCompletedを呼んでいます。

ちょっとGetEnumeratorやMoveNextなど生のIEnumeratorインターフェースメンバを使った実装でした。コンパイルは通りませんが、意味的には以下のようにも読みかえることができます。

Doのほぼ等価メソッド

public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action<Exception> onError, System.Action onCompleted)
{
    // nullチェックは省略
    try
    {
        foreach (var item in source)
        {
            onNext(item);
            yield return item;
        }
    }
    catch (Exception e)
    {
        onError(e);
        throw;
    }
    onCompleted();
}

この場合「try-catch内でyield returnが使えない」というコンパイルエラーになります、yield return も「try-finally内なら」使えるんですけどね。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中