よくあるコーディングパターンと LINQ to Objects の対応付け
あると便利ですよね、ということで書いてみた。
よくあるコーディングパターンには yield とか使ってないです。
こっちの方がよくありそうでしょ?
Select
全ての要素に何らかの処理を行いたいときに使用します。
// よくあるコーディングパターンその1 // 全ての要素を2倍するメソッド public IEnumerable<int> DoubleAll(int[] target) { var result = new int[target.Length]; for (int i = 0; i < target.Length; i++) { result[i] = target[i] * 2; } return result; }
// Selectで書き直し public IEnumerable<int> DoubleAll(IEnumerable<int> target) { return target.Select(n => n * 2); }
このように、一行になりました。場合によってはメソッド化する必要もないでしょう。
// よくあるコーディングパターンその2 // インデックスが偶数の要素のみ2倍するメソッド public IEnumerable<int> DoubleIfEvenIndex(int[] target) { var result = new int[target.Length]; for (int i = 0; i < target.Length; i++) { if (i % 2 == 0) { result[i] = target[i] * 2; } else { result[i] = target[i]; } } return result; }
// Selectで書き直し public IEnumerable<int> DoubleIfEvenIndex(IEnumerable<int> target) { return target.Select((n, i) => i % 2 == 0 ? n * 2 : n); }
2 行!
よくあるパターンだけど、細部が微妙に違うのであちこちに同じようなコードがある、という状況を考えてみてください。
例えば、全ての要素に 100 を加える、だとか、全ての要素を文字列化する、といった処理があった場合です。
// LINQを使わない場合こんなメソッドを定義・・・ // メソッドすら作らず、その場に書いてあることもしばしば・・・ public IEnumerable<int> Plus10(int[] target) { var result = new int[target.Length]; for (int i = 0; i < target.Length; i++) { result[i] = target[i] + 100; } return result; } // 上とほとんど同じなんだけど・・・ public IEnumerable<string> ToStrAll(int[] target) { var result = new string[target.Length]; for (int i = 0; i < target.Length; i++) { result[i] = target[i].ToString(); } return result; }
// LINQの場合、一行なのでその場に書いてあってもそれほど問題ではない var hoge = piyo.Select(n => n + 100); // 略 var foo = bar.Select(n => n.ToString());
Where
要素を何らかの条件でフィルタリングしたい場合に使用します。
// よくあるコーディングパターンその3 // 奇数をフィルタリング(偶数のみにする) public IEnumerable<int> FilterOdd(IEnumerable<int> target) { var result = new List<int>(); foreach (var n in target) { if (n % 2 == 0) { result.Add(n); } } return result; }
// Whereで書き直し public IEnumerable<int> FilterOdd(IEnumerable<int> target) { return target.Where(n => n % 2 == 0); }
これまた 1 行!
// よくあるコーディングパターンその4 // インデックスが偶数の要素のみフィルタリング public IEnumerable<int> FilterOddIndexElem(int[] target) { var result = new List<int>(); for (int i = 0; i < target.Length; i++) // target.Lengthをresult.Countにしてしまったり { var n = target[i]; if (i % 2 == 0) // iをnにしてしまったり { result.Add(n); // nをiにしてしまうというバグを埋め込みそう・・・ } } return result; }
// Whereで書き直し public IEnumerable<int> FilterOddIndexElem(IEnumerable<int> target) { return target.Where((_, i) => i % 2 == 0); }
1 行!
All
全ての要素がある条件を満たすかどうかを調べるときに使えます。
// よくあるコーディングパターンその5 // 全ての要素が.txtで終わるかどうか調べる public bool EndsWithTxtAll(IEnumerable<string> target) { foreach (var s in target) { if (s.EndsWith(".txt") == false) { return false; } } return true; }
// Allで書き直し public bool EndsWithTxtAll(IEnumerable<string> target) { return target.All(s => s.EndsWith(".txt")); }
Any
いずれかの要素がある条件を満たすかどうかを調べるときに使えます。
// よくあるコーディングパターンその6 // .txtで終わる要素が一つでもあるかどうか調べる public bool ExistsTxtSuffix(IEnumerable<string> target) { foreach (var s in target) { if (s.EndsWith(".txt")) { return true; } } return false; }
// Anyで書き直し public bool ExistsTxtSuffix(IEnumerable<string> target) { return target.Any(s => s.EndsWith(".txt")); }
Contains
ある要素を含むかどうか調べるときに使います。
// よくあるコーディングパターンその7 // "hoge"が含まれるかどうか調べる public bool ContainsHoge(IEnumerable<string> target) { foreach (var s in target) { if (s == "hoge") { return true; } } return false; }
// Anyを使って書き直し public bool ContainsHoge(IEnumerable<string> target) { return target.Any(s => s == "hoge"); }
// Containsを使って書き直し public bool ContainsHoge(IEnumerable<string> target) { return target.Contains("hoge"); }
Any の特殊バージョンですね。
Count
ある条件を満たす要素の個数を調べるときに使えます。
// よくあるコーディングパターンその8 // 偶数の要素の個数を調べる public int CountEven(IEnumerable<int> target) { var result = 0; foreach (var n in target) { if (n % 2 == 0) { result++; } } return result; }
// Countを使って書き直し public int CountEven(IEnumerable<int> target) { return target.Count(n => n % 2 == 0); }
Skip
先頭から n 個無視したい場合に使います。
// よくあるコーディングパターンその9 // 先頭10要素を捨てる public IEnumerable<int> Drop10(int[] target) { var result = new List<int>(); for (int i = 10; i < target.Length; i++) { result.Add(target[i]); } return result; }
public IEnumerable<int> Drop10(IEnumerable<int> target) { return target.Skip(10); }
SkipWhile
先頭から条件を満たすものを捨てたいときに使います。
// よくあるコーディングパターンその10 // 先頭にある空文字列もしくはnullを捨てる public IEnumerable<string> DropHeadEmpty(string[] target) { // 効率悪そうだけどシンプル var result = new List<string>(target); while (result.Count != 0 && string.IsNullOrEmpty(result[0])) { result.RemoveAt(0); } return result; }
// SkipWhileで書き直し public IEnumerable<string> DropHeadEmpty(IEnumerable<string> target) { // いやでもこっちの方がシンプル return target.SkipWhile(string.IsNullOrEmpty); }
ラムダ式さえ使っていません。
// 10要素捨てる例をSkipWhileで public IEnumerable<int> Drop10(IEnumerable<int> target) { return target.SkipWhile((_, i) => i < 10); }
Take
Skip とは逆に、先頭 n 要素がほしい場合に使えます。
// よくあるコーディングパターンその11 // 先頭10要素を取得する public IEnumerable<int> Take10(int[] target) { var result = new List<int>(); for (int i = 0; i < 10; i++) { result.Add(target[i]); // 10要素なかったら例外・・・ } return result; }
// Takeで書き直し public IEnumerable<int> Take10(IEnumerable<int> target) { return target.Take(10); // 10要素無かったらあったところまで返る }
TakeWhile
先頭から条件を満たすもだけを拾いたいときに使います。
// よくあるコーディングパターンその12 // nullまでの要素を取得 public IEnumerable<string> TakeNotNull(IEnumerable<string> target) { var result = new List<string>(); foreach (var s in target) { if (s == null) { break; } result.Add(s); } return result; }
// TakeWhileで書き直し public IEnumerable<string> TakeNotNull(IEnumerable<string> target) { return target.TakeWhile(s => s != null); }
First/FirstOrDefault
条件を満たす最初の要素がほしいときに使います。
条件を満たす要素がなかった場合、First は例外を投げますが、FirstOrDefault はデフォルト値を返します。
// よくあるコーディングパターンその13 // 空文字列でもnullでもない最初の要素を返す public string FirstNotEmpty(IEnumerable<string> target) { foreach (var s in target) { if (string.IsNullOrEmpty(s) == false) { return s; } } throw new Exception(); }
// Firstで書き直し public string FirstNotEmpty(IEnumerable<string> target) { return target.First(s => string.IsNullOrEmpty(s) == false); }
条件を満たす最後の要素を返す、Last/LastOrDefault もあります。
まとめ
これまで見てきたように、LINQ to Objects を使えば今まで書いてきた定型的な処理をほとんど書かなくてよくなります。
それだけではなく、これらはドットによって組み合わせることが可能です。
例えば、target.Where(...).Select(...) ならフィルターして変換ですし、target.SkipWhile(!A).TakeWhile(A) なら条件 (A が条件のつもり) を満たす最初の塊を取得です。
では、LINQ to Objects を使わずに自分でループを書く意味は何かあるのでしょうか?
言い換えると、自分でループを書くメリットには何があるでしょうか?
考えるに、2 つほどそれっぽいものが思いつきます。
- 処理を追うことで何をやっているかわかる
- 自分の好きなように書ける
他にもあるかもしれませんが、とりあえずこの 2 つを考えてみます。
まず、処理を追うことで何をやっているかわかる、ですが、これはつまり「LINQ の各メソッドが何をするか知らない」と言い換えることができるでしょう。
たしかに、いきなりコードに Select なんてメソッドが出てきても、最初はわけわかりませんよね。
ですが、これは「LINQ の各メソッドが何をするかわかれば、処理を追う必要がなくなる」ということも意味します。
このエントリで上げた LINQ のメソッドは十数個です。
しかも、SQL を知っていればどういうことをやるのか想像の付くメソッドも多いうえ、All や Any、First なんかはメソッド名からやっていることが簡単にわかります。
LINQ のすべてのメソッドを一気に覚える必要はありません。徐々に覚えていけばいいのです。
次に、自分の好きなように書ける、です。
これはつまり、あれもこれもやるループが LINQ では書けない!ということですが、これは誤解です。
LINQ の各メソッドは組み合わせが容易です。さっき見たように、「フィルターして変換」も Where と Select を組み合わせれば実現できます。
それに、自分でコードを書くということは、それだけバグを埋め込む余地を作ってしまう、ということでもあります。
このエントリでもいくつかの例に「こういうバグを埋め込みそう」というコメントを書きましたが、自分であれもこれもやるループなんてものを書けば、バグを埋め込む可能性はさらに上がります。
LINQ to Objects を使うことによって、そういった可能性を減らすことができるのです。
このように、自分でループを書くメリットはないと言っていいでしょう。
とはいうものの、いきなり移行するのは不可能でしょう。
なので、徐々に LINQ to Objects を使ったコーディングに慣れていきましょう。
LINQ の各メソッドについて、自分で実装してみてどういうものか理解するのがいいと思います。
ただ、ハードルの高いメソッドもいくつかありますので、おすすめのメソッドをあげておきます。
- All/Any
- Count/Sum
- Max/Min
- Skip/Take
- SkipWhile/TakeWhile
- Cast/Select
- OfType/Where
LINQ to Objects でよりハッピーなプログラマ生活が始まる!
よくあるコーディングパターンは本当によくある?
さすがにそのまま表れることはそうそうないです。
そういうのを置いておいたとしても、果たしてそんなによくある状況なのか・・・
むしろ、こっちの方が多いような気がします。
// 本当によくあるパターン public void DoubleAll(int[] target) { for (int i = 0; i < target.Length; i++) { target[i] = target[i] * 2; } }
そうです。引数を直接いじるパターンです。
これ、戻り値の型が void なのでまだいいですが・・・
// 結果を返すので便利! public int[] DoubleAll(int[] target) { for (int i = 0; i < target.Length; i++) { target[i] = target[i] * 2; } return target; }
こうなっていた場合、もう収拾がつきません。
これを書いた人としては、便利に使えるように、ということで書いたのでしょう。
でも、これを使う側からすると、いちいち引数を書き換えていないか確かめる必要があります。
これを怠り、「書き換えてないだろう」と決めつけてしまうとバグを生みます。
逆に、「書き換えてるかもしれない」と決めつけてしまうと、メソッドに渡す前に常にコピーするようなコードを書くことになり、コード中にノイズが増えます。
こういう、引数を書き換えているようなコードが多い場合、LINQ to Objects に単純に置き換えることができません。
LINQ to Objects は元になるシーケンスを書き換えることはしません。
そのため、渡した変数を書き換えていることを前提としたコードがあった場合、LINQ to Objects に書き換えることで既存コードを壊してしまうのです。
今から書くコードからでもいいので、こういう「引数を書き換える」ようなコードは書かない、書かせないようにしていくべきでしょう。
これは、LINQ を使う、使わない以前の問題です。