CodeGym /Kurse /C# SELF /Serialisierung verschachtelter und hierarchischer Objekte...

Serialisierung verschachtelter und hierarchischer Objekte

C# SELF
Level 46 , Lektion 2
Verfügbar

1. Einführung

Stell dir eine Anwendung für einen Buchladen vor: Objekte dort sind nicht nur Bücher, sondern auch Autoren mit Biografien, Verlage, die Bücher herausgeben, Mitarbeiter, Sektionen... Wenn die Serialisierung nur "flache" Objekte unterstützen würde, würde unsere App auf dem Niveau eines Adressbuchs stecken bleiben. In realen Projekten sind Daten fast immer mehrstufig und verschachtelt. Deshalb ist das Können, hierarchische Strukturen zu serialisieren und zu deserialisieren, eine Fähigkeit, die einen durchschnittlichen Entwickler von einem echten .NET-Serialisierungsmeister unterscheidet.

Wir werden durchgehen, wie moderne Serialisierer in C# (am Beispiel von System.Text.Json) nicht nur Baumstrukturen, sondern ganze "Dschungel" speichern können. Und wie man Klassen so gestaltet, dass man später nicht die Daten manuell Stück für Stück herausziehen muss.

2. Modellierung hierarchischer Strukturen

Erweitern wir unser Modell. In früheren Beispielen hatten wir Objekte Book, Author und die Klasse Library, die eine Liste von Büchern hält. Jetzt fügen wir noch eine Ebene hinzu: jeder Author hat eine Liste von Büchern, die er geschrieben hat (ja, das dupliziert Informationen — später besprechen wir, was das bedeuten kann!), und die Library bekommt zusätzlich eine Liste von Mitarbeitern Employee.

Klassen-Schema

So wird unsere Objektstruktur ungefähr aussehen:

classDiagram
    class Library {
        List~Book~ Books
        List~Employee~ Employees
        string Name
    }
    class Book {
        string Title
        Author Author
        int Year
    }
    class Author {
        string Name
        int BirthYear
        List~Book~ Books
    }
    class Employee {
        string Name
        string Position
    }

    Library "1" -- "many" Book
    Library "1" -- "many" Employee
    Book "1" -- "1" Author
    Author "1" -- "many" Book

Dieser Ansatz erlaubt uns, ein vollwertiges Bibliothekssystem abzubilden. Ja, es besteht das Risiko von "verketteten" Referenzen (z. B. ein Author hat Bücher und ein Buch hat einen Author: ein endloser Serialisierungsloop!). Darauf gehen wir weiter unten ein.

3. Beispielimplementierung der Klassen

Zuerst beschreiben wir die C#-Klassen für unser Modell. Gleichzeitig fügen wir Kommentare hinzu:

using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;

public class Library
{
    public string Name { get; set; }
    public List<Book> Books { get; set; } = new();
    public List<Employee> Employees { get; set; } = new();
}

public class Book
{
    public string Title { get; set; }
    public int Year { get; set; }

    public Author Author { get; set; }
}

public class Author
{
    public string Name { get; set; }
    public int BirthYear { get; set; }
    
    // WICHTIG! Dieses Feld ist potenziell "gefährlich": es kann eine zyklische Referenz erzeugen.
    // Für die Demonstration lassen wir es trotzdem drin.
    public List<Book> Books { get; set; } = new();
}

public class Employee
{
    public string Name { get; set; }
    public string Position { get; set; }
}

4. Aufbau der Struktur: wir erstellen die Bibliothek

Erzeugen wir jetzt eine Bibliothek mit Büchern, Autoren und Mitarbeitern, um die Serialisierung zu testen.

// Erstellen der Autoren
var author1 = new Author { Name = "Viljam Golding", BirthYear = 1911 };
var author2 = new Author { Name = "Dzhon Apdajjk", BirthYear = 1932 };
var author3 = new Author { Name = "Dzherom Selindzher", BirthYear = 1919 };

// Erstellen der Bücher
var book1 = new Book { Title = "Povelitel muh", Year = 1954, Author = author1 };
var book2 = new Book { Title = "Kentavr", Year = 1963, Author = author2 };
var book3 = new Book { Title = "Nad propastyu vo rzhi", Year = 1951, Author = author3 };

// Bücher zu den Autoren hinzufügen
author1.Books.Add(book1);
author2.Books.Add(book2);
author3.Books.Add(book3);

// Erstellen der Mitarbeiter
var emp1 = new Employee { Name = "Ivan Ivanov", Position = "Bibliotekar" };
var emp2 = new Employee { Name = "Sergej Sergeev", Position = "Direktor" };

// Zusammensetzen der Bibliothek
var library = new Library
{
    Name = "Gorodskaja biblioteka",
    Books = new List<Book> { book1, book2, book3 },
    Employees = new List<Employee> { emp1, emp2 }
};

Natürlich würdet ihr in echtem Code die Erstellung der Objekte und Verknüpfungen automatisieren, damit ihr nicht manuell jede Buch-Autor-Beziehung pflegen müsst. Für unser Beispiel reicht das so.

5. Serialisierung nach JSON

Wir nutzen den klassischen Weg mit System.Text.Json:

using System.Text.Json;

// Wir serialisieren die Bibliothek nach JSON
var options = new JsonSerializerOptions
{
    WriteIndented = true, // schönes, eingerücktes Format
    ReferenceHandler = ReferenceHandler.IgnoreCycles // Verhindert Endlosschleifen!
};

string json = JsonSerializer.Serialize(library, options);

// Ergebnis ausgeben
Console.WriteLine(json);

Interessanter Punkt: Wenn man die spezielle Option ReferenceHandler.IgnoreCycles nicht verwendet, würde die Serialisierung in eine Schleife laufen — weil der Author eine Liste von Büchern hat und das Buch wieder auf den Author referenziert, würde der Serialisierer endlos hin- und herspringen, bis er entweder abstürzt oder eine Ausnahme wirft. Die Option IgnoreCycles löst das Problem: wenn der Serialisierer beim Traversieren sieht, dass ein Objekt schon weiter oben serialisiert wurde, schreibt er einfach null anstelle einer erneuten Serialisierung.

Wie sieht das JSON aus?

{
  "Name": "Gorodskaja biblioteka",
  "Books": [
    {
      "Title": "Kentavr",
      "Year": 1963,
      "Author": {
        "Name": "Dzhon Apdajjk",
        "BirthYear": 1932,
        "Books": [
          {
            "Title": "Kentavr",
            "Year": 1963,
            "Author": null
          },
          {
            "Title": "Istvaikskie vedmy",
            "Year": 1984,
            "Author": null
          }
        ]
      }
    },
    {
      "Title": "Istvaikskie vedmy",
      "Year": 1984,
      "Author": {
        "Name": "Dzhon Apdajjk",
        "BirthYear": 1932,
        "Books": [
          {
            "Title": "Kentavr",
            "Year": 1963,
            "Author": null
          },
          {
            "Title": "Istvaikskie vedmy",
            "Year": 1984,
            "Author": null
          }
        ]
      }
    }
  ],
  "Employees": [
    {
      "Name": "Ivan Ivanov",
      "Position": "Bibliotekar"
    },
    {
      "Name": "Sergej Sergeev",
      "Position": "Direktor"
    }
  ]
}

Beachte: innerhalb des Arrays Books hat der Author in den Büchern keine Author-Information mehr — stattdessen ist Author: null. Das ist der Weg, den Zyklus bei der Serialisierung zu durchbrechen.

6. Deserialisierung zurück in Objekte

Und jetzt deserialisieren wir die Daten zurück:

// Wiederherstellen des Objekts aus JSON
var libraryCopy = JsonSerializer.Deserialize<Library>(json, options);

Console.WriteLine(libraryCopy.Name); // "Gorodskaja biblioteka"
Console.WriteLine($"Knig: {libraryCopy.Books.Count}");
Console.WriteLine($"Sotrudnikov: {libraryCopy.Employees.Count}");

Aber! Das Wiederherstellen zyklischer Referenzen funktioniert hier nicht zu 100%: in den verschachtelten Büchern in der Liste des Authors ist das Feld Author gleich null, weil der Serialisierer die Kette abgeschnitten hat, um unendliche Verschachtelung zu verhindern.

Wichtiger Punkt: Die Serialisierung komplexer wechselseitiger Verknüpfungen (z. B. Parent — Children — Parent) mit dem Standard-Serialisierer erfordert immer einen Kompromiss: entweder gehen einige Verknüpfungen verloren, oder man muss sie nach der Deserialisierung manuell wiederherstellen.

7. Zyklische Referenzen (Verschachtelung vs. Zyklen)

Wenn in deiner Struktur Fälle vorkommen, bei denen ein Objekt über eine Kette auf sich selbst referenziert (zyklische Referenz) — reagieren Standard-Serialisierer wie System.Text.Json und Newtonsoft.Json, besonders im strikt typisierten Modus, unterschiedlich. Vor der Einführung der Option ReferenceHandler.IgnoreCycles endete die Serialisierung mit einer Ausnahme "ReferenceLoopHandling detected". Jetzt setzt er stattdessen einfach null anstelle der wiederholten Referenz.

Wo ist der Haken?

Plus: dein Code stürzt nicht mit einem Fehler ab.

Minus: nach der Deserialisierung muss man einen Teil der Verknüpfungen manuell wiederherstellen. Zum Beispiel, wenn ein Graph von Nutzern sich gegenseitig referenziert (z. B. Mitarbeiter und dessen Vorgesetzter) — nach der Deserialisierung kann eine der Referenzen leer sein.

8. Wie man komplexe Strukturen für die Serialisierung entwirft

Wenn du im Voraus weißt, dass deine Objekte Zyklen bilden, oder es dir wichtig ist, dass nach dem Wiederherstellen alle Verknüpfungen wie im Original bleiben — ist es besser, nicht die Objekte selbst zu speichern, sondern ihre Identifikatoren.

Beispiel: Identifikatoren statt Referenzen speichern

Ändern wir die Klasse Book, sodass die Referenz auf den Author über einen Identifier erfolgt:

public class Book
{
    public string Title { get; set; }
    public int Year { get; set; }

    public int AuthorId { get; set; }
}

Statt einer Liste von Book-Objekten hält der Author dann eine Liste von Buch-IDs. Zum Wiederaufbau der Beziehungen nach der Deserialisierung muss man dann per id "mappen", aber dafür entstehen keine gefährlichen Zyklen.

Warum ist das für reale Projekte wichtig?

  • Datenbanken verwenden fast immer Identifikatoren, weil mit ihnen Export/Import einfacher ist.
  • REST API tauschen in der Regel auch id's aus, statt komplett verschachtelter, schwergewichtiger Strukturen.

9. Verschachtelte Collections: Serialisierung von Bäumen

Ein häufiger Fall sind Hierarchien beliebiger Tiefe: Ordnerbäume, Menüstrukturen, Produktkataloge mit Unterkategorien.

Beispielklasse für einen "Baum":

public class Folder
{
    public string Name { get; set; }
    public List<Folder> Children { get; set; } = new();
}

Wir erstellen einen Baum:

var root = new Folder
{
    Name = "Root",
    Children = new List<Folder>
    {
        new Folder { Name = "Sub1", Children = { new Folder { Name = "Sub1-1" } } },
        new Folder { Name = "Sub2" }
    }
};

Serialisieren und ausgeben:

string jsonTree = JsonSerializer.Serialize(root, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonTree);

Das JSON für so einen Baum ist eine anschauliche Hierarchie mit verschachtelten Arrays.

10. Besonderheiten bei Arrays und Listen

Wenn eine Eigenschaft ein Array (T[]) oder eine Collection (List<T>) ist, wird die Serialisierung es in ein normales JSON-Array verwandeln.

public class Shop
{
    public string Name { get; set; }
    public string[] Departments { get; set; }
}
var shop = new Shop
{
    Name = "Universam",
    Departments = new[] { "Ovoschi", "Frukty", "Myaso" }
};

string jsonShop = JsonSerializer.Serialize(shop, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonShop);

Das JSON wird ungefähr so aussehen:

{
  "Name": "Universam",
  "Departments": [
    "Ovoschi",
    "Frukty",
    "Myaso"
  ]
}

11. Einfluss von Attributen auf verschachtelte Objekte

Wenn man [JsonIgnore] für Eigenschaften in verschachtelten Objekten verwendet, werden diese ebenfalls nicht ins finale JSON gelangen, unabhängig von der Verschachtelungstiefe.

public class SecretBook : Book
{
    [JsonIgnore]
    public string SecretCode { get; set; }
}

Diese Technik wird oft genutzt, um private Informationen zu schützen: wenn du bestimmte interne Daten nicht serialisieren willst, setzt du einfach das Attribut — und sie sind weg aus dem JSON.

12. Praktische Tipps

  • Bei Interviews wird oft gefragt: „Wie serialisiere ich einen Tree (Tree)?“ und „Was macht man mit zyklischen Referenzen?“. Bereite Beispiele mit ReferenceHandler.IgnoreCycles und dem Speichern von Identifikatoren vor.
  • In kommerziellen Projekten serialisiert man Bestellungen, Rechnungen, Nutzer, Produktkataloge, komplexe Reports. Verschachtelung ist überall.
  • Wenn du mit Graphen oder Bäumen arbeitest, vermeide wenn möglich Zyklen oder nutze id-Referenzen.
  • Bei Verwendung externer APIs stimmt das Format der verschachtelten Strukturen vorher ab, um Überraschungen beim Parsen von JSON zu vermeiden.
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION