Получение ресурса есть инициализация

Эта статья находится на начальном уровне проработки, в одной из её версий выборочно используется текст из источника, распространяемого под свободной лицензией
Материал из энциклопедии Руниверсалис

Получение ресурса есть инициализация (англ. Resource Acquisition Is Initialization (RAII)) — программная идиома, смысл которой заключается в том, что с помощью тех или иных программных механизмов получение некоторого ресурса неразрывно совмещается с инициализацией, а освобождение — с уничтожением объекта.

Типичным (хотя и не единственным) способом реализации является организация получения доступа к ресурсу в конструкторе, а освобождения — в деструкторе соответствующего класса. Во многих языках программирования, например в C++, деструктор переменной немедленно вызывается при выходе из её области видимости, когда ресурс необходимо освободить. Это позволяет гарантировать освобождение ресурса при возникновении исключения: код становится безопасным при исключениях (англ. Exception safety).

В языках, использующих сборщик мусора, объект продолжает существовать, пока на него существуют ссылкиПерейти к разделу «#Управление владением ресурсов без RAII».

Применения

Эта концепция может использоваться для любых разделяемых объектов или ресурсов:

Важный случай использования RAII — «умные указатели»: классы, инкапсулирующие владение памятью. Например, в стандартной библиотеке шаблонов языка C++ для этой цели существует класс unique_ptr, начиная с C++11.

Пример

Пример класса на языке C++, реализующего захват ресурсов при инициализации:

#include <cstdio>
#include <stdexcept>
  
class file {
public:
    file( const char* filename ) : m_file_handle(std::fopen(filename, "w+")) 
    {
        if( !m_file_handle )
            throw std::runtime_error("file open failure") ;
    }
    ~file() 
    {
        if( std::fclose(m_file_handle) != 0 )
        {
            // fclose() может вернуть ошибку при записи на диск последних изменений
        }
    }

    void write( const char* str ) 
    {
        if( std::fputs(str, m_file_handle) == EOF )
            throw std::runtime_error("file write failure") ;
    }

private:
    std::FILE* m_file_handle ;

    // Копирование и присваивание не реализовано.  Предотвратим их использование,
    // объявив соответствующие методы закрытыми.
    file( const file & ) ;
    file & operator=( const file & ) ;
};

// пример использования этого класса
void example_usage() {
   // открываем файл (захватываем ресурс)
    file logfile("logfile.txt") ;
  
    logfile.write("hello logfile!") ;

    // продолжаем использовать logfile...
    // Можно возбуждать исключения или выходить из функции не беспокоясь о закрытии файла; 
    // он будет закрыт автоматически когда переменная logfile выйдет из области видимости.
}

Суть идиомы RAII в том, что класс инкапсулирует владение (захват и освобождение) некоторого ресурса — например, открытого файлового дескриптора. Когда объекты-экземпляры такого класса являются автоматическими переменными, гарантируется, что когда они выйдут из области видимости, будет вызван их деструктор — а значит, ресурс будет освобождён. В данном примере файл будет закрыт корректно, даже если вызов std::fopen() вернёт ошибку и будет возбуждено исключение. Более того, если конструктор класса file завершился корректно, это гарантирует то, что файл действительно открыт. В случае ошибки при открытии файла конструктор возбуждает исключение.

При помощи RAII и автоматических переменных можно просто управлять владением нескольких ресурсов. Порядок вызова деструкторов является обратным порядку вызова конструкторов; деструктор вызывается только если объект был полностью создан (то есть, если конструктор не возбудил исключения).

Использование RAII упрощает код и помогает обеспечить корректность работы программы.

Возможна реализация без исключений (например, это необходимо во встраиваемых приложениях). В этом случае используется конструктор по умолчанию, обнуляющий file handler, а для открытия файла используется отдельный метод типа bool FileOpen(const char *). Смысл использования класса сохраняется, особенно если точек выхода из метода, где создаётся объект класса, несколько. Естественно, в этом случае в деструкторе проверяется необходимость закрытия файла.

Управление владением ресурсов без RAII

В Java, использующей сборку мусора, объекты, на которые ссылаются автоматические переменные, создаются при выполнении команды new, а удаляются сборщиком мусора, который запускается автоматически с неопределённой периодичностью. В Java нет деструкторов, которые будут гарантированно вызваны при выходе переменной из области видимости, а имеющиеся в языке финализаторы для освобождения ресурсов, кроме памяти, не подходят, так как неизвестно, когда объект будет удалён и будет ли удалён вообще. Поэтому об освобождении ресурсов программист должен заботиться сам. Предыдущий пример на Java можно переписать так:

void java_example() {
    // открываем файл (захватываем ресурс)
    final LogFile logfile = new LogFile("logfile.txt") ;
 
    try {
        logfile.write("hello logfile!") ;

        // продолжаем использовать logfile...
        // Можно возбуждать исключения, не беспокоясь о закрытии файла.
        // Файл будет закрыт при выполнении блока finally, который
        // гарантированно выполняется после блока try даже в случае
        // возникновения исключений. 
    } finally {
        // явно освобождаем ресурс
        logfile.close();
    }
}

Тут бремя явного освобождения ресурсов возложено на программиста, в каждом месте кода, где выполняется захват ресурса. В качестве синтаксического сахара в Java 7 введена конструкция «try-with-resources»:

void java_example() {
    // открываем файл (захватываем ресурс) в заголовке конструкции try.
    // переменная logfile существует только внутри этого блока.
    try (LogFile logfile = new LogFile("logfile.txt")) {
        logfile.write("hello logfile!") ;
        // продолжаем использовать logfile...
    } // здесь автоматически произойдёт вызов logfile.close(), независимо от
      // возникновения исключений в блоке кода.
}

Чтобы этот код работал, класс LogFile должен реализовывать системный интерфейс java.lang.AutoCloseable и объявлять метод void close();. Данная конструкция является, по сути, аналогом конструкции using(){} языка C#, также выполняющей инициализацию автоматической переменной объектом и гарантированный вызов метода освобождения ресурсов при выходе этой переменной из области видимости.

Ruby и Smalltalk не поддерживают RAII, но в них существует похожий шаблон написания кода, который состоит в том, что методы передают ресурсы в блоки-замыкания. Вот пример на языке Ruby:

File.open("logfile.txt", "w+") do |logfile|
   logfile.write("hello logfile!")
end
# Метод 'open' гарантирует то, что файл будет закрыт без каких-либо
# явных действий со стороны кода, выполняющего запись в файл

Оператор 'with' языка Python, оператор 'using' языков С# и Visual Basic 2005 обеспечивают детерминированное управление владением ресурсов внутри блока и заменяют блок finally, приблизительно как и в языке Ruby.

В Perl время жизни объектов определяется при помощи подсчёта ссылок, что позволяет реализовывать RAII так же, как и в C++: объекты, на которые не существует ссылок, немедленно удаляются и вызывается деструктор, который может освободить ресурс. Но, время жизни объектов не обязательно привязано в какой-то области видимости. Например, можно создать объект внутри функции, а потом присвоить ссылку на него некоторой глобальной переменной, тем самым увеличив время жизни объекта на неопределённое время (и оставив ресурс захваченным на это время). Это может являться причиной утечки ресурсов, которые должны были быть освобождены в момент выхода объекта из области видимости.

При написании кода на языке Си требуется больше кода, управляющего владением ресурсов, так как он не поддерживает исключений, блоков try-finally или других синтаксических конструкций, позволяющих реализовать RAII. Обычно, код пишут по следующей схеме: освобождение ресурсов выполняется в конце функции и в начале этого кода ставится метка; в середине функции, в случае возникновения ошибок, выполняется их обработка, а затем переход на освобождение ресурсов при помощи оператора goto. В современном С не рекомендуется использовать оператор goto. Вместо него значительно чаще используются конструкции if-else. Таким образом, код освобождения ресурсов не дублируется в каждом месте обработки ошибки в рамках одной функции, однако его придется продублировать во всех функциях, работающих с файлами в безопасном стиле.

int c_example()
{
    int retval = 0; // возвращаем 0 в случае успешного завершения
    FILE *f = fopen("logfile.txt", "w+");
    if( f )
    {
        do
        {
            // Успешно открыли файл, работаем с ним
            if( fputs("hello logfile!", f) == EOF )
            {
                retval = -2;
                break;
            }
    
            // продолжаем использовать ресурс
            // ...
            
        } while(0);
        
        // Освобождаем ресурсы
        if ( fclose(f) == EOF )
        {
            retval = -3;
        }
    }
    else
    {
        // Не смогли открыть файл
        retval = -1;
    }
    return retval;
}

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

Псевдокод на Python

Выразить идею RAII на Python можно так:

#coding: utf-8
resource_for_grep = False
class RAII:
    g = globals()
    def __init__(self):
        self.g['resource_for_grep'] = True
    def __del__(self):
        self.g['resource_for_grep'] = False

print resource_for_grep #False
r = RAII()
print resource_for_grep #True
del r
print resource_for_grep #False

Пример на Perl

См. также

Примечания