1. Wprowadzenie
Słownik (albo Dictionary<TKey, TValue> w C#) — to kolekcja par "klucz-wartość". Taki typ danych jest niezastąpiony, gdy trzeba szybko znaleźć wartość po unikalnym identyfikatorze (np. szukać telefonu po imieniu w książce adresowej).
W przeciwieństwie do list (List<T>), gdzie kolejność elementów ma znaczenie, słownik skupia się na szybkim dostępie do danych po kluczu. Ale jeśli przy serializacji listy wszystko jest proste (JSON-tablica), to przy serializacji słownika pojawia się szereg niuansów:
- Klucz musi być typem serializowalnym (najczęściej string, ale czasem może to być liczba albo nawet inny obiekt).
- W JSON nie ma oddzielnych słowników — są tylko obiekty albo tablice.
Przyjrzyjmy się dokładniej, jak .NET serializuje słowniki, z jakimi trudnościami można się spotkać i jak poprawnie "nauczyć" nasz kod pracować z takimi strukturami.
2. Serializacja słownika z kluczami-stringami
Zacznijmy od klasyki — słownik, gdzie zarówno klucz, jak i wartość są stringami.
// Przykład słownika: książka i jej autor
var books = new Dictionary<string, string>
{
["The Master and Margarita"] = "Michaił Bułhakow",
["Harry Potter"] = "Dżoan Rouling",
["Lord of the Flies"] = "William Golding"
};
// Serializacja do łańcucha JSON
string json = JsonSerializer.Serialize(books, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine("Słownik zserializowany do JSON:\n" + json);
// Zapiszemy do pliku (synchronnie)
File.WriteAllText("books.json", json);
Console.WriteLine("JSON zapisany do pliku books.json.");
// Odczytamy z powrotem z pliku (synchronnie)
string jsonFromFile = File.ReadAllText("books.json");
// Deserializacja z powrotem do słownika
var restoredBooks = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonFromFile);
Console.WriteLine("Wynik deserializacji:");
foreach (var pair in restoredBooks)
Console.WriteLine($"{pair.Key} -> {pair.Value}");
Co znajdzie się w pliku books.json?
{
"The Master and Margarita": "Michaił Bułhakow",
"Harry Potter": "Dżoan Rouling",
"War and Peace": "William Golding"
}
Jak to działa?
Serializer zamienia nasz Dictionary<string, string> na obiekt JSON, gdzie każdy klucz staje się nazwą właściwości, a wartość — wartością właściwości. To wygodne, jeśli klucze są stringami i są unikalne.
3. Słownik z niestandardowym typem klucza
Wszystko jest proste, dopóki klucz to string. A co jeśli to na przykład liczba?
var bookIds = new Dictionary<int, string>
{
[1001] = "The Master and Margarita",
[1002] = "Harry Potter",
[1003] = "Lord of the Flies"
};
string jsonIntKeys = JsonSerializer.Serialize(bookIds, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonIntKeys);
Wynik:
{
"1001": "The Master and Margarita",
"1002": "Harry Potter",
"1003": "Lord of the Flies"
}
Co się stało?
- C# przekonwertował numeryczne klucze na stringi, ponieważ w JSON nazwy właściwości mogą być tylko stringami.
- Przy deserializacji z powrotem do Dictionary<int, string> serializer spróbuje z powrotem zrzutować string na liczbę.
Przykład deserializacji:
var restoredBookIds = JsonSerializer.Deserialize<Dictionary<int, string>>(jsonIntKeys);
// Wszystko działa! Klucze znowu są liczbami.
A co jeśli klucz to złożony typ, np. obiekt?
var dict = new Dictionary<Author, string>
{
[new Author { Name = "Golding", BirthYear = 1911 }] = "Lord of the Flies"
};
Próba serializacji takiego słownika doprowadzi do wyjątku:
System.NotSupportedException: Serialization and deserialization of 'Dictionary<Author, string>' instances are not supported.
Dlaczego?
Obiekt JSON nie może używać niczego poza stringiem jako nazwy właściwości. Dlatego klucze w słowniku przy serializacji muszą być prostymi typami, jednoznacznie dającymi się zamienić na string (najczęściej string lub liczba). Złożonych obiektów nie da się użyć jako kluczy przy serializacji do JSON standardowymi narzędziami.
4. Słownik z zagnieżdżonymi obiektami jako wartością
Klucze omówione — teraz zobaczmy, co się dzieje, gdy wartość to złożony obiekt (np. Book albo Author).
Przykład
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
}
var authorDirectory = new Dictionary<string, Author>
{
["bulgakov"] = new Author { Name = "Michaił Bułhakow", BirthYear = 1891 },
["golding"] = new Author { Name = "William Golding", BirthYear = 1911 }
};
string jsonAuthors = JsonSerializer.Serialize(authorDirectory, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonAuthors);
Wynik:
{
"bulgakov": {
"Name": "Michaił Bułhakow",
"BirthYear": 1891
},
"golding": {
"Name": "William Golding",
"BirthYear": 1911
}
}
- Wszystko, co jest zagnieżdżone, serializuje się zgodnie ze strukturą obiektów.
- Deserializacja z powrotem do Dictionary<string, Author> również działa bez problemów.
5. Słownik w składzie innego obiektu
Bardzo często słowniki używane są jako pole wewnątrz bardziej złożonego obiektu. Na przykład biblioteka ma katalog książek, gdzie każdy klucz to nazwa gatunku, a wartość to lista książek tego gatunku.
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
}
public class Library
{
public Dictionary<string, List<Book>> CatalogByGenre { get; set; }
}
var library = new Library
{
CatalogByGenre = new Dictionary<string, List<Book>>
{
["Fantastyka"] = new List<Book>
{
new Book { Title = "Solaris", Author = "Stanisław Lem" }
},
["Klasyka"] = new List<Book>
{
new Book { Title = "Lord of the Flies", Author = "William Golding" },
new Book { Title = "Visible Darkness", Author = "William Golding" }
}
}
};
string jsonLibrary = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonLibrary);
Fragment wyjściowego JSON:
{
"CatalogByGenre": {
"Fantastyka": [
{
"Title": "Solaris",
"Author": "Stanisław Lem"
}
],
"Klasyka": [
{
"Title": "Lord of the Flies",
"Author": "William Golding"
},
{
"Title": "Visible Darkness",
"Author": "William Golding"
}
]
}
}
Wszystko działa — zagnieżdżone słowniki i kolekcje serializują się i deserializują rekurencyjnie.
6. Cechy i "miny" serializacji słowników
Powtarzające się klucze
W słowniku klucze zawsze są unikalne. Ale jeśli ręcznie spróbujesz podstawić JSON z powtarzającymi się kluczami:
{
"foo": "first",
"foo": "second"
}
Wynik: ostatnia wartość ("second") nadpisze pierwszą, nie będzie błędu. Tak działają większość parserów JSON.
Kolejność elementów
Słownik to kolekcja nieuporządkowana. Przy serializacji kolejność kluczy w JSON może różnić się od oryginalnej. Jeśli kolejność jest krytyczna — użyj listy par (List<KeyValuePair<string, T>>), ale zwykle dla słownika kolejność nie jest istotna.
JSON i zagnieżdżone słowniki
Poziomy zagnieżdżenia nie są ograniczone, ale dla poprawnej pracy każdy poziom musi mieścić się w ograniczeniach JSON (klucze — stringi, wartości — poprawne obiekty/arraye JSON).
Użycie JsonSerializerOptions
Czasem trzeba, żeby nazwy właściwości nie były w PascalCase, lecz w camelCase. To szczególnie ważne, jeśli integrujesz się z frontendem w JavaScript, gdzie camelCase jest standardem dla nazw pól.
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
string camelJson = JsonSerializer.Serialize(authorDirectory, options);
Ważne: w słownikach ten parametr wpływa tylko na serializację zagnieżdżonych obiektów (ich właściwości), a nie na klucze słownika. Klucze w słowniku zawsze serializują się jako dokładnie ten string, który był podany w C#.
7. Problemy ze złożonymi i nie-stringowymi kluczami
Przy serializacji słowników z kluczami-stringami i numerycznymi (np. int, long, Guid) wszystko działa "out of the box". Ale jeśli spróbujesz użyć jako klucza własnej klasy lub struktury — dostaniesz błąd NotSupportedException.
Aby poradzić sobie w takich przypadkach, są obejścia:
- Użyć innego formatu przechowywania, np. serializować słownik jako tablicę obiektów z polami "Key" i "Value".
- Napisac konwerter (JsonConverter), który zamieni twój złożony klucz na string i z powrotem.
- Jeśli struktura jest naprawdę złożona — czasami warto przemyśleć architekturę i nie używać złożonych obiektów jako kluczy słownika.
Przykład obejścia przez serializację jako lista par
public class AuthorInfo
{
public Author Author { get; set; }
public string Book { get; set; }
}
// zamiast Dictionary<Author, string>
var list = new List<AuthorInfo>
{
new AuthorInfo { Author = new Author { Name = "William Golding", BirthYear = 1911 }, Book = "Lord of the Flies" }
};
// taka lista serializuje się bez problemu
Porównanie: słownik vs. lista par przy serializacji
| Typ kolekcji | Struktura JSON | Kiedy używać |
|---|---|---|
|
|
Klucze — proste stringi, potrzebne szybkie wyszukiwania i unikalność |
|
|
Klucz — złożony typ, potrzebna kontrola kolejności, możliwe duplikaty |
8. Typowe pytania na rozmowach kwalifikacyjnych
1. Czy można serializować Dictionary<DateTime, string>?
Tak, ale klucze są konwertowane do postaci stringowej (zwykle format ISO serii "yyyy-MM-ddTHH:mm:ss"). Czasem przy deserializacji mogą pojawić się problemy z lokalizacjami i formatami dat.
2. Co się stanie, jeśli zserializować Dictionary<int, string>?
Klucze zostaną zserializowane jako stringi, nawet jeśli w oryginalnym słowniku były liczby. Przy deserializacji wróci to do stanu pierwotnego.
3. Dlaczego nie można serializować słownika z obiektami jako kluczem?
Tylko stringi mogą być nazwami właściwości obiektu JSON, a obiekty — nie.
4. A jeśli chcemy serializować słownik z kompleksowym kluczem?
Lepiej przemyśleć strukturę albo serializować jako listę par "key-value", gdzie klucz serializuje się w pełni jako obiekt-pole, a nie jako nazwa właściwości.
GO TO FULL VERSION