【LINQ】Whereを使ってガード節continueをコードから消す

C#

こんにちは、働くC#プログラマーのさんさめです。

LINQの中でも基本中の基本、
Whereの紹介です。

Whereは、
「LINQを紹介する時に必ずセットで説明される」
と言っても過言ではないくらいの
重要メソッドです。

実際に使用するケースも当然多くなります。

しかし、LINQに慣れないうちは
「存在は知ってるけど、
あんまり使えてないなぁ」
という方もいるのではないでしょうか。

本記事では、Whereでできることを説明しつつ、
実際にどんな処理を書くときに、
Whereが有効なのかについて紹介します。

スポンサーリンク

Whereの概要

Whereメソッドはリストの中から、
指定した条件を満たすもののみを抽出します。

var intArray = new[] { 1, 2, 3, 4, 5 };
// 2で割り切れるもののみ抽出
var whereEnumerable = intArray.Where(x => x % 2 == 0);
// 2,4が該当要素なので「2」と出力される
Console.WriteLine(whereEnumerable.Count());

1つも条件を満たさなかった場合は、
要素数ゼロの列挙が返されます。

var intArray = new[] { 1, 2, 3, 4, 5 };
// 7で割り切れるもののみ抽出(※1つもない)
var whereEnumerable = intArray.Where(x => x % 7 == 0);
// 該当要素ゼロなので「0」と出力される
Console.WriteLine(whereEnumerable.Count());

うっかり要素数がゼロになった列挙にFirstとか呼ぶと
即例外で落ちるので本当に気を付けましょう。
(詳しくは
【LINQ】Firstは使ってはいけない!FirstOrDefaultを使おう
をご覧ください)

Whereの主な使い道はガード節的なcontinueを減らすこと

さて、Whereを使う場面とはどんな時でしょうか。

大雑把に言うならば、

「continue文」を使っている箇所全て

と言えます。

さんさめ
さんさめ

意図的にcontinueを使うときもあるけどね

具体的に見ていきましょう。

通常、リストの各要素に何らかの操作をするときは、
foreachで反復処理を記述しますね。

そんな時、
「リストの要素にnullが入っていた場合、
例外になるかも」
という状況があったとします。

例えば以下のコードでは、
要素をToString()して、
その戻り値をコンソール出力しようとしています。

foreach (var item in arrray)
{
    // itemがnullだと例外になってしまう
    Console.WriteLine(item.ToString());
}

当然ながら、要素変数itemがnullだった場合、
NullReferenceExceptionとなってしまいますね。

これはあかんですね。
nullチェックして弾いとかないと

そうですね、事前にnullチェックを行いましょう。
チェックを入れたものが以下です。

foreach (var item in array)
{
    if (item == null) { continue; }
    // これで安心
    Console.WriteLine(item.ToString());
}

nullだったらさっさと次に行くようにした。
満足~

これは、一般的なcontinue文を用いたガード節です。
CやJavaからC#に移った人は、
特に違和感なく「これでOK」となるのではないでしょうか。

ところが、こういったガード節的な記述が、
最もWhereに置き換えやすい箇所なのです。

えっ…?そうなの?

はい、実際にWhereを使うと以下のように書き直せます。
foreach の in の後に注目してください。

foreach (var item in array.Where(x => x != null))
{
    // ここまで来るのはnullじゃない要素のみ
    Console.WriteLine(item.ToString());
}

args.Where(x => x != null)
と、foreachに渡すリストそのものを、
Whereで加工しています。

つまり、
「要素がnullじゃないもののみ、
foreachで列挙せよ」
という記述になったわけです。

おー!
なんかスマートだね!

このように、
Whereを使い慣れてない場合は、
ガード節を減らすという使い方から
試してみると良いでしょう。

Where内で副作用のある処理を書くのはやめよう

さて、Whereの基本的な使い方を見てもらった段階で、
1つ気を付けてもらいたいことがあります。

それは、
「Whereに渡す処理の中では、
副作用を発生させないようにする」
ということです。

ここでいう副作用とは、
「本来条件のみを書くべき個所のはずなのに、
要素の状態などに変更を及ぼしてしまうこと」
を指します。

ちょっと分かりにくいかもしれませんね。

では、以下のコードを見て下さい。

var nullNum = 0;
var array = new int?[] { 0, null, 2, null, 4 };
var whereEnumerable = array.Where(x =>
{
    // nullじゃないのもののみ通しつつnullの数を数える
    if (x == null) { nullNum++; }
    return x != null;
});
foreach (var item in whereEnumerable)
{
    Console.WriteLine(item.ToString());
}
// "nullの数は2個"と表示される
Console.WriteLine($"nullの数は{nullNum.ToString()}個");

リスト中のnullの数を数えて、
外部の変数に記憶しています。

さて、このリストを別の個所でもう一度使いたくなったとしましょう。

var nullNum = 0;
var array = new int?[] { 0, null, 2, null, 4 };
var whereEnumerable = array.Where(x =>
{
    // nullじゃないのもののみ通しつつnullの数を数える
    if (x == null) { nullNum++; }
    return x != null;
});
foreach (var item in whereEnumerable)
{
    Console.WriteLine(item.ToString());
}
// もう一度同じリストをforeachする
foreach (var item in whereEnumerable)
{
    Console.WriteLine(item.ToString());
}
// "nullの数は4個"と表示される(?!)
Console.WriteLine($"nullの数は{nullNum.ToString()}個");

これで、実行してみます。

すると、最後に出力するnullの数が
本来の2倍になってしまいました。
立派なバグですね。

この根本原因としては
「Whereが遅延評価という仕組みで動いているから(後述)」
なのですが、
このようにWhere内で副作用のある処理を書いてしまうと、
原因が分かりづらいバグに発展する可能性があります。

Whereに渡す条件式は、
必ず副作用のないものにしましょう。
できる限りシンプルな比較演算子のみで記述できるとベストです。

逆に、ラムダの中が複数行になってしまった場合は、
本当にWhereの中ですべき処理なのか
再検討することをおススメします。

Whereしたものを複数個所で使うときはToArrayしておくべき

Whereやその他一部のLINQ拡張メソッドは、
遅延評価という実装になっています。

遅延評価とは、
「実際に列挙が行われる時までは、
処理を行わない」
仕組みです。

動作のイメージがしやすいように、
先ほどのサンプルコードを一部流用します。

var nullNum = 0;
var array = new int?[] { 0, null, 2, null, 4 };
var whereEnumerable = array.Where(x =>
{
    if (x == null) { nullNum++; }
    return x != null;
});
// "nullの数は0個"
Console.WriteLine($"nullの数は{nullNum.ToString()}個");
foreach (var item in whereEnumerable)
{
    Console.WriteLine(item.ToString());
}
// "nullの数は2個"
Console.WriteLine($"nullの数は{nullNum.ToString()}個");

foreachの前と後に、
同じコンソール出力の文を追加しています。

全く同じコンソール出力が2回…
結果は同じじゃないの?

ところが、これを実行すると、
以下のようになります。

var whereEnumerable
の直後にコンソール出力した方は、
「nullの数は0個」
と出ているのに対し、
foreach (var item in whereEnumerable)
の直後にコンソール出力した方は、
「nullの数は2個」
と出力結果が異なっています。

さんさめ
さんさめ

こうなる原因が、
忘れたころに引っかかるLINQの仕様である
遅延評価なのです。

詳しく説明すると、こうです。

Whereを使った宣言をした時点では、
まだ実際にはWhere処理が走っていないのです。

そして、foreachによって、
列挙が必要になった段階で、
Where処理が回ってくるのです。

この仕組みを、遅延評価と呼びます。

もし、遅延評価ではなく即時評価させたい場合は、
最後にToArrayというメソッドを呼んで
配列化しておくとその場で評価してくれます。

配列化するということは、列挙が必要になるためです。
そして一度配列化してしまえば、
もうそれ以上評価をする必要はありません。

var nullNum = 0;
var array = new int?[] { 0, null, 2, null, 4 };
var whereEnumerable = array.Where(x =>
{
    if (x == null) { nullNum++; }
    return x != null;
})
.ToArray(); // ToArrayしておけばその場で評価される
Console.WriteLine($"nullの数は{nullNum.ToString()}個");
foreach (var item in whereEnumerable)
{
    Console.WriteLine(item.ToString());
}
Console.WriteLine($"nullの数は{nullNum.ToString()}個");

このコードの実行結果は次のようになります。

ガード節よりもWhereの方が良いと考える理由

ここまで見ていただいたように、
「副作用のある処理」と「遅延評価」が混ざると、
非常に分かりづらいバグの温床となります…。

LINQを使おうと思っているのであれば、
これは必ず忘れないようにしておきましょう。

うーん、なんかめんどい…
やっぱりforeachで良くない?

最初はそう思うかもしれません。

しかし、LINQは、
C#を使うのならば覚えておけば必ず役に立ちます。

他の拡張メソッドとの親和性や、
ReactiveExtensionなどの列挙以外に使われるLINQなど、
Whereが基本となる仕様は多々あります。

それに、慣れてくると、
ガード節よりもWhereの方が記述量が減るし、
やりたい事をコードにそのまま書いている
という事に気づきます。

最初は慣れないかもしれませんが、
Whereは比較的置き換えが分かりやすい部類なので、
少しずつLINQに慣れるための初めの一歩としては
よい題材なのではないでしょうか。

「今までWhereあんまり使ってなかったなぁ」
という方は、この機会にぜひ試してみてください。

まとめ

まとめです。

  • LINQ拡張メソッドのWhereを紹介
  • Whereはリストの中から、
    指定した条件を満たすもののみを抽出するメソッドである
  • foreachのcontinueガード節は、
    Whereに置き換えることができる
  • 遅延評価という仕組みに注意
    • 困る場合はToArrayすれば即時評価される
  • 副作用のある処理はなるべく書かない方が良い
    • 遅延評価と合わさると、
      原因の分かりづらいバグに発展することも
  • WhereはLINQの中では分かりやすいので
    LINQの入門に適している

最後までお読みいただきありがとうございました。

コメント

タイトルとURLをコピーしました