クエリ式の基本

この記事では、C# でのクエリ式に関連する基本概念について説明します。

クエリとは何か。またどのような働きをするのか

クエリとは、指定したデータ ソース (単一または複数) からどのようなデータを取得し、それらのデータをどのような形式と編成で返すかを説明した、命令のセットです。 クエリは、それが生成する結果とは区別されます。

一般に、ソース データは、同じ種類の要素のシーケンスとして論理的に編成されます。 たとえば、SQL データベース テーブルには、行のシーケンスが含まれています。 XML ファイルには、XML 要素のシーケンスがあります (ただし、これらはツリー構造で階層化されています)。 メモリ内コレクションには、オブジェクトのシーケンスが含まれています。

アプリケーションの観点から言うと、元のソース データの特定の型や構造体はは重要ではありません。 アプリケーションは常に、ソース データを IEnumerable<T> または IQueryable<T> コレクションとして認識します。 たとえば、LINQ to XML では、ソース データは IEnumerable<XElement> として表示されます。

クエリは、このソース シーケンスに対して、次の 3 つのうち、いずれかの操作を行います。

  • 個々 の要素を変更することなく、要素のサブセットを取得して新しいシーケンスを生成する。 クエリはその後、次の例のように、返されたシーケンスをさまざまな方法で並べ替えたり、グループ化する場合があります (例では scoresint[] と想定)。

    IEnumerable<int> highScoresQuery =
        from score in scores
        where score > 80
        orderby score descending
        select score;
    
  • 上記の例のように要素のシーケンスを取得するが、それらを新しい型のオブジェクトに変換する。 たとえば、クエリでは、データ ソース内の特定の顧客レコードから姓だけを取得することがあります。 また、完全なレコードを取得し、それを使用して別のメモリ内オブジェクト型や XML データを構築した後、最終的な結果シーケンスを生成することもあります。 次の例では、int から string へのプロジェクションを行っています。 highScoresQuery の新しい型があることに注目してください。

    IEnumerable<string> highScoresQuery2 =
        from score in scores
        where score > 80
        orderby score descending
        select $"The score is {score}";
    
  • ソース データに関するシングルトン値を取得します。次に例を示します。

    • 特定の条件に一致する要素の数。

    • 最大値または最小値を持つ要素。

    • 条件に一致する最初の要素や、指定された要素セット内の特定の値の合計。 たとえば、次のクエリは、整数配列 scores から、80 より大きいスコアの数を返します。

      int highScoreCount = (
          from score in scores
          where score > 80
          select score
      ).Count();
      

      上記の例では、Count メソッドに対する呼び出しの前に、クエリ式を囲むかっこが使用されています。 これは、具体的な結果を格納する新しい変数を使用しても表現できます。 この手法では、クエリを格納する変数が、結果を格納するクエリとは別に保持されるので、コードがより読みやすくなります。

      IEnumerable<int> highScoresQuery3 =
          from score in scores
          where score > 80
          select score;
      
      int scoreCount = highScoresQuery3.Count();
      

上記の例では、Count に対する呼び出しの前でクエリが実行されています。これは、highScoresQuery によって返された要素の数を確認するために、Count が結果を反復処理する必要があるためです。

クエリ式とは何か

クエリ式とは、クエリ構文で表されたクエリのことです。 クエリ式は、ファーストクラスの言語コンストラクトです。 他の式とよく似ていて、C# 式が有効である任意のコンテキストで使用できます。 クエリ式の構文は、SQL や XQuery などのような宣言型の構文で記述された、一連の句で構成されます。 各句には 1 つ以上の C# 式が含まれていて、それらの式は、それ自体がクエリ式である場合もあれば、クエリ式を含んでいる場合もあります。

クエリ式は from 句で始まり、select または group 句で終わる必要があります。 最初の from 句と最後の select または group 句の間には、次の省略可能句を 1 つ以上含めることができます: fromselectgrouplet、および追加の from 句。 また、into キーワードを使用して、 句や group 句の結果を、同じクエリ式内の追加のクエリ句のソースとして使用することもできます。

クエリ変数

LINQ では、クエリの結果ではなく、クエリを格納する変数を、クエリ変数と呼びます。 より具体的に言うと、クエリ変数は常に列挙可能な型であり、foreach ステートメントか、または IEnumerator.MoveNext メソッドに対する直接呼び出しで反復処理された場合に、要素のシーケンスを生成します。

次のコード例は、データ ソース、フィルター句、および並べ替え句がそれぞれ 1 つずつあり、ソース要素の変換がない、簡単なクエリ式を示したものです。 select 句でクエリが終わっています。

// Data source.
int[] scores = { 90, 71, 82, 93, 75, 82 };

// Query Expression.
IEnumerable<int> scoreQuery = //query variable
    from score in scores //required
    where score > 80 // optional
    orderby score descending // optional
    select score; //must end with select or group

// Execute the query to produce the results
foreach (int testScore in scoreQuery)
{
    Console.WriteLine(testScore);
}

// Output: 93 90 82 82

上記の例では、scoreQueryscoreQueryです。クエリ変数は単にクエリと呼ばれることもあります。 クエリ変数には、foreach ループで生成された実際の結果データは格納されません。 また、foreach ステートメントが実行されたとき、クエリ結果はクエリ変数 scoreQuery を通じては返されません。 結果は反復変数 testScore を通じて返されます。 scoreQuery 変数は 2 番目の foreach ループで反復処理できます。 この変数とデータ ソースのどちらかが変更されないかぎり、同じ結果が生成されます。

クエリ変数には、クエリ構文、メソッド構文、またはそれら 2 つの組合せで表現されたクエリが格納される場合があります。 次の例では、queryMajorCitiesqueryMajorCities2 の両方がクエリ変数です。

//Query syntax
IEnumerable<City> queryMajorCities =
    from city in cities
    where city.Population > 100000
    select city;

// Method-based syntax
IEnumerable<City> queryMajorCities2 = cities.Where(c => c.Population > 100000);

これに対し、次の 2 つの例は、クエリで初期化されてはいるものの、クエリ変数ではない変数を示しています。 これらは結果を格納するので、クエリ変数ではありません。

int highestScore = (
    from score in scores
    select score
).Max();

// or split the expression
IEnumerable<int> scoreQuery =
    from score in scores
    select score;

int highScore = scoreQuery.Max();
// the following returns the same result
highScore = scores.Max();
List<City> largeCitiesList = (
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city
).ToList();

// or split the expression
IEnumerable<City> largeCitiesQuery =
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city;

List<City> largeCitiesList2 = largeCitiesQuery.ToList();

クエリのさまざまな表現方法については、「LINQ でのクエリ構文とメソッド構文」をご覧ください。

クエリ変数の明示的型指定と暗黙的型指定

このドキュメントでは通常、明示的な型のクエリ変数で説明を行います。これは、クエリ変数と select 句の関係を示すためです。 ただし、 var キーワードを使用すれば、コンパイル時にクエリ変数 (またはその他のローカル変数) の型を推論するようにコンパイラに指示することもできます。 たとえば、このトピックで先に示したクエリの例は、暗黙的な型指定を使用しても表現できます。

// Use of var is optional here and in all queries.
// queryCities is an IEnumerable<City> just as
// when it is explicitly typed.
var queryCities =
    from city in cities
    where city.Population > 100000
    select city;

詳しくは、「暗黙的に型指定されるローカル変数」および「LINQ クエリ操作での型の関係」をご覧ください。

クエリ式の開始

クエリ式は、from 句で始める必要があります。 この句では、データ ソースと範囲変数を指定します。 範囲変数は、ソース シーケンスが走査されるときの、ソース シーケンス内の連続する各要素を表します。 範囲変数は、データ ソース内の要素の型に基づいて厳密に型指定されます。 次の例では、countriesCountry オブジェクトの配列であるため、範囲変数も Country として型指定されています。 範囲変数は厳密に型指定されるので、ドット演算子を使用して、その型の利用可能なメンバーにアクセスすることができます。

IEnumerable<Country> countryAreaQuery =
    from country in countries
    where country.Area > 500000 //sq km
    select country;

範囲変数は、クエリがセミコロンまたは continuation 句で終了するまでスコープ内に維持されます。

クエリ式には、複数の from 句を含めることができます。 ソース シーケンス内の各要素がそれ自体コレクションであるか、またはコレクションを格納している場合には、追加の from 句を使用します。 たとえば、Country オブジェクトのコレクションがあり、各オブジェクトに、Cities という名前の City オブジェクトのコレクションが格納されているとします。 その場合、各 Country 内の City オブジェクトを照会するには、次のように 2 つの from 句を使用します。

IEnumerable<City> cityQuery =
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city;

詳しくは、「from 句」をご覧ください。

クエリ式の終了

クエリ式は、group 句または select 句のいずれかで終わる必要があります。

group 句

group 句は、指定したキーによって編成されたグループのシーケンスを生成するために使用します。 キーには、任意のデータ型を指定できます。 たとえば、次のクエリを実行すると、1 つ以上の Country オブジェクトが含まれ、国の名前の最初の文字を値とする char 型がキーである、グループのシーケンスが作成されます。

var queryCountryGroups =
    from country in countries
    group country by country.Name[0];

グループ化について詳しくは、「group 句」をご覧ください。

select 句

select 句は、その他すべての型のシーケンスを生成するために使用します。 シンプルな select 句は、データ ソース内に含まれるオブジェクトと同じ型のオブジェクトのシーケンスを生成します。 この例では、データ ソースに Country オブジェクトが含まれています。 orderby 句は要素を新しい順序に並べ替え、select 句は並べ替えられた Country オブジェクトのシーケンスを生成します。

IEnumerable<Country> sortedQuery =
    from country in countries
    orderby country.Area
    select country;

select 句は、ソース データを新しい型のシーケンスに変換するために使用できます。 この変換は、プロジェクションとも呼ばれます。 次の例では、select 句は元の要素内にあるフィールドのサブセットのみを含んだ、匿名型のシーケンスをselectします。 新しいオブジェクトはオブジェクト初期化子を使用して初期化されています。

// Here var is required because the query
// produces an anonymous type.
var queryNameAndPop =
    from country in countries
    select new
    {
        Name = country.Name,
        Pop = country.Population
    };

select 句を使用してソース データを変換する方法について詳しくは、「select」をご覧ください。

into を使用した継続

select 句または group 句で into キーワードを使用すると、クエリを格納する一時的な識別子を作成できます。 これは、grouping 操作や select 操作の後、クエリに対する追加のクエリ操作を実行する必要がある場合に便利です。 次の例では、1 千万という範囲の人口で countries をグループ化しています。 これらのグループが作成された後、追加の句で一部のグループを除外し、その後、グループを昇順で並べ替えようとしています。 これらの追加操作を実行するには、countryGroup によって継続を表す必要があります。

// percentileQuery is an IEnumerable<IGrouping<int, Country>>
var percentileQuery =
    from country in countries
    let percentile = (int)country.Population / 10_000_000
    group country by percentile into countryGroup
    where countryGroup.Key >= 20
    orderby countryGroup.Key
    select countryGroup;

// grouping is an IGrouping<int, Country>
foreach (var grouping in percentileQuery)
{
    Console.WriteLine(grouping.Key);
    foreach (var country in grouping)
    {
        Console.WriteLine(country.Name + ":" + country.Population);
    }
}

詳しくは、「into」をご覧ください。

フィルター処理、並べ替え、および結合

開始の from 句と、終了の select またはgroup句の間には、その他のすべての省略可能句 (wherejoinorderbyfromlet) を必要に応じて使用できます。 省略可能句は、クエリ本文で任意の回数 (0 回~複数回) 使用できます。

where 句

where 句は、1 つ以上の述語式に基づいて、ソース データから要素を除外するために使用します。 次の例では、where 句に 1 つの述語と 2 つの条件があります。

IEnumerable<City> queryCityPop =
    from city in cities
    where city.Population < 200000 && city.Population > 100000
    select city;

詳しくは、「where 句」をご覧ください。

orderby 句

orderby 句は、結果を昇順または降順で並べ替えるために使用します。 第 2 の並べ替え順序を指定することもできます。 次の例では、Area プロパティを使用して、country オブジェクトに対する 第 1 の並べ替えを実行しています。 その後、Population プロパティを使用して第 2 の並べ替えを実行しています。

IEnumerable<Country> querySortedCountries =
    from country in countries
    orderby country.Area, country.Population descending
    select country;

ascending キーワードは省略可能です。順序が指定されていない場合は、これが既定の並べ替え順序になります。 詳しくは、「orderby 句」をご覧ください。

join 句

join 句は、各要素内の指定したキー間での等値比較に基づいて、1 つのデータ ソースの要素を別のデータ ソースの要素と関連付けたり、組み合わせたりするために使用します。 LINQ では、join 操作は要素の型が異なるオブジェクトのシーケンスに対して実行されます。 2 つのシーケンスを結合した後には、select または group ステートメント使用して、出力シーケンスに格納する要素を指定する必要があります。 また、匿名型を使用して、関連付けられた各要素セットのプロパティを、出力シーケンス用の新しい型に結合することもできます。 次の例では、Category プロパティが categories 文字列配列内のいずれかのカテゴリと一致する prod オブジェクトを関連付けています。 Categorycategories 内のどの文字列とも一致しない製品は除外されます。select ステートメントにより、catprod の両方からプロパティが取得された新しい型のプロジェクションが行われます。

var categoryQuery =
    from cat in categories
    join prod in products on cat equals prod.Category
    select new
    {
        Category = cat,
        Name = prod.Name
    };

join キーワードを使用して join 操作の結果を一時変数に格納することにより、グループ結合を実行することもできます。 詳細については、「join 句」を参照してください。

let 句

let 句は、式の結果 (メソッド呼び出しなど) を新しい範囲変数に格納するために使用します。 次の例では、Split よって返された文字列配列の最初の要素を、範囲変数 firstName に格納しています。

string[] names = { "Svetlana Omelchenko", "Claire O'Donnell", "Sven Mortensen", "Cesar Garcia" };
IEnumerable<string> queryFirstNames =
    from name in names
    let firstName = name.Split(' ')[0]
    select firstName;

foreach (string s in queryFirstNames)
{
    Console.Write(s + " ");
}

//Output: Svetlana Claire Sven Cesar

詳しくは、「let 句」をご覧ください。

クエリ式内のサブクエリ

クエリ句には、それ自体にクエリ式が含まれることがあります。これは、サブクエリとも呼ばれます。 各サブクエリは、独自の from で始まります。この句は、最初の from 句と必ずしも同じデータ ソースを指している必要はありません。 たとえば、次のクエリは、select ステートメントで grouping 操作の結果を取得するために使用されるクエリ式を示しています。

var queryGroupMax =
    from student in students
    group student by student.Year into studentGroup
    select new
    {
        Level = studentGroup.Key,
        HighestScore = (
            from student2 in studentGroup
            select student2.ExamScores.Average()
        ).Max()
    };

詳細については、「グループ化操作でのサブクエリの実行」を参照してください。

関連項目