射影操作 (C#)

プロジェクションとは、多くの場合は後に使用されるプロパティだけで構成される別の形式にオブジェクトを変換する操作を指します。 射影を使用することにより、個々のオブジェクトから構築された新しい型を作成できます。 プロパティを投影し、それに対して数値演算関数を実行できます。 また、元のオブジェクトを変更せずに射影することもできます。

重要

これらのサンプルでは、System.Collections.Generic.IEnumerable<T> データ ソースを使用します。 System.Linq.IQueryProvider に基づくデータ ソースでは、System.Linq.IQueryable<T> データ ソースと式ツリーが使用されます。 式ツリーには、許可される C# 構文に制限があります。 さらに、EF Core などの各 IQueryProvider データ ソースでは、より多くの制限が課される場合があります。 ご利用のデータ ソースのドキュメントをご覧ください。

次のセクションでは、射影を実行する標準クエリ演算子メソッドの一覧を示します。

メソッド

メソッド名 説明 C# のクエリ式の構文 説明を見る
選択 変換関数に基づいて値を射影します。 select Enumerable.Select
Queryable.Select
SelectMany 変換関数に基づいて値のシーケンスを射影し、それを 1 つのシーケンスに平坦化します。 複数の from 句を使用 Enumerable.SelectMany
Queryable.SelectMany
Zip 指定された 2 つから 3 つのシーケンスの要素を持つタプルのシーケンスを生成します。 適用不可。 Enumerable.Zip
Queryable.Zip

Select

次の例では、select 句を使って、文字列リストにある各文字列の最初の文字を射影します。

List<string> words = ["an", "apple", "a", "day"];

var query = from word in words
            select word.Substring(0, 1);

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    a
    a
    a
    d
*/

次のコードでは、メソッド構文を使用した同等のクエリを示しています。

List<string> words = ["an", "apple", "a", "day"];

var query = words.Select(word => word.Substring(0, 1));

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    a
    a
    a
    d
*/

SelectMany

次の例では、from 句を複数使用して、文字列リストにある各文字列の各単語を射影します。

List<string> phrases = ["an apple a day", "the quick brown fox"];

var query = from phrase in phrases
            from word in phrase.Split(' ')
            select word;

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    an
    apple
    a
    day
    the
    quick
    brown
    fox
*/

次のコードでは、メソッド構文を使用した同等のクエリを示しています。

List<string> phrases = ["an apple a day", "the quick brown fox"];

var query = phrases.SelectMany(phrases => phrases.Split(' '));

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    an
    apple
    a
    day
    the
    quick
    brown
    fox
*/

SelectMany メソッドは、最初のシーケンス内のすべての項目と 2 番目のシーケンスのすべての項目をマッチさせた組み合わせを形成することもできます。

var query = from number in numbers
            from letter in letters
            select (number, letter);

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

次のコードでは、メソッド構文を使用した同等のクエリを示しています。

var method = numbers
    .SelectMany(number => letters,
    (number, letter) => (number, letter));

foreach (var item in method)
{
    Console.WriteLine(item);
}

Zip

Zip 射影演算子には複数のオーバーロードがあります。 すべての Zip メソッドは、2 つ以上の異なる可能性がある型のシーケンスに対して機能します。 最初の 2 つのオーバーロードからは、指定されたシーケンスからの対応する位置の型を含むタプルが返されます。

次のコレクションについて考えます。

// An int array with 7 elements.
IEnumerable<int> numbers = [1, 2, 3, 4, 5, 6, 7];
// A char array with 6 elements.
IEnumerable<char> letters = ['A', 'B', 'C', 'D', 'E', 'F'];

これらのシーケンスをまとめて射影するには、Enumerable.Zip<TFirst,TSecond>(IEnumerable<TFirst>, IEnumerable<TSecond>) 演算子を使用します。

foreach ((int number, char letter) in numbers.Zip(letters))
{
    Console.WriteLine($"Number: {number} zipped with letter: '{letter}'");
}
// This code produces the following output:
//     Number: 1 zipped with letter: 'A'
//     Number: 2 zipped with letter: 'B'
//     Number: 3 zipped with letter: 'C'
//     Number: 4 zipped with letter: 'D'
//     Number: 5 zipped with letter: 'E'
//     Number: 6 zipped with letter: 'F'

重要

Zip 操作から結果として得られるシーケンスの長さが、最短のシーケンスより長くなることはありません。 コレクション numbersletters は長さが異なります。結果のシーケンスでは、numbers コレクションの最後の要素は結合するものがないため省略されます。

2 番目のオーバーロードは、third シーケンスを受け取ります。 たとえば emoji という別のコレクションを作成してみましょう。

// A string array with 8 elements.
IEnumerable<string> emoji = [ "🤓", "🔥", "🎉", "👀", "⭐", "💜", "✔", "💯"];

これらのシーケンスをまとめて射影するには、Enumerable.Zip<TFirst,TSecond,TThird>(IEnumerable<TFirst>, IEnumerable<TSecond>, IEnumerable<TThird>) 演算子を使用します。

foreach ((int number, char letter, string em) in numbers.Zip(letters, emoji))
{
    Console.WriteLine(
        $"Number: {number} is zipped with letter: '{letter}' and emoji: {em}");
}
// This code produces the following output:
//     Number: 1 is zipped with letter: 'A' and emoji: 🤓
//     Number: 2 is zipped with letter: 'B' and emoji: 🔥
//     Number: 3 is zipped with letter: 'C' and emoji: 🎉
//     Number: 4 is zipped with letter: 'D' and emoji: 👀
//     Number: 5 is zipped with letter: 'E' and emoji: ⭐
//     Number: 6 is zipped with letter: 'F' and emoji: 💜

前のオーバーロードと同様に、Zip メソッドはタプルを射影しますが、今度は 3 つの要素を持ちます。

3 番目のオーバーロードは、Func<TFirst, TSecond, TResult> 結果セレクターとして機能する引数を受け入れます。 zip されているシーケンスから新しい結果のシーケンスのプロジェクションを行うことができます。

foreach (string result in
    numbers.Zip(letters, (number, letter) => $"{number} = {letter} ({(int)letter})"))
{
    Console.WriteLine(result);
}
// This code produces the following output:
//     1 = A (65)
//     2 = B (66)
//     3 = C (67)
//     4 = D (68)
//     5 = E (69)
//     6 = F (70)

前の Zip オーバーロードでは、対応する要素 numbersletter に指定した関数が適用され、string の結果の 1 つのシーケンスが生成されます。

SelectSelectMany

SelectSelectMany の機能はどちらも、ソース値から結果値 (複数も可) を生成することです。 Select は、ソース値ごとに結果値を 1 つ生成します。 そのため、結果全体は、ソース コレクションと同じ数の要素を持つ 1 つのコレクションになります。 これに対し、SelectMany は、各ソース値から連結されたサブコレクションを含む 1 つの全体的な結果を生成します。 SelectMany に引数として渡される変換関数は、ソース値ごとに列挙可能な値のシーケンスを返す必要があります。 SelectMany は、これらの列挙可能なシーケンスを連結して 1 つの大きなシーケンスを作成します。

これら 2 つのメソッドのアクションの概念的な違いを次の 2 つの図に示します。 どちらも、セレクター (変換) 関数が各ソース値から花の配列を選択することを想定しています。

次の図は、Select がソース コレクションと同じ数の要素を持つコレクションを返すしくみを示しています。

Select() のアクションを示すグラフィック

次の図は、SelectMany が中間配列シーケンスを、各中間配列の値を含む最終的な結果値に連結するしくみを示しています。

SelectMany() のアクションを示すグラフィック

コードの例

次の例は、SelectSelectMany の動作を比較しています。 コードは、ソース コレクションの花の名前の各リストから項目を取って "花束" を作成します。 この例では、変換関数 Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,TResult>) が使用する "単一の値" は値のコレクションになっています。 この例では、各サブシーケンスの文字列の列挙を行うために追加の foreach ループが必要です。

class Bouquet
{
    public required List<string> Flowers { get; init; }
}

static void SelectVsSelectMany()
{
    List<Bouquet> bouquets =
    [
        new Bouquet { Flowers = ["sunflower", "daisy", "daffodil", "larkspur"] },
        new Bouquet { Flowers = ["tulip", "rose", "orchid"] },
        new Bouquet { Flowers = ["gladiolis", "lily", "snapdragon", "aster", "protea"] },
        new Bouquet { Flowers = ["larkspur", "lilac", "iris", "dahlia"] }
    ];

    IEnumerable<List<string>> query1 = bouquets.Select(bq => bq.Flowers);

    IEnumerable<string> query2 = bouquets.SelectMany(bq => bq.Flowers);

    Console.WriteLine("Results by using Select():");
    // Note the extra foreach loop here.
    foreach (IEnumerable<string> collection in query1)
    {
        foreach (string item in collection)
        {
            Console.WriteLine(item);
        }
    }

    Console.WriteLine("\nResults by using SelectMany():");
    foreach (string item in query2)
    {
        Console.WriteLine(item);
    }
}

関連項目