Шаблон ликвидации

Примечание.

Это содержимое перепечатывается разрешением 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 реализует базовый шаблон удаления и предоставляет метод завершения для типов, содержащих ресурсы, которые должны быть освобождены явным образом, и у них нет средств завершения.

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

✓ РАССМОТРИТЕ возможность реализации шаблона basic 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 следует учитывать зарезервированное слово, чтобы помочь кодифицировать этот шаблон и предотвратить путаницу между реализутелями, пользователями и компиляторами. Некоторые языки могут автоматически реализовать этот шаблон для определенных типов.

✓ DO позволяет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 может возникнуть исключение, дальнейшая логика очистки блокировки не будет выполняться. Чтобы обойти эту проблему, пользователю потребуется упаковать каждый вызов Dispose (в пределах окончательного блока!) в блоке проб, что приводит к очень сложным обработчикам очистки. При выполнении метода никогда Dispose(bool disposing) не возникает исключение, если удаление равно false. Это приведет к прекращению процесса при выполнении внутри контекста средства завершения.

✓ 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 реализацию идентичной 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++ применяют это правило.

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

При реализации средства завершения просто вызовите 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 завершит весь процесс (начиная с платформа .NET Framework версии 2.0), предотвращая выполнение других методов завершения и освобождение ресурсов в управляемом режиме.

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

Фрагменты: © Корпорация Майкрософт (Microsoft Corporation), 2005, 2009. Все права защищены.

Перепечатано с разрешения Pearson Education, Inc. из книги Инфраструктура программных проектов. Соглашения, идиомы и шаблоны для многократно используемых библиотек .NET (2-е издание), авторы: Кржиштоф Цвалина (Krzysztof Cwalina) и Брэд Абрамс (Brad Abrams). Книга опубликована 22 октября 2008 г. издательством Addison-Wesley Professional в рамках серии, посвященной разработке для Microsoft Windows.

См. также