LoginSignup
152
116

More than 3 years have passed since last update.

LINQ とっかかりと自分が書く時に考えていることと注意点

Last updated at Posted at 2020-07-05

LINQ っていいですよね。2007 年に登場してるので、13 年前の技術です。枯れに枯れてる技術です。

LINQ to XXXX という感じで色々なものに LINQ 出来たりしますが、今回は LINQ to Objects についてのみ書きます。配列やリストに対してやる LINQ のことです。

今回のコードは全て Try .NET にコピペして試せる感じのコードを書くつもりです。

ということで徒然なるままに書いて行きます!

LINQ の前に

LINQ に入る前に以下のコードを見てください。

var array = new[] { 1, 2, 3, 4, 5 };
foreach (var x in array)
{
    if (x % 2 == 0)
    {
        Console.WriteLine(x);
    }
}

説明するまでもないとは思いますが 1 〜 5 までの数字の入った配列から偶数だけ抜き出してから表示しています

これはデータを加工するという処理(偶数のみ抜き出す)と、表示するという処理が 1 つのループの中に入っています。混ざってる。

今回の例では上のようなコードで十分で、これから書くコードは過剰な分離にはなるのですが、理想的には以下のように処理と表示はわけたほうがいいです。

var array = new[] { 1, 2, 3, 4, 5 };

// 偶数のみにフィルタリング
var evenNumbers = new List<int>();
foreach (var x in array)
{
    if (x % 2 == 0)
    {
        evenNumbers.Add(x);
    }
}

// 表示
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

何度も言いますが最初のコードと比べると性能面でも悪い(2回ループしてる)し、今回のようなシンプルな例で処理と表示を分離したおかげでコード量も増えてしまいました。

でも、実際のそこそこの大きさのコードを書くときは、上記のようになるべくデータに対する処理と、その処理結果をどのように表示するのかといった部分は分離するのがいいです。

この点は、頭に入れておいてください。

LINQ とは

配列やリストに対してフィルターや射影(変換)や集計やグルーピングなどを簡単に行えるようにしてくれる便利なメソッドの集まりです。

これを覚えるだけで、プログラムを書く上でかなりの部分を占める配列やリストに対して何かするという処理が非常にシンプルに書けるようになります。

例えばフィルタリングは Where というメソッドを使って書くことができます。

var array = new[] { 1, 2, 3, 4, 5 };

// 偶数のみにフィルタリング
var evenNumbers = array.Where(x => x % 2 == 0);

// 表示
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

Where が配列やリストから条件に合致するデータだけにフィルタリングしてくれるメソッドになります。Where を知っている人から見ると

// foreach
var evenNumbers = new List<int>();
foreach (var x in array)
{
    if (x % 2 == 0)
    {
        evenNumbers.Add(x);
    }
}

// Where
var evenNumbers = array.Where(x => x % 2 == 0);

上と下のどっちが読みやすいか?というのは個人的に重要なところだと思ってます。
LINQ のメソッド数は決して少なくはないけどよく使うものは、そんなに多くありません。ぶっちゃけ WhereSelect を覚えてるだけでも、かなりイケます。

ということで LINQ とは「配列やリストに対して皆が知ってる共通言語で簡単に、わかりやすくデータの加工処理などを行えるようになるもの」ということになります。個人的には LINQ で書いた方がシンプルそうなものを、あえて foreach などで書いてると「ここは LINQ が使えないような理由があるのか??何か元になるデータが特殊なのか?どういう意図があるんだろう?」と深読みをしてしまいます。

みんな LINQ を覚えて、素直な LINQ はバンバン書こう!!というか書いてください、お願いします。

フィルタリングと射影(変換)

さて、先ほど Where については説明しました。データのフィルタリングに使います。
次に射影です。射影ってなんでこんな難しい言い方が使われるのかわかりませんが変換と思ってもらえればいいです。変換には Select というメソッドを使います。

例えば先ほどの偶数のみにフィルタリングした結果に対して 2 乗するといったような変換処理を追加したい場合は Where の結果に対して Select を呼び出してやります。


var array = new[] { 1, 2, 3, 4, 5 };

var evenNumbers = array
    .Where(x => x % 2 == 0) // 偶数のみにして
    .Select(x => x * x); // 2 乗する

// 表示
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

結果は 4 と 16 が表示されます。
さて、LINQ を使うことで自然とデータのフィルタリングや加工と、それの表示が分離されるような書き方になりますね。素晴らしい。

効率悪いんじゃないの??

さて、この WhereSelect ですが普通に内部の実装をしようと考えると Where メソッドの中で foreach して、さらに Select メソッドの中で foreach して…と言ったようになると思います。
なので WhereSelect を連打するとパフォーマンスが最高に劣化するのではないか?という気持ちになりますが、そこら辺は WhereSelect を呼ぶだけでは実は処理が行われてなくて、こう言ったフィルタリングや変換をやるという予定だけが登録された状態になります。

そして、それらは実際に本当に必要になったタイミングで実行されます。今回の例では foreach ループで表示しているところまで実際の処理が行われません。

それがわかるように以下のようにコードを書き換えてみましょう。


var array = new[] { 1, 2, 3, 4, 5 };

var evenNumbers = array
    .Where(x => 
    {
        Console.WriteLine($"{x} についてフィルタリングしています");
        return x % 2 == 0;
    }) // 偶数のみにして
    .Select(x => 
    {
        Console.WriteLine($"{x} について変換しています");
        return x * x;
    }); // 2 乗する

// 表示
Console.WriteLine("結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

WhereSelect 内部でログを出すようにしました。また結果を表示する foreach ループの前でもログを出すようにしました。順当に行くと結果を表示します!!の前に WhereSelect 内のログが出て欲しいところですが、先ほど説明したように実際に必要になるまで処理が行われないので、以下のような結果になります。


結果を表示します!!
1 についてフィルタリングしています
2 についてフィルタリングしています
2 について変換しています
4
3 についてフィルタリングしています
4 についてフィルタリングしています
4 について変換しています
16

結果を表示します!!の後にフィルタリングと変換処理が行われているのがわかります。しかも foreach の前にフィルタリングと変換が一気に行われるのではなく、データを表示しつつ本当に必要になる直前まで処理が保留されていることがわかります。

なので、LINQ を使うと最初に処理を分離しようぜ!!って言って foreach でフィルタリングした時の処理よりも効率がいいということになりますね。foreach でのフィルタリングは 2 回ループが走ったのに、LINQ を使うと 1 回だけのループになるので。最高

注意すること

さて、LINQ のメソッドは本当に必要になるまで処理が行われないと言いましたが、注意すべき点があります。それは場合によっては、この動作のせいで予期せぬエラーになったりパフォーマンスの問題にぶち当たることがあるという点です。

例えば、複数回 LINQ の結果に対してループを行うと、都度都度全部の処理が走ります。見てみましょう。以下のように 2 回結果を表示するようにコードを書き換えました。


var array = new[] { 1, 2, 3, 4, 5 };

var evenNumbers = array
    .Where(x => 
    {
        Console.WriteLine($"{x} についてフィルタリングしています");
        return x % 2 == 0;
    }) // 偶数のみにして
    .Select(x => 
    {
        Console.WriteLine($"{x} について変換しています");
        return x * x;
    }); // 2 乗する

// 表示
Console.WriteLine("結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

Console.WriteLine("再度結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

結果を表示します!!
1 についてフィルタリングしています
2 についてフィルタリングしています
2 について変換しています
4
3 についてフィルタリングしています
4 についてフィルタリングしています
4 について変換しています
16
5 についてフィルタリングしています
再度結果を表示します!!
1 についてフィルタリングしています
2 についてフィルタリングしています
2 について変換しています
4
3 についてフィルタリングしています
4 についてフィルタリングしています
4 について変換しています
16
5 についてフィルタリングしています

今回のようなデータ量で、こんな軽い処理ならあまり問題になりませんが、データの量とフィルタとか変換処理が重たい場合は、この処理を遅らせるというのが原因で無駄に何回も処理を行うような問題が起きます。気をつけてね。

解決方法としては、LINQ の結果に対して ToArray などを呼び出して明示的に配列に変換をしてしまい、配列に対して foreach をするとフィルタリングや変換処理は foreach では走らなくなります。


var array = new[] { 1, 2, 3, 4, 5 };

var evenNumbers = array
    .Where(x => 
    {
        Console.WriteLine($"{x} についてフィルタリングしています");
        return x % 2 == 0;
    }) // 偶数のみにして
    .Select(x => 
    {
        Console.WriteLine($"{x} について変換しています");
        return x * x;
    }) // 2 乗する
    .ToArray(); // ここで明示的に配列にすることで LINQ の処理をやってしまう

// 表示
Console.WriteLine("結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

Console.WriteLine("再度結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

1 についてフィルタリングしています
2 についてフィルタリングしています
2 について変換しています
3 についてフィルタリングしています
4 についてフィルタリングしています
4 について変換しています
5 についてフィルタリングしています
結果を表示します!!
4
16
再度結果を表示します!!
4
16

配列にするところで余分に一回ループが走ってしまいますが LINQ の処理を再度やるのと、配列を再利用するのでどっちが早いかというトレードオフになります。
例えば、10000 件のデータから 3 件だけとってくるような Where があると考えます。これを後で何回かループで処理するような場合は ToArray をして配列にしてしまったほうが、毎回 10000 ループするのではなく 3 回のループになるので効率がいいですよね。
そのほかに、Select での変換処理でちょっと重ための計算とかが入ってる場合なんかも ToArray などで一回処理した結果を保持しておいたほうが、毎回計算処理が走るより早くなったりします。ケースバイケースですが、このようなことがあるということを頭に入れておいてください。きっと LINQ を使っていてパフォーマンスの問題にぶちあたったときに助けになってくれると思います。

あとは LINQ の元になるデータが途中で書き換わった時とかは思いもよらない結果になることもあります。


var list = new List<int>(new[] { 1, 2, 3, 4, 5 });

var evenNumbers = list
    .Where(x => x % 2 == 0) // 偶数のみにして
    .Select(x => x * x); // 2 乗する

// 表示
Console.WriteLine("結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

// 元データかわちゃった
list.Add(100);
list.Add(9999);
list.Add(1000);

Console.WriteLine("再度結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

最初の結果表示と2回目の結果表示の間でリストのデータが変わっています。LINQ は、その都度処理をやるので2回目のループでは結果が最初と変わります。

結果を表示します!!
4
16
再度結果を表示します!!
4
16
10000
1000000

今回のようにわかりやすい時はいいのですが、これがデータの元を管理しているクラスと結果を表示するクラスが離れていると、気づくのに時間がかかるので気をつけましょう。

因みに、LINQ のメソッド全てがこのような動きをするわけではなく、処理を遅らせることが可能なものだけがこのような動きをします。例えば Aggregate という集計処理を行う関数はその場で実行されます。集計結果を返すには全部舐めないといけないですからね。

まとめ

この記事で言いたかったこと。

  1. データに対する加工などの処理と表示は分離して書こう。ごちゃごちゃして大変になるので
  2. データに対する加工などの処理は LINQ で書こう。多くの場合スッキリ書けるし、LINQ を知ってる人の中ではループでやるよりわかりやすくなります。
  3. LINQ で書いた処理が実行されるのは本当に実行しないといけなくなったタイミングで行われる。一般的には性能面で有利になるんだけど、場合によっては性能劣化やわかりにくいバグに繋がることもあるので気をつけよう。

LINQ には沢山のメソッドがあります。これらは覚えるしかないので一度くらいメソッドのリストを眺めたりドキュメントをさらっとみておくことをお勧めします。

追記:クエリ構文について

LINQ には、ここまで説明してきたメソッド呼び出しの形式で書けるメソッド構文の他に以下のように SQL 文っぽく書けるクエリ構文があります。

var evenNumbers = from x in array where x % 2 == 0 select x;

これについては、存在について知ってるだけでいいと思います。
クエリ構文では使えない LINQ のメソッドもあるので、それなら最初からメソッド構文で書いたほうがいいなぁと個人的には思ってます。(個人の感想)

152
116
0

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
152
116