Урок 8.8 — Контекстный менеджер using
Автоматическое управление ресурсами: файлы, соединения, блокировки.
Когда твоя программа открывает файл, подключается к базе данных или захватывает блокировку — она использует системные ресурсы. Если их вовремя не освободить, программа начнёт «течь» ресурсами. На сервере это приведёт к тому, что через пару часов перестанут открываться файлы, а база данных откажется принимать подключения. Конструкция using гарантирует, что ресурсы будут освобождены, даже если произошла ошибка.
Проблема управления ресурсами
В C# есть сборщик мусора (GC), который освобождает память от неиспользуемых объектов. Но GC не управляет неуправляемыми ресурсами: файловые дескрипторы, подключения к БД, сокеты, графические объекты GDI+.
Что произойдёт, если не закрыть файл?
// ❌ ПЛОХО: файл может не закрыться
static void ReadFileBad()
{
var reader = new StreamReader("file.txt");
string content = reader.ReadToEnd();
Console.WriteLine(content);
// Если до reader.Close() произойдёт исключение —
// файл останется открытым!
reader.Close();
}
// ❌ ПЛОХО: ручное управление — легко забыть
static void ReadFileMessy()
{
var reader = new StreamReader("file.txt");
try
{
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
finally
{
reader.Close(); // гарантированно закроем
}
}
Ручное управление через try-finally работает, но оно громоздкое и подвержено ошибкам. Особенно когда нужно открыть несколько ресурсов.
Конструкция using (var resource = new Resource()) { }
Конструкция using — это синтаксический сахар для паттерна try-finally. Она гарантирует, что метод Dispose() будет вызван по завершении блока, даже при исключении.
Синтаксис:
using (var resource = new Resource()) { /* использование */ }
// ✅ ХОРОШО: using гарантирует закрытие
static void ReadFileGood()
{
using (var reader = new StreamReader("file.txt"))
{
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
// reader.Dispose() вызван автоматически
}
// Можно вкладывать using друг в друга:
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());
}
}
using-директиву (для импорта пространств имён) с using-конструкцией (для управления ресурсами). Это разные вещи с одинаковым ключевым словом!Интерфейс IDisposable и метод Dispose()
IDisposable — это интерфейс, содержащий единственный метод Dispose(). Любой класс, который использует неуправляемые ресурсы, должен реализовать IDisposable. Конструкция using работает только с объектами, реализующими IDisposable.
public class FileManager : IDisposable
{
private FileStream? _stream;
private bool _disposed = false;
public FileManager(string filePath)
{
_stream = File.Open(filePath, FileMode.OpenOrCreate);
Console.WriteLine($"Файл {filePath} открыт");
}
public void WriteData(string data)
{
if (_disposed)
throw new ObjectDisposedException(nameof(FileManager));
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(data);
_stream?.Write(bytes, 0, bytes.Length);
}
public void Dispose()
{
if (!_disposed)
{
_stream?.Close();
_stream?.Dispose();
_stream = null;
_disposed = true;
Console.WriteLine("Ресурсы освобождены");
}
}
}
// Использование:
using (var fm = new FileManager("test.txt"))
{
fm.WriteData("Hello, World!");
} // Dispose() вызван автоматически
Паттерн Dispose (правильная реализация):
- Флаг
_disposed— предотвращает повторное освобождение. - Проверка
_disposedв методах — защита от использования после освобождения. - Для наследников используют
protected virtual void Dispose(bool disposing). - Вызов
GC.SuppressFinalize(this)— финализация не нужна, если Dispose вызван.
finally как альтернатива using
До появления using (C# 1.0) разработчики использовали try-finally. Эта конструкция тоже гарантирует выполнение кода в блоке finally, независимо от того, было ли исключение.
// Эквивалент using через try-finally:
StreamReader reader = null;
try
{
reader = new StreamReader("file.txt");
Console.WriteLine(reader.ReadToEnd());
}
finally
{
if (reader != null)
reader.Dispose();
}
// То же самое с using:
using (var r = new StreamReader("file.txt"))
{
Console.WriteLine(r.ReadToEnd());
}
// using короче, читабельнее и не требует ручной проверки на null!
using — это и есть try-finally «под капотом». Компилятор транслирует using в try-finally с вызовом Dispose().Using declaration (C# 8+): using var resource = new Resource()
Начиная с C# 8.0, появилась более лаконичная форма — using declaration. Теперь не нужно писать фигурные скобки: ресурс будет освобождён в конце текущей области видимости.
// C# 8+ — using declaration:
static void ReadFileModern()
{
using var reader = new StreamReader("file.txt");
string content = reader.ReadToEnd();
Console.WriteLine(content);
// Dispose() будет вызван при выходе из метода
}
// Сравните с классическим using:
static void ReadFileClassic()
{
using (var reader = new StreamReader("file.txt"))
{
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
}
// Using declaration удобен для нескольких ресурсов:
static void ProcessFiles()
{
using var input = new StreamReader("input.txt");
using var output = new StreamWriter("output.txt");
string? line;
while ((line = input.ReadLine()) != null)
{
output.WriteLine(line.ToUpper());
}
// input и output будут освобождены в обратном порядке
}
Когда использовать какой стиль?
- using declaration (using var) — для временных ресурсов внутри метода, когда не нужен явный блок.
- Классический using (using { }) — когда нужно чётко ограничить время жизни ресурса: «открыл — использовал — закрыл до конца блока».
- try-finally — только если using недоступен (например, старый код, .NET Framework 4.x).
Встроенные классы, реализующие IDisposable
Многие классы .NET реализуют IDisposable. Вот наиболее часто используемые:
StreamReader / StreamWriter (работа с файлами)
using var reader = new StreamReader("data.txt");
string firstLine = reader.ReadLine() ?? "";
using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello");
SqlConnection (подключение к БД)
using var connection = new SqlConnection("connection string");
connection.Open();
// работа с БД
// соединение автоматически закроется при выходе из блока
HttpClient (HTTP-запросы)
// ⚠️ Внимание: HttpClient обычно используют как static/singleton
// Но в краткосрочных сценариях:
using var client = new HttpClient();
var response = await client.GetStringAsync("https://example.com");
Console.WriteLine(response);
Другие IDisposable классы
FileStream,MemoryStream,BufferedStreamSqlConnection,SqlCommand,SqlDataReaderStreamReader,StreamWriter,BinaryReader,BinaryWriterZipArchive,ZipFileBitmap,Font,Pen,Brush(GDI+)Mutex,Semaphore,ProcessNetworkStream,SslStream,TcpClient
Мини-тест
Вопрос 1: Для чего нужна конструкция using?
Правильный ответ: B
Вопрос 2: Какой интерфейс должен реализовать класс, чтобы его можно было использовать в using?
Правильный ответ: C
Вопрос 3: Когда вызывается Dispose() при использовании using declaration?
Правильный ответ: B
Вопрос 4: Что выведет код?
using var r = new StreamReader("test.txt");
// ... чтение
// Dispose() будет вызван?
Правильный ответ: A
Вопрос 5: В какой языковой версии появился using declaration (using var)?
Правильный ответ: C
Что важно запомнить
Тест: 8.8: Контекстный менеджер using
3 вопроса