Программисты других языков, кроме C++, используют, знают или понимают RAII?
Я заметил, что RAII привлекает много внимания к Stackru, но в моих кругах (в основном C++) RAII настолько очевиден, что все равно что спрашивать, что такое класс или деструктор.
Так что мне действительно любопытно, если это происходит потому, что я ежедневно окружен жесткими программистами C++, а RAII вообще не так хорошо известен (включая C++), или если все эти вопросы о Stackru связаны с тем, что я сейчас общаюсь с программистами, которые не выросли на C++, а на других языках люди просто не используют / не знают о RAII?
17 ответов
Для людей, которые комментируют в этой теме о 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
Есть много причин, почему 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, мы должны вызывать эту вспомогательную функцию, которая гарантирует, что она будет закрыта впоследствии.
Я все время использую 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
По завершении трудоемкой операции исходный курсор восстанавливается автоматически.
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++.;)
RAII.
Он начинается с конструктора и деструктора, но это больше, чем это.
Это все о безопасном управлении ресурсами при наличии исключений.
Что делает RAII лучше, чем finally и такие механизмы, так это то, что он делает код более безопасным для использования, поскольку он переносит ответственность за правильное использование объекта от пользователя объекта к разработчику объекта.
Пример правильного использования StdioFile с использованием RAII.
void someFunc()
{
StdioFile file("Plop","r");
// use file
}
// File closed automatically even if this function exits via an exception.
Чтобы получить ту же функциональность с наконец.
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 не основаны на стеке, но имеют очень контролируемый срок службы из-за подсчета ссылок.
Проблема с RAII - аббревиатура. Это не имеет очевидной корреляции с концепцией. Какое это имеет отношение к выделению стека? Вот к чему это сводится. C++ дает вам возможность размещать объекты в стеке и гарантировать, что их деструкторы вызываются при разматывании стека. В свете этого, звучит ли RAII значимым способом инкапсуляции этого? Нет. Я никогда не слышал о RAII, пока не приехал сюда несколько недель назад, и мне даже пришлось сильно смеяться, когда я прочитал, что кто-то написал, что они никогда не наймут программиста на C++, который не знал, что такое RAII. Конечно, концепция хорошо известна большинству компетентных профессиональных разработчиков C++. Просто аббревиатура плохо продумана.
Модификация ответа @ Пьера:
В 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() будет вызываться при выходе из блока with.
Общий Лисп имеет RAII:
(with-open-file (stream "file.ext" :direction :input)
(do-something-with-stream stream))
Прежде всего, я очень удивлен, что это не так хорошо известно! Я полностью думал, что RAII был, по крайней мере, очевиден для программистов на C++. Однако теперь я думаю, что могу понять, почему люди спрашивают об этом. Я окружен, и я должен быть сами собой, C++ уроды...
Так что мой секрет... Я предполагаю, что именно так я читал Мейерса, Саттера [РЕДАКТИРОВАТЬ] и Андрея много лет назад, пока я просто не ухмыльнулся.
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:
- разблокировка мьютекса после приобретения
- закрытие соединения с базой данных после открытия
- освобождение памяти после выделения
- регистрация входа и выхода блока кода
- ...
Смысл RAII в том, что он требует детерминированной финализации, что гарантировано для стековых объектов в C++. Такие языки, как C# и Java, основанные на сборке мусора, не имеют такой гарантии, поэтому их нужно каким-то образом "прикрутить". В C# это делается путем реализации IDisposable, и многие из тех же шаблонов использования затем возникают в основном, что является одним из мотиваторов для оператора "using", оно обеспечивает удаление и очень хорошо известно и используется.
Так что в основном идиома есть, у нее просто нет причудливого названия.
Это как-то связано с знанием, когда ваш деструктор будет вызван, правда? Так что это не является полностью независимым от языка, учитывая, что это не дано во многих языках GC.
У меня есть коллеги, которые твердо изучают типы C++, "читайте спецификации". Многие из них знают RAII, но я никогда не слышал, чтобы его использовали за пределами этой сцены.
RAII популярен в C++, потому что это один из немногих (только?) Языков, который может выделять сложные локальные переменные области видимости, но не имеет finally
пункт. C#, Java, Python, Ruby все имеют finally
или эквивалент С не имеет finally
, но также не может выполнить код, когда переменная выходит из области видимости.
Я думаю, что много других языков (те, которые не имеют delete
например, не дают программисту одинаковый контроль над временем жизни объекта, и поэтому должны быть другие средства для обеспечения детерминированного удаления ресурсов. В C#, например, используя using
с IDisposable
распространено
CPython (официальный Python, написанный на C) поддерживает RAII из-за использования объектов с подсчетом ссылок с немедленным уничтожением на основе области действия (а не при сборке мусора). К сожалению, Jython (Python в Java) и PyPy не поддерживают эту очень полезную идиому RAII, и она ломает много устаревшего кода Python. Так что для переносимого Python вы должны обрабатывать все исключения вручную, как Java.
RAII специфичен для C++. В C++ есть необходимая комбинация выделенных в стеке объектов, времени жизни неуправляемых объектов и обработки исключений.