Niektóre obiekty trzymają zasoby, którymi odśmiecacz nie zarządza: otwarte pliki, gniazda sieciowe, połączenia z bazą danych, uchwyty systemu operacyjnego. Implementują IDisposable, a zasób zwalniasz, wywołując Dispose(). Instrukcja using wywołuje ją za ciebie w ustalonym miejscu, nawet jeśli zostanie rzucony wyjątek.
Ta strona dotyczy instrukcji using. Linie using System; na początku pliku to inna funkcja, dyrektywa using, która importuje przestrzeń nazw.
Blok using
using (declaration) { body } tworzy obiekt, wykonuje ciało, a potem wywołuje Dispose. Mała klasa, która wypisuje coś z Dispose, pokazuje dokładnie, kiedy to się dzieje:
Wynik:
open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled
Drugi blok rzuca wyjątek, a "close api" i tak się wypisuje, zanim catch obsłuży wyjątek. Ta gwarancja to powód, żeby używać using zamiast samemu wywoływać Dispose() w ostatniej linii: ręczne wywołanie jest pomijane przez każdy wyjątek rzucony wcześniej.
Do czego kompiluje się using
Kompilator rozwija blok using do try/finally. Te dwa zapisy są równoważne:
using (var writer = new StreamWriter("log.txt"))
{
writer.WriteLine("started");
}
// is compiled as:
{
var writer = new StreamWriter("log.txt");
try
{
writer.WriteLine("started");
}
finally
{
if (writer != null) ((IDisposable)writer).Dispose();
}
}
Z tego rozwinięcia wynikają trzy szczegóły. Zmienna ma zakres bloku i jest w nim tylko do odczytu (nie możesz ponownie przypisać writer). Obiekt musi implementować IDisposable, inaczej kod się nie kompiluje. Dozwolona jest też wartość null: Dispose jest wtedy po prostu pomijane, co przydaje się, gdy fabryka może nic nie zwrócić.
Pliki i strumienie
Klasy plików i strumieni to przypadek codzienny. StreamWriter buforuje tekst w pamięci, a Dispose zapisuje bufor na dysk i zamyka uchwyt pliku:
Wynik:
id,total
1001,59.90
1002,12.50
Bez Dispose ostatnie linie mogą wciąż siedzieć w buforze writera, a w Windows otwarty uchwyt nie pozwala innym programom (ani twojemu następnemu File.Delete) dotknąć pliku. Metody pomocnicze, takie jak File.WriteAllText i File.ReadAllLines, otwierają i zwalniają plik wewnętrznie, więc nie potrzebują using. Więcej na stronie o plikach.
Kilka zasobów: kolejność zwalniania
Ułóż instrukcje using jedna pod drugą, bez nawiasów klamrowych między nimi, żeby otworzyć kilka zasobów w jednym bloku. Są zwalniane w odwrotnej kolejności tworzenia:
Wynik:
acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a
Odwrotna kolejność to dokładnie to, czego potrzebują zasoby warstwowe: writer opakowuje bufor, który opakowuje plik, więc writer musi zapisać dane do bufora, zanim bufor zapisze je do pliku, a to wszystko, zanim plik zostanie zamknięty.
Deklaracja using (C# 8)
C# 8 dodał formę bez nawiasów klamrowych. Zmienna jest zwalniana, gdy kończy się otaczający zakres, zwykle na końcu metody:
static void ExportReport(string path, IEnumerable<string> rows)
{
using var writer = new StreamWriter(path);
writer.WriteLine("Report");
foreach (string row in rows)
{
writer.WriteLine(row);
}
} // writer.Dispose() runs here, at the end of the method
Usuwa to jeden poziom wcięcia, co ma znaczenie, gdy metoda używa dwóch lub trzech zasobów. Zasada kolejności zwalniania jest ta sama: deklaracje w jednym zakresie są zwalniane w odwrotnej kolejności. Kosztem jest to, że zasób pozostaje otwarty do końca zakresu, więc gdy plik powinien zostać zamknięty, zanim metoda wykona dalszą pracę, zostań przy formie blokowej albo umieść deklarację we własnym bloku { }.
Dla obiektów, których sprzątanie jest asynchroniczne (IAsyncDisposable, jak wiele połączeń z bazą danych i strumieni), C# 8 ma też await using, które w tym samym miejscu czeka na DisposeAsync().
Implementacja IDisposable we własnej klasie
Implementuj IDisposable, gdy twoja klasa jest właścicielem czegoś, co trzeba zwolnić (utworzyła strumień, timer, połączenie), i dlatego sama musi to zwolnić. W typowym przypadku to krótka metoda:
Wynik:
1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException
Zasady, których ten kod przestrzega: Dispose zwalnia wszystko, czego właścicielem jest obiekt; wielokrotne wywołanie za drugim razem nic nie robi; a użycie obiektu po zwolnieniu rzuca ObjectDisposedException. using (log) pokazuje też, że blok może przyjąć istniejącą zmienną zamiast deklaracji.
Dłuższy "wzorzec dispose" z metodą protected virtual void Dispose(bool disposing) i finalizatorem jest potrzebny tylko wtedy, gdy klasa bezpośrednio trzyma niezarządzany uchwyt (surowy wskaźnik z kodu natywnego). We współczesnym .NET opakuj takie uchwyty w podklasę SafeHandle, a twojej klasie wystarczy prosta wersja powyżej.
Częste błędy
- Zwracanie obiektu utworzonego w bloku
using. Jest zwalniany, gdy blok się kończy, więc wywołujący dostaje zamknięty strumień. Zwróć dane albo pozwól, żeby to wywołujący był właścicielemusing. - Zapominanie o
usingprzy strumieniach i writerach. Zbuforowane dane mogą przepaść, a pliki pozostają zablokowane. - Zwalnianie współdzielonego obiektu. Zwalniaj tylko to, czego jesteś właścicielem. Długo żyjący
HttpClientna przykład ma być współdzielony i używany wielokrotnie, a nie tworzony i zwalniany przy każdym żądaniu. - Używanie obiektu po bloku. Metody zwolnionego obiektu rzucają
ObjectDisposedException. - Poleganie na odśmiecaczu. Zwalnia pamięć, a nie pliki czy gniazda, i nigdy nie wywołuje
Dispose.
Najczęściej zadawane pytania
Co robi instrukcja using w C#?
using (var x = ...) { ... } wywołuje x.Dispose(), gdy blok się kończy, niezależnie od tego, czy kończy się normalnie, przez return, czy przez rzucony wyjątek. Kompilator zamienia go na try/finally z wywołaniem Dispose w finally, więc zasób jest zwalniany w znanym miejscu, a nie wtedy, gdy akurat zadziała odśmiecacz.
Czym jest IDisposable w C#?
IDisposable to interfejs z jedną metodą, void Dispose(). Klasa go implementuje, gdy trzyma coś, co trzeba jawnie zwolnić: uchwyt pliku, gniazdo sieciowe, połączenie z bazą danych, timer. Każdy obiekt, którego typ implementuje IDisposable, powinien zostać zwolniony, gdy przestaje być potrzebny, zwykle przez using.
Jaka jest różnica między using a using var?
using (var x = ...) { } zwalnia obiekt na końcu swojego bloku. Deklaracja using using var x = ...; (C# 8 i nowsze) nie ma własnego bloku: zwalnia obiekt, gdy kończy się otaczający zakres, zwykle na końcu metody. Oszczędza zagnieżdżania, gdy zasób ma żyć do końca metody.
W jakiej kolejności są zwalniane obiekty z kilku using?
W odwrotnej kolejności tworzenia. Przy using (var a = ...) using (var b = ...) { } najpierw zwalniany jest b, a potem a. To kolejność, której potrzebujesz, gdy b zależy od a, na przykład gdy StreamWriter opakowuje FileStream.
Czy odśmiecacz wywołuje Dispose?
Nie. Odśmiecacz zwalnia pamięć i może uruchomić finalizator w jakimś nieprzewidywalnym późniejszym momencie, ale nigdy nie wywołuje Dispose. Niezwolniony plik może pozostać zablokowany, a jego zbuforowane dane mogą nigdy nie zostać zapisane. Właśnie dlatego istnieje using.