Почему Java / C # не может реализовать RAII?

29

Вопрос: Почему 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 #? Может ли он быть введен без нарушения старого кода?

mike30
источник
3
Это не практично, это невозможно . Стандарт C # не гарантирует деструкторов / Disposes будут всегда работать, независимо от того, как они вызвали. Добавление неявного уничтожения в конце области не поможет этому.
Теластин
20
@Telastyn А? То, что сейчас говорит стандарт C #, не имеет значения, так как мы обсуждаем изменение самого документа. Единственный вопрос заключается в том, является ли это практичным, и для этого единственное интересное в текущем отсутствии гарантии - это причины отсутствия гарантии. Обратите внимание , что для usingвыполнения Dispose будет гарантировано (хорошо, дисконтирование процесса внезапно умирают без выброса исключения, в этот момент всех очистки предположительно становятся спорным).
4
Дубликат Разве разработчики Java сознательно отказались от RAII? Хотя принятый ответ совершенно неверен. Короткий ответ заключается в том, что Java использует семантику ссылок (кучи), а не семантику значений (стека) , поэтому детерминированная финализация не очень полезна / невозможна. C # делает имеют значение семантики ( struct), но они , как правило , избегать , за исключением особых случаев. Смотрите также .
BlueRaja - Дэнни Пфлюгофт
2
Это похоже, а не точный дубликат.
Маньеро
3
blogs.msdn.com/b/oldnewthing/archive/2010/08/10/10048150.aspx является соответствующей страницей к этому вопросу.
Маньеро

Ответы:

17

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

если время жизни переменной стекового типа заканчивается, вызовите Disposeобъект, к которому она относится

к соответствующему разделу спецификации языка и будет сделано. Я проигнорирую проблему временных значений ( new Resource().doSomething()), которая может быть решена чуть более общей формулировкой, это не самая серьезная проблема. Например, этот код был бы неработоспособен (и такого рода вещи, вероятно, вообще невозможно сделать):

File openSavegame(string id) {
    string path = ... id ...;
    File f = new File(path);
    // do something, perhaps logging
    return f;
} // f goes out of scope, caller receives a closed file

Теперь вам нужны определяемые пользователем конструкторы копирования (или конструкторы перемещения) и начинайте вызывать их везде. Это не только влияет на производительность, но также делает эти вещи эффективно оценивающими типы, тогда как почти все другие объекты являются ссылочными типами. В случае с Java это радикальное отклонение от того, как работают объекты. В C # меньше (уже есть structs, но нет пользовательских конструкторов копирования для них AFAIK), но это все же делает эти объекты RAII более особенными. Альтернативно, ограниченная версия линейных типов (ср. Rust) также может решить проблему за счет запрета наложения имен, включая передачу параметров (если вы не хотите внести еще большую сложность, приняв заимствованные ссылки типа Rust и средство проверки заимствования).

Это можно сделать технически, но вы получите категорию вещей, которые сильно отличаются от всего остального в языке. Это почти всегда плохая идея, с последствиями для разработчиков (больше крайних случаев, больше времени / затрат в каждом отделе) и пользователей (больше понятий для изучения, больше возможностей ошибок). Это не стоит дополнительного удобства.


источник
Зачем вам нужен конструктор копирования / перемещения? Файл по-прежнему имеет ссылочный тип. В этой ситуации f, который является указателем, копируется в вызывающую программу, и он отвечает за удаление ресурса (вместо этого компилятор неявно помещает в вызывающую программу образец try-finally-dispose)
Maniero
1
@bigown Если вы относитесь к каждой ссылке на Fileэтот путь, ничего не меняется и Disposeникогда не вызывается. Если вы всегда звоните Dispose, вы ничего не можете сделать с одноразовыми предметами. Или вы предлагаете какую-то схему, чтобы иногда избавляться, а иногда нет? Если это так, пожалуйста, опишите это подробно, и я расскажу вам ситуации, в которых это не удается.
Я не понимаю, что вы сказали сейчас (я не говорю, что вы не правы). У объекта есть ресурс, а не ссылка.
Maniero
Насколько я понимаю, заменяя ваш пример просто возвратом, я считаю, что компилятор вставляет попытку непосредственно перед получением ресурса (строка 3 в вашем примере) и блок finally-dispose перед концом области действия (строка 6). Здесь нет проблем, согласен? Вернемся к вашему примеру. Компилятор видит передачу, он не может вставить сюда try-finally, но вызывающая сторона получит (указатель на) объект File, и, предполагая, что вызывающая сторона не передает этот объект снова, компилятор вставит в нее шаблон try-finally. Другими словами, каждый объект IDisposable, который не был перенесен, должен применять шаблон try-finally.
Маньеро
1
@bigown Другими словами, не звоните, Disposeесли ссылка ускользает? Анализ побега - старая и сложная проблема, которая не всегда будет работать без дальнейших изменений языка. Когда ссылка передается другому (виртуальному) методу ( something.EatFile(f);), должен f.Disposeвызываться в конце области? Если да, вы ломаете абонентов, которые хранят fдля дальнейшего использования. Если нет, вы теряете ресурс, если вызывающий не хранит f. Единственный несколько простой способ устранить это - система линейных типов, которая (как я уже обсуждал в моем ответе) вместо этого вводит много других сложностей.
26

Самая большая трудность в реализации чего-то подобного для Java или C # - определить, как работает передача ресурсов. Вам понадобится какой-то способ продлить срок службы ресурса за рамки. Рассмотреть возможность:

class IWrapAResource
{
    private readonly Resource resource;
    public IWrapAResource()
    {
        // Where Resource is scope bound
        Resource builder = new Resource(args, args, args);

        this.resource = builder;
    } // Uh oh, resource is destroyed
} // Crap, there's no scope for IWrapAResource we can bind to!

Что еще хуже, это может быть неочевидно для разработчика IWrapAResource:

class IWrapSomething<T>
{
    private readonly T resource; // What happens if T is Resource?
    public IWrapSomething(T input)
    {
        this.resource = input;
    }
}

Нечто похожее на usingутверждение C #, вероятно, настолько близко, насколько вы собираетесь иметь семантику RAII, не прибегая к ресурсам подсчета ссылок или форсируя семантику значений везде, как C или C ++. Поскольку Java и C # имеют неявное совместное использование ресурсов, управляемых сборщиком мусора, минимум, что программист должен уметь делать, это выбирать область действия, к которой привязан ресурс, и это именно то, что usingуже делает.

Билли ОНил
источник
Предполагая, что у вас нет необходимости ссылаться на переменную после того, как она вышла из области видимости (а в действительности такой необходимости не должно быть), я утверждаю, что вы все равно можете сделать объект самоустанавливающимся, написав для него финализатор , Финализатор вызывается непосредственно перед сборкой мусора. См. Msdn.microsoft.com/en-us/library/0s71x931.aspx
Роберт Харви,
8
@ Роберт: правильно написанная программа не может предполагать, что финализаторы когда-либо запускались. blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx
Билли ONeal
1
Гектометр Ну, наверное, поэтому они и выступили с usingзаявлением.
Роберт Харви
2
В точку. Это огромный источник ошибок новичков в C ++, а также в Java / C #. Java / C # не устраняет возможность утечки ссылки на ресурс, который должен быть уничтожен, но делая его явным и необязательным, они напоминают программисту и дают ему сознательный выбор того, что делать.
Александр Дубинский
1
@svick Это не до IWrapSomethingутилизации T. Кто бы ни создал Tпотребности , чтобы беспокоиться о том , что, будь то использование using, будучи IDisposableсам, или иметь некоторую специальную схему жизненного цикла ресурсов.
Александр Дубинский
13

Причина, по которой RAII не может работать на языке, подобном C #, но работает на C ++, заключается в том, что в C ++ вы можете решить, является ли объект действительно временным (размещая его в стеке) или долгосрочным ( выделение его в куче с использованием newи использованием указателей).

Итак, в C ++ вы можете сделать что-то вроде этого:

void f()
{
    Foo f1;
    Foo* f2 = new Foo();
    Foo::someStaticField = f2;

    // f1 is destroyed here, the object pointed to by f2 isn't
}

В C # вы не можете различить эти два случая, поэтому компилятор не будет знать, завершать ли объект или нет.

Что вы можете сделать, так это ввести какой-то особый вид локальной переменной, который вы не можете поместить в поля и т. Д. * И который будет автоматически удален, когда он выйдет из области видимости. Именно это и делает C ++ / CLI. В C ++ / CLI вы пишете такой код:

void f()
{
    Foo f1;
    Foo^ f2 = gcnew Foo();
    Foo::someStaticField = f2;

    // f1 is disposed here, the object pointed to by f2 isn't
}

Это компилирует в основном тот же IL, что и следующий C #:

void f()
{
    using (Foo f1 = new Foo())
    {
        Foo f2 = new Foo();
        Foo.someStaticField = f2;
    }
    // f1 is disposed here, the object pointed to by f2 isn't
}

В заключение, если бы я догадался, почему разработчики C # не добавили RAII, это потому, что они думали, что иметь два разных типа локальных переменных не стоит, в основном потому, что в языке с GC детерминированная финализация не полезна, что довольно часто.

* Не без эквивалента &оператора, который в C ++ / CLI есть %. Хотя это «небезопасно» в том смысле, что после завершения метода поле будет ссылаться на удаленный объект.

svick
источник
1
C # может тривиально сделать RAII, если он разрешает деструкторы для structтипов, как D.
Ян Худек
6

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

public void ReadFile ()
{
  string filename = "myFile.dat";
  local Stream file = File.Open(filename);
  file.Read(blah blah blah);
}

Видите localключевое слово, которое я добавил? Все это делает добавить немного больше синтаксический сахар, так же , как using, сообщая компилятор позвонить Disposeв finallyблоке в конце области видимости переменной. Вот и все. Это полностью эквивалентно:

public void ReadFile ()
{
  string filename = "myFile.dat";
  using (Stream file = File.Open(filename))
  {
      file.Read(blah blah blah);
  }
}

но с неявной областью, а не с явной. Это проще, чем другие предложения, так как мне не нужно определять класс как ограниченный областью действия. Просто чище, более скрытый синтаксический сахар.

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

Авнер Шахар-Каштан
источник
1
@ mike30, но перемещение его в определение типа приводит вас точно к проблемам, перечисленным другими - что произойдет, если вы передадите указатель другому методу или вернете его из функции? Таким образом, область действия объявляется в области, а не в другом месте. Тип может быть Устранимым, но вызывать Уничтожение не в его силах.
Авнер Шахар-Каштан
3
@ mike30: Мех. Все, что делает этот синтаксис - это удаление фигурных скобок и, как следствие, контроль области, который они предоставляют.
Роберт Харви
1
@RobertHarvey Точно. Он жертвует некоторой гибкостью для более чистого, менее вложенного кода. Если мы примем предложение @ delnan и повторно используем usingключевое слово, мы сможем сохранить существующее поведение и использовать его также для случаев, когда нам не нужна конкретная область действия. Имейте значение без скобок по usingумолчанию для текущей области.
Авнер Шахар-Каштан
1
У меня нет проблем с полупрактическими упражнениями по языковому дизайну.
Авнер Шахар-Каштан
1
@RobertHarvey. Кажется, у вас есть предубеждение против всего, что в настоящее время не реализовано в C #. У нас не было бы обобщений, linq, using-blocks, ipmlicit типов и т. Д., Если бы мы были довольны C # 1.0. Этот синтаксис не решает проблему неявности, но это хороший сахар для привязки к текущей области.
mike30
1

Для примера того, как RAII работает на языке сборки мусора, проверьте withключевое слово в Python . Вместо того чтобы полагаться на детерминированные уничтоженные объекты, он позволяет вам связывать __enter__()и __exit__()методы с заданной лексической областью действия. Типичный пример:

with open('output.txt', 'w') as f:
    f.write('Hi there!')

Как и в стиле RAII в C ++, файл будет закрыт при выходе из этого блока, независимо от того, является ли он «нормальным» выходом, a break, немедленным returnили исключением.

Обратите внимание, что open()вызов - это обычная функция открытия файла. чтобы заставить это работать, возвращенный объект файла включает два метода:

def __enter__(self):
  return self
def __exit__(self):
  self.close()

Это распространенная идиома в Python: объекты, связанные с ресурсом, обычно включают эти два метода.

Обратите внимание, что объект файла все еще может оставаться выделенным после __exit__()вызова, важно то, что он закрыт.

Хавьер
источник
7
withв Python почти так же, как usingв C #, и как таковой не RAII, насколько этот вопрос касается.
1
Python "with" - это управление ресурсами с привязкой к области, но ему не хватает неявности смарт-указателя. Сам факт объявления указателя как «умного» можно считать «явным», но если компилятор применяет умность как часть типа объектов, он склоняется к «неявному».
mike30
AFAICT, смысл RAII заключается в установлении строгого контроля над ресурсами. если вы заинтересованы только в том, чтобы делать это путем освобождения объектов, то нет, языки, которые собирают мусор, не могут этого сделать. если вы заинтересованы в последовательном освобождении ресурсов, то это способ сделать это (другой - deferна языке Go).
Хавьер
1
На самом деле, я думаю, что будет справедливо сказать, что Java и C # решительно поддерживают явные конструкции. В противном случае зачем возиться со всей церемонией, присущей использованию интерфейсов и наследования?
Роберт Харви
1
@delnan, Go имеет «неявные» интерфейсы.
Хавьер