この記事は「Effective C# 6.0/7.0」の読書メモとして、私的プラクティスをまとめています。特に重要だと感じた項目のみ簡潔にまとめています。より詳細な内容に興味のある方は、原著を読んでみることをお勧めします。
項目 29 コレクションを返すメソッドではなくイテレータを返すメソッドとすること
連続する要素(シーケンス)を作成する際には、List
や Array
などのコレクションだけではなく IEnumerable
のようなイテレータにすべきかどうかも視野に入れましょう。
特に、オープンな API を作成するときはイテレータを返す方が望ましいでしょう。イテレータは LINQ の ToList()
や ToArray()
で簡単にコレクションに変換できるため、使用者にとって選択肢が増える結果になります。
// コレクションを返す(即時実行)
public List<int> FromTo(int from, int to)
{
var list = new List<int>();
while (from <= to)
{
list.Add(from++);
}
return list;
}
// イテレータを返す(遅延実行)
public IEnumerable<int> FromTo(int from, int to)
{
while (from <= to)
{
yield return from++;
}
}
即時実行と遅延実行
上記の例において、コレクションを作成するメソッドでは、メソッド呼び出し時点でコレクションが作成されます。(即時実行)
一方、イテレータを返すメソッドでは、使用者が要素を取り出すタイミングで初めて要素が作成されます。(遅延実行)
遅延評価は、処理を組み合わせることによる再利用性や、実行時のパフォーマンスに優れています。
項目 30 ループよりもクエリ構文を使用すること
ループを使用するよりも、クエリ式を使用した方がより宣言的な記述ができるようになります。つまり「どうやって」より「何を」に注目できるようになります。
// ループ
private static IEnumerable<Tuple<int, int>> LoopIndices()
{
for (int x = 0; x < 100; x++)
{
for (int y = 0; y < 100; y++)
{
if (x + y < 100)
{
yield return Tuple.Create(x, y);
}
}
}
}
// クエリ式
private static IEnumerable<Tuple<int, int>> QueryIndices()
{
return from x in Enumerable.Range(0, 100)
from y in Enumerable.Range(0, 100)
where x + y < 100
select Tuple.Create(x, y);
}
クエリ式のパフォーマンス
クエリ式はループに比べパフォーマンスに劣ると言われていますが、アプリケーションにおいて、すべてのループでパフォーマンスが重要であるわけではありません。それよりも可読性を優先した方が良い場面も多々あります。クエリ式をループに書き換えるかどうかは、以下のように検討します。
- パフォーマンスが問題になる箇所を特定する
-
.AsParallel()
による並列化を試す - ダメならループに書き換える
項目 31 シーケンス用の組み合わせ可能な API を作成する
「イテレータを受け取って編集したイテレータを返す」ような API は複数組み合わせることができます。これにより遅延実行が受け継がれることになります。
// インクリメント
public static IEnumerable<int> Inc(IEnumerable<int> nums)
{
foreach (int num in nums)
{
yield return num + 1;
}
}
// 2乗
public static IEnumerable<int> Square(IEnumerable<int> nums)
{
foreach (int num in nums)
{
yield return num * num;
}
}
// 上記の組み合わせ
public static IEnumerable<int> IncSquare(IEnumerable<int> nums)
{
return Square(Inc(nums));
}
項目 35 拡張メソッドをオーバーロードしないこと
単にその型を使用しているからといって、安易に拡張メソッドを使用するべきではありません。拡張メソッドにするのは「その型にとって自然な処理」だけにしましょう。
また、拡張メソッド名が重複したときに、意図しないオーバーロードが発生する可能性があります。拡張メソッドのオーバーロードは、使用する名前空間によって動作が変わってしまうため、避けましょう。また static メソッドに変更することも検討しましょう。
// NG: 拡張メソッドのオーバーロード
namespace ConsoleExtensions
{
public static class ConsoleReport
{
public static string Format(this Person target) => target.Name;
}
}
namespace XmlExtensions
{
public static class XmlReport
{
public static string Format(this Person target)
{
return new XElement(nameof(target.Name), target.Name).ToString();
}
}
}
// OK: staticメソッドにして処理をまとめる
public static class PersonReports
{
public static string FormatAsText(Person target) => target.Name;
public static string FormatAsXml(Person target)
{
return new XElement(nameof(target.Name), target.Name).ToString();
}
}
項目 36 クエリ式とメソッド呼び出しの対応を把握する
クエリ式は内部的に標準クエリ演算子(LINQ で追加された Select や Where)に変換されます。
int[] numbers;
// 変換前: クエリ式
from n in numbers where n < 5 select n * n;
// 変換後: 標準クエリ演算子
numbers.Where(n => n < 5).Select(n => n * n);
ただし、標準クエリ演算子の中には、クエリ式でせないものが多数存在します。そのため、クエリ式で書けない処理は標準クエリ演算子を使用しましょう。
項目 37 クエリを即時評価ではなく遅延評価すること
クエリ式は遅延評価を行い IEnumerable
を返します。クエリの元となるシーケンスが IEnumerable
であれば、より効率的な処理を作成できるでしょう。
// ループでイテレータを作成する
foreach (int num in nums)
{
yield return num * num;
}
// 上記をクエリ式に書き換える
// yield return ではなく return
return from num in nums select num * num;
項目 40 即時実行と遅延実行を区別すること
評価が行われるタイミングを理解しましょう。
// Method1(), Method2(), Method3() は DoSomething() の呼び出し前に実行される
var answer = DoSomething(Method1(), Method2(), Method3());
// Method1(), Method2(), Method3() は DoSomething() 内で必要になったときに実行される
var answer = DoSomething(() => Method1(), () => Method2(), () => Method3());
項目 43 クエリに期待する意味を Single()や First()を使用して表現すること
要素が 1 つだけ必要な場合、要素が 1 つのシーケンスを返すよりも、Single()
や First()
を使用して、要素が 1 つだけであることを明示しましょう。
// 1つの要素を返す(2つ以上はエラー)
var single = sequence.Single();
// 要素がなければnullを返し、要素が1つならばその要素を返す(2つ以上はエラー)
var single = sequence.SingleOrDefault();