Як уникнути марного повернення методом Java?


115

У мене є ситуація, коли returnтвердження, вкладене в дві forпетлі, завжди буде досягнуто теоретично.

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

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

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range; count++) { // Run until return.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                return count;
            }
        }
    }
    return 0; // Never reached
}

9
Якщо останній елемент ніколи не досягнуто, ви можете скористатися while(true)швидше, ніж індексованим циклом. Це повідомляє компілятору, що цикл ніколи не повернеться.
Борис Павук

101
зателефонуйте на функцію з діапазоном як 0 (або будь-яке інше число менше 1) ( oneRun(0)), і ви побачите, що швидко return
дістаєтесь

55
Повернення досягається, коли поданий діапазон від'ємний. У вас також є 0 валідації для діапазону введення, тому ви зараз не переймаєте його іншим способом.
СподіваюсьДопомога

4
@HopefullyHelpful Звичайно, це справжня відповідь. Це зовсім не марне повернення!
Містер Лістер

4
@HopefullyHelpful ні, тому що nextIntкидає виняток для Єдиного range < 0випадку, коли повернення досягнуто, колиrange == 0
njzk2

Відповіді:


343

Евристика компілятора ніколи не дозволить вам опустити останню return. Якщо ви впевнені, що його ніколи не буде досягнуто, я замінив би його на, throwщоб зрозуміти ситуацію.

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range; count++) {
        ...
    }

    throw new AssertionError("unreachable code reached");
}

135
Не тільки читабельність, але і гарантує, що ви дізнаєтесь, чи є щось із вашим кодом. Це абсолютно правдоподібно, що генератор може мати помилку.
JollyJoker

6
Якщо ви АБСОЛЮТНО впевнені, що ваш код ніколи не зможе дістатись до цього "недоступного коду", тоді створіть кілька одиничних тестів для всіх кращих випадків, які можуть виникнути (діапазон 0, діапазон -1, діапазон мінімум / макс. Int). Це може бути приватний метод зараз, але наступний розробник може не тримати його таким чином. Те, як ви обробляєте несподівані значення (кидаючи виняток, повертаючи значення помилки, нічого не роблячи), залежить від того, як ви збираєтеся використовувати метод. На мій досвід, ви просто хочете зареєструвати помилку та повернутися.
Рік Райкер

22
Можливо, так і має бутиthrow new AssertionError("\"unreachable\" code reached");
богемський

8
Я б написав саме те, що виклав у своїй відповіді у виробничій програмі.
Джон Кугельман

1
Для даного прикладу є кращий спосіб розібратися, ніж просто додати throwте, що ніколи не буде досягнуто. Занадто часто люди просто хочуть швидко застосувати "одне рішення, щоб керувати ними всім", не замислюючись надто про основну проблему. Але програмування набагато більше, ніж просто кодування. Особливо, якщо мова йде про алгоритми. Якщо ви (уважно) вивчите дану проблему, то зрозумієте, що зможете визначити останню ітерацію зовнішньої forпетлі і, отже, замінити "непотрібне" повернення на корисне (див. Цю відповідь ).
a_guest

36

Як зазначав @BoristheSpider, ви можете переконатися, що друге returnтвердження семантично недоступне:

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    int count = 0;

    while (true) {
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                return count;
            }
        }
        count++;
    }
}

Компілює і працює нормально. І якщо ви коли-небудь отримаєтеArrayIndexOutOfBoundsException ви знаєте, що реалізація була семантично неправильною, не вимагаючи явного нічого.


1
Точно так: умова насправді не використовується для керування циклом, тому її там не повинно бути. Однак я б написав це як for(int count = 0; true; count++)натомість.
cmaster - відновити моніку

3
@cmaster: ви можете опустити true:for(int count = 0; ; count++) …
Holger

@Holger Ah. Я не був у цьому впевнений, оскільки мова йде про Java, і я не торкаюся цієї мови роками, оскільки я набагато більше вживаю C / C ++. Тож, побачивши використання while(true), я подумав, що, можливо, в цьому плані ява трохи суворіша. Приємно знати, що не потрібно було хвилюватися ... Насправді мені дуже подобається опущена trueверсія: Це робить візуально зрозумілим, що немає абсолютно жодної умови дивитись на :-)
cmaster - відновити моніку

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

1
@ l0b0 Весь питання, ймовірно, буде кращим для огляду коду, враховуючи, що попередження пов'язане з якістю коду.
Султан

18

Оскільки ви запитували про виривання двох forциклів, для цього можете використовувати мітку (див. Приклад нижче):

private static int oneRun(int range) {
    int returnValue=-1;

    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    OUTER: for (int count = 1; count <= range; count++) { // Run until return.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.   
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                returnValue = count;
                break OUTER;
            }
        }
    }
    return returnValue;
}

Не вдалося б цього зібрати, тому що returnValueїх можна використовувати неініціалізованими?
пт

1
Ймовірно. Об'єктом публікації було показати, як вирватися з обох циклів, оскільки про це запитував оригінальний плакат. ОП був впевнений, що виконання ніколи не закінчиться методом, тому те, що ви підняли, можна виправити, ініціалізуючи returnValue до будь-якого значення при його оголошенні.
Девід Човелер

4
Чи не етикетки таких речей, яких вам краще уникати будь-якими способами?
Serverfrog

2
Я вважаю, ти думаєш про готос.
Девід Хоуллер

4
Мітки на мові програмування високого (-ish) рівня. Абсолютно варварський ...
xDaizu

13

Хоча ствердження - хороше швидке рішення. Взагалі подібні проблеми означають, що ваш код занадто складний. Коли я дивлюся на ваш код, очевидно, що ви не дуже хочете, щоб масив містив попередні числа. Ви хочете Set:

Set<Integer> previous = new HashSet<Integer>();

int randomInt = generator.nextInt(range);
previous.add(randomInt);

for (int count = 1; count <= range; count++) {
    randomInt = generator.nextInt(range);
    if (previous.contains(randomInt)) {
       break;
    }

    previous.add(randomInt);
}

return previous.size();

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

Тепер ми можемо зрозуміти, що нам навіть цей countіндекс не потрібен :

Set<Integer> previous = new HashSet<Integer>();

int randomInt = generator.nextInt(range);

while (!previous.contains(randomInt)) {          
    previous.add(randomInt);      
    randomInt = generator.nextInt(range);
}

return previous.size();

8

Оскільки ваше значення повернення базується на змінній зовнішньої петлі, ви можете просто змінити умову зовнішньої петлі count < rangeта повернути це останнє значення (яке ви тільки що опустили) в кінці функції:

private static int oneRun(int range) {
    ...

    for (int count = 1; count < range; count++) {
        ...
    }
    return range;
}

Таким чином вам не потрібно вводити код, який ніколи не буде досягнутий.


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

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

5

Використовуйте змінну temp, наприклад "результат" та видаліть внутрішнє повернення. На певний час змініть цикл for на відповідний стан. Мені завжди елегантніше мати лише одне повернення як останнє твердження функції.


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

@ Solomonoff'sSecret Ваше твердження абсурдне. Це означає, що ми повинні «взагалі» прагнути до двох або більше заяв про повернення. Цікаво.
Девід

@ Solomonoff'sSecret Це питання бажаних стилів програмування, і незалежно від того, що класно на цьому тижні, який був кульгавим минулого тижня, є певні плюси мати лише одну точку виходу, і в кінці методу (просто подумайте про цикл з багато хто return resultпосипався навколо нього, а потім думають про те, що хочуть ще раз перевірити знайдене, resultперш ніж повернути його, і це лише приклад). Можуть бути і мінуси, але таке широке твердження, як "Взагалі, немає вагомих причин мати лише одне повернення", не підлягає.
SantiBailors

@David Дозвольте перефразувати: немає загальної причини мати лише одне повернення. У деяких випадках це можуть бути причини, але зазвичай це вантажний культ у найгіршому випадку.
Відновіть Моніку

1
Справа в тому, що робить код більш читабельним. Якщо в голові ви говорите: "якщо це так, поверніть те, що ми знайшли; інакше продовжуйте; якщо ми не знайшли щось, поверніть це інше значення". Тоді ваш код повинен мати два повернених значення. Завжди кодуйте те, що робить його відразу зрозумілим для наступного розробника. У компілятора є лише одна точка повернення з методу Java, навіть якщо нашим очам це виглядає як дві.
Рік Райкер

3

Можливо, це є ознакою того, що вам слід переписати свій код. Наприклад:

  1. Створіть масив цілих чисел 0 .. діапазон-1. Встановіть усі значення 0.
  2. Виконайте петлю. У циклі генеруйте випадкове число. Подивіться у своєму списку в цьому індексі, щоб побачити, чи є значення 1 Якщо воно є, вирвіться з циклу. В іншому випадку встановіть значення в цьому індексі на 1
  3. Порахуйте число 1 у списку та поверніть це значення.

3

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

Оскільки ви хочете, щоб ваш метод повернув перший результат, коли rInt [i] дорівнює rInt [count], реалізація лише вищезгаданої змінної недостатня, оскільки метод поверне останній результат, коли rInt [i] дорівнює rInt [count]. Один з варіантів - це реалізувати два "заяви про перерви", які викликаються, коли у нас є бажаний результат. Отже, метод буде виглядати приблизно так:

private static int oneRun(int range) {

        int finalResult = 0; // the above-mentioned variable
        int[] rInt = new int[range + 1];
        rInt[0] = generator.nextInt(range);

        for (int count = 1; count <= range; count++) {
            rInt[count] = generator.nextInt(range);
            for (int i = 0; i < count; i++) {
                if (rInt[i] == rInt[count]) {
                    finalResult = count;
                    break; // this breaks the inside loop
                }
            }
            if (finalResult == count) {
                break; // this breaks the outside loop
            }
        }
        return finalResult;
    }

2

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

private static int oneRun(int range) {
    int[] rInt = new int[range + 1];
    return IntStream
        .rangeClosed(0, range)
        .peek(i -> rInt[i] = generator.nextInt(range))
        .filter(i -> IntStream.range(0, i).anyMatch(j -> rInt[i] == rInt[j]))
        .findFirst()
        .orElseThrow(() -> new RuntimeException("Shouldn't be reached!"));
}

-1
private static int oneRun(int range) {
    int result = -1; // use this to store your result
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range && result == -1; count++) { // Run until result found.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.   
        for (int i = 0; i < count && result == -1; i++) { // Check for past occurence and leave after result found.
            if (rInt[i] == rInt[count]) {
                result = count;
            }
        }
    }
    return result; // return your result
}

Код також неефективний , так як один буде робити багато з result == -1перевірок , які можуть бути опущені з returnвнутрішньої сторони петлі ...
Willem Van Onsem
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.