次の方法で共有


データのクエリを実行する C# LINQ クエリを記述します

統合言語クエリ (LINQ) の入門的なドキュメントでは、ほとんどのクエリが、LINQ の宣言型クエリ構文を使用して記述されています。 C# コンパイラは、クエリ構文をメソッド呼び出しに変換します。 これらのメソッド呼び出しは、標準クエリ演算子を実装し、WhereSelectGroupByJoinMaxAverageなどの名前を持っています。 これらは、クエリ構文ではなくメソッド構文を使用して直接呼び出すことができます。

クエリの構文とメソッドの構文は意味的には同じものですが、多くの場合、クエリ構文のほうがシンプルで読みやすいと感じられます。 一部のクエリは、メソッド呼び出しとして表現する必要があります。 たとえば、指定した条件に一致する要素の数を取得するクエリを表すには、メソッド呼び出しを使用する必要があります。 また、ソース シーケンスで最大の値を持つ要素を取得するクエリにも、メソッド呼び出しを使用する必要があります。 System.Linq 名前空間の標準クエリ演算子のリファレンス ドキュメントでは、通常、メソッド構文が使用されます。 クエリやクエリ式自体でのメソッド構文の使い方を、よく理解しておく必要があります。

標準クエリ演算子の拡張メソッド

次の例は、シンプルなクエリ式と、メソッド ベースのクエリとして記述された、意味的に同等のクエリを示したものです。

int[] numbers = [ 5, 10, 8, 3, 6, 12 ];

//Query syntax:
IEnumerable<int> numQuery1 =
    from num in numbers
    where num % 2 == 0
    orderby num
    select num;

//Method syntax:
IEnumerable<int> numQuery2 = numbers
    .Where(num => num % 2 == 0)
    .OrderBy(n => n);

foreach (int i in numQuery1)
{
    Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
    Console.Write(i + " ");
}

2 つの例からの出力は同じです。 クエリ変数の型は、両方の形式で同じです: IEnumerable<T>.

式の右辺で、where 句が numbers 型の IEnumerable<int> オブジェクトのインスタンス メソッドとして表されていることに注意してください。 ジェネリック型の IEnumerable<T> インターフェイスについて知識があれば、それには Where メソッドがないことがわかります。 しかし、Visual Studio IDE で IntelliSense の入力候補一覧を呼び出すと、Where メソッドだけでなく、SelectSelectManyJoinOrderby など、他にも多くのメソッドが表示されます。 これらのメソッドでは、標準クエリ演算子が実装されています。

Intellisense の標準クエリ演算子をすべて示すスクリーンショット。

IEnumerable<T> にはさらにメソッドが含まれているように見えますが、そうではありません。 標準クエリ演算子は、"拡張メソッド" として実装されます。 拡張メソッドは、既存の型を "拡張" します。これらは、あたかもその型のインスタンス メソッドであるかのように呼び出すことができます。 標準クエリ演算子が IEnumerable<T> を拡張しているため、numbers.Where(...) を書き込むことができます。 拡張機能を呼び出す前に、using ディレクティブを使用して拡張機能をスコープに取り込みます。

拡張メソッドについて詳しくは、「拡張メソッド」をご覧ください。 標準クエリ演算子について詳しくは、「標準クエリ演算子の概要 (C#)」をご覧ください。 一部の LINQ プロバイダー (Entity Framework や LINQ to XML など) では、IEnumerable<T> 以外の型のために、独自の標準クエリ演算子と拡張メソッドが実装されています。

ラムダ式

前の例では、条件式 () が メソッドにインライン引数として渡されます。 このインライン式は、ラムダ式です。 この方法を使うと、本来であればより複雑な形式で記述しなければならないコードを、簡単に記述できます。 演算子の左側にある num は、クエリ式の num に対応する入力変数です。 コンパイラは、num がジェネリック numbers 型であることがわかっているため、IEnumerable<T> の型を推論できます。 ラムダの本体は、クエリ構文の式、または他の C# 式またはステートメントの式と同じです。 これには、メソッド呼び出しや他の複雑なロジックを含めることができます。 戻り値は式の結果です。 特定のクエリはメソッド構文でのみ表現でき、一部のクエリにはラムダ式が必要です。 ラムダ式は、LINQ ツールボックスの強力で柔軟なツールです。

クエリの構成可能性

前のコード例では、Enumerable.OrderBy メソッドは、Whereの呼び出しでドット演算子を使用して呼び出されます。 Where によってフィルター処理されたシーケンスが生成されます。その後、Orderby で生成されたシーケンスは、Where によって並べ替えられます。 クエリが IEnumerable を返すので、開発者は、メソッド呼び出しをつないでいきながら、メソッド構文でそれらを編成します。 クエリ構文を使ってクエリを記述すると、コンパイラによってこの構成が行われます。 クエリの結果はクエリ変数に格納されないので、いつでも (クエリを実行した後でも) それを変更したり、新しいクエリのベースとして使ったりできます。

次の例では、前に示した各アプローチを使用して、基本的な LINQ クエリをいくつか示します。

Note

これらのクエリはメモリ内コレクションに対して動作します。ただし、構文は LINQ to Entities および LINQ to XML で使用される構文と同じです。

例 - クエリ構文

ほとんどのクエリは、"クエリ構文" を使って "クエリ式" を作成することで記述します。 次の例は、3 つのクエリ式を示しています。 1 つ目のクエリ式は、where 句を使用した条件を適用することで、結果をフィルター処理または制限する方法を示しています。 このクエリは、値が 7 より大きいか、または 3 より小さいソース シーケンス内のすべての要素を返します。 2 つ目の式は、返された結果を並べ替える方法を示しています。 3 つ目の式は、キーに基づいて結果をグループ化する方法を示しています。 このクエリは、単語の頭文字に基づいて 2 つのグループを返します。

List<int> numbers = [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 ];

// The query variables can also be implicitly typed by using var

// Query #1.
IEnumerable<int> filteringQuery =
    from num in numbers
    where num is < 3 or > 7
    select num;

// Query #2.
IEnumerable<int> orderingQuery =
    from num in numbers
    where num is < 3 or > 7
    orderby num ascending
    select num;

// Query #3.
string[] groupingQuery = ["carrots", "cabbage", "broccoli", "beans", "barley"];
IEnumerable<IGrouping<char, string>> queryFoodGroups =
    from item in groupingQuery
    group item by item[0];

クエリの型は IEnumerable<T> です。 これらのクエリはすべて、次の例で示すように、var を使用しても記述できます。

var query = from num in numbers...

上の各例のクエリは、foreach ステートメントまたは他のステートメントでクエリ変数を反復処理するまで、実際には実行されません。

例 - メソッド構文

一部のクエリ操作は、メソッド呼び出しとして表す必要があります。 このようなメソッドで最も一般的なものは、SumMaxMinAverage など、単一の数値を返すメソッドです。 これらのメソッドは、1 つの値を返し、追加のクエリ操作のソースとして機能できないため、クエリで常に最後に呼び出す必要があります。 次の例は、クエリ式でのメソッド呼び出しを示したものです。

List<int> numbers1 = [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 ];
List<int> numbers2 = [ 15, 14, 11, 13, 19, 18, 16, 17, 12, 10 ];

// Query #4.
double average = numbers1.Average();

// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);

メソッドに System.Action または System.Func<TResult> パラメーターがある場合、これらの引数は、次の例で示すように、ラムダ式の形式で提供されます。

// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);

前のクエリでは、単一の値を返し、ジェネリック IEnumerable<T> コレクションを返さないクエリ #4 のみが、すぐに実行されます。 メソッド自体では、その値を計算するために foreach または同様のコードが使われます。

上記の各クエリは、var による暗黙的型指定を使用しても記述できます。次に例を示します。

// var is used for convenience in these queries
double average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);

例 - クエリとメソッド構文の併用

この例では、クエリ句の結果に対してメソッド構文を使用する方法を示します。 方法は、クエリ式をかっこで囲み、ドット演算子を適用して、メソッドを呼び出すだけです。 次の例では、Query #7 は、値が 3 ~ 7 である数値のカウントを返します。

// Query #7.

// Using a query expression with method syntax
var numCount1 = (
    from num in numbers1
    where num is > 3 and < 7
    select num
).Count();

// Better: Create a new variable to store
// the method call result
IEnumerable<int> numbersQuery =
    from num in numbers1
    where num is > 3 and < 7
    select num;

var numCount2 = numbersQuery.Count();

Query #7 はコレクションではなく単一の値を返すので、クエリはすぐに実行されます。

上記のクエリは、var による暗黙的型指定を使用しても記述できます。次に例を示します。

var numCount = (from num in numbers...

次のように、メソッド構文で記述することもできます。

var numCount = numbers.Count(n => n is > 3 and < 7);

次のように、明示的な型指定を使用して記述することもできます。

int numCount = numbers.Count(n => n is > 3 and < 7);

実行時に述語フィルターを動的に指定する

where 句のソース要素に適用しなければならない述語の数が実行時までわからない場合があります。 複数の述語フィルターを動的に指定する方法として、次の例のように、Contains メソッドを使用する方法があります。 クエリからは、クエリが実行された時点での id の値に基づいて、異なる結果が返されます。

int[] ids = [ 111, 114, 112 ];

var queryNames = from student in students
                 where ids.Contains(student.ID)
                 select new
                 {
                     student.LastName,
                     student.ID
                 };

foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Garcia: 114
    O'Donnell: 112
    Omelchenko: 111
 */

// Change the ids.
ids = [ 122, 117, 120, 115 ];

// The query will now return different results
foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Adams: 120
    Feng: 117
    Garcia: 115
    Tucker: 122
 */

Note

この例では、次のデータ ソースとデータを使用します。

record City(string Name, long Population);
record Country(string Name, double Area, long Population, List<City> Cities);
record Product(string Name, string Category);
static readonly City[] cities = [
    new City("Tokyo", 37_833_000),
    new City("Delhi", 30_290_000),
    new City("Shanghai", 27_110_000),
    new City("São Paulo", 22_043_000),
    new City("Mumbai", 20_412_000),
    new City("Beijing", 20_384_000),
    new City("Cairo", 18_772_000),
    new City("Dhaka", 17_598_000),
    new City("Osaka", 19_281_000),
    new City("New York-Newark", 18_604_000),
    new City("Karachi", 16_094_000),
    new City("Chongqing", 15_872_000),
    new City("Istanbul", 15_029_000),
    new City("Buenos Aires", 15_024_000),
    new City("Kolkata", 14_850_000),
    new City("Lagos", 14_368_000),
    new City("Kinshasa", 14_342_000),
    new City("Manila", 13_923_000),
    new City("Rio de Janeiro", 13_374_000),
    new City("Tianjin", 13_215_000)
];

static readonly Country[] countries = [
    new Country ("Vatican City", 0.44, 526, [new City("Vatican City", 826)]),
    new Country ("Monaco", 2.02, 38_000, [new City("Monte Carlo", 38_000)]),
    new Country ("Nauru", 21, 10_900, [new City("Yaren", 1_100)]),
    new Country ("Tuvalu", 26, 11_600, [new City("Funafuti", 6_200)]),
    new Country ("San Marino", 61, 33_900, [new City("San Marino", 4_500)]),
    new Country ("Liechtenstein", 160, 38_000, [new City("Vaduz", 5_200)]),
    new Country ("Marshall Islands", 181, 58_000, [new City("Majuro", 28_000)]),
    new Country ("Saint Kitts & Nevis", 261, 53_000, [new City("Basseterre", 13_000)])
];

if... elseswitch などの制御フロー ステートメントを使用すると、事前に定義された代替クエリの中から選択できます。 次の例では、studentQuery の実行時の値が where または oddYear である場合、true では異なる false 句が使われます。

void FilterByYearType(bool oddYear)
{
    IEnumerable<Student> studentQuery = oddYear
        ? (from student in students
           where student.Year is GradeLevel.FirstYear or GradeLevel.ThirdYear
           select student)
        : (from student in students
           where student.Year is GradeLevel.SecondYear or GradeLevel.FourthYear
           select student);
    var descr = oddYear ? "odd" : "even";
    Console.WriteLine($"The following students are at an {descr} year level:");
    foreach (Student name in studentQuery)
    {
        Console.WriteLine($"{name.LastName}: {name.ID}");
    }
}

FilterByYearType(true);

/* Output:
    The following students are at an odd year level:
    Fakhouri: 116
    Feng: 117
    Garcia: 115
    Mortensen: 113
    Tucker: 119
    Tucker: 122
 */

FilterByYearType(false);

/* Output:
    The following students are at an even year level:
    Adams: 120
    Garcia: 114
    Garcia: 118
    O'Donnell: 112
    Omelchenko: 111
    Zabokritski: 121
 */

クエリ式の null 値の処理

この例では、ソース コレクション内の可能な null 値を処理する方法を示します。 IEnumerable<T> のようなオブジェクト コレクションには、値が null の要素を含めることができます。 ソース コレクションが null であるか、null の値を持つ要素を含み、クエリで null 値を処理していない場合、クエリを実行すると NullReferenceException がスローされます。

次の例では、これらの型と静的データ配列を使用します。

record Product(string Name, int CategoryID);
record Category(string Name, int ID);
static Category?[] categories =
[
    new ("brass", 1),
    null,
    new ("winds", 2),
    default,
    new ("percussion", 3)
];

static Product?[] products =
[
    new Product("Trumpet", 1),
    new Product("Trombone", 1),
    new Product("French Horn", 1),
    null,
    new Product("Clarinet", 2),
    new Product("Flute", 2),
    null,
    new Product("Cymbal", 3),
    new Product("Drum", 3)
];

次の例に示すように、null 参照の例外を回避する防御的なコーディングをすることができます。

var query1 = from c in categories
             where c != null
             join p in products on c.ID equals p?.CategoryID
             select new
             {
                 Category = c.Name,
                 Name = p.Name
             };

前の例では、where 句によって、カテゴリ シーケンス内のすべての null 要素が除外されます。 この手法は、join 句での null チェックに依存しません。 この例の null 条件式が機能する理由は、Products.CategoryIDint? 型 (Nullable<int> の短縮形) であるためです。

join 句で、比較キーの一方だけが null 許容値型である場合、クエリ式でもう一方のキーを null 許容値型にキャストできます。 次の例では、EmployeeIDint? 型の値を含む列であるとします。

var query =
    from o in db.Orders
    join e in db.Employees
        on o.EmployeeID equals (int?)e.EmployeeID
    select new { o.OrderID, e.FirstName };

各例では、equals クエリ キーワードが使用されています。 パターン マッチングを使用することもできます。これには、is nullis not null に対するパターンが含まれます。 クエリ プロバイダーが新しい C# 構文を正しく解釈しない可能性があるため、これらのパターンは LINQ クエリでは推奨されません。 クエリ プロバイダーは、C# のクエリ式をネイティブ データ形式 (Entity Framework Core など) に変換するライブラリです。 クエリ プロバイダーには、System.Linq.IQueryProvider インターフェイスを実装するデータ ソースを作成するための System.Linq.IQueryable<T> インターフェイスが実装されています。

クエリ式の例外の処理

クエリ式のコンテキストで任意のメソッドを呼び出すことができます。 データ ソースのコンテンツの変更や、例外のスローといった副作用が発生する可能性のあるメソッドは、クエリ式で呼び出さないでください。 この例では、クエリ式でメソッドを呼び出すときに、例外処理に関する .NET の全般的なガイドラインに違反することなく例外の発生を避ける方法を示します。 これらのガイドラインでは、特定のコンテキストでスローされた理由を理解している場合に、特定の例外をキャッチすることは許容可能であると示されています。 詳細については、「例外の推奨事項」を参照してください。

最後の例では、クエリの実行中に例外をスローする必要がある場合の処理方法を示します。

次の例では、例外処理コードをクエリ式の外側に移動する方法を説明します。 このリファクタリングは、メソッドがクエリのローカル変数に依存しない場合にのみ可能です。 クエリ式の外部で例外を処理する方が簡単です。

// A data source that is very likely to throw an exception!
IEnumerable<int> GetData() => throw new InvalidOperationException();

// DO THIS with a datasource that might
// throw an exception.
IEnumerable<int>? dataSource = null;
try
{
    dataSource = GetData();
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation");
}

if (dataSource is not null)
{
    // If we get here, it is safe to proceed.
    var query = from i in dataSource
                select i * i;

    foreach (var i in query)
    {
        Console.WriteLine(i.ToString());
    }
}

前の例の catch (InvalidOperationException) ブロックでは、アプリケーションに適した方法で例外を処理しています (または処理していません)。

場合によっては、クエリ内からスローされる例外に対する最適な応答は、クエリの実行をすぐに停止することです。 次の例では、クエリ本体の内部からスローされる例外を処理する方法を示します。 SomeMethodThatMightThrow で、クエリの実行を停止することが必要な例外が発生する可能性があるとします。

try ブロックは、クエリ自体ではなく、foreach ループを囲んでいます。 foreach ループは、クエリが実行されるポイントです。 実行時例外は、クエリが実行されるとスローされます。 したがって、foreach ループ内で処理する必要があります。

// Not very useful as a general purpose method.
string SomeMethodThatMightThrow(string s) =>
    s[4] == 'C' ?
        throw new InvalidOperationException() :
        $"""C:\newFolder\{s}""";

// Data source.
string[] files = ["fileA.txt", "fileB.txt", "fileC.txt"];

// Demonstration query that throws.
var exceptionDemoQuery = from file in files
                         let n = SomeMethodThatMightThrow(file)
                         select n;

try
{
    foreach (var item in exceptionDemoQuery)
    {
        Console.WriteLine($"Processing {item}");
    }
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
}

/* Output:
    Processing C:\newFolder\fileA.txt
    Processing C:\newFolder\fileB.txt
    Operation is not valid due to the current state of the object.
 */

忘れずに、発生することが予想される例外をすべてキャッチし、finally ブロックで必要なクリーンアップを行ってください。

関連項目