Вопрос: Почему Java / C # не может реализовать RAII?
Пояснение: я знаю, что сборщик мусора не является детерминированным. Таким образом, при использовании текущих возможностей языка метод Dispose () объекта не может быть вызван автоматически при выходе из области видимости. Но можно ли добавить такую детерминистическую функцию?
Мое понимание:
Я считаю, что реализация RAII должна удовлетворять двум требованиям:
1. Время жизни ресурса должно быть связано с областью действия.
2. Неявный. Освобождение ресурса должно происходить без явного заявления программиста. Аналог сборщика мусора, освобождающего память без явного заявления. «Неявность» должна происходить только в момент использования класса. Создатель библиотеки классов, конечно, должен явно реализовать деструктор или метод Dispose ().
Java / C # удовлетворяет пункт 1. В C # ресурс, реализующий IDisposable, может быть связан с областью использования:
void test()
{
using(Resource r = new Resource())
{
r.foo();
}//resource released on scope exit
}
Это не удовлетворяет пункт 2. Программист должен явно привязать объект к специальной области «использования». Программисты могут (и делают) забыть явно привязать ресурс к области видимости, создав утечку.
Фактически блоки "using" преобразуются компилятором в код try-finally-dispose (). Он имеет такую же явную природу шаблона try-finally-dispose (). Без неявного освобождения крючок для области видимости является синтаксическим сахаром.
void test()
{
//Programmer forgot (or was not aware of the need) to explicitly
//bind Resource to a scope.
Resource r = new Resource();
r.foo();
}//resource leaked!!!
Я думаю, что стоит создать языковую функцию в Java / C #, позволяющую специальным объектам подключаться к стеку через смарт-указатель. Эта функция позволит вам пометить класс как ограниченный областью действия, чтобы он всегда создавался с привязкой к стеку. Могут быть варианты для различных типов умных указателей.
class Resource - ScopeBound
{
/* class details */
void Dispose()
{
//free resource
}
}
void test()
{
//class Resource was flagged as ScopeBound so the tie to the stack is implicit.
Resource r = new Resource(); //r is a smart-pointer
r.foo();
}//resource released on scope exit.
Я думаю, что неявность "стоит того". Так же, как неявность сборки мусора "стоит того". Явное использование блоков освежает глаза, но не дает семантического преимущества перед try-finally-dispose ().
Нецелесообразно ли реализовывать такую функцию в языках Java / C #? Может ли он быть введен без нарушения старого кода?
Dispose
s будут всегда работать, независимо от того, как они вызвали. Добавление неявного уничтожения в конце области не поможет этому.using
выполненияDispose
будет гарантировано (хорошо, дисконтирование процесса внезапно умирают без выброса исключения, в этот момент всех очистки предположительно становятся спорным).struct
), но они , как правило , избегать , за исключением особых случаев. Смотрите также .Ответы:
Такое расширение языка будет значительно сложнее и агрессивнее, чем вы думаете. Вы не можете просто добавить
к соответствующему разделу спецификации языка и будет сделано. Я проигнорирую проблему временных значений (
new Resource().doSomething()
), которая может быть решена чуть более общей формулировкой, это не самая серьезная проблема. Например, этот код был бы неработоспособен (и такого рода вещи, вероятно, вообще невозможно сделать):Теперь вам нужны определяемые пользователем конструкторы копирования (или конструкторы перемещения) и начинайте вызывать их везде. Это не только влияет на производительность, но также делает эти вещи эффективно оценивающими типы, тогда как почти все другие объекты являются ссылочными типами. В случае с Java это радикальное отклонение от того, как работают объекты. В C # меньше (уже есть
struct
s, но нет пользовательских конструкторов копирования для них AFAIK), но это все же делает эти объекты RAII более особенными. Альтернативно, ограниченная версия линейных типов (ср. Rust) также может решить проблему за счет запрета наложения имен, включая передачу параметров (если вы не хотите внести еще большую сложность, приняв заимствованные ссылки типа Rust и средство проверки заимствования).Это можно сделать технически, но вы получите категорию вещей, которые сильно отличаются от всего остального в языке. Это почти всегда плохая идея, с последствиями для разработчиков (больше крайних случаев, больше времени / затрат в каждом отделе) и пользователей (больше понятий для изучения, больше возможностей ошибок). Это не стоит дополнительного удобства.
источник
File
этот путь, ничего не меняется иDispose
никогда не вызывается. Если вы всегда звонитеDispose
, вы ничего не можете сделать с одноразовыми предметами. Или вы предлагаете какую-то схему, чтобы иногда избавляться, а иногда нет? Если это так, пожалуйста, опишите это подробно, и я расскажу вам ситуации, в которых это не удается.Dispose
если ссылка ускользает? Анализ побега - старая и сложная проблема, которая не всегда будет работать без дальнейших изменений языка. Когда ссылка передается другому (виртуальному) методу (something.EatFile(f);
), долженf.Dispose
вызываться в конце области? Если да, вы ломаете абонентов, которые хранятf
для дальнейшего использования. Если нет, вы теряете ресурс, если вызывающий не хранитf
. Единственный несколько простой способ устранить это - система линейных типов, которая (как я уже обсуждал в моем ответе) вместо этого вводит много других сложностей.Самая большая трудность в реализации чего-то подобного для Java или C # - определить, как работает передача ресурсов. Вам понадобится какой-то способ продлить срок службы ресурса за рамки. Рассмотреть возможность:
Что еще хуже, это может быть неочевидно для разработчика
IWrapAResource
:Нечто похожее на
using
утверждение C #, вероятно, настолько близко, насколько вы собираетесь иметь семантику RAII, не прибегая к ресурсам подсчета ссылок или форсируя семантику значений везде, как C или C ++. Поскольку Java и C # имеют неявное совместное использование ресурсов, управляемых сборщиком мусора, минимум, что программист должен уметь делать, это выбирать область действия, к которой привязан ресурс, и это именно то, чтоusing
уже делает.источник
using
заявлением.IWrapSomething
утилизацииT
. Кто бы ни создалT
потребности , чтобы беспокоиться о том , что, будь то использованиеusing
, будучиIDisposable
сам, или иметь некоторую специальную схему жизненного цикла ресурсов.Причина, по которой RAII не может работать на языке, подобном C #, но работает на C ++, заключается в том, что в C ++ вы можете решить, является ли объект действительно временным (размещая его в стеке) или долгосрочным ( выделение его в куче с использованием
new
и использованием указателей).Итак, в C ++ вы можете сделать что-то вроде этого:
В C # вы не можете различить эти два случая, поэтому компилятор не будет знать, завершать ли объект или нет.
Что вы можете сделать, так это ввести какой-то особый вид локальной переменной, который вы не можете поместить в поля и т. Д. * И который будет автоматически удален, когда он выйдет из области видимости. Именно это и делает C ++ / CLI. В C ++ / CLI вы пишете такой код:
Это компилирует в основном тот же IL, что и следующий C #:
В заключение, если бы я догадался, почему разработчики C # не добавили RAII, это потому, что они думали, что иметь два разных типа локальных переменных не стоит, в основном потому, что в языке с GC детерминированная финализация не полезна, что довольно часто.
* Не без эквивалента
&
оператора, который в C ++ / CLI есть%
. Хотя это «небезопасно» в том смысле, что после завершения метода поле будет ссылаться на удаленный объект.источник
struct
типов, как D.Если вас беспокоит
using
блоки - их явность, возможно, мы можем сделать маленький шаг в сторону меньшей явности, вместо того, чтобы изменять саму спецификацию C #. Рассмотрим этот код:Видите
local
ключевое слово, которое я добавил? Все это делает добавить немного больше синтаксический сахар, так же , какusing
, сообщая компилятор позвонитьDispose
вfinally
блоке в конце области видимости переменной. Вот и все. Это полностью эквивалентно:но с неявной областью, а не с явной. Это проще, чем другие предложения, так как мне не нужно определять класс как ограниченный областью действия. Просто чище, более скрытый синтаксический сахар.
Здесь могут быть проблемы с трудноразрешимыми областями, хотя я не могу видеть это прямо сейчас, и я был бы признателен всем, кто может найти это.
источник
using
ключевое слово, мы сможем сохранить существующее поведение и использовать его также для случаев, когда нам не нужна конкретная область действия. Имейте значение без скобок поusing
умолчанию для текущей области.Для примера того, как RAII работает на языке сборки мусора, проверьте
with
ключевое слово в Python . Вместо того чтобы полагаться на детерминированные уничтоженные объекты, он позволяет вам связывать__enter__()
и__exit__()
методы с заданной лексической областью действия. Типичный пример:Как и в стиле RAII в C ++, файл будет закрыт при выходе из этого блока, независимо от того, является ли он «нормальным» выходом, a
break
, немедленнымreturn
или исключением.Обратите внимание, что
open()
вызов - это обычная функция открытия файла. чтобы заставить это работать, возвращенный объект файла включает два метода:Это распространенная идиома в Python: объекты, связанные с ресурсом, обычно включают эти два метода.
Обратите внимание, что объект файла все еще может оставаться выделенным после
__exit__()
вызова, важно то, что он закрыт.источник
with
в Python почти так же, какusing
в C #, и как таковой не RAII, насколько этот вопрос касается.defer
на языке Go).