Как правильно управлять ресурсами в .NET Core
Сборщик мусора .NET отлично справляется с очисткой управляемых ресурсов, но с неуправляемыми справиться не может. Разбираемся, что тут к чему. Обсудить Перевод публикуется с сокращениями, автор оригинальной статьи Daniel Glucksman. *** Даже если вы сами не используете неуправляемые ресурсы напрямую или не знаете об их существовании, многие встроенные классы .NET используют их из коробки: сетевое взаимодействие (System.Net), потоки и обработка файлов (System.IO), обработка изображений (System.Drawing), криптография. Полный список доступен на сайте. Что произойдет, если вы неправильно распорядитесь неуправляемыми ресурсами? Если вы не используете неуправляемый код напрямую, ресурсы будут очищены, но: Если вы вручную распределяете неуправляемые ресурсы, очень важно избавиться от них, поскольку они никогда не будут очищены автоматически, вызовут утечку памяти и система может рухнуть. Вы спросите, как нам правильно распоряжаться ресурсами в .NET Core? Чтобы ответить на этот вопрос, позвольте мне познакомить вас с IDisposable. Что такое IDisposable? IDisposable – это встроенный .NET интерфейс. Согласно документации Microsoft, он обеспечивает механизм высвобождения неуправляемых ресурсов. Интерфейс предоставляет метод Dispose, который при реализации должен очистить все соответствующие ресурсы. В C# 8 появился асинхронный способ утилизации ресурсов с помощью IAsyncDisposable и DisposeAsync. Если класс реализует IDisposable, обычно (интерфейс иногда используется для других целей) это признак того, что он использует неуправляемые ресурсы прямо или косвенно и должен быть соответствующим образом утилизирован. Необходимо помнить, что IDisposable полагается на вызов метода Dispose программистом, поскольку в рантайме он сам вызываться не будет. Правильный способ реализации IDisposable Рекомендуемая практика заключается в реализации Dispose таким образом, чтобы независимо от количества вызовов метода очищался он только один раз. Прежде, чем пытаться освободить ресурсы, необходимо проверить, был ли объект уже удален: Если вы планируете наследовать от класса, необходимо сделать метод Dispose виртуальным, как показано ниже: Виртуальный метод дает наследуемому классу возможность переопределить функцию и очистить ресурсы: Предупреждение: если вы забудете вызвать метод базового класса Dispose, ресурсы не будут полностью очищены. Finalizers IDisposable полагается на разработчика, чтобы правильно вызвать метод Dispose. Вы можете добавить финализатор в свой класс для обеспечения автоматической очистки ресурсов: даже если вы забудете о методе Dispose, GC все сделает сам. Финализатор определяется с помощью символа «~», за которым следует имя класса: Предупреждение: использование финализатора может привести к дополнительным накладным расходам, поэтому лучше самостоятельно очищать ресурсы с помощью IDisposable. Рекомендуется создавать его, только когда вы непосредственно владеете любыми неуправляемыми ресурсами. При использовании финализатора стоит централизовать логику dispose в дополнительную функцию (мы назвали ее Cleanup), которая может быть вызвана как из финализатора, так и из метода Dispose. Функция Dispose должна сообщить сборщику мусора, что ему не нужно вызывать финализатор, поскольку ресурсы уже были очищены – это поможет избежать дополнительных расходов на процесс очистки GC. Управляемый код никогда не должен очищаться, если Dispose вызывается из финализатора, потому что он мог уже быть очищен GC. Если вы будете наследовать от приведенного выше класса, то можно пометить функцию Cleanup как виртуальную: Унаследованный класс переопределит ее следующим образом: Использование IDisposable Всякий раз, когда используете реализующий интерфейс IDisposable класс, вызывайте метод Dispose после завершения работы с ним. Возьмем, к примеру, класс StreamWriter. Инициализируем объект, запишем одну строку текста в файл, а затем очистим все. Если во время записи в файл возникнет исключение, то StreamWriter не будет удален должным образом. Правильный способ это исправить – обернуть все в блок try/finally, чтобы гарантировать вызов Dispose даже если возникнет исключение. Если ваш класс имеет переменную-член, поле или свойство, реализующее IDisposable, вы должны предусмотреть возможность избавиться от него. Например, класс Logger обертывает встроенный класс StreamWriter, чтобы включить запись в файл журнала. Поскольку класс StreamWriter реализует IDisposable, нужно реализовать IDisposable в нашем классе Logger для очистки ресурсов StreamWriter. Пользователь класса Logger может распорядиться ресурсами следующим образом: Если класс задействует какие-либо неуправляемые ресурсы напрямую, обязательно реализуйте IDisposable. GC может автоматически очищать управляемые ресурсы, но не знает, когда вы закончите работу с неуправляемыми. Никогда не создавайте необработанные исключения в методе Dispose. Поскольку Dispose будет действовать в блоке finally, любые необработанные исключения всплывут в приложении. Если Dispose вызывается из Finalizer, нормальная работа приложения не гарантируется. Оператор using – это конструкция, которая автоматически вызывает метод Dispose при выходе из своей области видимости, даже если внутри нее возникает исключение. Возьмем предыдущий код: После реализации блока using пример будет выглядеть следующим образом: Начиная с C# 8, блок using не нуждается в фигурных или обычных скобках, поэтому код можно упростить: Вы можете инициализировать несколько переменных одного типа в блоке using, если не используется ключевое слово var: Примечание: оператор using не всегда полезен, например, если класс StreamWriter не должен быть привязан к using или необходимо добавить оператор catch. С помощью оператора using можно не только красиво очищать ресурсы. Вот простой пример, в котором IDisposable применяется для создания тега Оператор using, выглядит следующим образом: Другой пример – использующий IDisposable класс и блок using для управления транзакциями базы данных: Заключение Правильное распределение ресурсов – ключ к сохранению нормальной работы .NET-приложения. При работе с управляемым кодом этот принцип сводится к вызову метода Dispose в любом IDisposable-классе, независимо от того, вызывается он напрямую через оператор using или нет. Работающим с неуправляемым кодом необходимо позаботиться не только о правильном способе его удаления, но и проследить за программистом, когда тот забудет о необходимости его утилизировать.
public class MyClass : IDisposable { public void Dispose() { // Тут чистим ресурсы } }
using System.Threading.Tasks; public class MyClass : IAsyncDisposable { public async ValueTask DisposeAsync() { // Тут чистим ресурсы } }
public class MyClass : IDisposable { private bool isDisposed = false; public void Dispose() { if(this.isDisposed) return; // Избавляемся от управляемых ресурсов // Избавляемся от неуправляемых ресурсов isDisposed = true; } }
public class MyClass : IDisposable { private bool disposed = false; public virtual void Dispose() { //... } }
public class MyInheritedClass : MyClass { public override void Dispose() { //Cleanup логика, специфичная для наследуемого класса base.Dispose(); // вызываем функцию cleanup в MyClass } }
public class MyClass { ~MyClass() { // Тут чистим ресурсы } }
public class MyClass : IDisposable { private bool disposed = false; public void Dispose() { Cleanup(); } private void Cleanup() { if(this.disposed) return; // Избавляемся от управляемых ресурсов // Избавляемся от неуправляемых ресурсов disposed = true; } ~MyClass() { Cleanup(); } }
public class MyClass : IDisposable { private bool disposed = false; public void Dispose() { Cleanup(); GC.SuppressFinalize(this); } private void Cleanup() { if(this.disposed) return; // Избавляемся от управляемых ресурсов // Избавляемся от неуправляемых ресурсов disposed = true; } ~MyClass() { Cleanup(); } }
public class MyClass : IDisposable { private bool disposed = false; public void Dispose() { Cleanup(false); GC.SuppressFinalize(this); } private void Cleanup(bool calledFromFinalizer) { if(this.disposed) return; if(!calledFromFinalizer) { // Избавляемся от управляемых ресурсов } // Избавляемся от неуправляемых ресурсов disposed = true; } ~MyClass() { Cleanup(true); } }
protected virtual void Cleanup(bool calledFromFinalizer)
public class MyInheritedClass : MyClass { protected override void Cleanup(bool calledFromFinalizer) { //Cleanup логика, специфичная для наследуемого класса base.Cleanup(calledFromFinalizer); } }
Правило 1: утилизируйте классы, реализующие IDisposable
StreamWriter writer = new StreamWriter("newfile.txt"); writer.WriteLine("Line of Text"); writer.Dispose();
StreamWriter writer = new StreamWriter("newfile.txt"); try { writer.WriteLine("Line of Text"); } finally { writer.Dispose(); }
Правило 2: Если ваш класс владеет объектом IDisposable, реализуйте IDisposable
public class Logger { private readonly StreamWriter _streamWriter; public Logger() { _streamWriter = new StreamWriter("logfile.txt"); } public void WriteToLog(string text) { _streamWriter.WriteLine(text); } }
public class Logger : IDisposable { private readonly StreamWriter _streamWriter; public Logger() { _streamWriter = new StreamWriter("logfile.txt"); } public void WriteToLog(string text) { _streamWriter.WriteLine(text); } public void Dispose() { _streamWriter.Dispose(); } }
Logger logger = new Logger();try { logger.WriteToLog("Log Line"); } finally { logger.Dispose(); }
Правило 3: при непосредственном использовании неуправляемых ресурсов реализуйте IDisposable и финализатор
public class UnmanagedClass : IDisposable { private IntPtr pointer; private bool disposed = false; public UnmanagedClass() { pointer = Marshal.AllocHGlobal(1024); } public void Dispose() { if(disposed) return; Marshal.FreeHGlobal(pointer); pointer = IntPtr.Zero; GC.SuppressFinalize(this); disposed = true; } ~UnmanagedClass() { Dispose(); } }
Правило 4: избегайте необработанных исключений
public class Logger : IDisposable { private readonly StreamWriter _streamWriter; public Logger() { _streamWriter = new StreamWriter("logfile.txt"); } public void WriteToLog(string text) { _streamWriter.WriteLine(text); } public void Dispose() { _streamWriter.Dispose(); throw new Exception(); } }
Оператор «using»
StreamWriter writer = new StreamWriter("newfile.txt");try { writer.WriteLine("Line of Text"); } finally { writer.Dispose(); }
using(StreamWriter writer = new StreamWriter("newfile.txt")) { writer.WriteLine("Line of Text");} //Тут Dispose вызовется автоматом
using StreamWriter writer = new StreamWriter("newfile.txt"); writer.WriteLine("Line of Text");
using StreamWriter writer = new StreamWriter("newfile.txt"), otherWriter = new StreamWriter("otherFile.txt");
Для чего еще применяется using
</HTML>
, не требуя от пользователя помнить об этом (тег будет закрыт, даже если внутри блока возникнет исключение).
public class HTMLWriter : IDisposable { public void Dispose() { // Закрывающийся тег } }
using(var HTML = new HTMLWriter()) { }
using (var scope = new TransactionScope) { //Любые запросы к БД }
Дополнительные материалы по теме
- 14 views
- 0 Comment