[C#] LINQ to Object

LINQ to Object (IEnumerableを実装するコレクションクラスに対して実行)。 拡張メソッドとして(見た目)使用する場合は標準クエリ演算子と呼ぶらしい。

便利。

クエリ式

標準クエリ演算子の一部はクエリ式で記述することができる。

from ~ in で 要素を取り出し、select で返す内容を選択する

in の後に続くオブジェクトは IEnumerable(<T>), IQueryable<T> を実装しているクラス

var data = new[]
  {
    new { id = 1, name = "One" },
    new { id = 2, name = "Two" },
    new { id = 3, name = "Three" },
  };

// 条件なしで name を取り出す
var names = from datum in data select datum.name;

foreach (string name in names)
{
  Debug.Write(name); // OneTwoThree
}

上のコードは下のコードと等価

var data = new[]
  {
    new { id = 1, name = "One" },
    new { id = 2, name = "Two" },
    new { id = 3, name = "Three" },
  };

var names2 = data.Select(datum => datum.name);

foreach (string name in names2)
{
  Debug.Write(name); // OneTwoThree
}

where で絞り込む

var data = new[]
  {
    new { id = 1, name = "One" },
    new { id = 2, name = "Two" },
    new { id = 3, name = "Three" },
  };

var names = from datum in data
            where datum.id == 2 // 条件指定 : イコール記号は1つではない
            select datum.name;

foreach (string name in names)
{
  Debug.WriteLine(name); // Two
}

// 上のコードと等価
names = data.Where(datum => datum.id  == 2)
  .Select(datum => datum.name);

foreach (string name in names)
{
  Debug.WriteLine(name); // Two
}
int[] nums = new int[] { 1, 3, 5, 7, 8 };

// 標準クエリ演算子ではselectを省略できる
var results = nums.Where(num => num <= 5);
foreach (int num in results)
{
  Debug.WriteLine(num); // 1, 3, 5 が出力される
}

// クエリ式ではコンパイルエラーとなる
results = from num in results where num <= 5; // コンパイルエラー

orderby で並べ替え

descending で降順に。

string[] strings = new string[]{
  "B",
  "D",
  "A",
  "C",
};

var results = from str in strings
              where str[0] < 'D'
              orderby str[0] descending
              select str;

foreach(string str in results)
{
  Debug.WriteLine(str);
  // C, B, A の順に出力される
}
var results = new List<int> { 3, 4, 1, 2 }
  .OrderByDescending(number => number)
  .Select(number => number * 100);

foreach (int number in results)
{
  Debug.WriteLine(number);
  // 400, 300, 200, 100 の順に出力される
}

orderby r.a, r.b descending, r.c のように複数指定できる。 標準クエリ演算子を使用する場合は .OrderBy(r=>r.a).ThenByDescending(r=>r.b).ThenBy(r=>r.c) となる。

from ~ in のネストと SelectMany 拡張メソッド

from ~ in を連続して記述することで、最初の from ~ in で選択したデータに対して、さらに from ~ in でデータを選択することができる。 SelectMany 拡張メソッドは from ~ in を2回使用した場合と同等。

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

var results = from record in table // tableからそれぞれの要素を取り出し
              from number in record.array // 取り出した要素から、その要素が持つ値を取り出す
              select number;

// ↓ "123456" と出力される
results.ToList().ForEach(number => Debug.Write(number));

Debug.WriteLine(string.Empty);
var selectMany = table.SelectMany(record => record.array);
//  ↓ "123456" と出力される
selectMany.ToList().ForEach(number => Debug.Write(number));
//
// SelectManyで要素のインデックスを利用する方法
//

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

table.SelectMany((record, index) =>
{
  Debug.Write(index + "," + record.array.Length + "|");
  return record.array;
}).ToList(); // "0,3|1,2|2,1|" と出力される
//
// SelectManyのもうちょい複雑なもの
// これのインデックス付きのものも有り
//
var result = table.SelectMany(
  record => record.array.Reverse(),
  (record, number) => number);

// ↓ "321546" と出力される
result.ToList().ForEach(number => Debug.Write(number));
//
// from~in + where の繰り返し
//
var table = new[]
{
  new { id=1, array = new int[]{ 1, 2, 3}},
  new { id=2, array = new int[]{ 4, 5}},
  new { id=3, array = new int[]{ 6}},
};

var result = from record in table
             where record.id != 2
             from number in record.array
             where number % 2 == 0
             select number * 10;

// ↓ "2060" が出力される
result.ToList().ForEach(number=>Debug.WriteLine(number));

group ~ by でグループ化

var table = new[]{
  new {id = "A", number = 1},
  new {id = "A", number = 2},
  new {id = "B", number = 1},
  new {id = "B", number = 10},
  new {id = "B", number = 100},
};

var groups = from record in table
             group record.number by record.id;

foreach (System.Linq.IGrouping<string, int> group in groups)
{
  Debug.Write("ID=" + group.Key + " Numbers=");
  foreach (int number in group)
  {
    Debug.Write(number + " ");
  }
  Debug.WriteLine("");
}
// 以下の通りに出力される
// ID=A Numbers=1 2
// ID=B Numbers=1 10 100

上と下は等価

var table = new[]{
  new {id = "A", number = 1},
  new {id = "A", number = 2},
  new {id = "B", number = 1},
  new {id = "B", number = 10},
  new {id = "B", number = 100},
};

var groups = table.GroupBy(
  record => record.id, // Key Selector
  record => record.number); // Element Selector

foreach (System.Linq.IGrouping<string, int> group in groups)
{
  Debug.Write("ID=" + group.Key + " Numbers=");
  foreach (int number in group)
  {
    Debug.Write(number + " ");
  }
  Debug.WriteLine("");
}
// 以下の通りに出力される
// ID=A Numbers=1 2
// ID=B Numbers=1 10 100

join ~ on ~ equal で結合

指定したキーで結合したものの組み合わせを全て抽出できる。 equals で結合できるものが無い部分は抽出されない抽出されない。

var tableA = new[]
{
  new { id=1, array = new int[]{ 1, 2, 3}},
  new { id=3, array = new int[]{ 4, 5}},
  new { id=5, array = new int[]{ 6}},
};

var tableB = new[]
{
  new { id=1, name = "hoge1" },
  new { id=1, name = "hoge2" },
  new { id=2, name = "piyo1" },
  new { id=2, name = "piyo2" },
  new { id=3, name = "foobar1" },
  new { id=3, name = "foobar2" },
};

var results = from a in tableA
              join b in tableB
              on a.id equals b.id
              select new { id = a.id, array = a.array, name = b.name };

foreach (var result in results)
{
  Debug.WriteLine("id={0}, array.Length={1}, name={2}",
    result.id,
    result.array.Length,
    result.name);

  // 出力は以下の通り
  // id=1, array.Length=3, name=hoge1
  // id=1, array.Length=3, name=hoge2
  // id=3, array.Length=2, name=foobar1
  // id=3, array.Length=2, name=foobar2
}

上と下は等価

var tableA = new[]
{
  new { id=1, array = new int[]{ 1, 2, 3}},
  new { id=3, array = new int[]{ 4, 5}},
  new { id=5, array = new int[]{ 6}},
};

var tableB = new[]
{
  new { id=1, name = "hoge1" },
  new { id=1, name = "hoge2" },
  new { id=2, name = "piyo1" },
  new { id=2, name = "piyo2" },
  new { id=3, name = "foobar1" },
  new { id=3, name = "foobar2" },
};

  var results = tableA.Join(tableB,
  a => a.id,
  b => b.id,
  (a, b) => new { id = a.id, array = a.array, name = b.name });

foreach (var result in results)
{
  Debug.WriteLine("id={0}, array.Length={1}, name={2}",
    result.id,
    result.array.Length,
    result.name);

  // 出力は以下の通り
  // id=1, array.Length=3, name=hoge1
  // id=1, array.Length=3, name=hoge2
  // id=3, array.Length=2, name=foobar1
  // id=3, array.Length=2, name=foobar2
}

join ~ on ~ equal ~ into でグループ結合(GroupJoin)

指定したキーで結合したものの組み合わせを1つのグループとして抽出。 Join と違って GroupJoin では結合元となる要素は全て抽出される

var tableA = new[]
{
  new { id=1, array = new int[]{ 1, 2, 3}},
  new { id=3, array = new int[]{ 4, 5}},
  new { id=5, array = new int[]{ 6}},
};

var tableB = new[]
{
  new { id=1, name = "hoge1" },
  new { id=1, name = "hoge2" },
  new { id=2, name = "piyo1" },
  new { id=2, name = "piyo2" },
  new { id=3, name = "foobar1" },
  new { id=3, name = "foobar2" },
};

var results = from a in tableA
              join b in tableB
              on a.id equals b.id
              into c
              select new { id = a.id, array = a.array, c = c };

foreach (var result in results)
{
  var dump = new StringBuilder();
  result.c.ToList().ForEach(c=>dump.Append(c));
  Debug.WriteLine("id={0}, array.Length={1}, c ={2}",
    result.id,
    result.array.Length,
    dump.ToString());
  // 出力は以下の通り
  // id=1, array.Length=3, c ={ id = 1, name = hoge1 }{ id = 1, name = hoge2 }
  // id=3, array.Length=2, c ={ id = 3, name = foobar1 }{ id = 3, name = foobar2 }
  // id=5, array.Length=1, c =
}

上と下は等価

var tableA = new[]
{
  new { id=1, array = new int[]{ 1, 2, 3}},
  new { id=3, array = new int[]{ 4, 5}},
  new { id=5, array = new int[]{ 6}},
};

var tableB = new[]
{
  new { id=1, name = "hoge1" },
  new { id=1, name = "hoge2" },
  new { id=2, name = "piyo1" },
  new { id=2, name = "piyo2" },
  new { id=3, name = "foobar1" },
  new { id=3, name = "foobar2" },
};

var results = tableA.GroupJoin(
  tableB, // Inner : 結合したいIEnumerable
  a => a.id, // outerKeySelector
  b => b.id, // innerKeySelector
  (a, c) => new { id = a.id, array = a.array, c = c });// resultSelector

foreach (var result in results)
{
  var dump = new StringBuilder();
  result.c.ToList().ForEach(c=>dump.Append(c));
  Debug.WriteLine("id={0}, array.Length={1}, c ={2}",
    result.id,
    result.array.Length,
    dump.ToString());
  // 出力は以下の通り
  // id=1, array.Length=3, c ={ id = 1, name = hoge1 }{ id = 1, name = hoge2 }
  // id=3, array.Length=2, c ={ id = 3, name = foobar1 }{ id = 3, name = foobar2 }
  // id=5, array.Length=1, c =
}

select ~ into

select の結果に対して更にクエリ式を繋げたい場合は into で繋げる

var numbers = new double[] { 0.4, 0.8, 1.2, 1.6 };

var results =
    from a in numbers
    select a * 10
      into b
      where b < 10
      select b * 10;

results.ToList().ForEach(num => Debug.WriteLine(num));
// 40
// 80
// と出力される
var numbers = new double[] { 1 };

var results =
    from a in numbers
    select a + 10
      into b
      select b + 100
        into c
        select c + 1000
          into d
          select d + 10000;

// 11111 と出力される
results.ToList().ForEach(num => Debug.WriteLine(num));

from での型指定と Cast

from で取り出す時に型を指定できる。Cast 標準クエリ演算子と等価。 ただし指定した型が異なると InvalidCastException が投げられる。

var arr = new object[] { 1, "hoge" };

var result = from int element in arr
             select element;

Debug.WriteLine(result.First().GetType()); // System.Int32

result.Last(); // InvalidCastException
var arr = new int[] { 1 };

var result = arr.Cast<long>();

result.First(); // InvalidCastException

let

let を利用するとクエリ式中で変数を使える。

int[] array = new int[] { 1 };

var results = from integer in array
              let x10 = integer * 10
              let x20 = integer * 20
              select new { a = integer, b = x10, c = x20 };

var r = results.First();

Debug.WriteLine(r.a); // "1" と出力される
Debug.WriteLine(r.b); // "10" と出力される
Debug.WriteLine(r.c); // "20" と出力される
Share
関連記事