Gdy kod C# zaczyna korzystać z klas takich jak File, HttpClient albo StringBuilder, szybko pojawia się słowo using. Problem w tym, że ta sama konstrukcja może skracać nazwy typów, tworzyć aliasy albo bezpiecznie zwalniać zasoby. Pokażę, jak rozróżniać te zastosowania, kiedy używać nowszej deklaracji using i jakie błędy najczęściej utrudniają pracę początkującym.
Jedno słowo, trzy ważne zastosowania w C#
- Dyrektywa using pozwala korzystać z typów z przestrzeni nazw bez wpisywania ich pełnych nazw.
- using alias rozwiązuje konflikty nazw, a using static upraszcza dostęp do metod statycznych.
-
Instrukcja using automatycznie wywołuje
Dispose()po zakończeniu pracy z zasobem. - Deklaracja using z C# 8 ogranicza liczbę nawiasów, ale kończy zakres zasobu dopiero wraz z blokiem kodu.
- global using i implicit usings zmniejszają liczbę powtarzalnych dyrektyw w wielu plikach projektu.

c# using bez skrótów myślowych
Najpierw trzeba rozdzielić trzy konstrukcje, które wyglądają podobnie, ale rozwiązują różne problemy. Dyrektywa using dotyczy głównie nazw typów, instrukcja using pilnuje cyklu życia zasobu, a deklaracja using łączy zarządzanie zasobem z prostszą składnią.
| Forma | Przykład | Do czego służy |
|---|---|---|
| Dyrektywa przestrzeni nazw | using System.Text; |
Skraca nazwy typów |
| Alias | using Json = Newtonsoft.Json; |
Tworzy własną nazwę dla typu lub przestrzeni nazw |
| Instrukcja using | using (var reader = ...) |
Zwalnia zasób po zakończeniu bloku |
| Deklaracja using | using var reader = ...; |
Zwalnia zasób przy opuszczeniu bieżącego zakresu |
To rozróżnienie ma praktyczne znaczenie. Samo dodanie using System.IO; nie zamknie pliku i nie zwolni uchwytu systemowego. Z kolei zapis using var stream = ... nie importuje żadnej przestrzeni nazw, tylko mówi kompilatorowi, jak zakończyć pracę z obiektem.
Dyrektywa using skraca nazwy klas i przestrzeni nazw
Przestrzeń nazw, czyli namespace, grupuje powiązane typy i pomaga unikać kolizji nazw. Bez dyrektywy musiałbym odwołać się do klasy w pełnej postaci:
System.Text.StringBuilder builder = new System.Text.StringBuilder();
builder.Append("Witaj");
Po dodaniu dyrektywy kod staje się znacznie czytelniejszy:
using System.Text;
StringBuilder builder = new StringBuilder();
builder.Append("Witaj");
Dyrektywa działa tylko w pliku, w którym została umieszczona, chyba że użyjemy wariantu global using. Nie instaluje biblioteki, nie dodaje pakietu NuGet i nie naprawia braku referencji do projektu lub assembly. Jeśli typ nadal nie jest dostępny, problem może dotyczyć zależności, a nie samego zapisu using.
Alias pomaga przy konflikcie nazw
Dwa różne pakiety mogą zawierać klasy o tej samej nazwie. Zamiast za każdym razem wpisywać długą nazwę, można nadać przestrzeni nazw własny alias:
using JsonNet = Newtonsoft.Json;
using TextJson = System.Text.Json;
string json1 = JsonNet.JsonConvert.SerializeObject(new { Id = 1 });
string json2 = TextJson.JsonSerializer.Serialize(new { Id = 1 });
Alias nie zmienia typu i nie tworzy nowej klasy. Jest tylko krótszą nazwą rozpoznawaną przez kompilator. W praktyce używam go wtedy, gdy pełne kwalifikowanie nazw pojawia się więcej niż raz albo gdy sam zapis z nazwą klasy byłby niejednoznaczny.
using static i global using
Dyrektywa using static importuje statyczne metody i pola konkretnego typu. Przykład z klasą Math wygląda tak:
using static System.Math;
double wynik = Sqrt(25);
double zaokraglenie = Round(3.14);
Zyskujemy krótszy kod, ale tracimy część informacji o pochodzeniu metody. Dlatego stosowałbym tę formę oszczędnie, szczególnie w dużych projektach, gdzie czytelność często jest ważniejsza niż kilka znaków mniej.
W projektach korzystających z nowszych wersji C# można spotkać także:
global using System.Net.Http;
Taka dyrektywa działa we wszystkich plikach projektu. Przydatne jest również implicit usings, czyli automatyczne importowanie często używanych przestrzeni nazw przez szablon projektu .NET. To wygodne, ale początkujący czasem nie wiedzą wtedy, skąd dana klasa stała się dostępna. Gdy kod zachowuje się niejasno, sprawdzenie ustawień projektu i plików z globalnymi importami szybko wyjaśnia sytuację.
Instrukcja using chroni zasoby systemowe
Drugie zastosowanie dotyczy obiektów, które trzeba po użyciu zamknąć lub zwolnić. Są to między innymi pliki, strumienie, połączenia z bazą danych i niektóre obiekty komunikacji sieciowej. Typ implementujący IDisposable udostępnia metodę Dispose(), która wykonuje sprzątanie zasobów.
Klasyczna forma wygląda następująco:
using (StreamReader reader = File.OpenText("dane.txt"))
{
string zawartosc = reader.ReadToEnd();
Console.WriteLine(zawartosc);
}
Po opuszczeniu bloku Dispose() zostanie wywołane również wtedy, gdy odczyt pliku zakończy się wyjątkiem. To najważniejsza zaleta tej konstrukcji. Ręczne sprzątanie w osobnym miejscu łatwo pominąć, a wtedy plik może pozostać zablokowany albo aplikacja zacznie zużywać coraz więcej uchwytów systemowych.
W uproszczeniu kompilator traktuje tę konstrukcję podobnie do kodu z blokiem try i finally:
StreamReader reader = File.OpenText("dane.txt");
try
{
string zawartosc = reader.ReadToEnd();
}
finally
{
if (reader != null)
{
reader.Dispose();
}
}
Nie należy jednak utożsamiać Dispose() z usunięciem obiektu z pamięci. Za pamięć odpowiada garbage collector, natomiast Dispose() zwalnia zasoby, których garbage collector sam nie obsługuje wystarczająco szybko lub bezpośrednio.
Zakres bloku decyduje o momencie zwolnienia
W tym przykładzie połączenie z bazą jest dostępne wyłącznie wewnątrz nawiasów klamrowych:
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
WykonajZapytanie(connection);
}
// Po tej linii połączenie jest już zwolnione.
To dobra granica dla krótkiej operacji. Nie otwierałbym połączenia wcześniej „na zapas”, bo zasób pozostawałby zajęty dłużej, niż jest potrzebny. Przy aplikacjach webowych ma to szczególne znaczenie, ponieważ wiele równoczesnych żądań może szybko wyczerpać pulę połączeń.
Jeśli korzystasz z asynchronicznego zasobu implementującego IAsyncDisposable, użyj await using:
await using (var stream = File.OpenReadAsync("dane.bin"))
{
// Operacje asynchroniczne
}
W tym przypadku sprzątanie może wymagać oczekiwania na zakończenie operacji. Zwykłe using i Dispose() nie zawsze wystarczą dla zasobów zaprojektowanych specjalnie do pracy asynchronicznej.
Deklaracja using upraszcza kod bez zmiany zasady działania
Od C# 8 można pominąć nawiasy i zdefiniować zasób bezpośrednio przy deklaracji zmiennej:
using var reader = File.OpenText("dane.txt");
string zawartosc = reader.ReadToEnd();
Console.WriteLine(zawartosc);
Obiekt zostanie zwolniony przy opuszczeniu bieżącego zakresu, na przykład metody, bloku if albo całej pętli, zależnie od miejsca deklaracji. To nie jest równoznaczne z natychmiastowym zwolnieniem zasobu po ostatnim użyciu zmiennej.
void PrzetworzPlik()
{
using var reader = File.OpenText("dane.txt");
string dane = reader.ReadToEnd();
// reader nadal istnieje do końca metody.
ZapiszDoLogu(dane);
}
Ta składnia dobrze sprawdza się w krótkich metodach, gdy zasób ma żyć do ich końca. Przy dłuższej metodzie klasyczny blok może być czytelniejszy, bo wyraźniej pokazuje, gdzie kończy się czas życia pliku lub połączenia.
Przeczytaj również: Python iterate over list - 5 metod, które musisz znać!
Kolejność zwalniania kilku zasobów
Gdy deklarujesz kilka zasobów, są one zwalniane w odwrotnej kolejności do deklarowania. Ma to sens, jeśli drugi zasób korzysta z pierwszego:
using var stream = File.OpenRead("dane.txt");
using var reader = new StreamReader(stream);
string tekst = reader.ReadToEnd();
Najpierw zostanie zwolniony reader, a dopiero potem stream. Taki porządek ogranicza ryzyko zamknięcia niższego poziomu zasobu, gdy obiekt wyższego poziomu nadal próbuje z niego korzystać.
Typowe błędy przy używaniu using
Najczęstszy błąd polega na traktowaniu każdej formy using jako sposobu na zwalnianie pamięci. Dyrektywa przestrzeni nazw nie zarządza cyklem życia obiektu. Jeśli kod otwiera plik, tworzy strumień albo pobiera połączenie, trzeba sprawdzić, czy dany typ wymaga using.
- Brak referencji do biblioteki nie zostanie naprawiony przez dodanie dyrektywy. Potrzebna może być paczka NuGet albo odwołanie do innego projektu.
- Zbyt szeroki zakres sprawia, że plik lub połączenie pozostaje otwarte dłużej, niż trzeba.
- Zbyt wąski zakres kończy życie obiektu przed wykonaniem kolejnej operacji.
-
Podwójne zwalnianie zwykle nie jest potrzebne, gdy obiekt znajduje się w
using. Ręczne wywołanieDispose()przed końcem zakresu może utrudnić analizę kodu. - Mylenie IDisposable z pamięcią prowadzi do błędnych założeń o garbage collectorze.
- Nieuważne using static może powodować konflikty nazw i utrudniać ustalenie, z jakiej klasy pochodzi metoda.
Nie każdy obiekt trzeba też umieszczać w using. Dotyczy to przede wszystkim zasobów, które implementują IDisposable, IAsyncDisposable albo odpowiedni wzorzec zwalniania zasobów. Dla zwykłego modelu danych nie ma sensu dopisywać tej konstrukcji tylko dlatego, że „tak jest bezpieczniej”.
W kodzie webowym zwracam szczególną uwagę na czas życia HttpClient, kontekstu bazy danych i strumieni żądania. Mechaniczne używanie using przy każdym obiekcie może być błędem, jeśli framework zarządza jego cyklem życia przez dependency injection. Najpierw sprawdzam, kto utworzył obiekt i kto powinien go zwolnić.
Jak wybrać właściwą formę w codziennym kodzie
Najprostsza reguła wygląda tak: jeśli chcesz skrócić nazwę typu, użyj dyrektywy przestrzeni nazw. Jeśli chcesz zagwarantować zwolnienie zasobu, użyj instrukcji albo deklaracji using. Jeśli nazwy się zderzają, wybierz alias zamiast przepisywać długie, pełne kwalifikacje w wielu miejscach.
| Potrzeba | Najlepszy wybór | Przykład |
|---|---|---|
| Krótsze nazwy klas | Dyrektywa przestrzeni nazw | using System.IO; |
| Konflikt dwóch typów o tej samej nazwie | Alias | using AppJson = MyApp.Json; |
| Krótki, wyraźnie ograniczony fragment pracy | Instrukcja using | using (...) { ... } |
| Zasób używany do końca metody | Deklaracja using | using var stream = ...; |
| Asynchroniczne zwalnianie zasobu | await using |
await using var resource = ...; |
Własne projekty zwykle porządkuję tak, aby dyrektywy były na początku pliku, aliasy pojawiały się tylko wtedy, gdy naprawdę rozwiązują problem, a deklaracje using obejmowały możliwie mały sensowny zakres. Taki styl ogranicza liczbę ukrytych zależności i ułatwia późniejsze przenoszenie kodu między projektami.
Mała konstrukcja, duża różnica w jakości kodu
Słowo using nie oznacza jednej operacji. Raz importuje typy z przestrzeni nazw, innym razem tworzy alias, a jeszcze innym gwarantuje wywołanie Dispose(). Najważniejsze jest więc patrzenie na całe wyrażenie i jego miejsce w kodzie, a nie na sam keyword.
Jeżeli zapamiętasz jedno rozróżnienie, niech będzie nim to: using System... upraszcza nazwy, a using var ... zarządza zasobem. Ta zasada wystarcza, aby poprawnie czytać większość przykładów C# i świadomie wybierać składnię w aplikacjach konsolowych, webowych oraz narzędziach pracujących z plikami lub bazami danych.