LINQは本当に強力だ (3) 拡張メソッドによる拡張

ToHashSet()とか、ToSortedList()とか、作ってみただろうか? :-)

もう一つ小ネタを行ってみよう。

複数の文字列を連結出来たら良いのにと思う事がある。ハードコードするなら、+演算子で繋げれば良いのだが、配列や列挙子だとこうは行かない。で、レベルの低い現場でよく見るのが、forで回しながら+演算子で連結という、眩暈のするコードだ(しかもStringBuilderも使っていない)。

もちろん、拡張メソッドを定義してみる。
StringBuilderを使ってもよい(その方がありがたみがあるかも?)が、忘れがちだが、System.StringにConcat()というメソッドがあり、簡単に実現出来る。

string[] words = new string[] { "ABC", "DEFGH", "IJK" };
string concatted = string.Concat(words);

これが忘れやすい理由の一つは、やはりStringクラスのスタティックメソッドであることではないだろうか。
このぐらい、えいやっと…

public static string Concat(this IEnumerable<string> words)
{
    return string.Concat(words);
}

もちろん、拡張メソッドのクラスが含まれる名前空間が using されていなければならない。例えば、プロジェクトで共通で使用するクラスライブラリに、LINQ向け拡張メソッドを含む名前空間を決めておくというのはどうだろうか?
他にも、文字群を連結して文字列にするというのも考えられる。

public static string Concat(this IEnumerable<char> chars)
{
    return new string(chars)
}

最初の例で示した文字列の連結は、たとえばカンマ区切りで取得したい場合もあるだろう。

public static string Concat(this IEnumerable<string> words, string separator)
{
    return string.Join(separator, words);
}

小ネタばっかりだが、そろそろまとめておく。

  • 列挙子を受け取って何かをするメソッドなら、拡張メソッドを使って定義しておくと、LINQクエリで使いまわしやすい。
  • しかし、同じ名前のオーバーロードを沢山定義すると、ニアミスが発生しやすい事は押さえておく必要がある。

例で挙げたメソッドは、全て”Concat”メソッドで、引数が異なるのでオーバーロードとして成立する(LINQのConcat含めて)。しかし、これが原因で使う際にどのオーバーロードを使用すべきか迷うことがある。

私は、”Contains”メソッドをいくつか定義してみたことがある。LINQのContainsは、追加引数で指定された値が列挙値内に含まれているかどうかをテストする。ここに、System.String.IndexOf()とよく似たContainsを定義してみた。また、列挙値が追加引数群の何れかに一致するかどうかをテストするContainsも作ってみた。

そうすると、当然のことながら、Containsのどの実装が欲しい機能を持っているのか、良く分からなくなってしまう。実際、インテリセンスはこれらのContainsを全て掲示してくれるが、やはりいまいちピンとこない。

問題は、似て異なる機能を持ったメソッドを、同じ名前で定義している事だろう。古典的な問題だが、拡張メソッドについても同じ事が発生するので注意した方が良い。”Concat”も、”ConcatWords”とか、”ConcatToString”のような、具体的な名前を使った方が良いこともある。

この問題はどのように考えれば良いだろうか。
LINQでは、拡張メソッドがあらゆる列挙子で使われることを想定している。例えばConcatの定義、

IEnumerable<T> Concat<T>(this IEnumerable<T> lhs, IEnumerable<T> rhs);

というのは、この拡張メソッドだけでT型列挙子の全てに応用が可能だ。つまり、このConcatだけで様々な用途に使用出来る。それだけ既に抽象化されているわけで、これと似た、しかし異なるメソッドを定義しなければならないとすれば、そもそも元のConcatと用途が異なるかも知れない、という事に注意を払えば良いと思う。


さて、LINQ向けに上記のような「部品」を作っておく事の良さを、もうちょっと別の面で示そうと思う。

今までの例は、拡張メソッド内で、直前の列挙子を評価してしまう。ToSortedDictionaryの場合、メソッド内でforeachで回した時点で列挙を開始する。目的がSortedDictionaryに詰め替えることだからこれで良いのだが、LINQ標準の拡張メソッド(たとえばWhereやOrderBy)のように、実際に列挙されるまで動作を遅延させ、列挙しながら処理を行うためにはどうすれば良いだろうか?

.NET 1.0/1.1では、IEnumerableインターフェイスを実装後、GetEnumerator()メソッドを書かなければならなかった。GetEnumerator()は、列挙子の実体処理を行うクラスのインスタンスを返す。これはIEnumeratorインターフェイスを実装していれば、非公開でも構わない。しかし、IEnumeratorはステートマシンの実装を強要するため、はっきりってこれを書くのは面倒だ。

.NET 2.0になって、画期的な構文「yield」が使えるようになった。詳細は省くが、つまりこれを使って実装すればGetEnumerator()の実装は難しくない。

例えば、何もしない拡張メソッドNop()を実装してみる。

public static IEnumerable<T> Nop<T>(this IEnumerable<T> enumerable)
{
    // 内部的な列挙子を返す
    return new NopEnumerable(enumerable);
}

// T型の何もしない列挙子
private sealed class NopEnumerable<T>
{
    // 元の列挙子
    private readonly IEnumerable<T> enumerable_;

    // コンストラクタ
    public NopEnumerable(IEnumerable<T> enumerable)
    {
        // 保存しておく
        enumerable_ = enumerable;
    }

    // 列挙を実行する
    public IEnumerator<T> GetEnumerator()
    {
        foreach (T value in enumerable_)
        {
            // yield構文を使って、要素を返す(ここでは何もしないでそのまま返す)
            yield return value;
        }
    }

    // 非ジェネリック
    IEnumerator IEnumerable.GetEnumerator()
    {
        // バイパス
        return GetEnumerator();
    }
}

NopEnumerable<T>クラスは、GetEnumerator()が呼び出されるまでは、元の列挙子を一切操作しない。

GetEnumerator()が呼び出されると、最初のforeachで初めて列挙子のGetEnumerator(enumerable.GetEnumerator())が呼び出される。ここで、元の列挙子も動作を開始するわけだ。
その後、foreachで得られた値を、yield return構文で一つずつ返却する。このメソッドのソースコードとバイナリコードはまったく異なり、記述どおりに振る舞うステートマシンとしてコンパイルされる。そのお蔭で、GetEnumeratorの実装は非常に簡単になっている。

このコードをスケルトンとして、GetEnumerator()の実装を肉付けすれば良いだろう。一例として、二重の構造を持った列挙子を、一重の列挙子(つまり普通の列挙子)に変換するメソッドを定義する。

// 二重の列挙子を順番に連結し、一重の列挙子に変換する
public static IEnumerable<T> Unloop<T>(this IEnumerable<IEnumerable<T>> enumerable)
{
    return new UnloopEnumerable(enumerable);
}

private sealed class UnloopEnumerable<T>
{
    private readonly IEnumerable<IEnumerable<T>> enumerable_;

    // コンストラクタ
    public UnloopEnumerable(IEnumerable<IEnumerable<T>> enumerable)
    {
        enumerable_ = enumerable;
    }

    // 列挙を実行する
    public IEnumerator<T> GetEnumerator()
    {
        foreach (IEnumerable<T> outer in enumerable_)
        {
            foreach (T inner in outer)
            {
                yield return inner;
            }
        }
    }

    // 非ジェネリック
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

#ひょっとして、LINQ to Objectに既にあるかも? 折を見て、MSDNライブラリにも目を通すと良い。
#思いつくようなメソッドは標準で提供されている事がある。
#なお、LINQ to Objectの拡張メソッドは、Enumerableクラスに定義されている。

“IEnumerable<IEnumerable<T>>”という型を見て、「えっ、これどういう構造?」と一瞬考えたことは無いだろうか?このUnloopを使えば、ループ構造を簡略化出来る。しかも、このメソッドは実際に列挙されるまで動作が遅延され、中間バッファは全く必要ない。LINQにはぴったりのメソッドだ。

しかしながら、この例で言えば、実はLINQクエリだけでも解決できる。

IEnumerable<T> results =
    from outer in enumerable
    from inner in outer
    select inner;

#対応する拡張メソッドは「SelectMany」だ。

selectの書き方次第では、outerとinnerを使った演算結果を返すとか、応用性も高い。Unloopは単純な例なので仕方がないが、逆に言えば、まずLINQクエリだけで解決出来るか考えて、それから拡張メソッドを実装する、という手順にした方が良いかもしれない。もちろん、メソッドを作れば文字通り「何でもアリ」となるので、究極的にはメソッド実装すれば、難しい問題も切り抜けられるはず。

この辺り、SQL Serverで、まずクエリだけでどうやって書けるか、クエリの書き方次第でどのように高速化出来るか、それがダメなら部分的にユーザー定義関数に逃がすか、と考えるのと同じ方法論であって、実際頭の中では同じ部位が働いている感じがする。
#横に逸れるけど、上記のクエリにAsParalell()とかやりたくなるよね? 迷っているなら、その事も考えておこう :-)
つづく

投稿者:

kekyo

A strawberry red slime mold. Likes metaprogramming. MA. Bicycle rider. http://amzn.to/1SeuUwD