CodeGym /Kursy /C# SELF /Zamykanie strumieni i zwalnianie zasobów (

Zamykanie strumieni i zwalnianie zasobów ( using)

C# SELF
Poziom 36 , Lekcja 0
Dostępny

1. Jak poprawnie zwalniać zasoby w C#?

Wyobraź sobie system operacyjny jako surowego bibliotekarza. Wziąłeś książkę (otworzyłeś plik/strumień), czytasz, a potem... zapomniałeś oddać! Bibliotekarz się wkurza: "Jak to — książka wciąż u ciebie?!" Tak samo jest ze strumieniami. Otwarty strumień zajmuje zasoby: deskryptor pliku, kawałek pamięci, a do tego blokuje plik dla innych aplikacji.

Jeśli nie zamkniesz strumienia, efekt może być od "coś nie działa" do "wszystko się wywaliło, nikt nie może zapisać do tego pliku". A jeśli w programie jest dużo niezamkniętych strumieni — system może zacząć "wyciekać" z zasobów i po prostu przestanie działać.

Na czym polega niebezpieczeństwo?

  • Plik nie jest zamknięty, polecenia nie trafiają na dysk (np. przy zapisie — dane mogą zostać w buforze).
  • Plik jest zablokowany dla innych procesów — koledzy i inne programy się wkurzają.
  • Limit deskryptorów: na Windows/Linux procesy mają limity otwartych plików/strumieni.

Interfejs IDisposable

Każda klasa, która pracuje z niezarządzanymi zasobami (strumienie, pliki, bazy danych, sockety), musi implementować interfejs IDisposable.


public interface IDisposable
{
    void Dispose();
}

W środku metody Dispose() zwykle następuje zwolnienie wszystkich biednych zasobów: plik w końcu się zamyka, połączenia są zrywane, pamięć jest zwalniana.

Strumienie — to obiekty, które trzymają w łapach różne ważne zasoby: pliki, połączenia, pamięć. Jeśli ich nie zamkniesz, plik może zostać zablokowany (twój Word powie "plik otwarty przez inną aplikację!"), a system — bez pamięci. Dlatego mega ważne jest zwalnianie strumienia po zakończeniu pracy.

W .NET strumienie implementują interfejs IDisposable. To znaczy: trzeba je zamykać, wywołując Dispose() (albo po prostu wrzucać w blok using).

2. Sposoby zamykania strumienia: od ryzykownych do pewnych

Wariant 1. "Ręczne" zamykanie: nie rób tak!

To stary sposób. I chociaż pokazywałem go w poprzednich przykładach, w nowoczesnym kodzie prawie nikt już tak nie robi :P


var stream = new FileStream("file.txt", FileMode.Open);
// Pracujemy ze strumieniem
stream.Close(); // albo stream.Dispose()

Problem: jeśli między otwarciem a zamknięciem pojawi się błąd/wyjątek, plik zostanie otwarty i zablokowany. To jakbyś wybiegł z biblioteki z książką, bo poczułeś zapach pizzy...

Wariant 2. Używamy try...finally


FileStream stream = null;
try
{
    stream = new FileStream("file.txt", FileMode.Open);
    // Pracujemy ze strumieniem
}
finally
{
    if (stream != null)
        stream.Dispose();
}

Taki wariant jest pewny: finally wykona się nawet przy błędzie. Ale, przyznajmy, pisać tak się nie chce.

Wariant 3. Ładnie i bezpiecznie: operator using

Klasyczna składnia (using ( ... ) { ... })


using (var stream = new FileStream("file.txt", FileMode.Open))
{
    // Pracujemy ze strumieniem
}
// Tutaj stream.Dispose() wywoła się automatycznie!

Kluczowa myśl — wszystko, co w bloku using, pracuje ze strumieniem, a gdy blok się kończy — plik się zamyka nawet jeśli coś poszło nie tak (np. poleciał wyjątek).

Wariant 4. Nowoczesna składnia

Nowoczesna składnia (using var)


using var stream = new FileStream("file.txt", FileMode.Open);
// Pracujemy ze strumieniem
// ... Dispose wywoła się automatycznie, gdy zmienna wyjdzie poza zakres

Super! Nie trzeba robić dodatkowych wcięć i nawiasów klamrowych.

Jak to działa "pod maską"?

Operator using kompilator zamienia na tego samego try...finally, tylko robi to za ciebie. Żart: "using pisze czysty kod za ciebie — może niedługo zacznie pić kawę i przeglądać Stack Overflow?".

Różnica między klasycznym a nowoczesnym using

Klasyczny blok using using var (deklaracja)
Wygląd
using (var x = ...) { ... }
using var x = ...; ...
Zakres działania Wewnątrz nawiasów klamrowych bloku Do końca bieżącego bloku (metody, pętli, itd.)
Zwięzłość Trochę bardziej rozwlekle Zwięźle, mniej wcięć
Od wersji C# 1.0 C# 8.0 i wyżej

3. Co to są using-deklaracje?

5 lat temu świat zobaczył nowy, zwięzły sposób pracy z obiektami IDisposable.

using-deklaracja — to gdy zamiast bloku deklarujesz zmienną ze słowem kluczowym using, i ona zostanie automatycznie zwolniona na końcu bieżącego bloku (np. metody lub pętli), a nie na końcu nawiasów klamrowych dodatkowego bloku.


using var stream = new FileStream("file.txt", FileMode.Open);
// Pracujemy ze strumieniem
Console.WriteLine(stream.Length);

// Tutaj plik wciąż jest otwarty!

// ... koniec metody
// stream.Dispose() wywoła się tutaj automatycznie

Kluczowe różnice względem klasycznego using:

  • Nie trzeba nawiasów klamrowych, nie powstaje zagnieżdżony blok kodu.
  • Zmienna jest dostępna do końca całego bloku, w którym została zadeklarowana (zwykle — metoda, czasem — pętla, klasa, jeśli zadeklarowana na poziomie klasy).
  • Zwolnienie zasobu nastąpi dopiero, gdy blok wykonania się zakończy.

Dlaczego to jest fajne?

  • Mniej poziomów zagnieżdżenia — kod jest dużo krótszy i czytelniejszy.
  • Łatwiej pracować z wieloma zasobami — deklaruj kilka using-zmiennych pod rząd, i wszystko się zwolni, gdy skończy się metoda.
  • Mniej szans na błąd — nie przegapisz miejsca, gdzie trzeba było wywołać Dispose().

4. Porównanie: klasyczny vs. nowoczesny using

Zobaczmy porównanie w kodzie.

Klasyczny sposób


using (var reader = new StreamReader("input.txt"))
{
    using (var writer = new StreamWriter("output.txt"))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            writer.WriteLine(line.ToUpper());
        }
    }
} // Tutaj oba pliki zostaną zamknięte

Nowoczesny sposób (C# 8+)


using var reader = new StreamReader("input.txt");
using var writer = new StreamWriter("output.txt");

string line;
while ((line = reader.ReadLine()) != null)
{
    writer.WriteLine(line.ToUpper());
}
// Oba pliki zamkną się tutaj, przy wyjściu z metody

Wygląda prościej, prawda? Zwłaszcza jeśli robić jeszcze większe zagnieżdżenia — nowoczesny sposób mocno ułatwia życie.

5. Kiedy i gdzie wywołuje się Dispose()?

Tu studenci często popełniają błąd: myślą, że Dispose wywoła się zaraz po linii użycia — ale to nie tak!

Zobacz ten przykład:


void MyMethod()
{
    using var fileStream = new FileStream("data.bin", FileMode.Open);
    // ... dużo kodu, może nawet pętle i zagnieżdżone wywołania
    // fileStream wciąż jest otwarty!

    // Tutaj mamy dostęp do fileStream
}
// Tutaj, na } metody, wywoła się fileStream.Dispose()

Ważny moment: Jeśli zadeklarujesz using-zmienną w pętli, Dispose wywoła się po każdej iteracji.


foreach (var path in filePaths)
{
    using var reader = new StreamReader(path);
    // pracujemy z reader
} // reader.Dispose() wywoła się po każdej iteracji (zamknie plik)

6. Błędy przy przenoszeniu starego kodu

Czasem przenosisz stary kod albo kopiujesz przykład z klasycznym using, a zmienna potrzebuje dłuższego “życia” niż zakres nawiasów. Wtedy klasyczny wariant się nie nada, a using-deklaracje — idealnie.

Ale są niuanse. Na przykład, jeśli w pętli masz dwa zasoby, ale jeden z nich musi "żyć" dłużej niż drugi — zadeklaruj je w odpowiedniej kolejności:


using var resource1 = ...;
for (int i = 0; i < 10; i++)
{
    using var resource2 = ...;
    // resource2 żyje jedną iterację
    // resource1 — całą funkcję
}

7. Praktyka

Rozwijamy dalej naszą edukacyjną apkę — mały symulator zamawiania kawy, który ulepszałeś ostatnio. Niech teraz potrafi zapisywać historię zamówień do pliku tekstowego i czytać ją przy starcie.

Krok 1: Zapisujemy zamówienie do pliku


using var writer = new StreamWriter("orders.txt", append: true);
writer.WriteLine("Kawa: Latte; Mleko: Owsiane; Rozmiar: Duży");

// Drugi parametr konstruktora StreamWriter append: true oznacza, że chcemy dopisywać, a nie nadpisywać plik.

Krok 2: Czytamy historię zamówień


using var reader = new StreamReader("orders.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
    Console.WriteLine($"Zamówienie: {line}");
}

I jak tylko program zakończy wykonywanie tej metody, pliki zamkną się automatycznie.

8. Najlepsze praktyki pracy z using-deklaracjami

1. Zawsze używaj using dla obiektów implementujących IDisposable
W .NET większość klas do pracy z plikami, strumieniami, zasobami implementuje ten interfejs. To sygnał: zwolnij mnie przez using!

2. Pamiętaj o zakresie: nie deklaruj using-var tam, gdzie zmienna może "przeszkadzać"
Jeśli zmienna jest potrzebna tylko przez kilka linii — używaj jej tam, gdzie trzeba, i nie wcześniej.

3. Nie zapominaj o kolejności zwalniania
Jeśli zadeklarujesz kilka using-zmiennych pod rząd — Dispose wywoła się w odwrotnej kolejności:


using var first = new Resource("First");
using var second = new Resource("Second");
// ... praca
// Najpierw Dispose dla second, potem dla first

To czasem ważne, jeśli jeden zasób zależy od drugiego (np. strumień zapisu powinien się zwolnić przed plikiem).

4. Nie używaj using-deklaracji poza metodą
using-deklaracje są zabronione na poziomie klasy (np. dla pól). Działają tylko wewnątrz metod, konstruktorów itp.

5. Łącz z obsługą błędów
Pamiętaj, że nawet z using nie wszystkie wyjątki są wygodne — warto dodać try-catch, jeśli chcesz mieć kontrolę nad błędami odczytu/zapisu.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION