CodeGym /Kursy /C# SELF /Praca z zagnieżdżonymi obiektami i kolekcjami

Praca z zagnieżdżonymi obiektami i kolekcjami

C# SELF
Poziom 45 , Lekcja 1
Dostępny

1. Wprowadzenie

Serializacja prostych obiektów — to jak wysyłanie pocztówki: wszystko proste, nic nie zginie. Ale często nasze obiekty stają się "rodzinnym albumem": mają zagnieżdżone obiekty, kolekcje, a nawet kolekcje kolekcji.

Wyobraź sobie, że nasz użytkownik to nie tylko Person z imieniem i wiekiem, ale np. ma zestaw kontaktów (Contact), kilka adresów domowych/służbowych, a może pole typu List<Pet>, jeśli osoba lubi zwierzęta. To wszystko to zagnieżdżone obiekty i kolekcje.

Jak serializatory .NET sobie z tym radzą? Jakie niuanse trzeba wziąć pod uwagę, kiedy serializujemy złożone drzewa obiektów? Czy wszystko może się zepsuć, jeśli w kolekcjach są inne kolekcje? Dziś się tego dowiemy — czego się spodziewać i co robić w trudnych przypadkach.

Co znaczy "zagnieżdżenie" w terminach .NET

Zagnieżdżony obiekt to po prostu kolejny obiekt jako właściwość lub pole wewnątrz twojego głównego obiektu. Na przykład, oto nasz rozszerzony model użytkownika:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<Contact> Contacts { get; set; }         // Kolekcja zagnieżdżonych obiektów
    public Address? HomeAddress { get; set; }           // Jeden zagnieżdżony obiekt (nullable)
}
public class Contact
{
    public string Type { get; set; }                    // Na przykład Email lub Phone
    public string Value { get; set; }
}
public class Address
{
    public string City { get; set; }
    public string Street { get; set; }
}

Zwróć uwagę — osoba może mieć kilka kontaktów, a adres tylko jeden (i może być null). To klasyka wielu modeli.

2. Serializacja obiektów i kolekcji z System.Text.Json

Prosty przykład: serializujemy i deserializujemy kolekcję

Sprawdźmy, jak to działa w praktyce:

using System.Text.Json;

var person = new Person
{
    Name = "Anna",
    Age = 28,
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "anna@example.com" },
        new Contact { Type = "Phone", Value = "+1234567890" }
    },
    HomeAddress = new Address { City = "Berlin", Street = "Alexanderplatz, 1" }
};

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

Wynik będzie mniej więcej taki:

{
  "Name": "Anna",
  "Age": 28,
  "Contacts": [
    {
      "Type": "Email",
      "Value": "anna@example.com"
    },
    {
      "Type": "Phone",
      "Value": "+1234567890"
    }
  ],
  "HomeAddress": {
    "City": "Berlin",
    "Street": "Alexanderplatz, 1"
  }
}

Serializator świetnie rozumie zagnieżdżenie. Jeśli właściwość to inny obiekt, serializuje go "zagnieżdżenie". Jeśli lista — serializuje jako tablicę.

Fakt: taka serializacja działa automatycznie dla dowolnych kolekcji i zagnieżdżonych obiektów, jeśli są publiczne i mają publiczny getter i setter (get/set).

Deserializacja: działa "out of the box"

string jsonInput = /* JSON, któryśmy właśnie otrzymali */;
Person deserializedPerson = JsonSerializer.Deserialize<Person>(jsonInput);
Console.WriteLine(deserializedPerson.Name); // "Anna"
Console.WriteLine(deserializedPerson.Contacts[0].Type); // "Email"

Wszystko działa — kontakty zamieniają się z tablicy JSON z powrotem w List<Contact>, HomeAddress — z powrotem w obiekt klasy Address.

3. Jak serializowane są kolekcje: List, tablice, Dictionary

Często w twoim modelu są nie tylko listy (List<T>), ale też słowniki (Dictionary<TKey, TValue>) lub wielowymiarowe tablice.

Przykład z tablicą

public class Team
{
    public string Name { get; set; }
    public Person[] Members { get; set; }
}

var team = new Team
{
    Name = "Programiści",
    Members = new[]
    {
        new Person { Name = "Alex", Age = 31 },
        new Person { Name = "Kate", Age = 27 }
    }
};
string json = JsonSerializer.Serialize(team, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "Name": "Programiści",
  "Members": [
    {
      "Name": "Alex",
      "Age": 31,
      "Contacts": null,
      "HomeAddress": null
    },
    {
      "Name": "Kate",
      "Age": 27,
      "Contacts": null,
      "HomeAddress": null
    }
  ]
}

Serializacja kolekcji jest podobna do pakowania sterty identycznych pocztówek: każdy element to osobna "kartka".

Przykład z Dictionary

public class Phonebook
{
    public Dictionary<string, string> Phones { get; set; }
}

var phonebook = new Phonebook
{
    Phones = new Dictionary<string, string>
    {
        { "Andy", "+12998887766" },
        { "Maria", "+12882223344" }
    }
};

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

JSON:

{
  "Phones": {
    "Andy": "+12998887766",
    "Maria": "+12882223344"
  }
}

W JSON słownik staje się obiektem z dynamicznymi kluczami.

4. Zagnieżdżone kolekcje (List wewnątrz List, tablice tablic)

.NET potrafi serializować też takie "matrioszki":

public class Zoo
{
    public List<List<Animal>> AnimalGroups { get; set; }
}
public class Animal { public string Name { get; set; } }

var zoo = new Zoo
{
    AnimalGroups = new List<List<Animal>>
    {
        new List<Animal> { new Animal { Name = "Lew" }, new Animal { Name = "Tygrys" } },
        new List<Animal> { new Animal { Name = "Niedźwiedź" } }
    }
};

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

JSON:

{
  "AnimalGroups": [
    [ { "Name": "Lew" }, { "Name": "Tygrys" } ],
    [ { "Name": "Niedźwiedź" } ]
  ]
}

To samo — przy deserializacji działa w drugą stronę.

5. Cechy serializacji zagnieżdżonych obiektów w XmlSerializer

Przykład serializacji zagnieżdżonego obiektu

using System.Xml.Serialization;
using System.IO;

var person = new Person
{
    Name = "Iwan",
    Age = 35,
    HomeAddress = new Address { City = "Bonn", Street = "Beethoven str., 100" },
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "ivan@domain.de" }
    }
};

var serializer = new XmlSerializer(typeof(Person));
using var writer = new StringWriter();
serializer.Serialize(writer, person);
Console.WriteLine(writer.ToString());

Wynik:

<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>Iwan</Name>
  <Age>35</Age>
  <Contacts>
    <Contact>
      <Type>Email</Type>
      <Value>ivan@domain.de</Value>
    </Contact>
  </Contacts>
  <HomeAddress>
    <City>Bonn</City>
    <Street>Beethoven str., 100</Street>
  </HomeAddress>
</Person>

Cechy kolekcji w XmlSerializer

Domyślnie XmlSerializer serializuje kolekcję jako tag "Contacts", wewnątrz którego dla każdego elementu tworzy osobny tag "Contact". Typ kolekcji musi być publiczny, a jej elementy — typami serializowalnymi.

Ważna rzecz. Jeśli kolekcja jest pusta, XML i tak zawiera pusty tag:

<Contacts />

Obsługiwane kolekcje. XmlSerializer wspiera List<T>, tablice T[], kolekcje implementujące ICollection<T>. Nie wspiera na przykład Dictionary<TKey, TValue>! Do serializacji słowników trzeba użyć opakowania słownika w listę par albo zastosować inne serializatory.

6. Wsparcie obiektów i kolekcji w Newtonsoft.Json

Newtonsoft.Json obsługuje zagnieżdżenia trochę bardziej elastycznie. W 99% przypadków działa podobnie jak System.Text.Json, ale są plusy: można serializować nawet prywatne pola (jeśli jawnie to ustawisz), słowniki z bardziej skomplikowanymi kluczami, typy dynamiczne, a nawet cykliczne referencje (przy użyciu specjalnych ustawień).

Przykład

using Newtonsoft.Json;

var person = new Person
{
    Name = "Pawel",
    Age = 40,
    Contacts = new List<Contact>
    {
        new Contact { Type = "SMS", Value = "+10000000013" }
    }
};
string json = JsonConvert.SerializeObject(person, Formatting.Indented);
Console.WriteLine(json);

Różnica. Jeśli obiekt ma prywatne pola, można je serializować przez ustawienia ContractResolver. Ale dla podstawowych zagnieżdżonych kolekcji i obiektów wszystko działa domyślnie "out of the box".

7. Typowe błędy, pułapki i niuanse

Błąd 1: Zagnieżdżone obiekty — null.
Jeśli obiekt, który serializujesz, ma właściwości nie wypełnione (null), to w JSON lub XML mogą one nie wystąpić wcale, albo będą zapisane jako null. Na przykład, jeśli HomeAddress u Person jest null, to w JSON:

"HomeAddress": null

W XML domyślnie takiego taga nie będzie — możesz go dodać ustawieniami serializatora ([XmlElement(IsNullable=true)]).

Błąd 2: Serializacja prywatnych właściwości/pól.
Domyślnie większość serializatorów (i JSON, i XML) działa tylko z publicznymi właściwościami. Jeśli chcesz serializować prywatne lub chronione pola, trzeba je uczynić publicznymi lub skonfigurować serializator (np. przez ContractResolver w Newtonsoft.Json).

Błąd 3: Klasy bez konstruktora domyślnego.
W serializacji XML (i często w JSON) klasa powinna mieć publiczny konstruktor bez parametrów. Jeśli takiego konstruktora brak — przy deserializacji dostaniesz wyjątek.

Błąd 4: Serializacja słowników w XML.
XmlSerializer nie obsługuje bezpośrednio serializacji Dictionary<TKey, TValue>. To często bywa niespodzianką. Rozwiązaniem może być opakowanie słownika w listę par albo użycie innych serializatorów.

Błąd 5: Cykliczne referencje.
Jeśli jeden obiekt przez referencję zawiera inny obiekt, który z kolei znowu odwołuje się do pierwszego (np. rodzic i dziecko), serializacja "po okręgu" może doprowadzić do przepełnienia stosu (StackOverflowException) lub wyjątku o cyklicznej referencji. W JSON-serializatorach można to obejść ustawieniami, ale często lepiej przemyśleć architekturę obiektu.

2
Zadanie
C# SELF, poziom 45, lekcja 1
Niedostępne
Serializacja i deserializacja złożonej struktury
Serializacja i deserializacja złożonej struktury
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION