Projektionsvorgänge (C#)

„Projektion“ bezeichnet einen Vorgang, bei dem ein Objekt in eine neue Form transformiert wird, die häufig nur aus den Eigenschaften besteht, die anschließend verwendet werden. Mithilfe der Projektion können Sie einen neuen Typ erstellen, der aus den einzelnen Objekten erstellt wird. Sie können eine Eigenschaft projizieren und eine mathematische Funktion für sie ausführen. Sie können auch das ursprüngliche Objekt projizieren, ohne es zu ändern.

Wichtig

In diesen Beispielen wird eine System.Collections.Generic.IEnumerable<T>-Datenquelle verwendet. Datenquellen, die auf System.Linq.IQueryProvider basieren, verwenden System.Linq.IQueryable<T>-Datenquellen und Ausdrucksbaumstrukturen. Ausdrucksbaumstrukturen haben Einschränkungen für die zulässige C#-Syntax. Darüber hinaus kann jede IQueryProvider-Datenquelle, z. B. EF Core, weitere Einschränkungen erzwingen. Konsultieren Sie die Dokumentation für Ihre Datenquelle.

Die Methoden des Standardabfrageoperators, die Projektion ausführen, sind im folgenden Abschnitt aufgeführt.

Methoden

Methodennamen BESCHREIBUNG C#-Abfrageausdruckssyntax Weitere Informationen
Auswählen Projektwerte, die auf einer Transform-Funktion basieren. select Enumerable.Select
Queryable.Select
SelectMany Projiziert Sequenzen von Werten, die auf einer Transform-Funktion basieren, und fasst diese dann in eine Sequenz zusammen. Mehrere from-Klauseln verwenden Enumerable.SelectMany
Queryable.SelectMany
Zip Erzeugt eine Tupelsequenz mit Elementen aus 2–3 angegebenen Sequenzen. Nicht zutreffend Enumerable.Zip
Queryable.Zip

Select

Im folgenden Beispiel wird die select-Klausel verwendet, um den ersten Buchstaben jeder Zeichenfolge in einer Liste von Zeichenfolgen zu projizieren.

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
*/

Die entsprechende Abfrage mit Methodensyntax ist im folgenden Code dargestellt:

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

Im folgenden Beispiel werden mehrere from-Klauseln verwendet, um die einzelnen Wörter aus den einzelnen Zeichenfolgen in eine Liste von Zeichenfolgen zu projizieren.

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
*/

Die entsprechende Abfrage mit Methodensyntax ist im folgenden Code dargestellt:

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
*/

Die SelectMany-Methode kann auch eine Kombination aus der Übereinstimmung mit jedem Element in der ersten Sequenz mit jedem Element in der zweiten Sequenz bilden:

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

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

Die entsprechende Abfrage mit Methodensyntax ist im folgenden Code dargestellt:

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

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

Zip

Für den Zip-Projektionsoperator gibt es mehrere Überladungen. Alle Zip-Methoden werden auf Sequenzen aus mindestens zwei möglicherweise heterogenen Typen angewendet. Die ersten beiden Überladungen geben Tupel mit dem entsprechenden Positionstyp aus den angegebenen Sequenzen zurück.

Betrachten Sie die folgenden Sammlungen:

// 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'];

Um diese Sequenzen zusammen zu projizieren, verwenden Sie den Operator 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'

Wichtig

Die aus einem ZIP-Vorgang resultierende Sequenz ist nie länger als die kürzeste Sequenz. Die Sammlungen numbers und letters unterscheiden sich in der Länge. In der resultierenden Sequenz wird das letzte Element aus der Sammlung numbers ausgelassen, weil ihm eine Entsprechung fehlt.

Die zweite Überladung akzeptiert eine third-Sequenz. Wir erstellen eine weitere Sammlung namens emoji:

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

Um diese Sequenzen zusammen zu projizieren, verwenden Sie den Operator 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: 💜

Ähnlich wie bei der vorherigen Überladung projiziert die Zip-Methode ein Tupel, dieses Mal jedoch mit drei Elementen.

Die dritte Überladung akzeptiert ein Func<TFirst, TSecond, TResult>-Argument, das als Ergebnisselektor fungiert. Sie können eine neue resultierende Sequenz aus den gezippten Sequenzen projizieren.

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)

Mit der obigen Zip-Überladung wird die angegebene Funktion auf die entsprechenden Elemente numbers und letter angewendet, wodurch eine Sequenz der string-Ergebnisse erzeugt wird.

Select im Vergleich mit SelectMany

Die Arbeit von jeweils Select und SelectMany besteht darin, einen Ergebniswert (oder Werte) aus den Quellwerten zu erstellen. Select generiert einen Ergebniswert für jeden Quellwert. Das Ergebnis ist daher eine Auflistung, die über die gleiche Anzahl von Elementen wie die Quellauflistung verfügt. Im Gegensatz dazu erzeugt SelectMany ein einziges Gesamtergebnis, das verkettete untergeordnete Auflistungen aus jedem Quellwert enthält. Die Transform-Funktion, die als Argument an SelectMany übergeben wird, muss eine aufzählbare Sequenz von Werten für jeden Quellwert zurückgeben. SelectMany verkettet diese enumerierbaren Sequenzen, um eine große Sequenz zu erstellen.

Die folgenden zwei Abbildungen zeigen den konzeptionellen Unterschied zwischen den Aktionen der beiden Methoden. In jedem Fall wird davon ausgegangen, dass die Auswahlfunktion (Transform) das Array von Blumen aus jedem Quellwert auswählt.

Die Abbildung zeigt, wie Select eine Auflistung zurückgibt, die über die gleiche Anzahl von Elementen wie die Quellauflistung verfügt.

Grafische Darstellung der Aktion „Select()“

Diese Abbildung zeigt, wie SelectMany die Zwischenmodus-Sequenz von Arrays in einem Endergebniswert verkettet, der jeden Wert aus jedem Zwischenmodus-Array enthält.

Grafische Darstellung der Aktion SelectMany()

Codebeispiel

Im folgenden Beispiel wird das Verhalten von Select und SelectMany verglichen. Der Code erstellt anhand der Elemente aus jeder Liste von Blumennamen in der Quellauflistung einen „Blumenstrauß“. Im folgenden Beispiel ist der „einzelne Wert“, den die Transformationsfunktion Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,TResult>) verwendet, eine Auflistung von Werten. Dieses Beispiel erfordert die zusätzliche foreach-Schleife, um jede Zeichenfolge in den einzelnen Untersequenzen aufzulisten.

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);
    }
}

Siehe auch