[C#] これだけ押さえておけば大丈夫!?LINQ拡張メソッドまとめ

csharp

はじめに

こんにちは。最近C#と戯れているモバイルアプリサービス部の加藤潤です。
今回は今更ではありますが、C#のLINQ拡張メソッドを復習してみたいと思います。

開発環境

本記事中のプログラムは以下の環境で動作確認しています。

  • Visual Studio for Mac Preview 2(7.0 build 560)
  • .NETコンソールプロジェクト
  • ターゲットフレームワーク Mono / .NET 4.5

LINQの便利メソッドは拡張メソッドで定義されている

LINQでよく使うWhereなどのコレクション操作の便利メソッドはSystem.Linq名前空間のEnumerableクラスに拡張メソッドとして定義されています。

準備

操作対象のコレクションとして以下のように1から10の整数値を用意しました。 以降、このコレクションに対してLINQ拡張メソッドを実行していきます。

// 1から10の整数値を取得(型はIEnumerable<int>)
var values = Enumerable.Range(1, 10);

また、コレクション操作後の値を出力するために以下の拡張メソッドを定義しました。

public static class Extension
{
    public static void Write<T>(this IEnumerable<T> source)
    {
        WriteLine(string.Join(",", source));
    }

    public static void Write<T>(this T obj)
    {
        WriteLine(obj);
    }
}

Where

Whereは指定した条件を満たす要素を抽出するために使います。

// 偶数のみ抽出
values.Where(v => v % 2 == 0).Write(); // 2,4,6,8,10

Take

Takeは先頭から指定した数分の要素を取得します。

// 先頭から3つ分を取得
values.Take(3).Write(); // 1,2,3

TakeWhile

TakeWhileは指定した条件を満たす間、要素を取得します。

values.TakeWhile(v => v < 9).Write(); // 1,2,3,4,5,6,7,8

「指定した条件を満たす間」であるため、コレクションがソートされていない場合は例えば以下のような結果となります。

var list = new List<int>() { 1, 10, 8 };
list.TakeWhile(v => v < 9).Write(); // 1

Skip

Skipは先頭から指定した数分要素をスキップし、残りの要素を返します。

// 先頭の3つをスキップ
values.Skip(3).Write(); // 4,5,6,7,8,9,10

SkipWhile

SkipWhileは指定した条件を満たす間、要素をスキップします。

// 9未満である間要素をスキップ
values.SkipWhile(v => v < 9).Write(); // 9,10

「指定した条件を満たす間」であるため、コレクションがソートされていない場合は例えば以下のような結果となります。

var list = new List<int>() { 1, 10, 8 };
list.SkipWhile(v => v < 9).Write(); // 10, 8

Any

Anyは要素が含まれているかどうかを確認するために使います。

// 要素が1つでも含まれていればtrue, 空であればfalse
values.Any().Write(); // True

また、条件を指定することで、その条件を満たす要素が含まれているかどうかを確認することも可能です。

// 条件を満たす要素が1つでも含まれていればtrue
values.Any(v => v == 10).Write(); // True
values.Any(v => v == 11).Write(); // False

All

Allは全ての要素が指定した条件を満たすかどうかを確認するために使います。

// 全ての要素が偶数かどうか
values.All(v => v % 2 == 0).Write(); // False

Max

Maxは最大値を得るために使います。

// 最大値
values.Max().Write(); // 10

Min

Minは最小値を得るために使います。

// 最小値
values.Min().Write(); // 1

Sum

Sumは合計値を得るために使います。

// 合計値
values.Sum().Write(); // 55

Average

Averageは平均値を得るために使います。

// 平均値
values.Average().Write(); // 5.5

First(OrDefault)

Firstは先頭要素を取得するために使います。

values.First().Write(); // 1

また、条件を指定することで、その条件を満たす先頭の要素を取得することも可能です。

values.First(v => v % 3 == 0).Write(); // 3 

同じようなメソッドにFirstOrDefaultがあります。こちらは要素が存在する場合はFirstと結果は同じです。

values.FirstOrDefault().Write(); // 1
values.FirstOrDefault(v => v % 3 == 0).Write(); // 3

FirstFirstOrDefaultの違いは要素が存在しない場合です。 Firstは要素が存在しない場合例外が発生しますが、FirstOrDefaultは例外は発生せず既定値(例えばintの場合は0)となります。

既定値について

参照型および null許容型の既定値はnullです。
値型の既定値については既定値の一覧表 (C# リファレンス)をご覧ください。

Last(OrDefault)

Firstが先頭要素を取得するのに対して、Lastは最後の要素を取得します。

values.Last().Write();                         // 10
values.Last(v => v % 3 == 0).Write();          // 9
values.LastOrDefault().Write();                // 10
values.LastOrDefault(v => v % 3 == 0).Write(); // 9

LastLastOrDefaultの違いはFirstFirstOrDefaultの違いと同様です。

ElementAt(OrDefault)

ElementAtは指定したインデックス位置にある要素を取得します。

values.ElementAt(5).Write(); // 6
values.ElementAtOrDefault(5).Write(); // 6

ElementAtElementAtOrDefaultの違いは指定したインデックスが範囲外の場合です。ElementAtは例外が発生しますが、ElementAtOrDefaultは既定値となります。

Single(OrDefault)

Singleは唯一の要素を取得するメソッドです。要素が複数存在する場合は例外が発生します。

values.Single(); // 例外(System.InvalidOperationException)発生
values.Single(v => v == 10).Write(); // 10
values.SingleOrDefault(); // 例外(System.InvalidOperationException)発生
values.SingleOrDefault(v => v == 10).Write(); // 10

SingleSingleOrDefaultの違いはコレクションが空の場合です。 Singleは例外が発生しますが、SingleOrDefaultは例外は発生せず既定値(例えばintの場合は0)となります。

values = Enumerable.Empty<int>();
values.Single(); // 例外(System.InvalidOperationException)発生
values.SingleOrDefault().Write(); // 0

Select

Selectは要素に変換関数を適用した結果を得ることができます。 この変換関数次第で様々な結果を返すことができます。

例えば以下のようにすれば、各要素を10倍したコレクションを返すことが出来ます。

// 各要素を10倍する
values.Select(v => v * 10).Write(); // 10,20,30,40,50,60,70,80,90,100

また、例えば以下のようなParentクラスとChildクラスがあったとします。

public class Parent
{
    public string Name { get; set; }
    public IEnumerable<Child> Children { get; set; }
}

public class Child
{
    public string Name { get; set; }
}

そしてこんな感じにParentのリストがあったとします。

var p1 = new Parent();
p1.Name = "Yamada Taro";
var p1Chidren = new List<Child> {
    new Child { Name = "Yamada Hanako" },
    new Child { Name = "Yamada Kenji" }
};
p1.Children = p1Chidren;

var p2 = new Parent();
p2.Name = "Saito Haruka";
var p2Chidren = new List<Child> {
    new Child { Name = "Saito Mamoru" },
    new Child { Name = "Saito Yuki" }
};
p2.Children = p2Chidren;

var pList = new List<Parent> { p1, p2 };

Selectを使えば以下のように特定のプロパティのみ取得することができます。

// Nameプロパティのみ取得
pList.Select(p => p.Name).Write(); // Yamada Taro,Saito Haruka

それ以外にも匿名型を返すことも可能なので便利です。

// 匿名型を返す
pList.Select(p => new { Id = p.Name });

SelectMany

SelectManySelectの結果を平坦化(フラット化)することができます。
例えばSelectを使ってChildrenプロパティのみ取得すると、Childrenプロパティがコレクションであるため、結果がコレクションのコレクションとなり、Childのインスタンスにアクセスするためには2重ループしなければいけません。

// selectResultの型はIEnumerable<IEnumerable<Child>>
var selectResult = pList.Select(p => p.Children);
foreach (var children in selectResult)
{
    foreach (var child in children)
    {
        // Childにアクセス
    }
}

これがSelectManyを使うと以下のようなフラットなコレクションにしてくれます。

// IEnumerable<Child>
var selectManyResult = pList.SelectMany(p => p.Children);
foreach (var child in selectManyResult)
{
    // Childにアクセス
}

OrderBy

OrderByは昇順に並び替えるメソッドです。

var values = new List<int> { 3, 9, 1, 45, 2, 6, 4 };
values.OrderBy(v => v).Write(); // 1,2,3,4,6,9,45

OrderByDescending

OrderByDescendingは降順に並び替えるメソッドです。

var values = new List<int> { 3, 9, 1, 45, 2, 6, 4 };
values.OrderByDescending(v => v).Write(); // 45,9,6,4,3,2,1

おわりに

LINQ拡張メソッドを復習してみました。今回は「1つ1つのメソッドでどういうことができるのか」という観点でまとめてみましたが、LINQはメソッドチェーンで書くことができますし、必要に応じて遅延実行されます。その辺りの仕組みや挙動を理解するにはこちらの書籍がオススメです。私ももう一度読み返してみたいと思います。

参考