IDisposable, using, финализаторы, IAsyncDisposable
↑ 2.2 .NET runtime: CLR, память, GC · ← Предыдущая · → Следующая
Зачем это на собесе
GC освобождает только управляемую память, а соединения с БД, файлы, сокеты и HTTP-ответы надо закрывать явно. Спросят паттерн Dispose, разницу
Disposeи финализатора,usingи кто освобождает сервисы в DI.
Объяснение
Управляемые и неуправляемые ресурсы
- Управляемая память — освобождает GC.
- Неуправляемые ресурсы — дескрипторы ОС (файлы, сокеты), соединения с БД, нативная память, блокировки. GC о них не знает и не может своевременно освободить.
IDisposable и using
using (var conn = new NpgsqlConnection(cs)) { ... } // Dispose в finally
using var file = File.OpenRead(path); // using-объявление: Dispose в конце области видимости
await using var conn = await dataSource.OpenConnectionAsync(ct); // IAsyncDisposable — асинхронное освобождениеusing — это try { ... } finally { obj?.Dispose(); }. Ресурс освобождается даже при исключении.
IAsyncDisposable
Для ресурсов, чьё закрытие требует I/O (сброс буфера в сеть, закрытие соединения): DisposeAsync(). Пример: DbConnection, Stream, DbContext, клиенты брокеров.
Реализация Dispose (упрощённый современный вариант)
Если класс только владеет другими IDisposable (частый случай) — финализатор не нужен:
public sealed class ReportRenderer : IDisposable, IAsyncDisposable
{
private readonly MemoryStream _buffer = new();
private readonly Report _report = new(); // FastReport — IDisposable
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
_report.Dispose();
_buffer.Dispose();
_disposed = true;
}
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_report.Dispose();
await _buffer.DisposeAsync();
_disposed = true;
}
public byte[] Render() { ObjectDisposedException.ThrowIf(_disposed, this); ... }
}Полный паттерн Dispose (для неsealed-классов с неуправляемым ресурсом)
public class NativeHandleOwner : IDisposable
{
private IntPtr _handle; // неуправляемый ресурс (лучше обернуть в SafeHandle!)
private bool _disposed;
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } // финализатор больше не нужен
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing) { /* освободить управляемые IDisposable-поля */ }
if (_handle != IntPtr.Zero) { NativeFree(_handle); _handle = IntPtr.Zero; } // неуправляемое — всегда
_disposed = true;
}
~NativeHandleOwner() => Dispose(false); // финализатор — страховка, если Dispose забыли
}На практике для нативных дескрипторов используют SafeHandle — он сам содержит финализатор, и свой финализатор писать не нужно.
Финализатор (~Class)
- Вызывается GC недетерминированно (когда-нибудь после того, как объект стал недостижим), в отдельном потоке финализации.
- Объект с финализатором переживает лишнюю сборку (попадает в очередь финализации) → дороже для GC.
- Нельзя обращаться к другим управляемым объектам (они могут быть уже собраны).
- Это страховка, а не способ освобождения ресурсов.
Кто освобождает объекты в DI
- Сервисы, созданные контейнером и реализующие
IDisposable/IAsyncDisposable, контейнер освобождает сам при завершении их scope (Scoped — в конце запроса, Singleton — при остановке приложения). - Объекты, созданные вами через
newвнутри сервиса, освобождаете вы. - Экземпляр, переданный при регистрации (
services.AddSingleton(new Foo())), контейнер не освобождает. - Не вызывайте
Disposeу сервисов, полученных из DI, — ими владеет контейнер.
Особые случаи
HttpClient: не оборачивать вusingна каждый запрос (исчерпание сокетов) →IHttpClientFactory.DbContext: в DI Scoped, освобождается автоматически. Вручную — черезIDbContextFactory+await using.HttpResponseMessage,Streamиз ответа — освобождать, иначе соединение не возвращается в пул.CancellationTokenSourceс таймером (CancelAfter) —Dispose, чтобы остановить таймер.
Нюансы и подводные камни
- Забытый
Disposeу соединения с БД → исчерпание пула соединений (The connection pool has been exhausted). Disposeдолжен быть идемпотентным (безопасный повторный вызов) и не бросать исключений.- Возврат
IDisposableиз метода передаёт владение вызывающему — документируйте это. usingна объекте, который отдаётся наружу (например, возвращаемый Stream), закроет его до того, как вызывающий начнёт читать.
Тестирование
[Fact]
public void Throws_after_dispose()
{
var r = new ReportRenderer();
r.Dispose();
r.Invoking(x => x.Render()).Should().Throw<ObjectDisposedException>();
}Вопросы с ответами
Зачем нужен IDisposable, если есть GC?
GC управляет только памятью и делает это недетерминированно. Соединения, файлы, сокеты и другие неуправляемые ресурсы надо освобождать своевременно и явно — через Dispose.
Чем Dispose отличается от финализатора?
Dispose вызывается явно и детерминированно (обычно через using). Финализатор вызывает GC когда-нибудь в отдельном потоке — это страховка, которая удорожает сборку мусора.
Зачем GC.SuppressFinalize в Dispose?
Ресурсы уже освобождены, поэтому финализатор не нужен. Вызов убирает объект из очереди финализации, и он собирается быстрее и дешевле.
Кто освобождает сервисы, зарегистрированные в DI?
Контейнер, для экземпляров, которые он сам создал: Scoped в конце scope (запроса), Singleton при остановке приложения, Transient — вместе со scope, в котором созданы.
Что такое IAsyncDisposable и когда нужен?
Интерфейс с DisposeAsync для ресурсов, которые при закрытии выполняют асинхронный I/O (сброс буферов, закрытие соединений). Используется через await using.
Связанные темы
- Предыдущая: Сборщик мусора: поколения, LOH, режимы GC · Следующая: Утечки памяти в .NET и как их искать
- Время жизни в DI: Время жизни: Singleton, Scoped, Transient
- HttpClient и IHttpClientFactory: HttpClient и IHttpClientFactory, HeaderPropagation
- DbContext: жизненный цикл: DbContext: жизненный цикл, pooling, Unit of Work
- Файловые дескрипторы: Файловая система, права, дескрипторы