射影操作 (C#)
射影とは、オブジェクトを、必要なプロパティだけで構成された別の形式に変換する操作のことをいいます。 射影を使用することにより、個々のオブジェクトから構築された新しい型を作成できます。 プロパティを投影し、それに対して数値演算関数を実行できます。 また、元のオブジェクトを変更せずに射影することもできます。
次のセクションでは、射影を実行する標準クエリ演算子メソッドの一覧を示します。
メソッド
メソッド名 | 説明 | 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 = new() { "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
*/
SelectMany
次の例では、from
句を複数使用して、文字列リストにある各文字列の各単語を射影します。
List<string> phrases = new() { "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
*/
Zip
Zip
射影演算子には複数のオーバーロードがあります。 すべての Zip
メソッドは、2 つ以上の異なる可能性がある型のシーケンスに対して機能します。 最初の 2 つのオーバーロードからは、指定されたシーケンスからの対応する位置の型を含むタプルが返されます。
次のコレクションについて考えます。
// An int array with 7 elements.
IEnumerable<int> numbers = new[]
{
1, 2, 3, 4, 5, 6, 7
};
// A char array with 6 elements.
IEnumerable<char> letters = new[]
{
'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 操作から結果として得られるシーケンスの長さが、最短のシーケンスより長くなることはありません。 コレクション numbers
と letters
は長さが異なります。結果のシーケンスでは、numbers
コレクションの最後の要素は結合するものがないため省略されます。
2 番目のオーバーロードは、third
シーケンスを受け取ります。 たとえば emoji
という別のコレクションを作成してみましょう。
// A string array with 8 elements.
IEnumerable<string> emoji = new[]
{
"🤓", "🔥", "🎉", "👀", "⭐", "💜", "✔", "💯"
};
これらのシーケンスをまとめて射影するには、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>
結果セレクターとして機能する引数を受け入れます。 シーケンスから結合する 2 つの型を指定すると、新しいシーケンスを作成できます。
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
オーバーロードでは、対応する要素 numbers
と letter
に指定した関数が適用され、string
の結果の 1 つのシーケンスが生成されます。
Select
と SelectMany
Select
と SelectMany
の機能はどちらも、ソース値から結果値 (複数も可) を生成することです。 Select
は、ソース値ごとに結果値を 1 つ生成します。 そのため、結果全体は、ソース コレクションと同じ数の要素を持つ 1 つのコレクションになります。 これに対し、SelectMany
は、各ソース値から、連結されたサブコレクションを含む 1 つの総合的な結果を生成します。 SelectMany
に引数として渡される変換関数は、ソース値ごとに列挙可能な値のシーケンスを返す必要があります。 この列挙可能なシーケンスは SelectMany
によって連結されて、1 つの大きなシーケンスが作成されます。
これら 2 つのメソッドのアクションの概念的な違いを次の 2 つの図に示します。 どちらも、セレクター (変換) 関数が各ソース値から花の配列を選択することを想定しています。
次の図は、Select
がソース コレクションと同じ数の要素を持つコレクションを返すしくみを示しています。
次の図は、SelectMany
が中間配列シーケンスを、各中間配列の値を含む最終的な結果値に連結するしくみを示しています。
コードの例
次の例は、Select
と SelectMany
の動作を比較しています。 コードは、ソース コレクションの花の名前の各リストから項目を取って "花束" を作成します。 この例では、変換関数 Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,TResult>) が使用する "単一の値" 自体が値のコレクションになっています。 各サブ シーケンスで各文字列を列挙するために追加の foreach
ループを使用しています。
class Bouquet
{
public List<string> Flowers { get; set; }
}
static void SelectVsSelectMany()
{
List<Bouquet> bouquets = new()
{
new Bouquet { Flowers = new List<string> { "sunflower", "daisy", "daffodil", "larkspur" }},
new Bouquet { Flowers = new List<string> { "tulip", "rose", "orchid" }},
new Bouquet { Flowers = new List<string> { "gladiolis", "lily", "snapdragon", "aster", "protea" }},
new Bouquet { Flowers = new List<string> { "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);
/* This code produces the following output:
Results by using Select():
sunflower
daisy
daffodil
larkspur
tulip
rose
orchid
gladiolis
lily
snapdragon
aster
protea
larkspur
lilac
iris
dahlia
Results by using SelectMany():
sunflower
daisy
daffodil
larkspur
tulip
rose
orchid
gladiolis
lily
snapdragon
aster
protea
larkspur
lilac
iris
dahlia
*/
}