57
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-08-10

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を使いこなしたいと思っている方の役に、少しでも立てれば幸いです。

57
55
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?