CodeGym /Kurse /C# SELF /Sammlungen "plattmachen" mit

Sammlungen "plattmachen" mit SelectMany

C# SELF
Level 31, Lektion 4
Verfügbar

1. Einführung

Wenn du nur mit einer "flachen" Liste von Objekten arbeitest (zum Beispiel mit einer Liste unserer Produkte oder User aus den vorherigen Vorlesungen), ist alles ziemlich easy: filtern, transformieren, sortieren. Aber in echten Business-Apps, bei Vorstellungsgesprächen (und sogar bei Hausaufgaben!) begegnet man oft Sammlungen, die wiederum andere Sammlungen enthalten.

Zum Beispiel gibt es eine Klasse User, die eine Liste von Bestellungen hat. Oder sagen wir, eine Klasse Product, die eine Liste von Bewertungen hat. Und jetzt die Aufgabe: Wie bekommst du eine Liste aller Bestellungen aller User? Oder alle Bewertungen zu allen Produkten? Und was, wenn du eine Liste aller Produkte in allen Bestellungen brauchst?

In solchen Situationen reicht ein normales Select und selbst die Magie von C# manchmal nicht aus. Hier kommt der Held ins Spiel – der Operator SelectMany, der eine "Liste von Listen" einfach in eine "Liste" verwandelt.

Küchen-Analogie

Stell dir vor, du hast eine Kiste, in der noch weitere Kisten sind, und darin Kekse. Deine Aufgabe: Alle Kekse in eine große Schüssel kippen. Du würdest nicht jede Kiste nehmen und einzeln Keks für Keks rausholen, sondern einfach alle Kisten und alle Kekse auf einmal in die Schüssel schütten.

Genau das macht SelectMany: Er "plättet" die Sammlung und verwandelt eine "Kiste mit Kisten" in eine große "Schüssel" mit Inhalt.

2. Erinnern wir uns an das Beispiel-Projekt

In den letzten Vorlesungen haben wir eine einfache App gebaut, um mit Usern und Produkten zu arbeiten. Für diese Vorlesung fügen wir Sammlungen von "verschachtelten" Elementen hinzu.

Angenommen, wir haben folgende Klassen:


public class User
{
    public string Name { get; set; }
    public List<Order> Orders { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public List<Product> Products { get; set; }
}

public class Product
{
    public string Name { get; set; }
}

Jetzt haben wir also eine Liste von Usern, jeder hat eine Liste von Bestellungen, und jede Bestellung hat eine Liste von Produkten.

Wie bekommt man alle Produkte aus allen Bestellungen aller User?

Angenommen, wir wollen eine Gesamtliste aller Produkte – ohne Verschachtelung, einfach alles in eine "Schüssel". Was passiert, wenn wir ein normales Select benutzen?


List<User> users = ...; // nehmen wir an, die Daten sind schon irgendwo da

var allOrders = users.Select(u => u.Orders);

Der Typ der Variable allOrders ist... IEnumerable<List<Order>>. Wir haben eine "Liste von Listen", also viele Bestellungs-Sammlungen. Wir wollen aber eine flache Liste aller Bestellungen.

Versuchen wir es nochmal:


var allProducts = users.Select(u => u.Orders)
                       .Select(oList => oList.Select(o => o.Products));

Und jetzt? Jetzt haben wir eine "Liste von Listen von Listen"! Wir machen die Struktur nur noch komplizierter, statt sie zu vereinfachen.

3. Die Lösung: Der Operator SelectMany

Hier liegt die ganze Power von SelectMany. Seine Aufgabe: Verschachtelte Sammlungen zu einer einzigen "flachen" (flatten) machen.

Allgemeiner Syntax:


collection.SelectMany(item => item.InnereSammlung)

In unserem Fall:


var allOrders = users.SelectMany(u => u.Orders);
// Typ: IEnumerable<Order>

Jetzt haben wir einfach eine Sequenz aller Bestellungen aller User.

Gehen wir noch tiefer! Alle Produkte aus allen Bestellungen aller User:


var allProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products);
// Typ: IEnumerable<Product>

Und in einer Zeile?

Klar, geht auch so:


var allProducts = users.SelectMany(u => u.Orders.SelectMany(o => o.Products));

Diese Kette liest sich besser in Teilen – außer du bist ein Roboter.

4. Großes Beispiel zum Durchkauen

Lass uns Testdaten bauen und ganz praktisch anschauen, wie SelectMany funktioniert.


var users = new List<User>
{
    new User
    {
        Name = "Alisa",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 1,
                Products = new List<Product>
                {
                    new Product { Name = "Keks" },
                    new Product { Name = "Milch" },
                }
            },
            new Order
            {
                Id = 2,
                Products = new List<Product>
                {
                    new Product { Name = "Schokolade" }
                }
            }
        }
    },
    new User
    {
        Name = "Bob",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 3,
                Products = new List<Product>
                {
                    new Product { Name = "Kaffee" },
                    new Product { Name = "Tee" }
                }
            }
        }
    }
};

Alle Produkte in eine Schüssel "ausschütten"


var allProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products);

foreach (var product in allProducts)
{
    Console.WriteLine(product.Name);
}

Ergebnis des Programms:

Keks
Milch
Schokolade
Kaffee
Tee

Was passiert in jedem Schritt?

  1. users.SelectMany(u => u.Orders) – Wir nehmen alle User und "entpacken" ihre Bestellungen in eine Sammlung von Bestellungen (jetzt haben wir 3 Bestellungen).
  2. .SelectMany(o => o.Products) – Wir entpacken die Produktlisten aus allen Bestellungen.

Achtung: Wenn wir ein normales Select benutzt hätten, hätten wir eine Sammlung von Sammlungen bekommen (zum Beispiel IEnumerable<List<Product>>). Mit SelectMany bekommen wir eine "flache" Produktsammlung: IEnumerable<Product>.

5. Wie sieht das als Schema aus?

Vor SelectMany


users (List<User>)
   └── User #1
   │      └── Orders (List<Order>)
   │           ├── Order #1 -> Products (List<Product>)
   │           └── Order #2 -> Products (List<Product>)
   └── User #2
          └── Orders (List<Order>)
               └── Order #3 -> Products (List<Product>)

Nach dem ersten SelectMany(u => u.Orders)


IEnumerable<Order>: [Order #1, Order #2, Order #3]

Nach dem zweiten SelectMany(o => o.Products)


IEnumerable<Product>: [Keks, Milch, Schokolade, Kaffee, Tee]

6. Vergleich: Select vs. SelectMany

Select SelectMany
Gibt zurück Sammlung von "Sammlungen" Flache Sammlung
Beispiel
users.Select(u => u.Orders)
→
IEnumerable<List<Order>>
users.SelectMany(u => u.Orders)
→
IEnumerable<Order>
Wird benutzt Wenn die Verschachtelung erhalten bleiben soll Wenn du eine flache Sequenz brauchst

Tabelle: Wann welchen Operator benutzen?

Was willst du machen? Was benutzen? Ergebnis
Liste von Listen bekommen (nicht "plattmachen") Select
IEnumerable<ICollection<T>>
Eine "flache" Sequenz bekommen SelectMany
IEnumerable<T>
Paare "Eltern-Kind-Element" bekommen SelectMany mit Result-Selector
IEnumerable<CustomType>
Nur Elemente einer "flachen" Sammlung transformieren Select
IEnumerable<TransformedType>

7. Nützliche Feinheiten

Query Syntax (für Fans von "SQL-Style")

Für alle, die es gerne ausdrucksstark mögen – LINQ erlaubt es, Abfragen auch im SQL-ähnlichen Syntax zu schreiben:


var allProducts = from user in users
                  from order in user.Orders
                  from product in order.Products
                  select product;

Dieser Code macht dasselbe wie die SelectMany-Kette, sieht aber wie ein mehrstufiges "from" aus.

Eine Matrix "plattmachen"

Ein klassisches Beispiel für SelectMany ist die Arbeit mit einem zweidimensionalen Array oder einer Liste von Listen.


List<List<int>> matrix = new List<List<int>>
{
    new List<int> { 1, 2, 3 },
    new List<int> { 4, 5 },
    new List<int> { 6 }
};

Wie bekommt man eine Liste aller Zahlen?


var flat = matrix.SelectMany(row => row);
foreach (var value in flat)
{
    Console.Write(value + " "); // 1 2 3 4 5 6
}

8. Fortgeschrittener Syntax

Manchmal kannst du mit einer Überladung von SelectMany nicht nur das Element der inneren Sammlung zurückgeben, sondern auch Infos aus der äußeren.

Hier ein Beispiel: Du willst Paare "User-Name – Produkt-Name" für jedes Produkt in jeder Bestellung bekommen.


var userProductPairs = users.SelectMany(
    user => user.Orders.SelectMany(
        order => order.Products,
        (order, product) => new { UserName = user.Name, ProductName = product.Name }
    )
);

foreach (var pair in userProductPairs)
{
    Console.WriteLine($"{pair.UserName} hat {pair.ProductName} bestellt");
}

Hier ist der zweite Parameter des Lambdas das Element der äußeren Sammlung. Sehr praktisch, damit du Infos aus den äußeren Ebenen nicht "verlierst".

9. Praktische Szenarien

Viele Bestellungen, viele Produkte

Du bist Entwickler in einem Online-Shop. Du willst wissen, wie viele einzigartige Produkte alle Kunden gekauft haben? So geht's:


var uniqueProductNames = users
    .SelectMany(user => user.Orders)
    .SelectMany(order => order.Products)
    .Select(product => product.Name)
    .Distinct();

foreach (var name in uniqueProductNames)
{
    Console.WriteLine(name);
}

Flache Sammlung aus Hierarchie bauen

Du hast eine Liste von Abteilungen in einer Firma, jede Abteilung hat Mitarbeiter. Du willst eine Liste aller Mitarbeiter der Firma:


var allEmployees = departments.SelectMany(d => d.Employees);

Mit Strings arbeiten: Array von Wörtern in Zeichen "plattmachen"


string[] words = { "hello", "world" };
var allChars = words.SelectMany(w => w.ToCharArray());
foreach (var c in allChars)
{
    Console.Write(c + " "); // h e l l o w o r l d
}

10. Typische Fehler und Feinheiten

Jetzt, wo du von der Power von SelectMany begeistert bist, ist es Zeit, vor ein paar häufigen Fehlern zu warnen.

Der beliebteste Fehler: Erwarten, dass Select eine "flache" Sammlung gibt. Tatsächlich gibt er immer eine Sammlung von Sammlungen zurück, wenn die innere Funktion eine Sammlung liefert. Am Ende musst du doppelte Schleifen schreiben oder verlierst dich in den Typen.

Der zweite Fehler: Nicht merken, dass das "Plattmachen" dazu führt, dass Infos über das äußere Element verloren gehen – zum Beispiel, wenn du nur mit Produkten arbeitest, weißt du nicht mehr, zu welcher Bestellung oder zu welchem User sie gehören, wenn du nicht die Überladung mit Result-Selector benutzt.

Dritter Punkt: Wenn deine inneren Listen null sein können, dann schütze dich vor SelectMany – entweder filtern oder durch eine leere Liste ersetzen, sonst bekommst du schneller ein NullReferenceException als du F5 drücken kannst.

2
Aufgabe
C# SELF, Level 31, Lektion 4
Gesperrt
Extrahieren aller Gegenstände aus verschachtelten Listen
Extrahieren aller Gegenstände aus verschachtelten Listen
1
Umfrage/Quiz
Einführung in LINQ, Level 31, Lektion 4
Nicht verfügbar
Einführung in LINQ
LINQ-Grundlagen: einfache Abfragen und Daten filtern
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION