1. Wprowadzenie
Już jesteśmy przyzwyczajeni do tworzenia obiektów w naszych programach. Pamiętasz, zaraz zaczniemy zgłębiać klasy i obiekty bardziej szczegółowo, ale już teraz rozumiemy, że zmienne, listy, nawet proste stringi – to nie jest nic przypadkowego, to pewne "byty" w naszym programie. Na przykład możemy stworzyć zmienną int age = 30; albo string name = "Wasia";. Ale co jeśli trzeba zapisać do pliku informacje o całym użytkowniku, który ma imię, wiek, adres, listę ulubionych książek i wiele więcej?
Wyobraź sobie: piszesz grę. Masz obiekt Player z masą cech: zdrowie, poziom, inwentarz (lista przedmiotów), współrzędne na mapie i tak dalej. Gracz gra, rozwija się, znajduje fajne artefakty. I nagle postanawia wyjść z gry. Co się stanie? Wszystkie dane o jego przygodach, które były w pamięci, znikną! Smutek! Żeby do tego nie dopuścić, trzeba zapisać stan obiektu Player do pliku, a kiedy gracz wróci, załadować go z powrotem.
I tu wchodzi na scenę serializacja. Ona zajmuje się właśnie budowaniem mostu między "żywymi" obiektami w pamięci a "martwymi", ale trwałymi danymi na dysku.
2. Proces serializacji (od obiektu do pliku)
Rozłóżmy na czynniki pierwsze, jak przebiega ta "magia" przemiany obiektu w bajty, które można zapisać do pliku.
Wyobraź sobie, że mamy taką klasę Book (Książka):
// To nasz "szkic" albo "plan" do tworzenia obiektów-książek
public class Book
{
// Właściwości książki
public string Title { get; set; } // Tytuł książki
public string Author { get; set; } // Autor
public int Year { get; set; } // Rok wydania
// Konstruktor - specjalna metoda do tworzenia nowych obiektów Book
public Book(string title, string author, int year)
{
Title = title;
Author = author;
Year = year;
}
// Metoda do wygodnego wyświetlania informacji o książce (na razie niekonieczna do serializacji, ale przydatna)
public void DisplayInfo()
{
Console.WriteLine($"Tytuł: {Title}, Autor: {Author}, Rok: {Year}");
}
}
Krok 1: Utworzenie obiektu do serializacji.
Najpierw oczywiście potrzebujemy obiektu, który chcemy zapisać. Na przykład utworzyliśmy egzemplarz Book:
Book myFavoriteBook = new Book("Autostopem po Galaktyce", "Douglas Adams", 1979);
Ten obiekt myFavoriteBook teraz znajduje się w pamięci operacyjnej.
Krok 2: Wybór narzędzia (serializatora).
Nie możemy po prostu wziąć i "skopiować" obiektu na dysk. Komputer nie rozumie obiektów bezpośrednio w plikach — potrzebne są bajty. Potrzebne jest specjalne narzędzie — serializator. Jego zadanie to rozebrać nasz obiekt na części składowe (jego właściwości: Title, Author, Year) i zamienić te części w sekwencję bajtów lub w tekst (na przykład JSON albo XML).
Dziś nie wchodzimy głęboko w konkretne implementacje — traktuj to jak specjalne "pudełko-przekształcacz".
Krok 3: Przekształcenie obiektu w strumień danych.
Serializator dostaje nasz obiekt myFavoriteBook, patrzy na jego właściwości (Title, Author, Year) i zamienia każdą z nich na format, który można zapisać. Wszystkie te bajty (lub znaki tekstowe) są składane w jeden strumień danych — długą "taśmę" informacji.
Krok 4: Zapis strumienia do pliku.
Teraz, kiedy mamy tę "taśmę bajtów", używamy naszych starych znajomych FileStream i ewentualnie StreamWriter (jeśli wybrano format tekstowy, np. JSON) albo po prostu FileStream (dla czysto binarnych danych), żeby zapisać ten strumień na dysk.
3. Proces deserializacji (od pliku do obiektu)
Krok 1: Odczyt strumienia danych z pliku.
Znów używamy FileStream i w razie potrzeby StreamReader (jeśli to format tekstowy), żeby przeczytać zawartość pliku. Dane przychodzą jako "taśma" bajtów albo tekstu.
Krok 2: Wybór narzędzia (deserializatora).
Potrzebne jest narzędzie odwrotne — deserializator. Musi wiedzieć, jak interpretować otrzymane bajty/tekst i poprawnie odtworzyć strukturę obiektu. Bardzo ważne: do deserializacji używa się zwykle tego samego typu serializatora (i zazwyczaj tej samej biblioteki), co do serializacji, inaczej twój "konstruktor" nie zrozumie instrukcji składania.
Krok 3: Przekształcenie strumienia z powrotem w obiekt.
Deserializator czyta dane, rozumie, gdzie jest Title, potem Author, później Year, i na tej podstawie tworzy nowy obiekt Book w pamięci, wypełniając jego właściwości.
Krok 4: Otrzymanie gotowego obiektu.
Wuala! Mamy z powrotem pełnoprawny obiekt Book w pamięci operacyjnej, z którym można pracować.
Przykład: Zapisujemy naszą "Super-Książkę" „ręcznie” (dla zrozumienia koncepcji)
Na razie nie będziemy używać wyspecjalizowanych bibliotek: zrobimy najprostsze „ręczne” serializowanie i deserializowanie za pomocą StreamWriter i StreamReader. To pomoże zrozumieć zasadę.
Nasz obiekt Book:
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
public Book(string title, string author, int year)
{
Title = title;
Author = author;
Year = year;
}
public void DisplayInfo()
{
Console.WriteLine($"Tytuł: \"{Title}\", Autor: {Author}, Rok: {Year}");
}
}
Ręczna serializacja: metoda SaveBookToTextFile
Stwórzmy metodę, która zapisze właściwości książki do pliku tekstowego, po jednej właściwości na linię.
void SaveBookToTextFile(Book book, string filePath)
{
using var writer = new StreamWriter(filePath);
writer.WriteLine(book.Title);
writer.WriteLine(book.Author);
writer.WriteLine(book.Year);
}
Co się dzieje? Otwieramy StreamWriter i kolejno zapisujemy Title, Author, Year — to nasz najprostszy schemat serializacji.
Jeśli uruchomisz kod, zawartość pliku będzie taka:
Autostopem po Galaktyce
Douglas Adams
1979
Ręczna deserializacja: metoda LoadBookFromTextFile
Napiszmy metodę, która odczyta dane i złoży nowy obiekt Book.
Book LoadBookFromTextFile(string filePath)
{
using var reader = new StreamReader(filePath);
string title = reader.ReadLine();
string author = reader.ReadLine();
int year = int.Parse(reader.ReadLine());
return new Book(title, author, year);
}
I używamy tych metod w Main:
//tworzymy obiekt
var myBook = new Book("Autostopem po Galaktyce", "Douglas Adams", 1979);
string filePath = "my_favorite_book.txt";
//zapisujemy go do pliku
SaveBookToTextFile(myBook, filePath);
//czytamy z pliku
Book loadedBook = LoadBookFromTextFile(filePath);
Co się dzieje w LoadBookFromTextFile? Otwieramy StreamReader i w tej samej kolejności czytamy linie: najpierw tytuł, potem autora, potem rok i konwertujemy go przez int.Parse. Potem tworzymy nową instancję Book.
W praktyce warto dodawać sprawdzenia (File.Exists) i obsługę błędów przez try-catch, ale tu skupiamy się na samej idei.
Dlaczego „ręczna” serializacja jest zła (i po co biblioteki)?
- Dużo ręcznego kodu. Każda właściwość musi być zapisana i potem odczytana. Jeśli obiektów i pól jest dużo — kod się rozrasta.
- Wrażliwość na zmiany. Dodano nowe pole Pages (int)? Trzeba zmieniać zapis i odczyt i pilnować porządku.
- Złożone struktury. Zagnieżdżone kolekcje i obiekty (np. List<Chapter>) zamienią kod w "spaghetti".
- Formaty i wydajność. Format tekstowy jest prosty, ale nie zawsze kompaktowy i bezpieczny; dla binarnych danych trzeba ręcznie pracować z bajtami, BinaryWriter/BinaryReader itd.
- Brak metadanych. Nasz plik nie "wie", że pierwsza linia to Title, a trzecia to Year. Specjalistyczne serializatory mogą zapisywać metadane i być bardziej odporne na zmiany modeli.
Dlatego w realnych projektach używa się gotowych bibliotek-serializatorów, które potrafią automatycznie rozbierać/składać obiekty, pracować z JSON, XML i formatami binarnymi oraz bezpiecznie przenosić zmiany modeli. O tym — w następnej lekcji!
Praktyczne zastosowanie: po co serializacja?
- Zapisywanie i ładowanie danych. Ustawienia, stany gier, konfiguracje.
- Przesyłanie danych po sieci. Wymiana złożonych obiektów między serwisami (często w JSON).
- Cache'owanie. Szybkie ponowne wykorzystanie wcześniej pobranych danych.
- Logowanie złożonych obiektów. Przydatne do debugowania i audytu.
- Głębokie kopiowanie obiektów. Serializacja + deserializacja jako sposób klonowania grafu obiektów.
Serializacja — jeden z fundamentów współczesnego oprogramowania: od zapisu postępu w grze po pracę z web‑service'ami — jest wszędzie!
GO TO FULL VERSION