Чи є оператор return просто для задоволення синтаксису поганою практикою?


82

Розглянемо такий код:

public Object getClone(Cloneable a) throws TotallyFooException {

    if (a == null) {
        throw new TotallyFooException();
    }
    else {
        try {
            return a.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
    //cant be reached, in for syntax
    return null;
}

Це return null;необхідно, оскільки виняток може бути спійманий, однак у такому випадку, оскільки ми вже перевірили, чи є воно нульовим (і припустимо, що ми знаємо, що клас, який ми викликаємо, підтримує клонування), тому ми знаємо, що оператор try ніколи не провалиться.

Чи погана практика вводити додатковий оператор return в кінці лише для того, щоб задовольнити синтаксис і уникнути помилок компіляції (з коментарем, який пояснює, що це не буде досягнуто), чи є кращий спосіб кодувати щось подібне, щоб додаткові оператор повернення непотрібний?


25
Ваш метод приймає Objectпараметр. Якщо aклас не підтримує cloneметод (або це визначено в Object?) Або якщо під час cloneметоду виникає помилка (або будь-яка інша помилка, про яку я зараз не можу подумати), може бути створено виняток, і ви отримаєте оператор повернення.
Arc676

26
Ваше припущення, що остаточного повернення неможливо досягти, є помилковим. Це повністю діє для класу, який реалізує Cloneableмета a CloneNotSupportedException. Джерело: javadocs
головоногий

20
Код, який ви прокоментували як "неможливо досягти", можна отримати. Як уже згадувалося вище, до цього рядка є шлях від CloneNotSupportedException.
Девід Вотерворт,

7
Основне питання тут: Якщо ви приймаєте клас, який ви викликаєте, підтримує клонування, чому ви взагалі ловите виняток? Винятки слід кидати за виняткову поведінку.
деворде

3
@wero я би пішов AssertionErrorзамість InternalError. Останнє, здається, призначене для спеціального використання, якщо щось у JVM піде не так. І ви в основному стверджуєте, що код не досягнутий.
кап

Відповіді:


134

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

if (a != null) {
    try {
        return a.clone();
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
}
throw new TotallyFooException();

Майже завжди можна возитись із замовленням, щоб у підсумку отримати більш прямий синтаксис, ніж той, який ви спочатку мали.


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

3
@BillK: Однак цей код має різну семантику, як зазначив Марун Марун.
Дедулікатор

5
AssertionError- це вбудований клас із подібним наміром TotallyFooException(який насправді не існує).
user253751

5
@BillK: "Якщо ви виявите, що боретеся з вимогами Java, це, як правило, означає, що ви намагаєтесь зробити щось неоптимально". або що Java вирішила не підтримувати функцію, яка полегшує ваше життя.
user541686

5
@Mehrdad, ... але, дивлячись на це так, ви нікуди практично не принесете користі, якщо тільки ви не працюєте над RFE для майбутніх версій Java або альтернативної мови. Вивчення локальних ідіом має реальну, практичну перевагу, тоді як звинувачення мови (будь-якої мови, якщо тільки вона не підтримує метапрограмування a-la-LISP, що дозволяє самостійно додавати новий синтаксис) у тому, що вона не підтримує те, як ви спочатку хотіли щось зробити.
Чарльз Даффі,

101

Це точно можна досягти. Зверніть увагу, що ви друкуєте стек трасування лише у цьому catchпункті.

У тому випадку, коли a != nullі буде виняток, return null буде досягнуто. Ви можете видалити це твердження і замінити його на throw new TotallyFooException();.

Загалом * , якщо nullє дійсним результатом методу (тобто користувач очікує його, а це щось означає), тоді повернення його як сигналу для "дані не знайдені" або виняток стався - це не гарна ідея. В іншому випадку я не бачу жодної проблеми, чому б вам не повернутися null.

Візьмемо для прикладу Scanner#ioExceptionметод:

Повертає IOExceptionостаннє викинуте базовим читачем цього сканера. Цей метод повертає нуль, якщо такого винятку не існує .

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

* Зверніть увагу, що іноді ви хочете повернутися, nullнавіть коли значення неоднозначне. Наприклад HashMap#get:

Повернене значення null не обов'язково означає, що карта не містить відображення ключа; також можливо, що карта явно відображає ключ у нуль . containsKeyОперація може бути використана , щоб відрізнити ці два випадки.

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


4
Хоча ваші пункти є дійсними, я хотів би зазначити, що це просто безглуздий дизайн API. Обидва методи повинні повертати Optional<Throwable>або Optional<TValue>, відповідно. Примітка: це не критика вашої відповіді, а дизайн API цих двох методів. (Однак, це досить поширена помилка дизайну, яка є поширеною не лише у всьому API Java, але, наприклад, і в .NET).
Йорг В Міттаг,

4
"дерьмовий" досить жорсткий, якщо ви не знаєте, чи можна використовувати засоби Java 8 чи ні.
Thorbjørn Ravn Andersen

2
Але коли nullце НЕ є допустимим значенням повернення, повернення nullє ще гірше ідея. Іншими словами, повернення nullдо несподіваної ситуації ніколи не є хорошою ідеєю.
Holger

1
@Holger Те, що я маю на увазі під недійсним, це, наприклад, коли користувач очікує отримати значення зі структури даних, яке не може містити null, тоді nullніколи не може бути "дійсним" повернутим значенням у тому сенсі, що воно повинно вказувати щось інше, а не звичайне повернене значення. У цьому випадку його повернення має особливе значення, і я не бачу в цьому дизайні нічого поганого (звичайно, користувач повинен знати, як вирішити цю ситуацію).
Марун

1
Ось у чому суть: "користувач повинен знати, як вирішити цю ситуацію", означає, що вам потрібно вказати, яке nullсаме можливе значення повернення має мати справу із абонентом. Це робить його дійсним значенням, і ми маємо таке саме розуміння поняття "дійсний" тут, як ви описуєте "дійсний", як "користувач очікує цього і це щось означає". Але тут ситуація все одно інша. OP зазначає, що за коментарем коду він стверджує, що return null;твердження "неможливо досягти", що означає, що повернення nullє неправильним. Це має бути throw new AssertionError();замість цього ...
Холгер,

26

Чи погана практика вкладати в кінець додатковий оператор return, щоб задовольнити синтаксис і уникнути помилок компіляції (з коментарем, який пояснює, що це не буде зроблено)

Я думаю, що return nullце погана практика для кінця недосяжної гілки. Краще кинути RuntimeException( AssertionErrorце також було б прийнятно), оскільки для того, щоб дістатися до цього рядка щось пішло дуже не так, і програма знаходиться в невідомому стані. Більше подібного є (як вище), оскільки розробник щось пропустив (Об’єкти можуть бути ненульовими та не клонованими).

Швидше за все, я б не використовував, InternalErrorякщо я не дуже впевнений, що код недосяжний (наприклад, після a System.exit()), оскільки, швидше за все, я роблю помилку, ніж віртуальна машина.

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


3
Як зазначалося в інших місцях у коментарях, також AssertionErrorможе бути розумним вибором взяти участь у події "не може статися" .
Ілмарі Каронен

2
Особисто я б кинув SomeJerkImplementedClonableAndStillThrewACloneNotSupportedExceptionException. Очевидно, це продовжиться RuntimeException.
w25r

@ w25r Я не використовую код вище, а відповідаю на основне питання. Вказаний Cloneableне реалізує загальнодоступний clone()метод, а clone()в object is protectedнаведений вище код фактично не компілюється.
Michael Lloyd Lee mlk

Я б не використовував пасивно-агресивний виняток на всякий випадок, якщо він уникне (настільки смішним, як це було б для одного з наших клієнтів отримати JerkException, я не думаю, що це буде оцінено).
Michael Lloyd Lee mlk

14

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


12

Я вважаю за краще використовувати, Objects.requireNonNull()щоб перевірити, чи не є параметр a нульовим. Отже, коли ви читаєте код, стає зрозуміло, що параметр не повинен бути нульовим.

А щоб уникнути перевірених винятків, я б перекинув CloneNotSupportedExceptionяк a RuntimeException.

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

public Object getClone(Object a) {

    Objects.requireNonNull(a);

    try {
        return a.clone();
    } catch (CloneNotSupportedException e) {
        throw new IllegalArgumentException(e);
    }

}

7

У такій ситуації я б написав

public Object getClone(SomeInterface a) throws TotallyFooException {
    // Precondition: "a" should be null or should have a someMethod method that
    // does not throw a SomeException.
    if (a == null) {
        throw new TotallyFooException() ; }
    else {
        try {
            return a.someMethod(); }
        catch (SomeException e) {
            throw new IllegalArgumentException(e) ; } }
}

Цікаво, що ви стверджуєте, що "заява" спробувати ніколи не провалиться ", але ви все-таки взяли на себе зусилля, написавши заяву, e.printStackTrace();яка, як ви стверджуєте, ніколи не буде виконана. Чому?

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

До речі, ваш код для мене не компілюється. Ви не можете телефонувати, a.clone()навіть якщо тип aє Cloneable. Принаймні так говорить компілятор Eclipse. Вираз a.clone()дає помилку

Метод clone () невизначений для типу Cloneable

Я б зробив для вашого конкретного випадку

public Object getClone(PubliclyCloneable a) throws TotallyFooException {
    if (a == null) {
        throw new TotallyFooException(); }
    else {
        return a.clone(); }
}

Де PubliclyCloneableвизначається

interface PubliclyCloneable {
    public Object clone() ;
}

Або, якщо вам абсолютно потрібен тип параметра Cloneable, принаймні компілюється наступне.

public static Object getClone(Cloneable a) throws TotallyFooException {
//  Precondition: "a" should be null or point to an object that can be cloned without
// throwing any checked exception.
    if (a == null) {
        throw new TotallyFooException(); }
    else {
        try {
            return a.getClass().getMethod("clone").invoke(a) ; }
        catch( IllegalAccessException e ) {
            throw new AssertionError(null, e) ; }
        catch( InvocationTargetException e ) {
            Throwable t = e.getTargetException() ;
            if( t instanceof Error ) {
                // Unchecked exceptions are bubbled
                throw (Error) t ; }
            else if( t instanceof RuntimeException ) {
                // Unchecked exceptions are bubbled
                throw (RuntimeException) t ; }
            else {
                // Checked exceptions indicate a precondition violation.
                throw new IllegalArgumentException(t) ; } }
        catch( NoSuchMethodException e ) {
            throw new AssertionError(null, e) ; } }
}

Помилка під час компіляції змусила мене задуматися, чому Cloneableне оголошує cloneяк загальнодоступний метод, який не видає жодних перевірених винятків. На bugs.java.com/view_bug.do?bug_id=4098033
Теодор Норвелл,

2
Я хотів би зазначити, що переважна більшість коду Java не використовує фігурні дужки Lisp, і ви будете брудно виглядати, якщо спробуєте використовувати це в робочому середовищі.
Позов до Моніки

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

6

Наведені вище приклади є дійсними і дуже Java. Однак, ось як я міг би відповісти на запитання ОП про те, як обробляти це повернення:

public Object getClone(Cloneable a) throws CloneNotSupportedException {
    return a.clone();
}

Немає користі від перевірки, aчи є воно нульовим. Це буде до NPE. Друк трасування стека також не корисний. Трасування стека однакове незалежно від того, де з ним обробляється.

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

(Зверніть увагу, що OP включав помилку в обробці винятків; саме тому потрібно було повернення. OP не помилився б із запропонованим методом.)


5

Чи є оператор return просто для задоволення синтаксису поганою практикою?

Як зазначали інші, у вашому випадку це насправді не застосовується.

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

    switch (var)
   {
     case A:
       break;
     default:
       return;
       break;    // Unreachable code.  Coding standard violation?
   }

Один скаржився, що відсутність перерви є порушенням стандартного кодування. Інший скаржився, що його наявність - один, бо це недоступний код.

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

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


5

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

Ніякої `` поганої практики '' щодо цього, або щодо задоволення компілятора загалом.

У будь-якому випадку, будь то синтаксис чи семантика, у вас немає вибору щодо цього.


2

Я б переписав це, щоб повернення було в кінці. Псевдокод:

if a == null throw ...
// else not needed, if this is reached, a is not null
Object b
try {
  b = a.clone
}
catch ...

return b

Як відповідне побічне зауваження, ви майже завжди можете переформатувати свій код, щоб у кінці був оператор return. Насправді це хороший стандарт кодування, оскільки полегшує обслуговування та виправлення помилок, оскільки вам не потрібно враховувати багато точок виходу. Також було б гарною практикою оголошувати всі змінні на початку методу з тієї ж причини. Також врахуйте, що маючи повернення в кінці, можливо, доведеться оголосити більше змінних, як у моєму рішенні, мені потрібна була додаткова змінна "Об'єкт b".
Пабло Пазос,

2

Про це ще ніхто не згадував, тому йдеться:

public static final Object ERROR_OBJECT = ...

//...

public Object getClone(Cloneable a) throws TotallyFooException {
Object ret;

if (a == null) 
    throw new TotallyFooException();

//no need for else here
try {
    ret = a.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
    //something went wrong! ERROR_OBJECT could also be null
    ret = ERROR_OBJECT; 
}

return ret;

}

Мені не подобається returnвсередині tryблоків саме з цієї причини.


2

Повернення null; необхідний, оскільки може бути вилучено виняток, однак у такому випадку, оскільки ми вже перевірили, чи є воно нульовим (і припустимо, що ми знаємо клас, який ми викликаємо, підтримує клонування), тому ми знаємо, що оператор try ніколи не провалиться.

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

У будь-якому випадку, компілятор, на жаль, не є читачем думок. Він бачить функцію та її вхідні дані, і з огляду на інформацію, якою вона володіє, йому потрібна ця returnзаява внизу, як у вас є.

Чи погана практика вкладати додатковий оператор return в кінці лише для задоволення синтаксису та уникнення помилок компіляції (з коментарем, який пояснює, що це не буде досягнуто), чи є кращий спосіб кодувати щось подібне, щоб додаткові оператор повернення непотрібний?

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

Сказано, особливо про цю частину:

try {
    return a.clone();
} catch (CloneNotSupportedException e) {
   e.printStackTrace();
}
...
//cant be reached, in for syntax
return null;

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

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

У цьому випадку просто надрукувати стек-трас, а потім змусити його повернути null - це не зовсім спосіб мислення транзакції / відновлення. Код передає відповідальність за обробку помилок усьому коду, що викликає, getCloneщоб вручну перевірити наявність помилок. Можливо, ви віддаєте перевагу зловити CloneNotSupportedExceptionта перекласти його в іншу, більш значущу форму винятку і кинути це, але ви не хочете просто проковтнути виняток і повернути нуль у цьому випадку, оскільки це не схоже на сайт для відновлення транзакцій.

У кінцевому підсумку ви випустите обов'язки перед абонентами, щоб перевірити вручну та впоратися з несправністю таким чином, коли створення виключення дозволить уникнути цього.

Це як якщо ви завантажуєте файл, це транзакція високого рівня. Можливо, у вас try/catchтам є. Під час tryingзавантаження файлу ви можете клонувати об'єкти. Якщо в цій операції високого рівня (завантаження файлу) де-небудь є збій, зазвичай потрібно повернути винятки до цього try/catchблоку транзакцій найвищого рівня, щоб ви могли елегантно відновитись після помилки під час завантаження файлу (чи це через помилку в клонуванні або щось інше). Тому ми, як правило, не хочемо просто проковтнути виняток у такому деталізованому місці, як це, а потім повернути нуль, наприклад, оскільки це переможе велику частину значення та мети винятків. Натомість ми хочемо поширювати винятки аж до веб-сайту, де ми можемо суттєво з цим боротися.


0

Ваш приклад не ідеально ілюструє ваше запитання, як зазначено в останньому абзаці:

Чи погана практика вкладати додатковий оператор return в кінці лише для задоволення синтаксису та уникнення помилок компіляції (з коментарем, який пояснює, що це не буде досягнуто), чи є кращий спосіб кодувати щось подібне, щоб додаткові оператор повернення непотрібний?

Кращим прикладом може бути реалізація самого клону:

 public class A implements Cloneable {
      public Object clone() {
           try {
               return super.clone() ;
           } catch (CloneNotSupportedException e) {
               throw new InternalError(e) ; // vm bug.
           }
      }
 }

Тут речення catch ніколи не слід вводити. Проте синтаксис або вимагає кинути щось, або повернути значення. Оскільки повернення чогось не має сенсу, символ an InternalErrorвикористовується для позначення важкого стану ВМ.


5
Суперклас, який кидає a CloneNotSupportedException, не є "помилкою вм" - це цілком законно, Cloneableщоб кинути його. Це може становити помилку у вашому коді та / або в суперкласі, і в цьому випадку його обгортання у RuntimeExceptionабо, можливо, у AssertionError(але не в InternalError) може бути доцільним. Або ви можете просто викрити виняток - якщо ваш суперклас не підтримує клонування, то ви цього не робите, тому CloneNotSupportedExceptionце семантично доречно.
Ілмарі Каронен

@IlmariKaronen, можливо, ви захочете надіслати цю пораду Oracle, щоб вони могли виправити всі методи клонування в JDK, які
викликають

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