Возвращение GOTO +5





Сейчас все понимают, что использовать оператор GOTO это не просто плохая, а ужасная практика. Дебаты по поводу его использования закончились в 80-х годах XX века и его исключили из большинства современных языков программирования. Но, как и положено настоящему злу, он сумел замаскироваться и воскреснуть в XXI веке под видом исключений.


Исключения, с одной стороны, являются достаточно простой концепцией в современных языках программирования. С другой же стороны, их часто используют неправильно. Есть простое и хорошо известное правило – исключения только для обработки поломок. И именно слишком вольная интерпретация понятия «поломка» приводит ко всем проблемам использования GOTO.


Теоретический пример


Разница между поломками и негативными бизнес-сценариями хорошо видна на окне входа в систему с очень простым сценарием использования:


  1. Пользователь вводит логин/пароль.
  2. Пользователь нажимает кнопку «Войти в систему».
  3. Клиентское приложение отправляет запрос на сервер.
  4. Сервер успешно проверяет логин/пароль (под успехом считает наличие соответствующей пары).
  5. Сервер отсылает клиенту информацию, что аутентификация прошла успешно и ссылку на страницу перехода.
  6. Клиент осуществляет переход на указанную страницу.

И одно негативное расширение:


4.1. Сервер не нашел соответствующую пару логин/пароль и посылает клиенту уведомление об этом.


Считать, что сценарий 4.1 является «проблемой» и поэтому его надо реализовывать с помощью исключения – достаточно распространенная ошибка. На самом деле это не так. Несоответствие логина и пароля – это часть нашего стандартного взаимодействия с пользователем, предусмотренная бизнес-логикой сценария. Наши бизнес-заказчики ожидают такого развития событий. Следовательно – это не поломка и использовать здесь исключения нельзя.


Поломки, это: разрыв соединения между клиентом и севером, недоступность СУБД, неправильная схема в БД. И еще миллион причин, ломающих наши приложения и не имеющих никакого отношения к бизнес-логике пользователя.


В одном из проектов, в разработке которого я участвовал, была более сложная логика входа в систему. Введя 3 раза подряд неправильный пароль, пользователь временно блокировался на 15 минут. Попадая 3 раза подряд во временную блокировку, пользователь получал постоянную блокировку. Также были дополнительные правила в зависимости от типа пользователя. Реализация с помощью исключений привела к тому, что внесение новых правил было крайне затруднительно.


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


Пример Загрузка свойств


Попробуйте посмотреть данный код и четко понять, что он делает. Процедура не большая с достаточно простой логикой. При хорошем стиле программирования понимание ее сути не должно превышать больше 2-3 минут (я не помню сколько времени ушло у меня на полное понимание этого кода, но точно больше 15 минут).


private WorkspaceProperties(){

    Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH, true);
    //These mappings will replace any mappings that this hashtable had for any of the 
    //keys currently in the specified map.
    getProperties().putAll( loadedProperties );

    //Это файл имеет право отсутствовать
    loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH, false);
    if (loadedProperties != null){
        getProperties().putAll( loadedProperties );
    }
    System.out.println("Loaded properties:" + getProperties());
}

/**
 * Возвращает свойства, загруженные из указанного файла.
 * @param filepath  
 * @param throwIfNotFound - кинуть FileNotFoundException, если файл не найден
 * @return Загруженные свойства или null, если файл не найден и !throwIfNotFound
 * @throws FileNotFoundException throwIfNotFound и файла с таким именем не надено
 * @throws IOException ошибка загрузки найденного файла
 */
private Properties readPropertiesFromFile(String filepath, boolean throwIfNotExists){
    Properties loadedProperties = new Properties();
    System.out.println("Try loading workspace properties" + filepath);

    InputStream is = null;
    InputStreamReader isr = null;
    try{
        int loadingTryLeft = 3;
        String relativePath = "";
        while (loadingTryLeft > 0){
            try{
                File file = new File(relativePath + filepath);
                is = new FileInputStream(file);
                isr = new InputStreamReader( is, "UTF-8");
                loadedProperties.load(isr);
                loadingTryLeft = 0;
            } catch( FileNotFoundException e) {             
                loadingTryLeft -= 1;
                if (loadingTryLeft > 0)
                    relativePath += "../";
                else
                    throw e;
            } finally {
                if (is != null)
                    is.close();
                if (isr != null)
                    isr.close();
            }
        }
        System.out.println("Found file " + filepath);
    } catch( FileNotFoundException e) {
        System.out.println("File not found " + filepath);
        if (throwIfNotExists)
            throw new RuntimeException("Can`t load workspace properties." + filepath + " not found", e );
    }catch (IOException e){
        throw new RuntimeException("Can`t read " + filepath, e);
    }
    return loadedProperties;
}

Итак, раскроем тайну – что же здесь происходит. Осуществляется загрузка свойств из двух файлов – обязательного WORK_PROPERTIES и дополнительного MY_WORK_PROPERTIES, добавляя в общее хранилище свойств. При этом есть нюанс – нам точно не известно, где лежит конкретный файл свойств – он может лежать как в текущем каталоге, так и в каталогах-предках (до трех уровней вверх).


Здесь смущает, как минимум, две вещи: параметр throwIfNotExists и большой блок логики в catch FileNotFoundException. Все это непрозрачно намекает – исключения используются для реализации бизнес-логики (а как иначе объяснить, что в одном сценарии выброс исключения – это поломка, а в другом – нет?).


Делаем правильный контракт


Сначала разберемся с throwIfNotExists. При работе с исключениями очень важно понимать – где именно его нужно обработать с точки зрения сценариев использования. В данном случае очевидно, что сам метод readPropertiesFromFile не может принять решение – когда отсутствие файла «плохо», а когда – «хорошо». Такое решение принимается в точке его вызова. По комментариям видно, что мы решаем – должен существовать этот файл или нет. Но на самом деле нам интересен не сам файл, а настройки из него. К сожалению, это никак не следует из кода.


Исправим оба этим недостатка:


Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH);
if (loadedProperties.isEmpty()) {
    throw new RuntimeException("Can`t load workspace properties");
}
loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH);
getProperties().putAll( loadedProperties );

Теперь четко показана семантика –
WORK_PROPERTIES обязательно должны быть заданы, а MY_WORK_PROPERTIES — нет. Также при рефакторинге я обратил внимание, что readPropertiesFromFile никогда не сможет вернуть null и воспользовался этим при чтении MY_WORK_PROPERTIES.


Проверяем не ломая


Предыдущий рефакторинг также затронул и реализацию, но не значительно. Я просто удалил блок обработки throwIfNotExists:


if (throwIfNotExists)
            throw new RuntimeException(…);

Рассмотрев реализацию более пристально, мы начинаем понимать логику автора кода по поиску файла. Сначала проверяется, что файл находится в текущем каталоге, если не нашли – проверяем на уровне выше и т.д. Т.е. становится понятно, что алгоритм предусматривает отсутствие файла. При этом проверка делается с помощью исключения. Т.е. нарушен принцип – исключение воспринимается не как «что-то поломалось», а как часть бизнес-логики.


Существует функция проверки доступности файла для чтения File.canRead(). Используя ее можно избавиться от бизнес-логики в блоке catch


            try{
                File file = new File(relativePath + filepath);
                is = new FileInputStream(file);
                isr = new InputStreamReader( is, "UTF-8");
                loadedProperties.load(isr);
                loadingTryLeft = 0;
            } catch( FileNotFoundException e) {             
                loadingTryLeft -= 1;
                if (loadingTryLeft > 0)
                    relativePath += "../";
                else
                    throw e;
            } finally {
                if (is != null)
                    is.close();
                if (isr != null)
                    isr.close();
            }
        }

Изменив код, получаем следующее:


private Properties readPropertiesFromFile(String filepath) {
    Properties loadedProperties = new Properties();
    System.out.println("Try loading workspace properties" + filepath);

    try {
        int loadingTryLeft = 3;
        String relativePath = "";
        while (loadingTryLeft > 0) {
            File file = new File(relativePath + filepath);
            if (file.canRead()) {
                InputStream is = null;
                InputStreamReader isr = null;
                try {
                    is = new FileInputStream(file);
                    isr = new InputStreamReader(is, "UTF-8");
                    loadedProperties.load(isr);
                    loadingTryLeft = 0;
                } finally {
                    if (is != null)
                        is.close();
                    if (isr != null)
                        isr.close();
                }
            } else {
                loadingTryLeft -= 1;
                if (loadingTryLeft > 0) {
                    relativePath += "../";
                } else {
                    throw new FileNotFoundException();
                }
            }

        }

        System.out.println("Found file " + filepath);
    } catch (FileNotFoundException e) {
        System.out.println("File not found " + filepath);
    } catch (IOException e) {
        throw new RuntimeException("Can`t read " + filepath, e);
    }

    return loadedProperties;
}

Также я снизил уровень переменных (is, isr) до минимально допустимого.


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


Выявляем GOTO


Рассмотрим детально происходящее в ситуации, если файл не был найден:


} else {
    loadingTryLeft -= 1;
    if (loadingTryLeft > 0) {
        relativePath += "../";
    } else {
        throw new FileNotFoundException();
    }
}

Видно, что здесь исключение используется для того, чтобы прервать цикл выполнения и фактически выполняют функцию GOTO.


Для сомневающихся сделаем еще одно изменение. Вместо использования мелкого костыля в виде loadingTryLeft = 0 (костыль, потому что на самом деле успешная попытка неизменяет количество оставшихся попыток) явно укажем, что считывание файла приводит к выходу из функции (не забыв при этом написать сообщение):


try {
    is = new FileInputStream(file);
    isr = new InputStreamReader(is, "UTF-8");
    loadedProperties.load(isr);
    System.out.println("Found file " + filepath);                       
    return loadedProperties;
} finally {

Это позволяет нам заменить условие while (loadingTryLeft > 0) на while(true):


try {
    int loadingTryLeft = 3;
    String relativePath = "";
    while (true) {
        File file = new File(relativePath + filepath);
        if (file.canRead()) {
            InputStream is = null;
            InputStreamReader isr = null;
            try {
                is = new FileInputStream(file);
                isr = new InputStreamReader(is, "UTF-8");
                loadedProperties.load(isr);
                System.out.println("Found file " + filepath);
                return loadedProperties;
            } finally {
                if (is != null)
                    is.close();
                if (isr != null)
                    isr.close();
            }
        } else {
            loadingTryLeft -= 1;
            if (loadingTryLeft > 0) {
                relativePath += "../";
            } else {
                throw new FileNotFoundException(); // GOTO: FFN
            }
        }

    }

} catch (FileNotFoundException e) { // LABEL: FFN
    System.out.println("File not found " + filepath);
} catch (IOException e) {
    throw new RuntimeException("Can`t read " + filepath, e);
}

Чтобы избавиться от явного дурно пахнущего throw new FileNotFoundException, нужно вспомнить контракт функции. Функция в любом случае возвращает набор свойств, если не смогли считать файл – возвращаем его пустым. Поэтому нет никаких причин выбрасывать исключение и перехватывать его. Достаточно обычного условия while (loadingTryLeft > 0):


private Properties readPropertiesFromFile(String filepath) {
    Properties loadedProperties = new Properties();
    System.out.println("Try loading workspace properties" + filepath);

    try {
        int loadingTryLeft = 3;
        String relativePath = "";

        while (loadingTryLeft > 0) {
            File file = new File(relativePath + filepath);
            if (file.canRead()) {
                InputStream is = null;
                InputStreamReader isr = null;
                try {
                    is = new FileInputStream(file);
                    isr = new InputStreamReader(is, "UTF-8");
                    loadedProperties.load(isr);
                    System.out.println("Found file " + filepath);
                    return loadedProperties;
                } finally {
                    if (is != null)
                        is.close();
                    if (isr != null)
                        isr.close();
                }
            } else {
                loadingTryLeft -= 1;
                if (loadingTryLeft > 0)
                    relativePath += "../";
            }
        }

        System.out.println("file not found");
    } catch (IOException e) {
        throw new RuntimeException("Can`t read " + filepath, e);
    }

    return loadedProperties;
}

В принципе, с точки зрения правильной работы с исключениями здесь все. Остается сомнение в необходимости выбрасывать RuntimeException в случае проблем IOException, но оставим его как есть ради совместимости.



Остались немного мелочей, исправив которые мы сделаем код еще более гибким и понятным:


  • Название метода readPropertiesFromFile раскрывает его реализацию (кстати, равно как и throws FileNotFoundException). Лучше назвать более нейтрально и лаконично – loadProperties(…)
  • Метод одновременно и ищет, и считывает. Для меня это две разных обязанности, которые можно разделить в разных методах.
  • Изначально код писался под Java 6, а сейчас используется на Java 7. Это позволяет использовать closable resources.
  • По опыту знаю, что при выводе информации о найденном или не найденном файле лучше использовать полный путь к файлу, а не относительный.
  • if (loadingTryLeft > 0) relativePath += "../"; — если внимательно посмотреть код, то видно – эта проверка лишняя, т.к. при исчерпании лимита поиска все равно новое значение использовано не будет. А если в коде что-то лишнее, это мусор, который следует убрать.

Окончательная версия исходного кода:


private WorkspaceProperties() {
    super(new Properties());

    if (defaultInstance != null)
        throw new IllegalStateException();

    Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH);
    if (loadedProperties.isEmpty()) {
        throw new RuntimeException("Can`t load workspace properties");
    }

    getProperties().putAll(loadedProperties);

    loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH);
    getProperties().putAll(loadedProperties);

    System.out.println("Loaded properties:" + getProperties());
}

private Properties readPropertiesFromFile(String filepath) {
    System.out.println("Try loading workspace properties" + filepath);

    try {
        int loadingTryLeft = 3;
        String relativePath = "";

        while (loadingTryLeft > 0) {
            File file = new File(relativePath + filepath);
            if (file.canRead()) {
                return read(file);
            } else {
                relativePath += "../";
                loadingTryLeft -= 1;
            }
        }
        System.out.println("file not found");
    } catch (IOException e) {
        throw new RuntimeException("Can`t read " + filepath, e);
    }

    return new Properties();
}

private Properties read(File file) throws IOException {
    try (InputStream is = new FileInputStream(file);
            InputStreamReader isr = new InputStreamReader(is, "UTF-8")) {
        Properties loadedProperties = new Properties();
        loadedProperties.load(isr);
        System.out.println("Found file " + file.getAbsolutePath());
        return loadedProperties;
    }
}

Резюме


Разобранный пример наглядно показывает, к чему приводит небрежное обращение с исходным кодом. Вместо использования исключения для обработки поломки, было принято решение использовать его для реализации бизнес-логики. Это сразу привело к сложности его поддержки, что и нашло отражение в его дальнейшей доработке под новые требования и в итоге – к отходу от принципов структурного программирования. Использование простого правила – исключения только для поломок – поможет вам избежать возвращения в эру GOTO и содержать ваш код чистым, понятным и расширяемым.

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (105):

  1. DMGarikk
    /#21168058 / +9

    Сейчас все понимают, что использовать оператор GOTO это не просто плохая, а ужасная практика.

    да никто этого не понимает, никто, языки где был тот самый страшной goto пропали ещё в конце 80 вместе с обязательной нумерации строк и пререхода на процедурное программирование

    народ по привычке продолжает пересказывать страшилки про goto и приводить, этот единственный пример с выходом из цикла причём в яве для этого можно использовать переход по метке через break или continue

    люди, если вы ниразу не писали на языке где используется goto который приводит к тяжелым последствиям, не стоит пересказывать эту страшилку, вы просто не понимаете о чем говорите, в современных языках сам принцип программирования исключает использование goto независимо от вашего понимания 'ужасных практик'

    p.s. устриц ел, писал на atari basic
    p.p.s. в современных бейсиках goto уже тоже давно нет в том самом опасном виде

    • usdglander
      /#21168204 / +3

      Интересен факт того, что в PHP оператор goto, как раз появился только в версии 5.3. До сих пор не понимаю для чего.

    • E_STRICT
      /#21168284 / -2

      языки где был тот самый страшной goto пропали ещё в конце 80
      PHP, С, C++, C# вроде никуда не пропали.

      • DMGarikk
        /#21168296 / +3

        в них можно делать goto по метке за пределы процедуры?

        • fougasse
          /#21168330

          C++

          The goto statement unconditionally transfers control to the statement labeled by the identifier. The identifier shall be a label (6.1) located in the current function.

          C
          The identifier in a goto statement shall name a label located somewhere in the enclosing function. A goto statement shall not jump from outside the scope of an identifier having a variably modified type to inside the scope of that identifier.

        • ss-nopol
          /#21168346 / +1

          В С/С++ можно вообще куда угодно с помощью longjmp. Классический goto только в пределах функции.

        • E_STRICT
          /#21168360 / +1

          PHP

          The target label must be within the same file and context, meaning that you cannot jump out of a function or method, nor can you jump into one.

    • edogs
      /#21168334 / +3

      Мы начинали с языка где goto вполне себе используется — assembler.
      Потом, что интересно, был basic потом c.
      Потом был период когда преподавали basic и это позволило увидеть много интересных вещей происходящих с goto.

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

      Но с этим есть две проблемы
      а) Зачастую с водой выплескивают и ребенка. Правильное использование goto в 1% случаев может сделать код проще, быстрее, компактнее и понятнее. Но goto нет, т.к. в 99% случаев его будут использовать неправильно.
      б) Народ не понимая сути проблемы с опасными практиками применения goto начинает эмулировать его другими способами, как верно подмечено в статье — например исключениями. В результате goto нет, а опасные практики есть.

      Ближайший аналог из чего-то относительно свежего — это register_globals в php. Они влияли только на безопасность неграмотно написанного кода и их отключили, потому что неграмотно написанного кода было слишком много.
      На самом деле депрессивная причина, когда удобный инструмент не используют только потому, что куча народа не умеет им правильно пользоваться.

      • fougasse
        /#21168356

        Интересно стало посмотреть на необходимость goto в Java.

        • Tangeman
          /#21169440 / +1

          ext_loop: while (...) {
            while (...) {
              ...
              if (...) break ext_loop;
              ...
            }
          }
          

          Не всегда просто (и бесплатно) переписать часть кода так чтобы можно было без подобной конструкции обойтись, и тот факт что вместо «goto» используется «break» не меняет сути — и первое и второе перепрыгивают через кусок кода (как, собственно, вообще любые «break» и «continue», которые по сути те же самые «goto», хотя называются иначе).

          Как выше уже отметили, не сам факт использования «goto» печален, а неадекватное его использование.

          • somurzakov
            /#21169542

            return это тоже GOTO в своем роде, мало кто этого понимает правда

            • DMGarikk
              /#21170100

              если совсем точно проводить аналогии то return это resume/return из gosub

              • somurzakov
                /#21170146

                я имел в виду, что иногда встречаешься с кодом где излишне злоупотребляют return'ом и он читается ненамного лучше чем goto, например return в начале тела функции, в середине внутри двух-трех вложенных циклов и в конце тела.
                а по сути да, return это прыжок в конец тела функции с каким-то значением

                • tuxi
                  /#21170366 / +4

                  например return в начале тела функции,

                  Так это же наоборот хорошо, так как «только начал читать метод/функцию, как сразу уже понятно, что при определенных условиях, можно уже не продолжать ее анализ».

                  • KvanTTT
                    /#21170422

                    Раньше старался писать "правильно", чтобы на функцию был только один return и в конце. Теперь же пишу как удобно и короче, поэтому return зачастую встречается и в начале. Из-за return в начале приходится городить блоки if else, из-за которых появляется дополнительный уровень вложенности.

                    • Tangeman
                      /#21170510 / +3

                      Из чего следует что «правильно» это «один return»?

                      Если функция вида:

                      f() {
                        if (!condition) return;
                        ...
                        if (!condition2) return;
                        ...
                      }
                      

                      то всё вполне «правильно», да и несколько return в середине (если уже ясен результат или выполнено всё что нужно) тоже вполне правильно, и совсем не хуже (если не лучше) чем:
                      f() {
                        if (condition) {
                          ...
                          if (condition2) {
                            ....
                          }
                        }
                      }
                      

                      return в начале (или перед куском кода) как раз часто позволяет избавится от дополнительной вложенности.

                      • KvanTTT
                        /#21172750

                        Из чего следует что «правильно» это «один return»?

                        Из каких-то источников в интернете. Слово и обособлено в кавычки, потому что на самом деле не правильно, да и нельзя возводить какой-то принцип в абсолют.


                        return в начале (или перед куском кода) как раз часто позволяет избавится от дополнительной вложенности.

                        Я об этом и писал тоже.

                        • Eswcvlad
                          /#21174440

                          Из чего следует что «правильно» это «один return»?
                          Из каких-то источников в интернете. Слово и обособлено в кавычки, потому что на самом деле не правильно, да и нельзя возводить какой-то принцип в абсолют.
                          Не уверен, насколько это правда, но вроде как с этим правилом такая же ситуация как и с правилом про goto. Изначально имелось ввиду «делайте return в самой функции, а не делайте goto в середину другой функции, где этот return потом есть».
                          Структурированное программирование, в итоге, победило, и правило преобразовалось в «только один return в функции», так как о прыжках в середину других функций уже мало кто вспомнит…

                          • mayorovp
                            /#21174578

                            Да нет, источник правила — другой, и про него уже писали где-то тут. Если вам надо перед выходом из функции освободить ресурсы — разумно делать это только в одном месте; это просто следствие DRY. Работает правило в языках без автоочистки ресурсов.

                    • Ryppka
                      /#21171170

                      Уже много лет назад в c++.moderated была жаркая флейма по поводу multiple return vs. single return. Я из нее вынес примерно такую позицию: в C, где нет исключений, single return имеет смысл, если не требует акробатических трюков в коде. А в C++, где есть конструкторы/деструкторы и исключения и из блока можно вылететь в любом месте — разницы между множественным и единственным возвратом не много. В C goto, кстати, часто приводит и к single return'у, и без goto его не всегда легко достичь.
                      В конечном случае решает понятность кода и эффективность. Может быть субъективным подходом, но в целом консенсус достижим.
                      Кстати, отличие в сгенерированном коде multiple return от goto обычно чуть менее, чем отсутствует.

                • VolCh
                  /#21171214 / +1

                  У нас принято делать ранний return для проверки валилности аргументов и контекста, для edge кейсов алгоритма типа логарифм 1 = 0, а в остальном рекомендуется использовать один, если это не сильно щагромождает код

                  • chapuza
                    /#21171236 / -2

                    Забавно, что в любой теософической ветке рано или поздно появится пример с кодом (или с описанием кода) на каком-то языке, без упоминания, какой именно это язык.


                    Надо в правилах, что ли, обозначить, на заглавной странице: «Дефолтсити: Москва. Дефолтлангадж: Джавапитонскрипт.»


                    Я это к чему. На языках, на которых в последнее время пишу я — вообще нет такой конструкции «return».

                    • fougasse
                      /#21171420

                      Мы рады за вас, но какое это имеет отношение к обсуждению?

                      • chapuza
                        /#21171486

                        В обсуждении всплыло слово «return», которое ведет к потере общности дискуссии в целом. Это неочевидно?

                    • VolCh
                      /#21171712 / +2

                      Если всплыло упоминание, значит контекст был (или стал) про языки, где есть подобные конструкции. У нас это правило применяется на PHP, TypeScript, Java, Go и паре диалектов SQL, если ничего не забыл

                  • VolCh
                    /#21171288

                    В общем можно сформулировать: должен быть один главный return (не обязательно в конце, часто в теле цикла)

                    • rjhdby
                      /#21171940 / +1

                      Так и представил...


                      func a(){
                          if (!cond_1) auxiliaryReturn null;
                          if (!cond_2) auxiliaryReturn null;
                          doSomeStuff();
                      
                          mainReturn result;
                      }

                      • VolCh
                        /#21175092

                        Ну как-то так, да. :) Если в контексте статьи оставаться, то 2 и 3 строчка будут выбрасыванием эксешена :)

      • w0lf
        /#21168566

        У register_globals другая проблема, почему от него отказались. На мой взгляд основная беда с ним, что его поведение зависит от настроек PHP конкретной хостинг-площадки и разработчик в большинстве случаев не мог на него повлиять. То есть не мог расчитывать, из каких именно и в каком порядке переменные из суперглобальных массивов туда попадут. А то, на что не можешь повлиять, лучше вообще не использовать.
        Использование Goto же, вполне поддаётся контролю.

        • VolCh
          /#21171172 / +1

          Не просто повлиять не мог, а, скажем так, нетипичные настройки могли приводить к серьёзным уязвимостям безопасности. Например давать возможность перезаписать env переменные в query параметрах, от "безобидного" APP_DEBUG=1 до обхода аутентификации и авторизации

    • IgorPie
      /#21170080

      goto — просто эквивалент jmp в ассемблере. Смысл выкидывать слова из песни?

      Бывает и for приводит к тяжелым последствиям, тем более, это слегка пропатченный while. Давайте его тоже запретим?

      Все эти гонения на goto — это как корреляция «убийства и хлеб». Проблема в головах, а не в операторе.

      • chapuza
        /#21170092 / -4

        Вы, может быть, удивитесь, но в мире есть масса языков, в которых нет циклов (ни for, ни while, никаких), и нет return, и вот на них писать — сплошное наслаждение.

        • IgorPie
          /#21170116

          Не такая уж и масса и языки эти не в топе. В некоторых DSP нет for/while, но в их ассемблере их тоже нет. Языки экспертных систем тоже как-то обходились. Но это все специфика, а не мэйнстрим.

          • chapuza
            /#21170142 / -3

            А я думал, ФП уже лет пять, как мейнстрим. Да и платят лучше.

            • IgorPie
              /#21170168

              Лучше, чем за джаву? Сомневаюсь. И шансы обучиться/трудоустроиться сильно меньше.

              • chapuza
                /#21171212 / -2

                Сомневаюсь.

                Обожаю взвешенные пуленепробиваемые аргументы.

                • IgorPie
                  /#21174834

                  Ну, Вы первым начали.
                  Достаточно показать статистику нуждаемости рынка труда в программистах с той или иной специализацией (на хабре были целые статьи про это) и помножить на медианную ЗП (пусть с тех же сайтов, раз более точных данных нет).
                  Все годы больше всего было нужно джавистов и объем в $$ — самый большой. Причем, голод такой, что уважаемые фирмы, которые раньше стеснялись, теперь чуть ли не открыто пишут, что мол купить условный мяч — нажмите эту кнопку, а если вы джава программист — соседнюю. Т.е. на рекрутеров и сайты поиска работы уже не надеются.

                  • chapuza
                    /#21175258

                    больше всего было нужно джавистов и объем в $$ — самый большой

                    И как из всего этого следует, что зарплата джависта выше? Функциональщиков ищут прицельно, в обход всех этих агентств, эйчаров, и больших кнопок справа. Хотя уже сейчас видно, что ситуация потихонечку меняется. Не, если надо устроиться в нейпомикуда миддлом — то джавистам проще, конечно, тут спору нет.

        • nickolaym
          /#21182532

          Жырные намёки в сторону ФП, где… внезапно, есть и for (map), и while, и прямо документировано в спецификациях языков, что концевая рекурсия реализуется через зацикливание (потому что иначе это смерть для стека).


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

          • chapuza
            /#21183102

            есть и for (map), и while [...]

            Если вы считаете, что for — это map, то вам сто?ит расширить горизонты использования for. while есть только в форме reduce_while.


            [...] концевая рекурсия реализуется через зацикливание

            Что как реализуется — это вопрос тридцать пятый, потому что понятие «цикл» — тоже высокоуровневое, так-то все реализуется через jmp.


            Масса этих языков, кстати, довольно маленькая.

            Ну вот Twitter написан на скале. WhatsApp — на эрланге. Довольно много внутреннего финтеха — на доказательных языках, типа агды / кока. Хаскелл пролезает во все щели. Назовите хоть один язык, не вызывающий рвотного рефлекса одним синтаксисом, по сравению с которым эта масса невелика.

            • nickolaym
              /#21186524

              Если вы считаете, что я не знаю, что такое ФП и не умею в него, то давайте на этом месте закончим разговор.


              Реализация концевой рекурсии — не тридцать пятый вопрос.
              Это либо честная рекурсия и смерть стеку, либо гарантии на размер служебных данных (стека или санок).
              А когда у нас есть эти гарантии, то мы наконец можем спокойно выдохнуть и сделать высокоуровневое действие, то есть, например, цикл.
              А в языках, где из коробки забыли поддержать линзы, бананы и колючую проволоку, писать циклы рекурсией — это такой же лютый колхоз, как писать циклы на goto.


              А называть языки я даже не подумаю, потому что мне надо для этого залезть к вам в глотку и выяснять, с чего лично вас тошнит, а с чего нет.
              Меня, например, тошнит со скалы и эрланга. Почему вас с них не тошнит, я не знаю.
              С питона вас тошнит или нет?

              • chapuza
                /#21186542 / -1

                [...] давайте на этом месте закончим разговор.

                А и давайте.

          • VolCh
            /#21185686

            прямо документировано в спецификациях языков, что концевая рекурсия реализуется через зацикливание (потому что иначе это смерть для стека).

            В JavaScript задокументировали, заспецифировали, но вроде до сих пор нигде не реализовали

    • rsync
      /#21170170 / +2

      если заглянуть в ядро Линукс, то там можно увидеть массу примеров кода с goto. Если этот код переписать без goto будет потеряна лаконичность и код станет ужасно нечитаем.


      например такой паттерн:


      void foo() {
           if (!init_resource1())
               goto DEINIT1;
           if (!init_resource2())
               goto DEINIT2;
           if (!init_resource3())
               goto DEINIT3;
      
           // тут код работающий с ресурсами 1, 2, 3
      
           deinit_resource3();
           DEINIT3: deinit_resource2()
           DEINIT2: deinit_resource1();
           DEINIT1:
      }
      

  2. dlinyj
    /#21168098 / +4

    Если уважаемый заглянет в код ядра linux, особенно в быстрые секции, например, драйверов, то ужаснётся обилию goto. Его вовсю используют, просто умело.

    • DMGarikk
      /#21168258

      А в си goto используется внутри процедур? или можно делать goto по метке находящейся в другой процедуре/модуле? (именно это и есть тот самый ужасный паттерн который и повлек за собой такой страх этого оператора)
      p.s. я си не знаю

      • fougasse
        /#21168350 / +1

        Если подумать логически, то простой goto так делать не должен уметь в контексте С-шных функций.
        Что не отменяет возможностей стрелять себе в ногу функциями setjmp() и longjmp().

        • DMGarikk
          /#21168390 / +1

          Что не отменяет возможностей стрелять себе в ногу функциями setjmp() и longjmp()

          ну фактически это и есть тот самый goto которого все боятся и ненавидят

          тем не менее про goto такие статьи появляются, а про longjmp нет.

          • Ryppka
            /#21168504

            setjump/lognjump — это скорее про continuation, а их со времен лиспа принято называть трудными и сложными, а не нежелательными)))

      • oam2oam
        /#21169582

        Я выше привел пример такого, но повторюсь

        void * ptr;
        a() {
         ptr = &&a;
         a:;
        }
        
        b() {
          goto *ptr;
        }
        
        

      • IgorPie
        /#21170106

        В условиях локальных меток (снова, со времен ассемблера) компилятор скажет аяяй. Если метки не локальные и видны из других функций, тут снова очко не в пользу программиста, если он не понимает что делает, это не проблема языка.

    • w0lf
      /#21168278 / +1

      Ну пока есть инструкции процессора условного-безусловного перехода по адресу, высокоуровневые обертки к ним никуда не денутся.

    • ProLimit
      /#21168728 / +1

      Еще в разработке для встраиваемых устройств иногда идет битва за каждую лишнюю инструкцию или место в стеке. И иногда goto остается единственным решением, когда и код красив и понятен, и работает оптимально.

  3. fougasse
    /#21168178

    А вас switch в Java не смущает? Чисто с точки зрения подхода к правильности.

    • rjhdby
      /#21172080 / +1

      А switch то вам чем не угодил?

  4. Tyusha
    /#21168322

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

    • funca
      /#21168396

      В статье goto упомянут 7 раз, и лишь один из них — в коде (да и то в комментарии). Можете объяснить смысл претензии?

      • DMGarikk
        /#21168416

        и лишь один из них — в коде

        ну так следуя описываемой страшилке, разработчики языков боятся называть этот оператор — goto чтобы недайбог не вызвать холивар что 'его запрещено использовать'
        при том его, правда зачастую урезанный и относительно безопасный вариант этого оператора, есть во многих языках

  5. vladbarcelo
    /#21168542

    Лихо вы обозвали исключения goto. Так ведь можно и любую event-based систему заклеймить.

    • ingumsky
      /#21169058 / +1

      Насколько я понимаю, goto названы не исключения, а конкретный вариант использования исключений для того, чтобы делать на нём бизнес-логику.

  6. ProstoUser
    /#21168660

    Когда-то давным давно, когда Windows был еще 16-битным, в составе Windows SDK компания Microsoft поставляла файлик с исходником функции DefWindowProc. Без тех функций, которые она вызывала и может быть даже исходники были какие-то порезанные, но понять, в первом приближении как обрабатываются разные сообщения было можно.
    Так вот. В тексте этой процедуры была строчка:

    goto ICantImagineIveUsedGotoStatement;
    
    и соответствующая метка чуть ниже.

    К чему это я? Да к тому, что программирование достаточно сложная область, чтобы «простые правила» были применимы безусловно, всегда и во всем. Конечно, использование исключений для реализации бизнес-логики — это порочная практика и в данном случае автор стопроцентно прав. Но опыт показывает, что время от времени происходит нечто такое, что делает вполне оправданным нарушение самых «базовых» правил. Типа исключений в бизнес-логике, или даже использования оператора goto.
    Зачастую просто не имеет смысла переписывать здоровенный кусок нормально работающего кода ради того, чтобы обойтись без использования goto в угоду абстрактной «чистоте».
    Согласен с DMGarikk. Языки, в которых оператор goto создавал проблемы, давно уже не используются, а то, что в современных языках этот оператор все-таки присутствует, говорит, скорее, о том, что не так уж все с ним страшно.

  7. defuz
    /#21168674

    Без GOTO невозможно эффективно реализовать VM для байт-кода. GOTO позволяет избавиться от «флаговых» переменных, единственная задача которых – совершить правильное ветвление после цикла. GOTO – более универсальный и выразительный инструмент, чем while/for/break/continue. Так что говорить что GOTO это однозначное зло как минимум странно.

  8. chapuza
    /#21168744 / +3

    воскреснуть в XXI веке под видом исключений

    Software exception handling developed in Lisp in the 1960s and 1970s. This originated in LISP 1.5 (1962).  — https://en.wikipedia.org/wiki/Exception_handling#History

    Мда.

  9. agent10
    /#21168868

    А кто что считает по поводу того корректного применения onError в RxJava?
    Лагерь сейчас разделился: кто-то говорит, что бизнес ошибки стоит засовывать в onError, а кто-то решает использовать врапперы типа Result(T data, Exception e).

  10. PVoLan
    /#21168884

    Автор, имхо, смешал в своем примере две проблемы в одну.

    Первая проблема — использование эксепшена для выхода из цикла, которую автор назвал «большой блок логики в catch FileNotFoundException» — это действительно боль, и решение, приведенное автором, вполне разумно и логично.

    Вторая проблема — наличие параметра boolean throwIfNotExists и тот факт, что метод readPropertiesFromFile() кидает эксепшн — вообще не является проблемой. Из сигнатуры явно следует, что метод может кинуть ексепшн, и пользователю метода предоставляется выбор, кидать эксепшн или не кидать. Проблемой, однако, является тот факт, что метод кидает RuntimeException в ситуации, когда файла нет — эта ситуация кажется относительно штатной, и метод должен кидать что-то более внятное. Причем метод кидает RuntimeException как в первоначальном примере, так и в исправленном.

    Имхо, наилучшим решением было бы переписать метод как
    private Properties readPropertiesFromFile(String filepath) throws IOException

    Дайте клиенту возможность самому решать, что делать, если попытка чтения файла завершилось ошибкой!

  11. oam2oam
    /#21169588

    Проблемы goto нет и никогда не было. Проблема эта надуманная, так как всегда всё сводится к командам процессора, а одна из них — прямой переход по адресу, то есть goto! Ну и понятно, почему выигрывают языки, в которых (как в С) это явно реализовано…

    Но статья то явно не о том…

  12. genuimous
    /#21169590

    Ну так-то стандартные return, break и т.п. тоже есть безусловные переходы и эквивалентны goto по смыслу. В коде их использование повсюду. Более того, даже существуют code style, навязывающие безусловные переходы.

  13. Thoth777
    /#21169702

    В PHP есть break и continue, но не все знают что у них есть параметр, определяющий на какой из уровней выходить
    Такой вот мутировавший GOTO

    • valis
      /#21173670

      И не только в php
      И слава богу что не все знают…

    • VolCh
      /#21175100

      Это не учитвая, что сам goto добавили не так давно.

  14. Tanner
    /#21169864

    Исключения для управления потоком выполнения нехороши только потому, что приводят к неопределённым затратам времени на разворачивание стека. То есть в С++ и Java. В Python, например, никакого разворачивания стека не происходит, поэтому исключения стоят столько же, сколько вызовы ? пренебрежимо мало в большинстве ситуаций. Поэтому управление потоком выполнения через исключения широко используется в Python, даже в системных библиотеках.

    Я, кстати, не в первый раз замечаю, что плюсовики и джависты обобщают свой опыт на все ЯП, включая те, для которых он нерелевантен. Интересно, почему это?

    • funca
      /#21170698

      Обработка исключений в python сопоставима с другими языками — бросить почти ни чего не стоит, а перехват обходится дорого. Есть буквально несколько оптимизаций для частных случаев (вроде StopIteration), которые в общем случае погоды не делают. В Java можно отключить захват стека при создании объекта-исключения, что увеличивает перформанс в разы. Но использовать исключения для control flow все равно не стоит.

      • Tanner
        /#21170804

        бросить почти ни чего не стоит, а перехват обходится дорого.

        Во-первых, перехват эксепшна ? это никаким боком не дорого в Python. Да, я читал документацию. Тем не менее, технически перехват эксепшна сводится к поиску в некоем фиксированном количестве словарей. Как и вызов функции/метода. То есть если вы хотите оптимизировать, например, выход из двойного цикла, который в других языках можно было бы оптимизировать с помощью goto, то выход с помощью эксепшна в Python будет как минимум так же эффективен, как вынос внутреннего цикла в отдельную функцию. Плюс, выход по эксепшну может быть даже более читаем, чем вариант с объявлением дополнительной функции.

        Во-вторых, «никакого разворачивания стека» и «не собирать стек трейс» ? это совершенно несопоставимые вещи, вам не кажется?

        • chapuza
          /#21171224 / -1

          не дорого в Python

          А питон тут как самозародился? Или он, как рак, везде?

          • Tanner
            /#21172106

            Правда ваша, кто про что, а шелудивый (я) про баню…

            С другой стороны, тут также самозародились C, Fortran, ассемблер, Go. А на горизонте маячат загадочные языки без return. Видимо, не я один проморгал тег “Java”.

            • chapuza
              /#21172122 / +1

              загадочные языки без return

              Все функциональные языки — довольно крупное подмножество вообще всех языков. Плюс LISP и бо?льшая половина процедурных.


              Но я, замечу, не приводил примеров на этих языках, я только упомянул об их наличии.

        • funca
          /#21171692

          Просто запустите тест http://gerg.ca/blog/attachments/try-except-speed.py. Если нет исключений, то try ничего не стоит. Если кидать часто, как в типичных сценариях для control flow, то разница с проверкой кода возврата оказывается значительной.

          • Tanner
            /#21172056 / +1

            Этот тест сравнивает эксепшн с if. if, конечно, менее затратен, чем поимка эксепшна, но не способствует читаемости кода. Я сравниваю поимку эксепшна с вызовом функции.

            Например, выход из двойного цикла.

            Вариант 1 ? двойная проверка
            def t1():
                result = False
                for i in range(10):
                    for j in range(20):
                        if i == j == 5:
                            break
                    if i == j == 5:
                        result = True
                        break
                return result

            • funca
              /#21176300

              Идеоматически в примере 2 кидают StopIteration. Это позволяет отличить такой вот псевдо-goto от прочих исключений, которые могут быть брошены бизнес логикой. В самом деле это работает почти без пенальти, как goto, но лишь пока исключение кидается и ловится внутри одной функции — нет нужды разворачивать стек. В других сценариях будет заметная разница.


              Лично по мне, если внутри функции появляются какие-то трюки, лучше подумать и сделать рефакторинг. Первое, что приходит на ум — убрать вложенность с помощью product(range(10), range(20)) из itertools. Вынести в отдельную функцию — вполне разумный вариант 3.

  15. apapacy
    /#21170250 / +3

    Пример с проверкой на существование файла не удачен. Т.к. с момента проверки до момента обращения файл могут удалить.


    Что касается исключений. Первая серьезная книга по прграммированию каоторую я прочитал была книга Барбары Лисков "Использование абстракций и спецификаций при разработке программ" на примере ею же разработанного очень красивого языка CLU который не получил большого распространения. Так вот там "исключения" трактовались как результат выполнения функции который принадлежит к другому множеству. Например рассмотрим функцию ПолучитьИндексЭлементВМассиве(). Тип целочисленный у функции. А что возвращать если элемент не найден? Уж не -1 ли? Или можетбыть сначала проверить есть ли элемент в массиве а потом найти его индекс? Или же вернуть кортеж (1, true) (nil, false)? Так вот оказывается что для этого как раз и удобно использовать исключения поэтому оператор throw можно воспринимать как return another value. То есть если мы нашли индекс — возвращаем индекс, а если не нашли то возвращаем исключение NotFound.


    Как мне кажется нелогичные вещи начинаются не на стороне вызова throw а на стороне блоков try/catch

    • somurzakov
      /#21170702 / +1

      в этом плане Win32 API мне нравится — если функция сработала успешно — то она всегда вернет 0. Если была ошибка (исключение) то возвратится номер ошибки которые сравнивают с константой. Если ошибок несколько то будет битовый OR всех ошибок.

      А любые значения/структуры которые мы хотим получим передаются в функцию по ссылке.
      То, что весь публичный API работал одинаковым образом и был глобальный список возможных ошибок делало программирование очень структурированным и легко читаемым. Неважно какую часть огромного win API используешь, все легко читается и понятно.

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

      • Ryppka
        /#21171200

        в этом плане Win32 API мне нравится — если функция сработала успешно — то она всегда вернет 0. Если была ошибка (исключение) то возвратится номер ошибки которые сравнивают с константой.

        Справедливости ради следует отметить, что это соглашение появилось задолго до Win32 API…

      • VolCh
        /#21175168

        если функция сработала успешно — то она всегда вернет 0

        Обычно под функциями подразумевают что-то возвращающее полезное, а не служебное значение.

        • somurzakov
          /#21175592

          если бы вы программировали под winapi вы бы знали, что функции возвращают полезные данные через структуры/буферы, ссылки на которые вы передаете функции

          • VolCh
            /#21185702

            Я программировал и знаю. И это не только в winapiю Но это не возврат значения, а изменения по ссылкам, которые переданы аргументами.

          • gecube
            /#21187292

            Вообще-то это ад. Потому что зачастую это приводит к конструктам типа void** или другая штука, что очень четко надо понимать какая функция выделяет память — вызывающая или вызываемая. Консистентностью в этом вопросе в win32apin в принципе не пахнет. Как и определением размера необходимого буфера и фактически записанного количества данных. В принципе, с этим жить можно (писали же относительно надёжные win32 программы), но интерфейс больно низкоуровневый получается и он не страхует программиста от ошибок...

        • chapuza
          /#21175618 / +1

          И что такого полезного должна вернуть функция log_to_file, например, если не служебное значение «успех / код ошибки»?

          • VolCh
            /#21185704

            Количество записанных символов, например.

      • funca
        /#21176362

        WinAPI сделано в лучших традициях того времени, в которых несомненно есть рационализм. Но проверку кода возврата легко пропустить и программисту за это ни чего не будет, а с проверками код быстро превращается в месиво из бизнес-логики и control flow. Сигнатуры таких функций не поддаются стандартизации — out параметры могут быть в любом порядке и количестве, а вызовы не compose'ятся между собой. Поскольку out структура не принадлежит вызванной функции, довольно легко накосячить с потокобезопасностью. Собственно, от этого и пытались уйти, изобретая исключения.

    • funca
      /#21170716 / +1

      Проблема возникает из-за разного рода неопределенностей, для моделирования которых по-хорошему есть свои паттерны: значение существует или нет (Optional/MayBe), значение может существовать сейчас или в будущем (Task/Promise), нормальный ход вычислений или альтернативный (Either), есть результат или ошибка (Result) и т.п.

  16. red_andr
    /#21170328 / +3

    Эх GOTO, сколько прелестных часов и даже дней было связано с ним в попытках разобраться что и куда идёт в программе на Фортране. Ведь там были такие замечательные операторы как computed GOTO, assigned GOTO и arithmetic IF:

          READ(5,3)L         
          IF(L.LT.0)GOTO160
          IF(L.GT.4)GOTO180
    10    FORMAT(I)
          LP=L+1
          GOTO(20,30,40,50,60),LP
    20    P=1.0      
          GOTO100  
    30    P=X      
          GOTO100  
    40    P=1.5*X**2-0.5
          GOTO100  
    50    P=2.5*X**3-1.5*X      
          GOTO100  
    60    P=4.375*X**4-3.75*X**2+0.375 
    100   IF(P)120,130,140
    120   Q=-(PI/2.0)      
          GOTO150
    130   Q=0.0      
          GOTO150
    140   Q=PI/2.0
    150   CONTINUE
    160   WRITE(5,170)
    170   FORMAT(5X,'L IS NEGATIVE')
          GOTO200
    180   WRITE(5,190)
    190   FORMAT(5X,'L OUT OF RANGE')
    200   CONTINUE
    И да, почему без пробелов? Потому что в Фортране нет пробелов, точнее они игнорируются. Так что, «GO TO 1», «GOTO1» и «G OT O1» равнозначны. И в былые времена для экономии места они вообще не использовались:
    Consistently separating words by spaces became a general custom about the tenth century A. D., and lasted until about 1957, when FORTRAN abandoned the practice. — Sun FORTRAN Reference Manual.

    • rjhdby
      /#21172160

      И вот как теперь это развидеть?

    • DMGarikk
      /#21172312 / +1

      это же прекрасно! (вспомнил свои первые программки на бейсике)
      (утащил в закладки чтобы показывать как пример почему goto был ужасен тогда, но сейчас уже нет)

  17. bugdesigner
    /#21170610

    Странное название статьи. Я уж думал, что в новых версиях языков, таких как golang, например, собираются оператор goto добавить.

    • qioalice
      /#21170960

      В Go есть goto.

      • chapuza
        /#21171240 / +1

        Ходят слухи, что Go вообще был назван в честь goto.

  18. elegorod
    /#21174542 / +1

    При рефакторинге забыли ситуацию, когда файл настроек существует, но пустой. Изначально функция не выбрасывала исключение в этом случае, а после рефакторинга этот код его выбросит:

    if (loadedProperties.isEmpty()) {
        throw new RuntimeException("Can`t load workspace properties");
    }


    И теперь, чтобы восстановить правильное поведение, надо или возвращать null, если файла нет, и пустые Properties, если файл есть (а возвращать коллекцию null — это костыль). Или добавлять класс типа
    class LoadFileResult {
      Properties properties
      boolean fileExists
    }

    И возвращать этот класс из функции чтения файла. И в результате получается ничуть не лучше, чем изначальный флаг boolean throwIfNotExists.

  19. DeuterideLitium6
    /#21186738

    Сейчас goto даже в ассемблере почти не используется, т.е. jmp.
    Всё в .if .endif, .while .endw заворачиваю.
    Хотя в некоторых случаях без goto хуже код получается. Тот же выход из циклов.