LINQ好きはLINQを書く時にどう考えているのか?

  • 34
    いいね
  • 6
    コメント

LINQ好きですか?

最近ではLINQが書けるプログラマは大分多くなってきたと思いますが、一方でLINQに対する温度は人それぞれだと思います。「LINQは読めるけど、自分で書くとあまりLINQが出てこない」人もいれば「LINQでできることは全部LINQにしたい」人もいるでしょう。
ちなみに私は後者です。

もちろんLINQが常に絶対正義という訳でもないでしょうが、同僚がLINQを使いまくっているけど、読みにくい、慣れないと思っているですとか、LINQを使いこなしたいと思っているけど中々実戦でぱっと出てこない、という悩みがある方もいらっしゃるのではないかと思い、そうした方の一助になればと思い、この記事を書いてみました。

何でもLINQでやりたい族の人がなぜLINQにしたがるのか、どういう風に考えてLINQを組み立てているのかをサンプルコードを元に文章化してみます。

お題

以下のお題をLINQを使わない昔ながらの実装と、LINQを使った実装で書いてみます。

パス「C:\temp」の子フォルダの中のファイルのうち、拡張子「txt」と「log」ものを全て列挙する

実装例

LINQを使わないパターンでよく見かけるのが、以下のような実装です。

var extensions = new[] { ".txt", ".log" };
var files = new List<string>();

foreach (var directory in Directory.GetDirectories(@"C:\temp"))
{
    foreach (var file in Directory.GetFiles(directory))
    {
        if (extensions.Contains(Path.GetExtension(file)))
            files.Add(file);
    }
}

まず「C:\temp」直下のフォルダをforeachで列挙して、そのフォルダでGetFilesしたファイルをforeachで列挙。拡張子が関係ないものはif文で弾く、という感じ。
foreachがネストするので、ちょっとインデントが深くなるところがイヤな感じです。
一方で、LINQを使った例としては、以下のような実装があります。

var extensions = new[] { ".txt", ".log" };

var files = Directory.EnumerateDirectories(@"C:\temp")
                     .SelectMany(d => Directory.EnumerateFiles(d))
                     .Where(f => extensions.Contains(Path.GetExtension(f)))
                     .ToArray();

LINQ使いならGetFilesじゃなくてEnumerateFilesですよね(?)
foreachを使う例に比べると、かなりあっさりした感じの実装になります。文的には2行だけ。
(extensionsは配列じゃなくてHashSetの方が役割とか考えるといい気もしますが、本筋と離れるのでとりえずこれで)

で、LINQが書きたいのに上手く書けないという人は、たぶん頭の中には1番目の実装が浮かんでいる状態なのだと思います。すでに1番目の実装が頭に浮かんでいるのに、それを2番目の実装に「翻訳」しようとすると、結構ストレスになるのではないかなと思います。わざわざ書き直しているような感じになるので。

人によるかもしれませんが、私の場合、コードを書いている時に1番目の実装というのはほとんど意識に浮かんでいなくて、いきなり2番目の実装で書いている感じです。どういう思考プロセスで2番目の実装が組み立てされるのかを文章にしてみます。

LINQで書く時の思考プロセス

問題文の一番左をコードに書き始める

問題文を左から読んだ順番にコードにしていきます。まずは問題文の『パス「C:\temp」の子フォルダ』というところを、以下のコードで表現してみます。

Directory.EnumerateDirectories(@"C:\temp") // 戻り値 IEnumerable<string> (フォルダのリスト)

後ろにとりあえずSelectを続けてみる

当然ながらEnumerateDirectoriesだけだと本来ほしいファイルのデータにならないので、とりあえず後ろにSelectを続けてみます。割と「とりあえずSelectして様子見る」的な感じがあり、Selectしてラムダ式に渡ってきた変数の型を見ながら、続きがどうなればいいのかを考えたりしています。
さて、問題文の続きは『パス「C:\temp」の子フォルダの中のファイルのうち』なので、Selectした先でファイル列挙してみます。

Directory.EnumerateDirectories(@"C:\temp")
         .Select(d => Directory.EnumerateFiles(d)) // 戻り値 IEnumerable<IEnumerable<string>> (フォルダごとのファイルリストのリスト)

ここで、IntelliSenseなどで戻り値をチェックすると、EnumerateFilesの戻り値がIEnumerable(というか複数)なので、最終的な戻り値はIEnumerable<IEnumerable<string>>になっていることが分かります。しかし、最終的にほしいのは「リストのリスト」ではなくただのリストなので、「あー、Selectじゃないな」と考え直します。大抵の場合、とりあえずSelectで書いて進めて、Selectした先が1対1ならそのままにし、予期せず1対NになったらSelectManyに替えてやり直す、みたいな感じのプロセスでやっています。

SelectManyに替えてみる

という訳で、SelectだったところをSelectManyに替えてみます。

Directory.EnumerateDirectories(@"C:\temp")
         .SelectMany(d => Directory.EnumerateFiles(d)) // 戻り値 IEnumerable<string> (ファイルのリスト)

戻り値がIEnumerableになったので、ほしい結果の型と一致していていい感じな気がします!

絞り込み条件を加える

上の状態になると、全部のファイルを列挙することはできているので、後は絞り込みを追加するだけです。残るお題の部分『パス「C:\temp」の子フォルダの中のファイルのうち、拡張子「txt」と「log」ものを全て列挙する』を実現するため、Whereでいらないデータを弾きます。

Directory.EnumerateDirectories(@"C:\temp")
         .SelectMany(d => Directory.EnumerateFiles(d))
         .Where(f => new[] { ".txt", ".log" }.Contains(Path.GetExtension(f)))

これで概ね完成です。
後は、拡張子の配列を毎回作るのが無駄なのでLINQの上に持ってきたり、ToArrayして結果を変数で受けたり、とすれば回答例の状態になります。

まとめ:LINQは思考とコードの流れが一致する

私がLINQが好きな理由の1つが、LINQを使うと問題文を左から右に素直にコードにしていけば実現できることが多いためです。LINQを使わない場合foreachのネストになるので、試行錯誤の過程でコードが上に行ったり下に行ったり、結構広い範囲を移動しないといけなくなることがあります。LINQの場合は、比較的狭い範囲の行き来で済むことが多く、コードを書くことに集中できる感じがします。また、変数の間違いが少ないのも集中が削がれにくい要素です。
LINQを使わない場合、間違えてリストに積むのをfileではなくdirectoryにしても(さすがにそんな間違いはあまりしないでしょうが、例えばの話として)、コンパイル時にエラーで教えてくれることはありません。
LINQの場合、EnumerateDirectoriesで列挙された「d」とEnumerateFilesで列挙された「f」はスコープが違うので、間違って使うことはありません。
(だからこそ変数名を「d」のように短くしてもリスクが少ないです)
変数間違いを気にしなくていいのは、やりたいことに集中ができて気持ちがいいです。

最後に

コードを書く時の思考を文章化するというのは初めてやったので、読んでくださった方の参考になったか分からないですが・・・
LINQを使いこなしたいと思っている方の役に、少しでも立てれば幸いです。