Чи використання пункту нарешті для роботи після повернення поганого стилю / небезпечно?


30

Як частина написання Iterator, я опинився, коли я написав наступний фрагмент коду (зняття помилок з обробки)

public T next() {
  try {
    return next;
  } finally {
    next = fetcher.fetchNext(next);
  }
}

знайти його трохи легше, ніж читати

public T next() {
  T tmp = next;
  next = fetcher.fetchNext(next);
  return tmp;
}

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

Якщо це погано: чому? Стиль, продуктивність, підводні камені, ...?

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


10
Я особисто міг би зрозуміти другий більше, ніж перший, але я рідко використовую finallyблоки.
Крістіан Манн

2
повернутий струм = fetcher.fetchNext (струм); // Як щодо цього, ака вам справді потрібна попередня завантаження?
scfridge

1
@scarfridge Приклад стосується впровадження Iterator, де вам справді потрібна якась попередня завантаження, hasNext()щоб працювати. Спробуйте самі.
maaartinus

1
@maaartinus Я про це знаю. Іноді hasNext () просто повертає справжнє, наприклад, послідовність граду, а іноді отримання наступного елемента є надмірно дорогим. В останньому випадку слід вдатися до отримання елемента лише на вимогу, подібного до лінивої ініціалізації.
шарфридж

1
@scarfridge Проблема при загальному використанні Iteratorполягає в тому, що вам потрібно отримати значення в hasNext()(оскільки його отримання досить часто є єдиним способом з'ясувати, чи існує воно) і повернути його next()так, як це робило ОП.
maaartinus

Відповіді:


18

Особисто я б задумався над ідеєю розділення командних запитів . Коли ви переходите до нього безпосередньо, next () має дві цілі: рекламоване призначення отримання елемента наступним і прихований побічний ефект мутації внутрішнього стану next. Отже, те, що ви робите, - це виконувати рекламоване призначення в тілі методу, а потім застосувати прихований побічний ефект у остаточному підрозділі, який здається… незручним, хоча і не «неправильним».

Я б сказала, що це насправді зводиться до того, наскільки зрозумілий код, і в цьому випадку відповідь - "мех". Ви "пробуєте" просту операцію повернення, а потім виконуєте прихований побічний ефект у кодовому блоці, який повинен бути призначений для безпечного відновлення помилок. Те, що ви робите, - це "розумний", а "розумний" код часто спонукає обслуговуючих людей бурмотіти, "що ... о, я думаю, я це розумію". Людям, які читають ваш код, краще бурмотати "так, так, так, сенс, так ..."

Отже, що робити, якщо ви відмежували мутацію стану від дзвінків доступу? Я думаю, що питання читабельності, яким ви займаєтесь, стає суперечливим, але я не знаю, як це впливає на вашу більш широку абстракцію. Щось врахувати, все одно.


1
+1 за коментар "розумного". Це дуже точно фіксує цей приклад.

2
Дія "повернення і пересування" next () примусово сприймається на OP через нестримний інтерфейс ітератора Java.
Кевін Клайн

37

Чисто з точки зору стилю, я думаю, що ці три лінії:

T current = next;
next = fetcher.getNext();
return current;

... є і більш очевидними, і коротшими, ніж блок "спроба / нарешті". Оскільки ви не очікуєте, що будь-які винятки будуть кинуті, використання tryблоку просто заплутає людей.


2
Блок спробувати / catch / нарешті може також додати додаткові накладні витрати, я не впевнений, чи javac або компілятор JIT позбавлять їх, навіть якщо ви не кидаєте виняток.
Мартійн Вербург

2
@MartijnVerburg Так, javac все ще додає запис до таблиці винятків і дублює остаточний код, навіть якщо блок спробу порожній.
Даніель Любаров

@Daniel - дякую, що я був занадто ледачий, щоб виконати javap :-)
Martijn Verburg,

@Martijn Verburg: AFAIK, блок "try-catch-finallt" по суті є безкоштовним, доки немає жодного фактичного виключення. Тут javac не намагається і не потрібно нічого оптимізувати, оскільки JIT подбає про це.
maaartinus

@maaartinus - спасибі, що має сенс - потрібно ще раз прочитати вихідний код OpenJDK :-)
Martijn Verburg

6

Це залежало б від призначення коду всередині остаточно блоку. Канонічний приклад - це закриття потоку після читання / написання з нього, якесь «очищення», яке потрібно робити завжди. Відновлення об'єкта (в даному випадку ітератора) до дійсного стану IMHO також вважається очищенням, тому я не бачу тут жодних проблем. Якщо OTOH ви використовували, returnяк тільки було знайдено ваше повернене значення, і додавали багато незв'язаного коду в остаточному блоці, то це затьмарило б мету і зробило б це все менш зрозумілим.

Я не бачу жодної проблеми в його використанні, коли "немає ніяких винятків". Дуже часто користуватися try...finallyбез того, catchколи код може викидатись, RuntimeExceptionі ви не плануєте поводитися з ними. Іноді, нарешті, це лише гарантія, і ви знаєте, за логікою вашої програми, що ніколи не буде викинуто жодного винятку (класична умова "цього не повинно відбуватися").

Підводні камені: будь-який виняток, піднятий всередині tryблоку, змусить finallyзапустити роботу. Це може привести вас до непослідовного стану. Отже, якщо ваша заява про повернення була чимось на зразок:

return preProcess(next);

і цей код, який створив виняток, fetchNextвсе ще працюватиме. OTOH, якщо ви кодували його так:

T ret = preProcess(next);
next = fetcher.fetchNext(next);
return ret;

тоді вона не бігла б. Я знаю, що ви припускаєте, що код спробу ніколи не може викликати будь-якого винятку, але для випадків, складніших за цей, як ви можете бути впевнені? Якщо ваш ітератор був довговічним об'єктом, він продовжував би існувати, навіть якщо в поточній нитці сталася непоправна помилка, ніж було б важливо постійно тримати його у дійсному стані. Інакше це не дуже важливо ...

Продуктивність: було б цікаво декомпілювати такий код, щоб побачити, як він працює під кришкою, але я не знаю достатньої кількості JVM, щоб навіть добре здогадатися ... Винятки, як правило, "виняткові", тому код, який обробляє їх не потрібно оптимізувати за швидкістю (отже, порада ніколи не використовувати винятки в нормальному контрольному потоці ваших програм), але я не хочу про це finally.


Хіба ви насправді не надаєте досить вагомих причин уникати використання finallyтаким способом? Я маю на увазі, як ви кажете, що в результаті код має небажані наслідки; ще гірше, ви, напевно, навіть не думаєте про винятки.
Андрес Ф.

2
На нормальну швидкість потоку управління істотно не впливають конструкції, що обробляють винятки. Штраф за продуктивність сплачується лише при фактичному киданні та вилученні винятків, а не при введенні пробного блоку чи введенні остаточного блоку в звичайній роботі.
yfeldblum

1
@AndresF. Щоправда, але це також справедливо для будь-якого коду очищення. Наприклад, припустимо, що посилання на відкритий потік задається nullкодом помилок; намагання прочитати з нього підніме виняток, спроба закрити його в finallyблоці призведе до іншого винятку. Крім того, це ситуація, коли стан обчислень не має значення, ви хочете закрити потік, відбувся виняток чи ні. Можуть бути й інші подібні ситуації. З цієї причини я розглядаю це як підводний камінь для дійсних цілей використання, а не рекомендація ніколи не використовувати його.
mgibsonbr

Існує також проблема, що коли випробування і блок нарешті викинути виняток, виняток у спробі ніхто не бачить, але це, мабуть, причина проблеми.
Ганс-Пітер Штерр

3

Заголовок твердження: "... нарешті пункт про виконання роботи після повернення ..." є помилковим. Нарешті блок відбувається до повернення функції. У цьому вся суть нарешті насправді.

Тут ви зловживаєте - це порядок оцінювання, де значення next зберігається для повернення, перш ніж ви мутуєте його. Це не є звичайною практикою, і, на мою думку, неправильною, оскільки це робить вас код не послідовним, а тому набагато складніше дотримуватися.

Передбачуване використання остаточно блоків - це обробка винятків, коли певні наслідки повинні відбуватися незалежно від викидів, наприклад, очищення деякого ресурсу, закриття з'єднання з БД, закриття файлу / сокета тощо.


+1, я хотів би додати, що навіть якщо мовна специфікація гарантує, що значення буде збережене до його повернення, воно неминуче суперечить очікуванню читача, і тому його слід уникати, коли це можливо. Налагодження вдвічі складніше, ніж кодування ...
jmoreno

0

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

А ще може бути глист - це те, що загалом деякі корисні дії, нарешті, можуть кинути винятки, тож можна закінчити справді безладним методом.

Через них хтось, хто її читає, повинен подумати над цими проблемами, тому важче зрозуміти.

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