Поделиться через


Шаблон удаления

Замечание

Это содержимое перепечатывается разрешением Pearson Education, Inc. из руководства по проектированию платформы: соглашения, идиомы и шаблоны для повторно используемых библиотек .NET, 2-го выпуска. Этот выпуск был опубликован в 2008 году, и книга с тех пор была полностью пересмотрена в третьем выпуске. Некоторые сведения на этой странице могут быть устаревшими.

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

Среда CLR обеспечивает поддержку автоматического управления памятью. Управляемая память (выделенная память с помощью оператора newC#) не должна быть явно освобождена. Он освобождается автоматически сборщиком мусора (GC). Это освобождает разработчиков от мученной и сложной задачи освобождения памяти и является одной из основных причин беспрецедентной производительности, предоставляемой платформой .NET Framework.

К сожалению, управляемая память — это лишь один из многих типов системных ресурсов. Ресурсы, отличные от управляемой памяти, по-прежнему должны быть выпущены явным образом и называются неуправляемыми ресурсами. GC специально не предназначен для управления такими неуправляемыми ресурсами, что означает, что ответственность за управление неуправляемых ресурсов лежит в руках разработчиков.

Среда CLR предоставляет некоторую помощь в освобождении неуправляемых ресурсов. System.Object объявляет виртуальный метод Finalize (также называемый методом завершения), который вызывается GC до того, как память объекта будет удалена GC и может быть переопределен для освобождения неуправляемых ресурсов. Типы, переопределяющие финализатор, называются финализируемыми типами.

Хотя методы завершения эффективны в некоторых сценариях очистки, они имеют два существенных недостатка:

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

  • Когда среда CLR должна вызвать метод завершения, она должна отложить сбор памяти объекта до следующего раунда сборки мусора (методы завершения выполняются между коллекциями). Это означает, что память объекта (и все объекты, к которым он ссылается) не будет освобождена в течение длительного периода времени.

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

Платформа предоставляет System.IDisposable интерфейс, который следует реализовать, чтобы предоставить разработчику ручной способ выпуска неуправляемых ресурсов, как только они не нужны. Он также предоставляет GC.SuppressFinalize метод, который может сообщить GC, что объект был вручную удален и больше не должен быть завершен, в этом случае память объекта может быть удалена ранее. Типы, реализующие IDisposable интерфейс, называются одноразовыми типами.

Шаблон Dispose предназначен для стандартизации использования и реализации финализаторов и IDisposable интерфейса.

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

✓ DO реализует шаблон basic Dispose Pattern на типах, содержащих экземпляры одноразовых типов. Дополнительные сведения о базовом шаблоне см. в разделе "Базовый шаблон удаления ".

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

✓ DO реализуйте базовый шаблон удаления и предоставьте финализатор для типов, содержащих ресурсы, которые должны быть освобождены явно и не имеют финализаторов.

Например, шаблон следует реализовать для типов, которые хранят неуправляемые буферы памяти. В разделе "Завершенные типы" рассматриваются рекомендации, связанные с реализацией методов завершения.

✓ РАССМОТРИТЕ возможность реализации основного шаблона Dispose на классах, которые сами не хранят неуправляемые ресурсы или объекты, реализующие Dispose, но, скорее всего, могут иметь подтипы с такими объектами.

Примером этого является System.IO.Stream класс. Хотя это абстрактный базовый класс, который не содержит никаких ресурсов, большинство его подклассов содержат их, поэтому он реализует этот шаблон.

Базовый шаблон удаления

Базовая реализация шаблона включает в себя реализацию интерфейса System.IDisposable и объявление метода Dispose(bool), который реализует всю логику очистки ресурсов для совместного использования между методом Dispose и опциональным финализатором.

В следующем примере показана простая реализация базового шаблона:

public class DisposableResourceHolder : IDisposable {

    private SafeHandle resource; // handle to a resource

    public DisposableResourceHolder() {
        this.resource = ... // allocates the resource
    }

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

    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            if (resource!= null) resource.Dispose();
        }
    }
}

Логический параметр disposing указывает, был ли метод вызван из реализации IDisposable.Dispose или из финализатора. Реализация Dispose(bool) должна проверить параметр перед доступом к другим эталонным объектам (например, поле ресурса в предыдущем примере). К таким объектам следует обращаться только при вызове IDisposable.Dispose метода из реализации (если disposing параметр равен true). Если метод вызывается из средства завершения (disposing имеет значение false), доступ к другим объектам не должен быть получен. Причина заключается в том, что объекты финализированы по непредсказуемому порядку, поэтому они или любые из их зависимостей, возможно, уже были финализированы.

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

✓ DO объявляет protected virtual void Dispose(bool disposing) метод для централизации всей логики, связанной с освобождением неуправляемых ресурсов.

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

protected virtual void Dispose(bool disposing) {
    if (disposing) {
        if (resource!= null) resource.Dispose();
    }
}

✓ DO реализует IDisposable интерфейс путем простого вызова Dispose(true) , за которым следует GC.SuppressFinalize(this).

Вызов SuppressFinalize должен выполняться только в том случае, если Dispose(true) выполняется успешно.

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

X НЕ делает метод без Dispose параметров виртуальным.

Метод Dispose(bool) — это тот, который должен быть переопределен подклассами.

// bad design
public class DisposableResourceHolder : IDisposable {
    public virtual void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

// good design
public class DisposableResourceHolder : IDisposable {
    public void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

X НЕ объявляйте любые перегрузки метода, кроме Dispose, Dispose() и Dispose(bool).

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

✓ ДОПУСКАЙТЕ вызов метода Dispose(bool) более одного раза. Метод может решить ничего не делать после первого вызова.

public class DisposableResourceHolder : IDisposable {

    bool disposed = false;

    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

X ИЗБЕГАЙТЕ выброса исключения изнутри Dispose(bool) за исключением критических ситуаций, когда исполняемый процесс поврежден (утечки, несогласованное общее состояние и т. д.).

Пользователи ожидают, что вызов Dispose не вызовет исключение.

Если Dispose может вызвать исключение, дальнейшая логика очистки в блоке finally не будет выполняться. Чтобы обойти эту проблему, пользователю потребуется упаковать каждый вызов Dispose (в пределах окончательного блока!) в блоке проб, что приводит к очень сложным обработчикам очистки. Если выполняется метод Dispose(bool disposing), никогда не выбрасывайте исключение, если удаление объекта не требуется. Это приведет к завершению процесса, если он выполняется в контексте финализатора.

✓ DO создает исключение ObjectDisposedException из любого элемента, который не может использоваться после удаления объекта.

public class DisposableResourceHolder : IDisposable {
    bool disposed = false;
    SafeHandle resource; // handle to a resource

    public void DoSomething() {
        if (disposed) throw new ObjectDisposedException(...);
        // now call some native methods using the resource
        ...
    }
    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

✓ РАССМОТРИТЕ предоставление метода Close() в дополнение к Dispose(), если "close" является стандартной терминологией в данной области.

При этом важно сделать реализацию Close идентичной Dispose и рассмотреть возможность явной реализации метода IDisposable.Dispose.

public class Stream : IDisposable {
    IDisposable.Dispose() {
        Close();
    }
    public void Close() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Типы, доступные для завершения

Типы, допускающие завершение, — это типы, расширяющие шаблон basic Dispose, переопределяя средство завершения и предоставляя путь к коду завершения в методе Dispose(bool) .

Методы завершения, как известно, трудно реализовать правильно, в первую очередь потому что вы не можете сделать определенные (обычно допустимые) предположения о состоянии системы во время их выполнения. Следует внимательно учитывать следующие рекомендации.

Обратите внимание, что некоторые из рекомендаций применяются не только к методу Finalize, но и к любому коду, вызываемому из финализатора. В случае с ранее определенным шаблоном базового удаления, это означает логику, которая выполняется внутри Dispose(bool disposing) , если параметр disposing имеет значение false.

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

В следующем коде показан пример типа, который может быть завершен.

public class ComplexResourceHolder : IDisposable {

    private IntPtr buffer; // unmanaged memory buffer
    private SafeHandle resource; // disposable handle to a resource

    public ComplexResourceHolder() {
        this.buffer = ... // allocates memory
        this.resource = ... // allocates the resource
    }

    protected virtual void Dispose(bool disposing) {
        ReleaseBuffer(buffer); // release unmanaged memory
        if (disposing) { // release other disposable objects
            if (resource!= null) resource.Dispose();
        }
    }

    ~ComplexResourceHolder() {
        Dispose(false);
    }

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

X ИЗБЕГАЙТЕ делать типы финализируемыми.

Тщательно рассмотрите любой случай, в котором вы думаете, что требуется финализатор. Существует реальная стоимость, связанная с экземплярами с финализаторами, как с точки зрения производительности, так и с точки зрения сложности кода. Предпочитайте использовать обертки ресурсов, такие как SafeHandle, чтобы инкапсулировать неуправляемые ресурсы, если это возможно, в этом случае финализатор становится ненужным, так как оболочка отвечает за очистку своих ресурсов.

X НЕ делает типы значений завершенными.

Только ссылочные типы фактически финализируются CLR, поэтому любая попытка добавить финализатор к типу значения будет игнорироваться. Компиляторы C# и C++ применяют это правило.

✓ ДЕЛАЙТЕ тип финализируемым, если он отвечает за освобождение неуправляемого ресурса, который не имеет собственного финализатора.

При реализации средства завершения просто вызовите Dispose(false) и поместите всю логику очистки ресурсов внутри Dispose(bool disposing) метода.

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing) {
        ...
    }
}

✓ DO реализуйте основной шаблон высвобождения ресурсов на каждом финализируемом типе.

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

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

Например, завершаемый объект A, имеющий ссылку на другой завершаемый объект B, не может надежно использовать B в методе завершения A или наоборот. Финализаторы вызываются в произвольном порядке (без гарантии слабого порядка для критической финализации).

Кроме того, помните, что объекты, хранящиеся в статических переменных, собираются в определенных точках во время выгрузки домена приложения или при выходе из процесса. Доступ к статической переменной, которая относится к завершаемому объекту (или вызову статического метода, который может использовать значения, хранящиеся в статических переменных), может быть небезопасным, если Environment.HasShutdownStarted возвращает значение true.

✓ Сделайте метод Finalize защищенным.

Разработчики C#, C++и VB.NET не должны беспокоиться об этом, так как компиляторы помогают применить эту инструкцию.

X НЕ разрешать исключениям выйти из логики завершения, за исключением системных критических сбоев.

Если из финализатора выбрасывается исключение, среда CLR завершит весь процесс (начиная с версии 2.0 .NET Framework), предотвращая выполнение других финализаторов и освобождение ресурсов управляемым образом.

✓ РАССМОТРИТЕ возможность создания и использования критического завершаемого объекта (типа с иерархией типов, содержащей CriticalFinalizerObject) для ситуаций, в которых метод завершения абсолютно должен выполняться даже перед принудительной выгрузкой домена приложения и прерыванием потоков.

© Часть 2005, 2009 Корпорация Майкрософт. Все права защищены.

Перепечатан с разрешения Pearson Education, Inc. из Руководство по проектированию: Соглашения, идиомы и шаблоны для повторного использования библиотек .NET, 2-е издание Кшиштоф Чвалина и Брэд Абрамс, опубликованное 22 октября 2008 года Addison-Wesley Профессиональный в рамках серии разработки Microsoft Windows.

См. также