Освобождение ресурсов в деструкторе C#

114
18 февраля 2021, 09:50

В конструкторе объекта класса осуществляется подключение к БД MySQL, в деструкторе подключение закрывается. Допустимо ли подобное в C#?

Знаю, что, например, в java нет никакой гарантии, что метод finalize будет вызван. А как дела обстоят с деструкторами в C#? Будет ли соединение с БД корректно закрываться или же будет зависать при завершении приложения?

Как быть? Спасибо.

Answer 1

C# и платформа .NET предоставляет программисту возможность освобождать захваченные ресурсы с помощью интерфейса IDisposable и оператора using. Это работает следующим образом:

1) Вы пишете класс и реализуете в нем IDisposable

public class Foo : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Dispose called!");
    }
}

2) После этого вы исользуете using

using(var foo = new Foo())
{
    // ... do stuff
}

3) using компилятором разворачивается в следующее

var foo = new Foo()
try
{
    // ... do stuff
}
finally
{
    if (foo != null)
        ((IDisposable)foo).Dispose();
}

Таким образом, метод Dispose() будет вызван и освободит ваши ресурсы. В выводе мы конечно увидим

Dispose called!

Но вот вопрос: а что, если программист забудет использовать using для вашего класса? В имплементации выше ваш метод Dispose() тогда не будет вызван никогда, так как кроме как оператору using факт того, что вы реализовали IDisposable больше никому не интересен. Как с этим работать? Для этого есть финализатор. Финализатор - это аналог деструктора. Когда GC удаляет из памяти объекты, он особенно относится к объектам, у которых определен финализатор - они встают в отдельную очередь и, в итоге, GC вызовет финализатор при уничтожении объекта. С этом случае что можно сделать? Вот например такой класс

public class Foo : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Dispose called!");
    }
    ~Foo()
    {
        Dispose();
    }
}

Но у этого класса явно проблема, ведь если программист использует using и вызывает Dispose(), и потом GC при удалении объекта вызовет финализатор который вызовет Dispose(), тогда получается Dispose() будет вызван дважды. Чтобы этого избежать, мы можем добавить вот жту строчку GC.SuppressFinalize(this); в Dispose() метод. Она говорит GC, что не надо вызывать деструктор объекта, так как он уже был вызван и ресурс уже освобожден. С таким фокусом наш класс превращается в

public class Foo : IDisposable
{
    public void Dispose()
    {
        RealDispose();
        GC.SuppressFinalize(this);
    }
    protected void RealDispose()
    {
        Console.WriteLine("Dispose called!");
        // тут будет сама логика освобождения ресурса           
    }
    ~Foo()
    {
        RealDispose();
    }
}

С таким подходом, если программист вызовет Dispose() сам или с помощью using - то GC повторно его вызывать уже не будет. Если программист забудет это сделать, то GC вызовет очистку ресурсов через финализатор во время уничтожения объектов.

Теперь, отвечая на ваш изначальный вопрос, если вы создаете подключение к БД и сами его не закрываете, то это сделает за вас GC, но когда он это сделает - никто не знает заранее. Поэтому, если вы пишете класс и у него есть захваченный ресурс, то имеет смысл реализовать IDisposable освобождать ресурс в этот момент.

Но тут есть нюанс. Например, если вы захватываете такую вещь, как подключение к БД, которая сама по себе является классом, реализующем IDisposable, то вам нет никакого резона отпускать это соединение в финализаторе, так как если у вас вызван финализатор, то ваш объект на пути к смерти и, скорее всего, то же самое происходит и с подключением к БД, которое, кстати, может быть уже уничтожено GC. То есть, говоря о подключении к БД вот такой реализации будет достаточно

public class Foo : IDisposable
{
    SqlConnection conn;
    public Foo()
    {
        conn = new SqlConnection(....);
    }
    public void Dispose()
    {
        conn?.Dispose();
    }       
}

Так как если программист вызвал Dispose() метод вручную (или через using), то подключение будет освобождено. Если программист это забыл сделать, то Dispose() метод никогда не будет вызван, зато GC вызовет финализатор класса SqlConnection и все равно его закроет и уничтожит, вы просто не будете знать когда он это сделает.

Подробнее про реализацию IDisposable можно почитать тут.

Answer 2

В C# есть механизм, который может освобождать ресурсы при сборке мусора. В общем случае вам необходимо реализовать паттерн освобождаемых объектов.

Этот путь чреват тонкостями и сложностями. В худшем случае освобождение объектов будет происходить при сборке мусора, то есть в неопределённый момент времени. Памяти у вас может быть много, а подключений к БД всего 20 или 100, и все эти подключения могут быть заняты висящими в памяти объектами.

Альтернативой является использование IoC-контейнеров, таких, как Autofac, NInject, Castle, Unity.

Этот метод требует изучения. Рекомендую книгу Марка Симана Внедрение зависимостей в .NET. Она есть в переводе в бумажном виде, и, возможно, в электронном тоже ходит по просторам интернета.

Answer 3

Думаю, не стоит работать с подключением к БД через конструкторы и деструкторы. Открытие и закрытие подключение очень важные аспекты получения данных, которые желательно контролировать напрямую (не беру в расчет готовые ORM). Если конкретнее отвечать на ваш вопрос, то вариант закрытия соединения в деструкторе возможен, но плохо для этого подходит, т.к. деструктор вызывается перед удалением объекта GC (сборщиком мусора), работу которого вы напрямую не контролируете. О том, как работать с подключениями можно почитать на MSDN

READ ALSO
Парсинг одинаковых тегов в HtmlAgilityPack

Парсинг одинаковых тегов в HtmlAgilityPack

Моя проблема заключается в том, что у меня есть несколько тегов и они одинаковыеМне необходимо сделать парсинг первого тега из этих нескольких...

106
Раздельные логи микросервисов

Раздельные логи микросервисов

У меня микросервисное API на dotnet core 22 Изучал и брал в пример: https://github

133
Перезаписываются поля модели

Перезаписываются поля модели

Собственно в list каждый раз должен записываться x,y он и записывается в дебагере видно но после в итоге получается как на скрине все элементы...

85
PHP preg_replace заменить GET параметр

PHP preg_replace заменить GET параметр

Так вот, у меня есть вот такой ЧПУ:

104