C# LINQの基本:.NET 10で動かし直した入門

from・where・orderby・select・group・join を例で整理し、すべて .NET 10 で動かし直した。遅延実行の落とし穴、新しい LeftJoin、ループとの速度差も実測。

2026年11月改訂: この記事は2021年に韓国語で書いた講座を、日本語で書き直したものです。すべての例を .NET 10.0.14(C# 14)で動かし直しました。出力は元の記事と同じでしたが、1つの例は型名の打ち間違いでコンパイルできず、コードには目に見えないBOM文字が混ざり、実行結果の改行も崩れていました。修正したコードに加えて、遅延実行、.NET 10 の LeftJoin、速度の実測を足しています。

LINQ(Language-Integrated Query)は、コレクションのように見えるあらゆるデータにC#の中から直接「問い合わせ」を書ける仕組みです。複数のループと条件分岐が必要な絞り込み・並べ替え・グループ化が、1つの式になります。

ループとLINQ

配列から偶数だけを取り出して並べ替える処理を、ループで書くとこうなります。

List<int> intList = new List<int>();
int[] numbers = { 1, 3, 4, 6, 5, 9, 8, 12, 15, 18, 17, 11, 22 };

foreach (int num in numbers)
{
    if (num % 2 == 0)
        intList.Add(num);
}
intList.Sort();

foreach (int num in intList)
    Console.Write("{0} ", num);
4 6 8 12 18 22

同じことをLINQのクエリ式で書くとこうです。

int[] numbers = { 1, 3, 4, 6, 5, 9, 8, 12, 15, 18, 17, 11, 22 };
var data = from num in numbers
           where num % 2 == 0
           orderby num
           select num;

foreach (var i in data)
    Console.Write("{0} ", i);
4 6 8 12 18 22

メソッド構文でも書けます。コンパイラーはクエリ式を最終的にこのようなメソッド呼び出しに変換します(Order() は .NET 7 以降)。

var data = numbers.Where(n => n % 2 == 0).Order();

from

クエリ式は必ず from 句で始まります。

from 範囲変数 in データソース

範囲変数は foreach のループ変数に似ていますが、データそのものを保持するわけではありません。データソースは IEnumerable<T>(または IQueryable<T>)を実装した型で、配列や List<T> が当てはまります。

where

where は条件が真の要素だけを通すフィルターです。複数書くこともできます。

var data = from num in numbers
           where num < 10
           orderby num
           select num;
// 1 3 4 5 6 8 9

orderby

orderby は並べ替えです。既定は昇順(ascending)で、降順は descending を付けます。カンマで複数のキーを指定できます。コンパイル後は OrderBy・OrderByDescending・ThenBy の呼び出しになります。

var data = from num in numbers
           orderby num descending
           select num;
// 22 18 17 15 12 11 9 8 6 5 4 3 1

select

select は最終結果を作り、その型がクエリ全体の型を決めます。char を選べば IEnumerable<char> です。クエリ式は select か group で終わる必要があります。

char[] chars = "str12i3!@$1ng".ToCharArray();
var data = from vchar in chars
           where vchar >= 97 && 122 >= vchar      // 'a'〜'z'
           select vchar;

foreach (char c in data)
    Console.Write("{0}", c);
// string

実務では 97 と 122 の代わりに、vchar >= 'a' && vchar <= 'z' や char.IsAsciiLetterLower(vchar)(.NET 7以降)のほうが読みやすいでしょう。

group

group A by B into C は、要素Aを基準Bで分けてグループCを作ります。グループに対して続けて処理するときだけ into が必要です。

var listStudent = new List<Student>      // 2021年版は List<student>(小文字)でコンパイルできなかった
{
    new Student() { Name = "佐藤", Average = 78.5 },
    new Student() { Name = "鈴木", Average = 91.2 },
    new Student() { Name = "高橋", Average = 77.3 },
    new Student() { Name = "田中", Average = 80.8 }
};

var queryStudent = from student in listStudent
                   orderby student.Average
                   group student by student.Average < 80.0;

foreach (var studentGroup in queryStudent)
{
    Console.WriteLine(studentGroup.Key ? "平均80点未満:" : "平均80点以上:");
    foreach (var student in studentGroup)
        Console.WriteLine("\t{0}: {1}点", student.Name, student.Average);
}
平均80点未満:
    高橋: 77.3点
    佐藤: 78.5点
平均80点以上:
    田中: 80.8点
    鈴木: 91.2点

元の記事の List<student> は、C#が大文字と小文字を区別するため次のように失敗します。

error CS0246: 型または名前空間の名前 'student' が見つかりませんでした

点数帯ごとに分けるときは、into でグループに名前を付けて続けます。

var queryStudent = from student in listStudent
                   group student by (int)student.Average / 10 into g
                   orderby g.Key
                   select g;

foreach (var g in queryStudent)
{
    Console.WriteLine("{0}点から{1}点:", g.Key * 10, g.Key * 10 + 10);
    foreach (var student in g)
        Console.WriteLine("\t{0}: {1}点", student.Name, student.Average);
}

7人(54.2、61.4、90.8を追加)で実行すると、50〜60、60〜70、70〜80(2人)、80〜90、90〜100(2人)のグループに分かれます。

join

join は関係のない2つのデータソースをキーで結びます。比較には == ではなく equals キーワードを使い、等しいかどうかしか比べられません。

内部結合

両方にあるものだけが出てきます。

var query = from s in listAverage
            join h in listHobby on s.Name equals h.Name
            select new { s.Name, s.Average, h.Hobby };

7人のうち、趣味のデータがある4人だけが出ます。

左外部結合:2021年の書き方と .NET 10 の書き方

趣味のない生徒も結果に残すには、左外部結合が必要です。2021年にはこう書いていました。

var query = from s in listAverage
            join h in listHobby on s.Name equals h.Name into sg
            from h in sg.DefaultIfEmpty(new MyHobby() { Hobby = "なし" })
            select new { s.Name, h.Hobby };

.NET 10 からは LeftJoin メソッドが加わり、ずっと簡単になりました。

var query = listAverage.LeftJoin(listHobby,
    s => s.Name, h => h.Name,
    (s, h) => new { s.Name, Hobby = h?.Hobby ?? "なし" });

.NET 10 で両方を実行すると、まったく同じ7行が返り、趣味のない3人は「なし」になります。

必ず知っておきたい落とし穴:遅延実行

LINQのクエリは、書いたときではなく読み出したときに実行されます。2021年の記事でいちばん抜けていた点です。

var src = new List<int> { 1, 2, 3 };
var evens = from n in src where n % 2 == 0 select n;   // まだ何も実行されていない
src.Add(4);
Console.WriteLine(string.Join(" ", evens));             // 2 4  ← あとから追加した4も出る

int calls = 0;
var q = src.Where(n => { calls++; return n > 0; });
_ = q.Count(); _ = q.Count();
Console.WriteLine(calls);                               // 10  ← 5件×2回、毎回最初から回る

結果を固定したいときや何度も使うときは、ToList() や ToArray() で一度だけ実体化しましょう。とくにデータベースへの問い合わせ(IQueryable)は、読み出すたびに問い合わせが送られます。

ループとの速度比較(実測)

1000万個の整数から偶数の合計を求めました(Releaseビルド、.NET 10、Apple M1 Pro、3回計測)。

方法 1回目 2回目 3回目
foreach + if 15.4 ms 7.8 ms 8.1 ms
Where(...).Sum(...) 24.2 ms 21.7 ms 21.8 ms

単純なループでは、LINQは2〜3倍遅くなりました。デリゲートの呼び出しと列挙子のコストのためです。ただ、1000万件で20ミリ秒程度なので、多くのコードでは読みやすさが勝ります。毎秒何千回も通る処理だけ、手書きのループを検討すれば十分です。

まとめ

句 役割 メソッド構文
from … in データソースと範囲変数 —
where 絞り込み Where
orderby 並べ替え OrderBy、OrderByDescending、ThenBy
select 結果を作る Select
group … by グループ化 GroupBy
join … on … equals 結合 Join、GroupJoin、LeftJoin(.NET 10)

詳しくは Microsoft の >LINQ ドキュメントを参照してください。

よくある質問

LINQのクエリはいつ実行される?

定義したときではなく、結果を取り出すとき(foreach、ToList、Countなど)です。クエリを定義したあとに元のリストへ追加した値も結果に出てきます。同じクエリでCount()を2回呼ぶと、条件式も2回ずつ実行されます。

LINQで左外部結合(Left Join)はどう書く?

.NET 10からは LeftJoin メソッドがあります。それ以前は join … into グループ と from … in グループ.DefaultIfEmpty() の組み合わせでした。.NET 10で両者の結果が同じことを確認しました。

LINQはfor/foreachより遅い?

単純な処理では少し遅いです。1000万個の整数で偶数の合計を求めると、foreachは約8ミリ秒、Where+Sumは約22ミリ秒でした(Release、Apple M1 Pro)。多くのコードでは読みやすさのほうが大事で、非常に頻繁に回るループだけ気にすれば十分です。

  • #C#
  • #LINQ
  • #.NET
  • #dotnet10

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑