バックグランド検索をするWPF TextBox

以前WPFで入力補完が効くTextBoxを作成してみました。似たところで、入力の度にバックグランドで非同期タスクにより検索を走らせる処理を考えてみます。

 

form

簡単にはこんなところでしょうか。

MainWindow.xaml.cs

<Window x:Class="BackgroundSearchTextBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="174" Width="334">
    <Grid>
        <Label Content="検索ワード" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <TextBox x:Name="SearchWordTextBox" HorizontalAlignment="Left" Height="23" Margin="95,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="201"/>
        <Label Content="検索結果" HorizontalAlignment="Left" Margin="10,43,0,0" VerticalAlignment="Top"/>
        <TextBlock x:Name="SearchResultTextBlock" HorizontalAlignment="Left" Margin="95,43,0,0" TextWrapping="Wrap" Text="Enter Keyword" VerticalAlignment="Top" Height="91" Width="201"/>
    </Grid>
</Window>

MainWindow.xaml

namespace BackgroundSearchTextBox
{
    using System;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Windows;

    public partial class MainWindow : Window
    {
        private string[] candidates = new string[]{
            "apple",
            "banana",
            "orange",
            "grape",
            "pineapple",
            "mango"
        };

        public MainWindow()
        {
            InitializeComponent();

            // Rxなし
            SearchWordTextBox.TextChanged += (s, e) =>
            {
                Task.Factory.StartNew(
                    search =>
                    {
                        string searchWord = search as string;
                        if (String.IsNullOrEmpty(searchWord)) return "Enter Keyword.";
                        var candidatesQuery = candidates.Where(cand => cand.StartsWith(searchWord));
                        return candidatesQuery.IsEmpty() ? "No Result." : candidatesQuery.Aggregate((a, b) => a + Environment.NewLine + b);
                    },
                    SearchWordTextBox.Text)
                .ContinueWith(t => SearchWordTextBox.Dispatcher.Invoke(() => SearchResultTextBlock.Text = t.Result));
            };

        }
    }
}

これを動かすにはNugetパッケージインストーラから

PM> install-package ix-main

としてInteractive Extensionsをインストールしておく必要があります。

上記はイマイチ。文字入力の度にタスクを起動させていますが、頻繁な文字入力の度にタスクを生成してたら結構オーバーヘッドが大きいです。そこでRxを使ってみます。

 

MainWindow.xaml.cs

namespace BackgroundSearchTextBox
{
    using System;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Reactive.Linq;
    using System.Reactive.Concurrency;

    public partial class MainWindow : Window
    {
        private string[] candidates = new string[]{
            "apple",
            "banana",
            "orange",
            "grape",
            "pineapple",
            "mango"
        };

        public MainWindow()
        {
            InitializeComponent();

            // Rxあり
            Observable.FromEvent<TextChangedEventHandler, TextChangedEventArgs>(
                action => (s, e) => action(e),
                handler => SearchWordTextBox.TextChanged += handler,
                handler => SearchWordTextBox.TextChanged -= handler)
                .Select(_ => SearchWordTextBox.Text)
                .Throttle(TimeSpan.FromMilliseconds(100))
                .Select(searchWord =>
                    {
                        if (String.IsNullOrEmpty(searchWord)) return "Enter Keyword.";
                        var candidatesQuery = candidates.Where(cand => cand.StartsWith(searchWord));
                        return candidatesQuery.IsEmpty() ? "No Result." : candidatesQuery.Aggregate((a, b) => a + Environment.NewLine + b);
                    }
                )
                .ObserveOn(new DispatcherScheduler(App.Current.Dispatcher))
                .Subscribe(t => SearchResultTextBlock.Text = t);

        }

    }
}

Rxあり版を動かすためにはNugetパッケージマネージャーから

PM> install-package rx-main
PM> install-package rx-wpf

としてReactive Extensions(Rx)のインストールも必要です。Rxを使うことで、

  • ちゃんとイベントリッスン解除ができる
  • 100ms間値が落ち着くのを待つことができる
  • 全体的に、特にスレッド行き来する処理の記述が容易&見やすい

と言えますね。まだまだ学習が必要だ・・。

広告

Interactive Extensionsを自分で実装する – 11.IsEmpty

Interactive Extensions(Ix)を自分で実装する、11回目はIsEmptyです。

その名から予想できる通り、シーケンスが空かどうかを判断するためのものです。

static void IsEmptyTest()
{
    IEnumerable<int> noSequence = Enumerable.Range(1, 0); // count = 0
    var noSequenceResult = noSequence.IsEmpty();
    Console.WriteLine(noSequenceResult);

    IEnumerable<int> oneSequence = Enumerable.Range(1, 1);  // count = 1
    var oneSequnenceResult = oneSequence.IsEmpty();
    Console.WriteLine(oneSequnenceResult);
}

isempty
列挙アイテムが1つもなければTrue, 1つでもあればFalseを返します。実行例は省略しますが、nullに対してはちゃんとArgumentNullExceptionがスローされました。
では実装です。(Let’s Implement It!)

IsEmpty…シーケンスに列挙アイテムが1つでもあるかを確認する。

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static bool IsEmpty<TSource> (this IEnumerable<TSource> source)
        {
            if (source == null)
            {
                throw new ArgumentNullException();
            }
            return !source.GetEnumerator().MoveNext();
        }
    }
}

実装方法はいろいろあるかと思いますが、1文で済むのでIEnumeratorを使ってアイテムがあるかないかを判断しています。

Interactive Extensionsを自分で実装する – 10.Hide, If, IgnoreElements

Interactive Extensionsを自分で実装する、第10回目はHideから。

まずは、使用例です。

static void HideTest()
{
    var sequence = new List<int>() { 0, 1, 2, 3, 4, 5 };
    var hideSequence = sequence.Hide();
    // cast original source
    try{
        IList<int> castToListSuccess = (IList<int>)sequence;
        Console.WriteLine("cast succeeded.");
    }
    catch (InvalidCastException ) {
        Console.WriteLine();
    }

    // cast hided sequence
    try{
        IList<int> castToList = (IList<int>)hideSequence;
    }
    catch (InvalidCastException){
        Console.WriteLine("Invalid cast.");
    }
    finally{
        foreach (var item in hideSequence){
            Console.WriteLine(item);
        }
    }
}

hide
正直このメソッド、利用価値がよくわかりません。おそらく具象クラスを完全に隠すことでユーザが暗黙の仮定を置いて危険なアップキャストをしていることを避ける目的があるのだと思いますが、それってどういうシチュエーションなんでしょうか。。では実装です。(Let’s Implement It!)

Hide…元シーケンスの実際の型を隠し、IEnumerable<T>のみを公開する

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static IEnumerable<TSource> Hide<TSource>(this IEnumerable<TSource> source)
        {
            if (source == null)
            {
                throw new ArgumentNullException();
            }
            foreach (var item in source)
            {
                yield return item;
            }
        }
    }
}

 

次はIfです。Ifのシグネチャは簡単。

static void IfTest()
{
    var sequence1 = Enumerable.Range(0, 3);
    var sequence2 = Enumerable.Range(0, 3);
    int i = 0;
    Func<bool> selector = () => i++ % 2 == 0;
    var ifSequnece = EnumerateEx.If(selector, sequence1, sequence2);
    foreach(var item in ifSequnece){
        Console.WriteLine(item);
    }
}

if
ちょっと気になったこととしては、御大層なメソッドな気がしたのでひょっとして列挙のたびにFuncデリゲート呼ばれてる?と思ったので、複数回呼び出されればそのたびに戻り値が変わるようにしたのですが、一度しか呼ばれてませんでした。ということで実装はシンプルにこんな感じ、Let’s Implement It!

If…Func<bool>デリゲートを取りシーケンス選択を遅延させる

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static IEnumerable<TResult> If<TResult>(Func<bool> condition, IEnumerable<TResult> thenSource)
        {
            return If(condition, thenSource, Enumerable.Empty<TResult>());
        }
        public static IEnumerable<TResult> If<TResult>(Func<bool> condition, IEnumerable<TResult> thenSource, IEnumerable<TResult> elseSource)
        {
            if (condition == null || thenSource == null || elseSource == null)
            {
                throw new ArgumentNullException();
            }
            return condition() ? thenSource : elseSource;
        }
    }
}

 

最後はIgnoreElementsです。使ってみます。

static void IgnoreElementsTest()
{
    IEnumerable<int> sequence = Enumerable.Range(0, 4);
    var result = sequence.IgnoreElements();
    foreach (var item in result)
    {
        Console.WriteLine(item);
    }
    Console.WriteLine("end");
}

ignoreelements
ダメです、もう完全に有難みがわかりません・・・。実装です。

IgnoreElements…アイテム列挙しない。

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static IEnumerable<TSource> IgnoreElements<TSource>(this IEnumerable<TSource> source){
            if(source == null){
                throw new ArgumentNullException();
            }
            return Enumerable.Empty<TSource>();
        }
    }
}

Interactive Extensionsを自分で実装する – 9.Expand, Generate

Interactive Extensions(Ix)を自分で実装する、第9回目はExpandです。

このメソッドの動作を探るにはちょっとてこずりました、適当に組んだだけだと無限ループになってしまったからです。実用性は一旦横に置いて、まずは以下の使用例を見てください。

static IEnumerable<int> EmptySequence() {
    return Enumerable.Empty<int>();
}
static IEnumerable<int> Sequence1(){
    yield return 0;
}
static IEnumerable<int> Sequence2(){
    yield return 1;
    yield return 4;
}
static IEnumerable<int> Sequence3(){
    yield return 2;
    yield return 6;
}
static void ExpandTest(){
    IEnumerable<int>[] sequenceSelector = new[]{
        EmptySequence(),
        Sequence1(),
        Sequence2(),
        Sequence3()
    };
    var testEnum = Enumerable.Range(3, 1);  // 3のみ
    var expandedEnum = testEnum.Expand(i => sequenceSelector[i % 4]);
    foreach (var item in expandedEnum)
    {
        Console.WriteLine(item);
    }
}

expand
まずsourceは{3}だけのIEnumerable<int>です、これをExpandします。まずsourceを全て列挙します、この場合アイテムは3だけなのでまず3が列挙されます。次に、{3}に対しててselectorをかけたシーケンスが作成されます、この場合3%4=3でSequence3()が選択され、これを列挙します。このアイテムは{2, 6}なので最初の{3}と合わせて、{3,2,6}と列挙されることになります。

次に、この{2,6}に対して再度selectorをかけたシーケンスが作成されます、この場合2%4=2, 6%4=2でSequence2()が2回列挙されますので、先ほどの{3,2,6}に続いて{1,4}, {1,4}と続くので、{3,2,6,1,4,1,4}と列挙されることになります。

さらに{1,4,1,4}に対してselectorを適用したシーケンスが作成されますが、1%4=1, 4%4=0ですので、Seuence1()、EmptySequence(), Sequence1(), EmptySequence()と列挙されることになります。EmptySequence()は何もないので出力には無関係で、Sequence1()からは0が1回、これが2コあるので先ほどの{3,2,6,1,4,1,4}に続いて、{3,2,6,1,4,1,4,0,0}と続きます。

さらに{0}が2回列挙されていますがこれはEmptySequence()なので列挙アイテムに変化はなく最終出力も{3,2,6,1,4,1,4,0,0}となるわけです。

では実装してみます。(Let’s Implement It!)

Expand…幅優先探索でシーケンス列挙を繰り返す

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static IEnumerable<TSource> Expand<TSource>(this IEnumerable<TSource> source,
                                                           Func<TSource, IEnumerable<TSource>> selector)
        {
            if (source == null || selector == null){
                throw new ArgumentNullException();
            }
            var sourcesA = new Queue<IEnumerable<TSource>>();
            var sourcesB = new Queue<IEnumerable<TSource>>();
            Queue<IEnumerable<TSource>> currentSources = null,
                                        nextSources = null;

            bool side = true;
            sourcesA.Enqueue(source);
            do
            {
                currentSources = side ? sourcesA : sourcesB;
                nextSources = side ? sourcesB : sourcesA;
                do
                {
                    var childSource = currentSources.Dequeue();
                    foreach (var item in childSource)
                    {
                        yield return item;
                        nextSources.Enqueue(selector(item));
                    }
                } while (currentSources.Count() != 0);
                side = !side;
            } while (nextSources.Count() != 0);
        }
    }
}

sourceシーケンスを列挙しつつselectorをかけたシーケンスをAに保持、次にシーケンスAを列挙しつつselectorをかけたシーケンスをBに保持、その次にシーケンスBを列挙しつつselectorをかけたシーケンスをAに保持、…と繰り返していく実装になります。使い方を間違えるとすぐに無限ループになるのでご注意。

 

次はGenerate。このシグネチャはこんな感じでややこしく見えますが・・・

public static IEnumerable<TResult> Generate<TState, TResult>(TState initialState, Func<TState, bool> condition, Func<TState, TState> iterate, Func<TState, TResult> resultSelector);

これは使い方を見れば簡単じゃんって思うことでしょう、と同時に利用価値も理解いただけると思います。

static void GenerateTest()
{
    var testSequence = EmulateIxExtensions.Generate(0, i => i < 10, i => i + 1, i => Math.Exp(i));
    foreach (var item in testSequence)
    {
        Console.WriteLine(item);
    }
}

generate
そう、forの構文そっくり!であれば説明は不要ですね。では実装。Let’s Implement It!

Generate…forステートメント類似の構文でシーケンスを生成する。

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static IEnumerable<TResult> Generate<TState, TResult>(TState initialState,
                                                                     Func<TState, bool> condition,
                                                                     Func<TState, TState> iterate,
                                                                     Func<TState, TResult> resultSelector)
        {
            if (condition == null || iterate == null || resultSelector == null)
            {
                throw new ArgumentNullException();
            }
            TState current = initialState;
            while (condition(current))
            {
                yield return resultSelector(current);
                current = iterate(current);
            }
        }
    }
}

Interactive Extensionsの利用価値はForEachだけじゃないですよ!Generateスキ。

Interactive Extensionsを自分で実装する – 8.Finally, For

InteractiveExtensions(Ix)を自分で実装する、第8回はまずFinallyです。

これはDoのonCompletedにActionを設定したものと同じですね、オシマイ!ではさびしすぎるのでまずシグネチャの確認から。

public static IEnumerable<TSource> Finally<TSource>(this IEnumerable<TSource> source, Action finallyAction);

もうわかってきた気がします。。一応使ってみましょう。

static void FinallyTest()
{
    var sequence = Enumerable.Range(1, 5);
    var finallySequence = sequence.Finally(() => Console.WriteLine("finished"));
    foreach (var item in finallySequence)
    {
        Console.WriteLine(item);
    }
}

finally
やや、やっぱり。。Doメソッドでいーぢゃんと思ってしまいますが、イヤチョットマテ。Finallyということは、、

static IEnumerable<int> FinallyTestSequenceWithException()
{
    yield return 1;
    yield return 2;
    throw new Exception("test Exception");
    yield return 3;
}
static void FinallyTest2()
{
    var sequence = FinallyTestSequenceWithException();
    var finallySequence = sequence.Finally(() => Console.WriteLine("finished"));
    try{
        foreach (var item in finallySequence){
            Console.WriteLine(item);
        }
    }
    catch (Exception e){
        Console.WriteLine(e.Message);
    }
}

finally_2

やっぱり!Doの例外発生時はonErrorが呼ばれたあとonCompletedは呼ばれませんが、Finallyは名前の通り呼ばれるようです。

それでは実装です。

Finally…シーケンスの最後に渡したActionを実行する

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static IEnumerable<TSource> Finally<TSource>(this IEnumerable<TSource> source, Action finallyAction)
        {
            if (source == null || finallyAction == null){
                throw new ArgumentNullException();
            }

            try{
                foreach (var item in source){
                    yield return item;
                }
            }
            finally{
                finallyAction();
            }
        }
    }
}

必要なのはtry-finallyステートメントなので前回のDoメソッドの時入れられなかったyield returnを入れられてます。

 

続いてFor!シグネチャはこちら。

public static IEnumerable<TResult> For<TSource, TResult>(IEnumerable<TSource> source, Func<TSource,IEnumerable<TResult>> resultSelector);

Caseにちょっとばかし似ていますがsourceがシーケンスなので、resultSelectorにsourceを渡した時のシーケンスが合体して出力されるようですね。SelectManyみたいな動きでしょうか。動作を見てみます。

static void ForTest()
{
    var selector = new[]{
        Enumerable.Range(0, 4),
        Enumerable.Range(4, 4),
        Enumerable.Range(8, 4)
    };
    var sequence = Enumerable.Range(0, 3);
    var forSequence = EmulateIxExtensions.For(sequence, i => selector[i]);
    foreach (var item in forSequence)
    {
        Console.WriteLine(item);
    }
}

for
わかりやすいですね。ではちゃちゃっと実装いきましょう。(Let’s Implement It!)

For…複数のシーケンスを入力シーケンスに基づき平坦化する

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static IEnumerable<TResult> For<TSource, TResult>(IEnumerable<TSource> source, Func<TSource,IEnumerable<TResult>> resultSelector)
        {
            if (source == null || resultSelector == null)
            {
                throw new ArgumentNullException();
            }
            foreach (var seed in source)
            {
                var subSequence = resultSelector(seed);
                foreach (var item in subSequence)
                {
                    yield return item;
                }
            }
        }
    }
}

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内なら」使えるんですけどね。

Interactive Extensionsを自分で実装する – 6.Distinct

Interactive Extensions(Ix)を自分で実装する、第6回目はDistinctです。

標準のクエリ演算子の中にもDistinctはありますが、IxにあるDistinctとは少しシグネチャが違います。
標準のクエリ演算子のDistinct:

public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source);
public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource>);

InteractiveExtensionsのDistinct:

public static IEnumerable<TSource> Distinct<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);
public static IEnumerable<TSource> Distinct<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer);

ややこしく見えますが、Ixの2つのDitinctは標準の2つのDistinctに比べてそれぞれ

Func<TSource, TKey> keySelector

が追加になっているだけです。

使用例1

static IEnumerable<int> GetDistinctSequence()
{
    yield return 1;
    yield return 4;
    yield return 2;
    yield return 4;
    yield return 1;
}
static void DistinctTest1()
{
    var sequence = GetDistinctSequence();
    var result = sequence.Distinct(a => a % 2); // 2の剰余をkeyにする
    foreach (var item in result)
    {
        Console.WriteLine(item);
    }
}

distinct_1
ソースに {1,4,2,4,1} というシーケンスを設定しました。このまま標準のDistinct(重複排除)をすると、{1,4,2}となります。 しかしここではシーケンスの剰余が一致するかで比較するようにしました。2の剰余は{1,0,0,0,1}となりますので重複排除すると1つめのアイテムと2つめのアイテムだけが残ります。 そのため出力は{1,4}となるわけです。

使用例2

static void DistinctTest2()
{
    string[] distinctTestArray = new[]{
        "tocs",
        "Tocs",
        "tOES",
        "TOCS",
        "toGs",
        "tocs"
    };
    var sequence = Enumerable.Range(0, 6);
    var distinctWithComparer = sequence.Distinct(
        a => distinctTestArray[a],                  // int => stringのFunc
        StringComparer.InvariantCultureIgnoreCase); // 比較時は大文字小文字気にしない
    foreach (var item in distinctWithComparer){
        Console.WriteLine(item);
    }
}

distinct_2
まどろっこしい例ですみません。重複評価対象にしようとしているシーケンスはdistinctTestArrayです。ただ、IxのDistinctはFunc<TSource, TKey>を取るのでムリヤリこれを使おうとすると、何かの出力がstringにならないといけないので元のシーケンスはEnumerable.Range(0,6)で{0,1,2,3,4,5}のシーケンスをひねり出しました。{0,1,2,3,4,5}のムリヤリ作ったシーケンスからFuncを介してdistinctTestArrayができます。

そのあと、これを渡したIEqualityComparerで評価するわけですが.NETに用意されているStringComparerをそのまま拝借して、大文字小文字の区別なしでの比較をさせています。そうすると、{“tocs”, “Tocs”, “toES”, “TOCS”, “toGs”, “tocs”}のうち、tocsが重複していますのでそれを排除した{“tocs”, “toES, “toGs}が残ります。ただこれはint => stringのFuncを介して作られたものなので、最終的に戻ってくるのはint値で、{0, 2, 4}となるわけです。標準クエリのDistictは使う頻度そこそこ出てきますが、IxのDistinctオーバーロードを使うのは1年に1回あるかないか・・

それでは実装してみます。(Let’s Implement It!)

Distinct…シーケンスの中から指定した一致判断方法で重複を排除したシーケンスを返す。

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static IEnumerable<TSource> Distinct<TSource, TKey>(this IEnumerable<TSource> source,
                                                                   Func<TSource, TKey> keySelector)
        {
            var comparer = EqualityComparer<TKey>.Default;
            return source.Distinct(keySelector, comparer);
        }
        public static IEnumerable<TSource> Distinct<TSource, TKey>(this IEnumerable<TSource> source,
                                                                   Func<TSource, TKey> keySelector,
                                                                   IEqualityComparer<TKey> comparer)
        {
            if (source == null || keySelector == null || comparer == null){
                throw new ArgumentNullException();
            }

            List<TKey> alreadyAppeared = new List<TKey>();
            foreach (var item in source)
            {
                bool exist = false;
                TKey thisKey = keySelector(item);
                foreach (var savedKey in alreadyAppeared){
                    if (comparer.Equals(thisKey, savedKey)){
                        exist = true;
                        break;
                    }
                }
                if (!exist){
                    alreadyAppeared.Add(thisKey);
                    yield return item;
                }
            }
        }
    }
}