Программисты других языков, помимо C ++, используют, знают или понимают RAII? - PullRequest
33 голосов
/ 03 октября 2008

Я заметил, что RAII привлекает много внимания к Stackoverflow, но в моих кругах (в основном C ++) RAII настолько очевиден, что все равно, что спрашивать, что такое класс или деструктор.

Так что мне действительно любопытно, если это происходит потому, что меня ежедневно окружают жесткие программисты C ++, а RAII вообще не так хорошо известен (включая C ++), или если все эти вопросы о Stackoverflow связаны к тому, что я сейчас общаюсь с программистами, которые не выросли на C ++, а на других языках люди просто не используют / не знают о RAII?

Ответы [ 17 ]

24 голосов
/ 28 декабря 2008

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

Другое - это то, что он не работает так же хорошо в языках без детерминированной очистки.

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

В большинстве современных языков все собирается мусором, что делает реализацию RAII более сложной. Нет причины, по которой было бы невозможно добавить RAII-расширения, скажем, в C #, но это не так очевидно, как в C ++. Но, как уже упоминали другие, Perl и другие языки поддерживают RAII, несмотря на сборку мусора.

Тем не менее, все еще возможно создать свою собственную оболочку в стиле RAII на C # или других языках. Я сделал это в C # некоторое время назад. Мне нужно было что-то написать, чтобы соединение с базой данных было закрыто сразу после использования - задача, которую любой программист на С ++ считает очевидным кандидатом на RAII. Конечно, мы можем обернуть все в using -состояниях всякий раз, когда мы используем соединение с БД, но это просто грязно и подвержено ошибкам.

Мое решение состояло в том, чтобы написать вспомогательную функцию, которая принимала делегат в качестве аргумента, а затем при вызове открывала соединение с базой данных и внутри оператора using передавала его функции делегата, псевдокод:

T RAIIWrapper<T>(Func<DbConnection, T> f){
  using (var db = new DbConnection()){
    return f(db);
  }
}

Все еще не так хорошо или очевидно, как C ++ - RAII, но он достиг примерно того же самого. Всякий раз, когда нам нужен DbConnection, мы должны вызывать эту вспомогательную функцию, которая гарантирует, что она будет закрыта впоследствии.

20 голосов
/ 03 октября 2008

Я все время использую C ++ ˚ RAII, но я также долгое время разрабатывал VB6, и RAII всегда был широко используемым понятием (хотя я никогда не слышал, чтобы кто-то так его называл).

На самом деле, многие программы VB6 довольно сильно зависят от RAII. Одно из наиболее любопытных применений, которое я неоднократно видел, - это следующий небольшой класс:

' WaitCursor.cls '
Private m_OldCursor As MousePointerConstants

Public Sub Class_Inititialize()
    m_OldCursor = Screen.MousePointer
    Screen.MousePointer = vbHourGlass
End Sub

Public Sub Class_Terminate()
    Screen.MousePointer = m_OldCursor
End Sub

Использование:

Public Sub MyButton_Click()
    Dim WC As New WaitCursor

    ' … Time-consuming operation. '
End Sub

Как только трудоемкая операция заканчивается, исходный курсор восстанавливается автоматически.

14 голосов
/ 03 октября 2008

RAII расшифровывается как Получение ресурсов - инициализация . Это не зависит от языка вообще. Эта мантра здесь, потому что C ++ работает так, как работает. В C ++ объект не создается до завершения его конструктора. Деструктор не будет вызван, если объект не был успешно построен.

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

class OhMy {
public:
    OhMy() { p_ = new int[42];  jump(); } 
    ~OhMy() { delete[] p_; }

private:
    int* p_;

    void jump();
};

Если вызов jump() в конструкторе приводит к возникновению проблем, то из-за утечки p_. Мы можем исправить это так:

class Few {
public:
    Few() : v_(42) { jump(); } 
    ~Few();

private:
    std::vector<int> v_;

    void jump();
};

Если люди не знают об этом, то это из-за одной из двух вещей:

  • Они плохо знают C ++. В этом случае они должны снова открыть TCPPPL , прежде чем писать следующий класс. В частности, в разделе 14.4.1 в третьем издании книги говорится об этой технике.
  • Они совсем не знают C ++. Все в порядке. Эта идиома очень C ++ y. Либо изучите C ++, либо забудьте все об этом и продолжайте свою жизнь. Желательно изучать C ++. ;)
11 голосов
/ 03 октября 2008

Для людей, которые комментируют в этой теме о RAII (получение ресурсов является инициализацией), вот пример мотивации.

class StdioFile {
    FILE* file_;
    std::string mode_;

    static FILE* fcheck(FILE* stream) {
        if (!stream)
            throw std::runtime_error("Cannot open file");
        return stream;
    }

    FILE* fdup() const {
        int dupfd(dup(fileno(file_)));
        if (dupfd == -1)
            throw std::runtime_error("Cannot dup file descriptor");
        return fdopen(dupfd, mode_.c_str());
    }

public:
    StdioFile(char const* name, char const* mode)
        : file_(fcheck(fopen(name, mode))), mode_(mode)
    {
    }

    StdioFile(StdioFile const& rhs)
        : file_(fcheck(rhs.fdup())), mode_(rhs.mode_)
    {
    }

    ~StdioFile()
    {
        fclose(file_);
    }

    StdioFile& operator=(StdioFile const& rhs) {
        FILE* dupstr = fcheck(rhs.fdup());
        if (fclose(file_) == EOF) {
            fclose(dupstr); // XXX ignore failed close
            throw std::runtime_error("Cannot close stream");
        }
        file_ = dupstr;
        return *this;
    }

    int
    read(std::vector<char>& buffer)
    {
        int result(fread(&buffer[0], 1, buffer.size(), file_));
        if (ferror(file_))
            throw std::runtime_error(strerror(errno));
        return result;
    }

    int
    write(std::vector<char> const& buffer)
    {
        int result(fwrite(&buffer[0], 1, buffer.size(), file_));
        if (ferror(file_))
            throw std::runtime_error(strerror(errno));
        return result;
    }
};

int
main(int argc, char** argv)
{
    StdioFile file(argv[1], "r");
    std::vector<char> buffer(1024);
    while (int hasRead = file.read(buffer)) {
        // process hasRead bytes, then shift them off the buffer
    }
}

Здесь, когда создается экземпляр StdioFile, получается ресурс (в данном случае файловый поток); когда он уничтожен, ресурс освобождается. Блок try или finally не требуется; если чтение вызывает исключение, fclose вызывается автоматически, потому что оно в деструкторе.

Деструктор гарантированно будет вызван, когда функция выйдет из main, как обычно, так и в порядке исключения. В этом случае файловый поток очищается. Мир снова в безопасности. : -D

9 голосов
/ 03 октября 2008

RAII.

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

Что делает RAII лучше, чем наконец, и такие механизмы заключаются в том, что он делает код более безопасным для использования, поскольку переносит ответственность за правильное использование объекта от пользователя объекта к разработчику объекта.

Читать это

Пример правильного использования StdioFile с использованием RAII.

void someFunc()
{
    StdioFile    file("Plop","r");

    // use file
}
// File closed automatically even if this function exits via an exception.

Чтобы получить те же функциональные возможности с finally.

void someFunc()
{
      // Assuming Java Like syntax;
    StdioFile     file = new StdioFile("Plop","r");
    try
    {
       // use file
    }
    finally
    {
       // close file.
       file.close(); // 
       // Using the finaliser is not enough as we can not garantee when
       // it will be called.
    }
}

Поскольку вы должны явно добавить блок try {} finally {}, это делает этот метод кодирования более подверженным ошибкам (, т. Е. - это пользователь объекта, который должен думать об исключениях). Используя RAII, безопасность должна быть закодирована один раз при реализации объекта.

На вопрос, это специфично для C ++.
Краткий ответ: №

Более длинный ответ:
Требуются Конструкторы / Деструкторы / Исключения и объекты с определенным временем жизни.

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

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

8 голосов
/ 28 декабря 2008

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

5 голосов
/ 03 октября 2008

Модификация @ ответа Пьера :

В Python:

with open("foo.txt", "w") as f:
    f.write("abc")

f.close() вызывается автоматически независимо от того, было ли вызвано исключение.

В общем случае это можно сделать, используя contextlib.closing , из документации:

closing(thing): вернуть контекст менеджер, который закрывает вещь на завершение блока. Это в основном эквивалентно:

from contextlib import contextmanager

@contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()

И позволяет вам написать код, подобный этому:

from __future__ import with_statement # required for python version < 2.6
from contextlib import closing
import urllib

with closing(urllib.urlopen('http://www.python.org')) as page:
    for line in page:
        print line

без необходимости явного закрытия стр. Даже если ошибка происходит, page.close () будет вызываться, когда с блоком выхода.

3 голосов
/ 27 февраля 2009

Обычный Лисп имеет RAII:

(with-open-file (stream "file.ext" :direction :input)
    (do-something-with-stream stream))

См .: http://www.psg.com/~dlamkins/sl/chapter09.html

2 голосов
/ 03 октября 2008

Прежде всего, я очень удивлен, что это не так хорошо известно! Я полностью думал, что RAII был, по крайней мере, очевиден для программистов на C ++. Однако теперь я думаю, что могу понять, почему люди спрашивают об этом. Я окружен, и я должен быть, С ++, уроды ...

Так что мой секрет ... Я полагаю, что это было то, что я читал Мейерса, Саттера [РЕДАКТИРОВАТЬ] и Андрея все время, пока я просто не схватил его.

1 голос
/ 03 октября 2008

RAII - это способ в C ++ убедиться, что процедура очистки выполняется после блока кода независимо от того, что происходит в коде: код выполняется до конца должным образом или вызывает исключение. Уже приведенный пример автоматически закрывает файл после его обработки, см. ответ здесь .

На других языках вы используете другой механизм для достижения этого.

В Java у вас есть конструкции try {} finally {}:

try {
  BufferedReader file = new BufferedReader(new FileReader("infilename"));
  // do something with file
}
finally {
    file.close();
}

В Ruby у вас есть аргумент автоматического блока:

File.open("foo.txt") do | file |
  # do something with file
end

В Лиспе у вас есть unwind-protect и предопределенный with-XXX

(with-open-file (file "foo.txt")
  ;; do something with file
)

В схеме у вас есть dynamic-wind и предопределенный with-XXXXX:

(with-input-from-file "foo.txt"
  (lambda ()
    ;; do something 
)

в Python вы наконец-то попробовали

try
  file = open("foo.txt")
  # do something with file
finally:
  file.close()

Решение C ++ как RAII довольно неуклюже в том смысле, что оно заставляет вас создавать один класс для всех видов очистки, которые вы должны выполнить. Это может заставить вас написать много маленьких глупых классов.

Другие примеры RAII:

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