Чому 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. Програміст повинен явно прив'язати об'єкт до спеціальної області "використання". Програмісти можуть (і робити) забути явно прив’язати ресурс до сфери, створюючи витік.

Насправді блоки "використання" перетворюються компілятором для спроби остаточного розпорядження () кодом. Він має такий самий чіткий характер, як шаблон спроби остаточного розпорядження (). Без неявного випуску гачком сфери є синтаксичний цукор.

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.

Я думаю, що імпліцитність "варта того". Так само, як імпліцитність збирання сміття "варта". Явне використання блоків освіжає очі, але не дає семантичної переваги перед спробою остаточно-розпорядитися ().

Чи недоцільно реалізувати таку функцію на мовах Java / C #? Чи можна його ввести, не порушуючи старий код?


3
Це непрактично, неможливо . Стандарт C # не гарантує, що деструктори / Disposeз ніколи не запускаються, незалежно від того, як вони спрацьовують. Додавання неявного знищення в кінці області не допоможе цьому.
Теластин

20
@Telastyn Так? Те, що зараз говорить стандарт C #, не має жодного значення, оскільки ми обговорюємо зміну самого документа. Єдине питання полягає в тому, чи це практично робити, і для цього єдиний цікавий біт про нинішню відсутність гарантії - це причини цієї відсутності гарантії. Зверніть увагу , що для usingвиконання Dispose буде гарантовано (добре, дисконтування процесу раптово вмирають без викиду винятку, в цей момент всіх очищення імовірно стають спірним).

4
дублікат Чи розробники Java свідомо відмовилися від RAII? , хоча прийнята відповідь є абсолютно невірною. Коротка відповідь полягає в тому, що Java використовує референтну (купу) семантику, а не значення (стек) семантику, тому детермінована доопрацювання не дуже корисна / можлива. C # робить мають значення семантики ( struct), але вони , як правило , уникати , за винятком особливих випадків. Дивіться також .
BlueRaja - Danny Pflughoeft

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 більш спеціальними. Крім того, обмежена версія лінійних типів (пор. Іржа) також може вирішити проблему ціною заборони псевдоніму, включаючи передачу параметрів (якщо ви не хочете ввести ще більшу складність, прийнявши іржаві позикові позики та перевірку позик).

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


Для чого потрібен конструктор копіювання / переміщення? Файл все ще зберігає еталонний тип. У цій ситуації f, який є вказівником, копіюється на абонент, і він несе відповідальність за розпорядження ресурсом (компілятор неявно поставив би схему спроб остаточного розпорядження в абонента)
Maniero

1
@bigown Якщо ви ставитесь до будь-яких посилань Fileтаким чином, нічого не змінюється і Disposeніколи не викликається. Якщо ви завжди телефонуєте Dispose, ви не можете нічого робити з одноразовими предметами. Або ви пропонуєте якусь схему для розпорядження, а іноді ні? Якщо це так, будь ласка, опишіть його детально, і я розповім вам ситуації, в яких він не вдається.

Я не бачу, що ви сказали зараз (я не кажу, що ви неправі). Об'єкт має ресурс, а не посилання.
Маньєро

Моє розуміння, змінивши ваш приклад на лише повернення, полягає в тому, що компілятор буде вставляти спробу безпосередньо перед придбанням ресурсу (рядок 3 у вашому прикладі) та блок нарешті-розпорядження безпосередньо перед кінцем області застосування (рядок 6). Тут немає проблем, погодьтеся? Поверніться до вашого прикладу. Компілятор бачить передачу, він не міг вставити спробу-нарешті сюди, але абонент отримає файл (вказівник на) файловий об’єкт і припускаючи, що абонент не переносить цей об’єкт знову, компілятор вставить туди шаблон спробу нарешті. Іншими словами, кожен не переданий ID об'єкт, що не передається, повинен застосовувати шаблон "спробу".
Маньєро

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
@Robert: Правильно написана програма не може припускати, що фіналізатори ніколи не запускаються. blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx
Billy 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 є %. Хоча це є "небезпечним" в тому сенсі, що після закінчення методу, поле посилається на розміщений об'єкт.


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, використання-блоків, типів 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 - це управління ресурсами, пов'язане з обсягами, але в ньому відсутня неявність інтелектуального вказівника. Акт оголошення покажчика розумним можна вважати "явним", але якщо компілятор наклав інтелект на інтелекту як частину об'єктів типу, він схилятиметься до "неявного".
mike30

AFAICT, точка RAII полягає у встановленні суворого визначення ресурсів. якщо вас цікавить лише розміщення об'єктів, то ні, зібрані сміття мови не можуть цього зробити. якщо вам цікаво послідовно випускати ресурси, то це спосіб це зробити (інша мова - deferна Go).
Хав'єр

1
Власне, я вважаю, що справедливо сказати, що Java і C # сильно віддають перевагу явним конструкціям. В іншому випадку, навіщо турбуватися з усією церемонією, властивою використанню інтерфейсів та успадкування?
Роберт Харві

1
@delnan, Go має "неявні" інтерфейси.
Хав'єр
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.