読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

LINQにオレオレ機能を追加

LINQは便利です。LINQが使える環境なら率先して使いたいところです。LINQに出会ってからは、LINQに依存し過ぎてLINQがないと生きられない体になってしまいました。LINQさん、愛してます...///

と、くだらない前フリはさておき、LINQを使っていて思うことは「あってほしいメソッドが (稀に) ない」ということです。その多くはInteractive Extensions (Ix)を利用すれば何とかなるのですが、それでも他にも「あるといいのになー」と思うものはあります。それにIxはまだExperimental (実験版) リリースで、仕事で採用するにはそれだけでお許しが...。ということで、あると地味ぃに便利そうなものをいくつか紹介します。

ForEachメソッド

言わずと知れたForEachメソッドLINQやったことある方なら、一度は作ったことあるんじゃないでしょうか。

public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
    foreach (var item in source)
        action(item);
}

「なんでお前は標準搭載じゃないんだ!(クワッ」と誰もが思うところでしょうが、C#コンパイラチームのEricさんのブログ記事によると、「副作用を避けるため」とか言う理由で搭載が見送られているようです。「じゃあなんでIxには搭載したんだよ」などと言いたくもなりますが、ないものは仕方ないので、Ixが使えない場合はサクッと作りましょう。

Convertメソッド

先日@shibayanこんなクイズを出してました。標準のLINQを使うのであればこれより短い書き方はないと思われますが、CastしてからSelectって比較的あるケースだと思うのに1メソッドで書けません。ということで、作ったのがこちら。

public static IEnumerable<T> Convert<T>(this IEnumerable source, Func<object, T> converter)
{
    foreach (var item in source)
        yield return converter(item);
}

まさにCastとSelectのコンビネーション。メソッド名をCastにするかSelectにするか悩んだのですが、WhereとCastを合体させた感のある機能にOfTypeメソッドというのがあるので、「じゃあこっちも変えましょうか」とConvertにしました。

これがあれば、前出のクイズはもう少し文字数少なく書けますね。

Concatメソッド

稀にありますよね、単一要素だけを繋げたいとき。そんなときは、例えばこう書きます。

var query = Enumerable.Range(0, 3).Concat(new []{ 3 });

Concatの引数がIEnumerable<T>しか受け付けないので、単一要素なのを配列に変えたりして対応します。が、「そもそも可変長引数に対応していれば問題ない」という考えを持ち出して作ったのがこちら。

public static IEnumerable<T> Concat<T>(this IEnumerable<T> first, params T[] second)
{
    //--- first.Concat(second)って書くとStackOverflowException!!
    return Enumerable.Concat(first, second);
}

これで少し気持ち良くなれる気がします。

Chunkメソッド

「要素をいくつか毎に纏めたい」という要望も稀にあると思います。そういった場合はコレを使いましょう。実装自体もLINQを使うと簡単にできちゃって素晴らしいですね。

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int count)
{
    var result = new List<T>(count);
    foreach (var item in source)
    {
        result.Add(item);
        if (result.Count == count)
        {
            yield return result;
            result = new List<T>(count);
        }
    }
    if (result.Count != 0)
        yield return result.ToArray();

    //--- 何度も回って効率が悪いという@neuecc様のご指摘により廃版
    /*
    while (source.Any())
    {
        yield return source.Take(count);
        source = source.Skip(count);
    }
    */
}

あと、IxのBufferというメソッドが同じ機能のようなので、メソッド名をChunkでなくてBufferというようにした方がいいというご指摘もありました。その通りですね!

Indexedメソッド

意識の高いLINQ星人たちは「for文を使ったら負け」みたいな気持ちになると聞きます。僕はちっともLINQ星人じゃないですが、そう思ってしまうことはあります。でも、「for文は書きたくないけど要素のインデックスは欲しい」などというワガママもあり、そういうときはインデックス付きで要素を返すSelect文を使います。例えばこんな感じ。

source
.Select((x, i) => new { Element = x, Index = i })
.DoSometing(...);

特に難しくはないですが、地味にめんどくさい。ということで、インデックス付きの型に変換してしまえ作戦。

//--- 要素とインデックスを格納するクラス
public class IndexedItem<T>
{
    public T Element{ get; private set; }
    public int Index{ get; private set; }
    public IndexedItem(T element, int index)
    {
        this.Element = element;
        this.Index   = index;
    }
}

//--- クラス生成をラップ
public static IEnumerable<IndexedItem<T>> Indexed<T>(this IEnumerable<T> source)
{
    return source.Select((x, i) => new IndexedItem<T>(x, i));
}

これで簡単にインデックス付き要素を取得できます。めんどくさい定型句とはお別れです。

使用例

今回の紹介したメソッドを無理やり (?) 全部使ってみると、以下のような感じになります。地味ぃに楽になった気がしますね。

(Enumerable.Range(1, 7) as IEnumerable)  //--- わざとIEnumerableに変換
.Convert(x => x.ToString() + "日目")     //--- 一気に文字列に変換
.Concat("I", "Love", "LINQ")             //--- 可変長引数の指定OK
.Chunk(3)      //--- 3要素ずつまとめる
.Indexed()     //--- インデックス付加
.ForEach(x =>  //--- foreachをメソッドチェインで記述
{
    Console.WriteLine("Index: {0}", x.Index);
    x.Element.ForEach(y => Console.WriteLine("\\t{0}", y));
});

/*
Index: 0
        1日目
        2日目
        3日目
Index: 1
        4日目
        5日目
        6日目
Index: 2
        7日目
        I
        Love
Index: 3
        LINQ
*/

何よりもIxの正式版の公開や.NET標準への追加搭載があるのが嬉しいですが、それ以外の独自機能もこのようにちょっとずつ作って行くと、LINQがますます便利になってもっと素敵なコーディングができることと思います。LINQで使えるメソッドをTipsとして集めたサイトでもオープンしないかなー。そしたらみんなHappyになれる予感!

Enjoy LINQ more and more!!