Действительно ли необходимо реализовать шаблон утилизации только для управляемых ресурсов? - PullRequest
3 голосов
/ 16 апреля 2019

Я внимательно прочитал эту статью , и кажется, что в ней четко указано, что шаблон dispose должен быть реализован во всех случаях реализации IDisposable. Я пытаюсь понять, почему мне нужно реализовать шаблон dispose в тех случаях, когда мой класс содержит только управляемые ресурсы (т.е. другие IDisposable члены или безопасные дескрипторы). Почему я не могу просто написать

class Foo : IDisposable
{
    IDisposable boo;

    void Dispose()
    {
        boo?.Dispose();
    }
}

Если точно известно, что неуправляемых ресурсов нет и нет смысла вызывать метод Dispose из финализатора, поскольку управляемые ресурсы не освобождаются из финализатора?

Обновление : для большей ясности. Обсуждение, похоже, сводится к вопросу о том, нужно ли реализовывать шаблон dispose для каждого базового открытого незапечатанного класса, который реализует IDisposable, или нет. Однако я не могу найти потенциальных проблем с иерархией, когда базовый класс без неуправляемых ресурсов не использует шаблон dispose, в то время как дочерние классы, которые имеют неуправляемые ресурсы, действительно используют этот шаблон:

class Foo : IDisposable
{
    IDisposable boo;

    public virtual void Dispose()
    {
        boo?.Dispose();
    }
}

// child class which holds umanaged resources and implements dispose pattern
class Bar : Foo
{
    bool disposed;
    IntPtr unmanagedResource = IntPtr.Zero;

    ~Bar()
    {
        Dispose(false);
    }

    public override void Dispose()
    {
        base.Dispose();
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposed)
            return;

        if (disposing)
        {
            // Free any other managed objects here.
            //
        }
        // close handle

        disposed = true;
    }
}

// another child class which doesn't hold unmanaged resources and merely uses Dispose 
class Far : Foo
{
    private IDisposable anotherDisposable;

    public override void Dispose()
    {
        base.Dispose();
        anotherDisposable?.Dispose();
    }
}

Более того, для меня это выглядит как лучшее разделение интересов, когда реализации отвечают только за те вещи, о которых они знают.

Ответы [ 4 ]

5 голосов
/ 16 апреля 2019

Это

private class Foo : IDisposable
{
    IDisposable boo;

    public void Dispose()
    {
        boo?.Dispose();
    }
}

Отлично. Как это

public sealed class Foo : IDisposable
{
    IDisposable boo;

    public void Dispose()
    {
        boo?.Dispose();
    }
}

Что может пойти не так, если у меня есть общедоступный незапечатанный базовый класс, реализованный, как указано выше, с помощью виртуального метода Dispose?

Из документов :

Потому что порядок, в котором уничтожает сборщик мусора объекты во время финализации не определены, вызывая это Dispose перегрузка со значением false не позволяет финализатору пытаться освободить управляемые ресурсы, которые, возможно, уже были возвращены.

Доступ к управляемому объекту, который уже был возвращен, или доступ к его свойствам после его удаления (возможно, другим финализатором) приведет к возникновению исключения в финализаторе, что bad :

Если Finalize или переопределение Finalize выдает исключение, и среда выполнения не размещается приложением, которое переопределяет значение по умолчанию политика, среда выполнения завершает процесс, и нет активной попытки / наконец блоки или финализаторы выполнены. Такое поведение обеспечивает процесс целостность, если финализатор не может освободить или уничтожить ресурсы.

Так что, если у вас было:

   public  class Foo : IDisposable
    {
        IDisposable boo;

        public virtual void Dispose()
        {
            boo?.Dispose();
        }
    }
    public class Bar : Foo
    {
        IntPtr unmanagedResource = IntPtr.Zero;
        ~Bar()
        {
            this.Dispose();
        }

        public override void Dispose()
        {
            CloseHandle(unmanagedResource);
            base.Dispose();
        }

        void CloseHandle(IntPtr ptr)
        {
            //whatever
        }
    }

~ Bar -> Bar.Dispose () -> base.Dispose () -> boo.Dispose () Но GC, возможно, восстановил бу.

1 голос
/ 16 апреля 2019

Я еще не видел упомянутое конкретное использование Dispose, поэтому я решил указать общий источник утечек памяти, если не использовать шаблон dispose.

Visual Studio 2017 на самом деле жалуетсяоб этом через статический анализ кода, который я должен "реализовать шаблон утилизации".Обратите внимание, что я использую SonarQube и SolarLint, и я не верю, что Visual Studio поймает это в одиночку.FxCop (другой инструмент статического анализа кода), вероятно, будет, хотя я не проверял это.

Я отмечаю приведенный ниже код для демонстрации шаблона dispose, который также предназначен для защиты от чего-то подобного, у которого нет неуправляемых ресурсов.:

public class Foo : IDisposable
{
    IDisposable boo;

    public void Dispose()
    {
        boo?.Dispose();
    }
}

public class Bar : Foo
{
    //Memory leak possible here
    public event EventHandler SomeEvent;

    //Also bad code, but will compile
    public void Dispose()
    {
        someEvent = null;
        //Still bad code even with this line
        base.Dispose();
    }
}

Выше показан очень плохой код.Не делай этого.Почему этот ужасный код?Вот из-за этого:

Foo foo = new Bar();
//Does NOT call Bar.Dispose()
foo.Dispose();

Предположим, этот ужасающий код был выставлен в нашем публичном API.Рассмотрим перечисленные выше классы, используемые потребителями:

public sealed class UsesFoo : IDisposable
{
    public Foo MyFoo { get; }

    public UsesFoo(Foo foo)
    {
        MyFoo = foo;
    }

    public void Dispose()
    {
        MyFoo?.Dispose();
    }
}

public static class UsesFooFactory
{
    public static UsesFoo Create()
    {
        var bar = new Bar();
        bar.SomeEvent += Bar_SomeEvent;
        return new UsesFoo(bar);
    }

    private static void Bar_SomeEvent(object sender, EventArgs e)
    {
        //Do stuff
    }
}

Являются ли потребители идеальными?Нет .... UsesFooFactory, вероятно, следует также отказаться от подписки на событие.Но он выделяет общий сценарий, когда событие подписчик переживает издатель .

Я видел, как события вызывают бесчисленные утечки памяти.Особенно в очень больших или чрезвычайно высокопроизводительных кодовых базах.

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

Опять чрезмерно упрощенный пример и ужасный код.Но на самом деле никогда не рекомендуется вызывать Dispose для объекта, а , а не ожидать, что он удалит весь объект, независимо от того, был он получен миллион раз или нет.

Edit

Обратите внимание, что этот ответ преднамеренно касается только управляемых ресурсов, демонстрируя, что шаблон dispose также полезен в этом сценарии.Это целенаправленно не относится к варианту использования для неуправляемых ресурсов, так как я чувствовал, что недостаточно внимания уделяется только управляемым видам использования.И здесь есть много других хороших ответов, которые говорят об этом.

Я, однако, отмечу несколько быстрых вещей, которые важны, когда речь идет о неуправляемых ресурсах.Приведенный выше код может не адресовать неуправляемые ресурсы, но я хочу пояснить, что он не противоречит тому, как они должны обрабатываться.

Чрезвычайно важно использовать финализаторы , когдаВаш класс отвечает за неуправляемые ресурсы.Вкратце, финализаторы автоматически вызываются сборщиком мусора.Таким образом, это дает вам разумную гарантию, что его всегда вызывают в определенный момент времени.Он не пуленепробиваемый, но далек от надежды на то, что пользовательский код вызывает Dispose.

Эта гарантия не верна Dispose.GC может вернуть объект без вызова Dispose.Это основная причина, по которой финализаторы используются для неуправляемых ресурсов.Сам GC обрабатывает только управляемые ресурсы.

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

GC, видя, что объект свободен для восстановления, ноимеет финализатор, задержит сбор, поместив объект в очередь финализатора.Это добавляет значительное ненужное время жизни объекту, а также усиливает давление на GC.

Наконец, я отмечу, что финализаторы по этой причине недетерминированы, несмотря на то, что синтаксис похож на деструктор в C ++.Они очень разные звери.Вы никогда не должны полагаться на финализатор для очистки неуправляемых ресурсов в определенный момент времени.

0 голосов
/ 16 апреля 2019

Если Dispose() реализован с использованием общедоступного виртуального метода, производные классы, которые ожидают переопределения этого метода, могут это сделать, и все будет хорошо.Если, однако, что-либо в цепочке наследования реализует IDisposable.Dispose() с помощью средств, отличных от переопределения общедоступного виртуального метода Dispose(), это может сделать невозможным для дочернего производного класса реализовать свой собственный IDisposable.Dispose(), сохраняя при этом возможность доступародительская реализация.

Шаблон Dispose(bool) может использоваться независимо от того, существует или нет общедоступный метод Dispose(), и, таким образом, избегает необходимости иметь отдельные шаблоны для случаев, когда класс делает или делаетне выставлять публичный Dispose() метод.GC.SuppressFinalize(this) обычно можно заменить на GC.KeepAlive(this), но для классов без финализаторов стоимость примерно одинакова.В отсутствие этого вызова финализаторы для любых объектов, на которые ссылается класс, могут сработать во время работы собственного метода Dispose класса.Не вероятный сценарий, ни тот, который обычно вызывает проблемы, даже если он возникает, но передача this в GC.KeepAlive(Object) или GC.SuppressFinalize(Object) делает такие странные ситуации невозможными.

0 голосов
/ 16 апреля 2019

Возможно, вы ошиблись. Вам не нужно реализовывать финализатор, если у вас нет неуправляемых ресурсов. Вы можете проверить это, используя автоматическую реализацию шаблонов в Visual Studio (она даже сгенерирует комментарии о том, что вы должны раскомментировать финализатор ТОЛЬКО если вы используете неуправляемые ресурсы).

Шаблон dispose используется только для объектов, которые обращаются к неуправляемым ресурсам.

Если вы разрабатываете базовый класс и некоторые из наследующих классов обращаются к неуправляемым ресурсам, наследующие классы сами с этим справятся, переопределив Dispose(bool) и определив финализатор.

Это объяснено в этой статье, все финализаторы будут вызваны в любом случае, если не будут подавлены. И в случае подавления все было бы освобождено цепочкой Diapose(true) вызовов в первую очередь.

...