Pull to refresh

Comments 191

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

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

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

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

p.s. устриц ел, писал на atari basic
p.p.s. в современных бейсиках goto уже тоже давно нет в том самом опасном виде
Интересен факт того, что в PHP оператор goto, как раз появился только в версии 5.3. До сих пор не понимаю для чего.
языки где был тот самый страшной goto пропали ещё в конце 80
PHP, С, C++, C# вроде никуда не пропали.
в них можно делать goto по метке за пределы процедуры?
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.

не совсем "куда угодно" правда

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.
UFO just landed and posted this here
UFO just landed and posted this here
Конечно это язык. Только это GNU C, а не чистый C.
UFO just landed and posted this here
Мы начинали с языка где goto вполне себе используется — assembler.
Потом, что интересно, был basic потом c.
Потом был период когда преподавали basic и это позволило увидеть много интересных вещей происходящих с goto.

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

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

Ближайший аналог из чего-то относительно свежего — это register_globals в php. Они влияли только на безопасность неграмотно написанного кода и их отключили, потому что неграмотно написанного кода было слишком много.
На самом деле депрессивная причина, когда удобный инструмент не используют только потому, что куча народа не умеет им правильно пользоваться.
Интересно стало посмотреть на необходимость goto в Java.
ext_loop: while (...) {
  while (...) {
    ...
    if (...) break ext_loop;
    ...
  }
}

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

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

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

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

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

Если функция вида:
f() {
  if (!condition) return;
  ...
  if (!condition2) return;
  ...
}

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

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

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

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


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

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

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

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

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

ну так язык он для программиста (а не транслятора кода \ компилятора)

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

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

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

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


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


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

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

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

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

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

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


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

    mainReturn result;
}

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

ext_loop: while (...) {
while (...) {

if (...) break ext_loop;

}
}

вы привели какойто очень специфичный слуай, что у вас 2 штуки while и еще break на метку что по сути третий цикл, но замаскированный… эта маскировка она только усложняет понимание что тут по факту 3 while а не 2… и с какой семантикой? без контекста не оправдано, может быть ради оптимизации какого-то алгоритма перебора… ну наверняка есть другой вариант того же алгоритма со стеком или списком и одним циклом (или 2мя) а не 3мя, т.е. кидать в стек или в список и делать break из 2го цикла например

хотя я могу путать… какова семантика «break ext_loop;» это выход на метку или выход из обоих циклов за раз? в последнем случае — решается return при if

Да два тут цикла, два. Оператор "break с меткой" — это всё ещё break, а не goto.


А return не поможет потому что после цикла может ещё код быть.

Да два тут цикла, два. Оператор «break с меткой» — это всё ещё break, а не goto.

«break с меткой» это *почти* тот же самый goto

p.s. некоторая структурность все же есть — т.к. *обычно* выход не в произвольное место, а например для выхода из всех циклов (как в вашем примере)

но понимание от этого всеравно усложняется — в более сложном случае, и написанном как goto — например

public static void main(String[] args) {
    boolean t = true;
    first:
    {
      second:
      {
        third:
        {
          System.out.println("Перед оператором break.");
          if (t) {
            break second; // выход из блока second
          }
          System.out.println("Данный оператор никогда не выполнится");
        }
        System.out.println("Данный оператор никогда не выполнится ");
      }
      System.out.println("Данный оператор размещен после блока second.");
    }
  }


Выполнение этой программы генерирует следующий вывод:

Перед оператором break.
Данный оператор размещен после блока second.

© javarush.ru/groups/posts/1389-operatorih-perekhoda
«break с меткой» это почти тот же самый goto

Только если вообще любой break считать разновидностью goto.


В данном случае их ключевое различие — в том, что break переходит только вперед и наружу, но никогда не переходит назад и не заходит внутрь блоков.

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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


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

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


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

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

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


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


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

UFO just landed and posted this here

Не посоветуете источник, где описывается реализация функциональных фишек на чём-нибудь низкоуровневом типа С? Колупать Хаскелевский компилятор ранних версий очень лень.

UFO just landed and posted this here

Просто не совсем понимаю, как тот же map реализовать иначе как через цикл или через SIMD-инструкции.

Ну, например, его можно развернуть или вычислить на этапе компиляции.

UFO just landed and posted this here

Я хоть программист не настоящий, но эмбеддед мне сильно мозг поел: не могу осознать высокоуровневые абстракции, в том числе модную функционалку, не разобрав их реализацию на чём-то низкоуровневом. Можете привести пример мапа для произвольных функторов? Я не очень понимаю, в чём сложность, ну обернём в массив указателей или список.
PS Конечно, можно написать мап на С в чистом препроцессоре, но это будет а) ужасно, б) крайне геморно в отладке, в) не С, а скорее всего Рефал.

UFO just landed and posted this here

Спасибо, теперь понятно. Кстати, а constan propagation в compile time, если opt ~= true, отработается?

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

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

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

TCO — часть спецификации языка, а не реализаций. Была впервые добавлена в спецификацию EcmaScript4, позднее выпилена оттуда и добавлена в EcmaScript6.


На сегодняшний день не поддерживается примерно никем.

Можете привести ссылку на спецификацию? Я не вижу в ней ничего похожего на TCO.

Goals for ECMAScript 2015 include providing better support for large applications, library creation, and for use of ECMAScript as a compilation target for other languages. Some of its major enhancements include modules, class declarations, lexical block scoping, iterators and generators, promises for asynchronous programming, destructuring patterns, and proper tail calls.

— https://www.ecma-international.org/ecma-262/6.0/


Если что, PTC (Proper Tail Call) — альтернативное название TCO, используемое только екмокомитетом. Предполагаю потому, что JS весь весьма альтернативный.

… и это единственное место, где tail call в документе упоминается. Нормативным раздел Introduction не является.

Some of its major enhancements include [...].

Нормативным раздел Introduction не является.

А, так можно было, да?


Что не отменяет, кстати, постулата «TCO — часть спецификации языка».

A tail position call must either release any transient internal resources associated with the currently executing function execution context before invoking the target function or reuse those resources in support of the target function.

И прочие окрестности 14.6

О, спасибо. Почему-то я проглядел этот пункт...

UFO just landed and posted this here
вполне структурированный код на java или C# юзает try-with-resources (java) или using (c#)
void foo() {
    try (Res1 res1 = init_resource1())
    {
        try (Res2 res2 = init_resource2())
        {
            try (Res3 res3 = init_resource3())
            {
                // тут код работающий с ресурсами 1, 2, 3
            }
        }
    }
}


что этот код не читаемый чтоли? или не лаконичный? уж полаконичней вашего исходного

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

т.е. по факту ваш аргумент — потому что в Линуксе сделано так, и там оптимально, а он такой авторитетный этот Линукс, что надо по нему все мерять… не все пишут Линукс, и не потому что дураки или недорасли, а потому что у многих задачи другие (не по-проще, а другие!)
UFO just landed and posted this here
я подразумевал деструктор, и порядок деструкторов при желании сможет обеспечить порядок деинициализации
но да это не про перфоманс в текущей имплементации деструкторов или исключений (если вдруг будет оно — в try — все равно деинициализация будет)
UFO just landed and posted this here
если в деструкторе своя логика деструкции (что тоже плюс — логика скрыта, это не усложнение, это инкапсуляция), и такие объекты постоянно создаются и уничтожаются *в разных местах кода*, вам не придется оборачивать это в одинаковые паттерны в разных местах кода
!init_resource1())
         goto DEINIT1;
// work
     DEINIT3: deinit_resource2()

и от такого копипаста вы избавитесь, потому что паттерн вот таких вот goto будет скрыт в высокоуровневой конструкции
try (Res1 res1 = new Res1())
{
}

а порядок деструкторов, *при желании* в подходящем языке есть

более того, если объекты зависимые, то при конструировании 2го объекта — можно передавать ему обязанность по уничтожении и вложенного
т.е.
try (Res1 res1 = new Res1())
{
  try (Res2 res2 = new Res2(res1))
  {
    // work res2
  }
}

тут Res2 внутри себя может и подчистить за res1
обычно это тоже плюс, т.к. хозяин один, и никакого порядка тут не надо соблюдать уже т.е. 2й почистит за собой и уничтожит 1го

в cpp на сколько я знаю тоже есть owner эксклюзивный, и передаются права на уничтожение — что сделано для упрощения и избежания от багов
UFO just landed and posted this here
если Вы напишете всю логику (включая деинит) Вы получите что Ваш код суммарно занимает раза в три больше чем мой.

Только если логика в deinit индивидуальна для каждого случая (но и тут помогут лямбды).

UFO just landed and posted this here

Да не вопрос:


try (Res1 res1 = Res1.Create(deinit_resource1))
{
  try (Res2 res2 = Res2.Create(deinit_resource2))
  {
    // work 
  }
}
выделяем три блока памяти (програмируем же ядро)
один за другим.
удаляя их в обратном порядке — гарантируем что энтропия нас не беспокоит. Проблемы с фрагметнацией минимизированы (пример с памятью — синтетический, но в ядре много подобных оптимизаций)

определитесь всетаки — зависимые или нет
1) в данном случае 3 блока памяти никак не зависимы, и удаляя один блок, у вас остаются другие 2, а значит тут порядок выделения блоков и освобождения — не важен
2) в случае важного порядка (уже не блоки памяти) — деструкторы, и\или вообще передача owner права и удаление всего из одного главного объекта (выше я писал)

если Вы напишете всю логику (включая деинит) Вы получите что Ваш код суммарно занимает раза в три больше чем мой.

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

Проблемы с фрагметнацией минимизированы (пример с памятью — синтетический, но в ядре много подобных оптимизаций)

специфичные проблемы, согласен они есть, но очень в малом числе случаев
да и решаются другим подходом — выделяйте большие объекты со старта программы, а для мелких нет проблем фрагментации
UFO just landed and posted this here
UFO just landed and posted this here
если Вы напишете всю логику (включая деинит) Вы получите что Ваш код суммарно занимает раза в три больше чем мой.

ок давайте просуммируем
try (Res1 res1 = Res1.Create(deinit_resource1))
{
  try (Res2 res2 = Res2.Create(deinit_resource2))
  {
      try (Res3 res3 = Res3.Create(deinit_resource3))
      {
    // work 
      }
  }
}

вариант с лямбдами = 10 строк основной блок (work = 1 строка)

плюс системное:
если это java, то try как try-with-resources кол-ва кода сокращает
можно 1 раз написать Res (вместо 3 классов Res1, Res2, Res3, каждый просто со своим деструктором например, или если нужны private свойства то отнаследовать, или нет — не суть, всеравно кода не будет больше, будут просто скобки а они не код а структура)
class Res implements AutoCloseable {
private Runnable destructor;
public Res(Runnable destructor) {this.destructor = destructor;}
    public void close() throws Exception {
        destructor.run();
    }
}

7 класс + 10 основной = 17 в случае лямбды (не 3 класса, а 1 достаточен тут)

считаем что без лямбд, каждый класс такой
class Res1 implements AutoCloseable {
    public void close() throws Exception {
        // deinit_res1() или inline-код
    }
}


ну скажем 5 строк на класс лишних, причем куча скобов (они не усложняют)
на 3 класса будет 5*3 = 15
+10 основное
суммарно 25

а если сравнивать не с лямбдой, а с деструктором
и на C++ например
class Res1{
   public:
      Res1() {}
      ~Res1() { /** deinit_res1() или inline-код */}
};

ну те же 5 строк на класс
и те же 25 строк суммарно

vs ваш вариант = 11 строк
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 раза больше кода, а не в 3

хотя и это можно написать по-другому
и скобки за усложнения не считаются в случае ООП

и в случае СИ и GOTO можно пробелов понаставить и их тоже считать, и метки вот так сделать

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:


ваш код станет 18 строк, он сложней стал? или проще? 11 vs 18, но тут просто по-другому оформлено

так что даже тезис «меньше кода — воспринимается проще» не верный в общем случае, а в данном он противоположный т.е. «добавленные скобки и пробелы и переводы строк упрощают понимание хоть кода и больше становится»

так что тоже не в 3 раза, а в самом крайнем случае плюс проценты, ну на системные структуры
… а когда кода work в 10-100 раз больше чем 1 строка, то системные try \ деструкторы \ или goto или все такое — не заметны будут в общей массе кода, и кода будет примерно столько же \ на проценты больше в случае ООП, и то не всегда

а теперь если мы развеяли миф что кода в 3 раза больше у нас, чем у вас с goto, получается что и восприятие нашего — проще — потому что более структурировано — есть нормальные try и деструкторы
UFO just landed and posted this here
я отредактировал комментарий, теперь все что вам хотелось там есть?
(больше редактировать не могу, если вам не понравился синтаксис или гдето есть мелкая ошибка — суть от этого не поменяется существенно)

а в случае лямбд код будет такой
(чтоб не придирались, тут 3 раза 1 и тот же класс с параметром — что вызывать для получения ресурса, и что в деструкторе)
try (Res res1 = new Res(init_res1, deinit_resource1))
{
  try (Res res2 = new Res(init_res2, deinit_resource2))
  {
      try (Res res3 = new Res(init_res3, deinit_resource3))
      {
    // work 
      }
  }
}


и если у вас задача — блоки памяти выделять
можно 1 класс сделать и передавать туда просто размер блока
или один общий менеджер, с методом allocate(size) как malloc только со своим пулом например

т.е. вообще более ООП-шно даже с памятью
class Res implements AutoCloseable {
private int startOfBlock;
private int sizeOfBlock;
private Manager memoryManager;
public Res(int startOfBlock, int sizeOfBlock, Manager memoryManager) {
this.startOfBlock = startOfBlock;
this.sizeOfBlock = sizeOfBlock;
this.memoryManager = memoryManager;
}
    public void close() throws Exception {
        memoryManager.free(startOfBlock, sizeOfBlock);
    }
}

class Manager
{
private List<byte[]> blocks;
// выделить память
Res allocate(int size) {
// пересчитать start
return new Res(start, size, this);
};

void free() { /* вернуть память */ }
}

try (Res res1 = Manager.allocate(100))
{
  try (Res res2 = Manager.allocate(200))
  {
      try (Res res3 = Manager.allocate(300))
      {
    // work 
      }
  }
}

да тут кода больше чем в 1м случае — но структура тут еще более понятная и более поддерживаемая и переиспользовать теперь этот менеджер можно в куче мест, а не копипастит эти init_resource1(), deinit_resource1() тыщу раз

пусть даже 38 строк кода, плюс еще что комментом обозначено не заполнено

зато у вас — не написана начинка init_resource1(), deinit_resource1()
а у меня почти написана
и если вы ее бы выложили — получилось бы что у вас и там простыни, которые вы скрывали
так что суммарно еще посмотреть надо — где более читабельно и меньше копипаста будет
UFO just landed and posted this here
может дать сбой в порядке вызова деструкторов (в зависимости от кода использующего ресурсы 1,2,3).

Каким образом?

UFO just landed and posted this here

Что-то я так и не увидел от вас контрпримера.

UFO just landed and posted this here
понятно, malloc+free вы тоже не видели? он тоже не влязит в экран, и исходники более-менее серьезной программы вы в жизни не писали, судя по заявлениям о кол-ве строк и экранов, досвидания :)

мой — влазит в половину.

да вашего кода там 0 — он НЕ ваш!
и даже не удосужились посмотреть сколько экранов занимает init_res и deinit_res — и не приложили их сюда! там наверняка больше в разы! :)
UFO just landed and posted this here
под malloc/free мой паттерн очень даже заточен.
он же взят из кода ядра, а там каждый модуль делает постоянно эти самые malloc/free

вы НЕ написали malloc/free, но используете чужое готовое, и заявляете что ваш код меньше занимает, при том что НЕ считаете кол-во чужого кода, это ваша логика
а я написал ВСЕ включая malloc manager (упрощенный, да), и вы заявили что кода больше, и он в 1 экран не влезает
эпик
UFO just landed and posted this here
кстати ещё хороший паттерн для goto — обработчик со стейтмашиной. Переходы между стейтами красиво оформляются как раз goto.

Странно, я думал, через разбор сумм-типа это выглядит более красиво.

Это разные красоты.


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


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

UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
значительно менее
читабельный

Каждый раз когда я вижу слово сlass в коде применённый не по делу на меня нападают тоска и уныние...

научитесь читать для начала классы, просто вы не умеете это делать, очевидно

приведите хоть 1 пример системы, которую вы лично написали, без ООП, ну скажем на СИ, и не пример чужих кодов с goto, а потом мы посмотрим у кого компетенция выше, и кто тут лишнего или не лишнего применяет ООП, пожалуйста, иначе выглядит то что вы якобы профи, дошли что ООП тут хуже, а тут не хуже, с чего вы решили что вам решать, решать мне — и в моем классе все читаемо для меня и для многих других профи, я все сказал

я даже очень сомневаюсь что ваш Manager (смотрите мой ООП пример) будет меньше строк занимать, зуб даю, что на СИ у вас будет больше чем malloc (или вы считаете что если вы юзаете malloc или любую библиотечную функцию — она магией работает и ее код не надо считать? ну давайте ее посчитаем тоже и строки сравним? и вообще кто по строкам судит, давайте по сущностям и цикломатической сложности считать, и интерфейсы соблюдать — тогда будет все понятно — где сложно а где просто)
UFO just landed and posted this here
Ваш пример — из того же разряда.

ставишь задачу перед программистом: надо 2 умножить на 2 и напечатать результат.

ха-ха выделение ресурсов, сравнимое с malloc, это у нас оказывается 2х2, ну удачи написать
UFO just landed and posted this here
так заявить может только тот, кто использует чужой код этого malloc и выделения ресурсов

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

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

А что там нетривиального? При выходе из функции деструкторы будут вызваны в порядке, противоположном тому, в котором объекты создавались. Ничего специально для этого делать не надо, только код в деструкторе прописать ровно один раз на всё приложение.

UFO just landed and posted this here

Вы, конечно же, сможете назвать ещё хотя бы 1 фактор?

UFO just landed and posted this here
мы инстанцировали A, затем B, затем C

затем создали еще 22 объекта имеющих связь с A и B
предсказать в каком порядке удалятся A, B и C — может оказаться непросто

если предсказать сложно — надо сделать чтобы система делала все надежно за вас, используя
— более подходящие структуры и понятие owner (shared_ptr, unique_ptr)
— сборку мусора
— другие алгоритмы
UFO just landed and posted this here
во 1х в примере выше нет 22 объектов, и вы сравниваете несравнимое, вот покажите нам портянку где эти 22 объекта будут, и циклические зависимости (вы далее пишете), и мы покажем как это срефакторить на C++ \ Java \ C# с применением современных техник, без goto, с меньшим кол-во кода

сейчас получается что кода одинаково (деструктор занимает 3 строчки макс, а в случае лямбды — у нас кода еще меньше раза в 2 чем у вас) в нашем и вашем случае с 3мя объектами A, B, C
UFO just landed and posted this here

А каким образом эти 22 объекта повлияют на порядок уничтожения A, B и С?

UFO just landed and posted this here
с goto и эти 22 объекта — у вас нет примера, и написать его даже если напишете — там будет лапша
UFO just landed and posted this here

Какая нафиг зависимость, о чем вы?


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


Во-вторых, откуда в объектах A, B и С возьмется ссылка на 22 более поздних объекта, если те на момент создания A, B и С не существовали?

UFO just landed and posted this here

В данном контексте "деструктор" следует читать как метод close, который вызывается конструкцией try-with-resource:


try (Res1 res1 = new Res1()) {
  // ...
} // сюда компилятор добавит вызов res1.close()

И никакие 22 объекта никак этот вызов отменить не сумеют, если только не прибьют поток либо процесс средствами ОС или не устроят другое UB.


А финализатор, вызываемый сборщиком мусора — это просто подстраховка.

Можно переписать на языке с RAII (да хоть том же C++) и будет ещё проще.

UFO just landed and posted this here
Если уважаемый заглянет в код ядра linux, особенно в быстрые секции, например, драйверов, то ужаснётся обилию goto. Его вовсю используют, просто умело.
А в си goto используется внутри процедур? или можно делать goto по метке находящейся в другой процедуре/модуле? (именно это и есть тот самый ужасный паттерн который и повлек за собой такой страх этого оператора)
p.s. я си не знаю
Если подумать логически, то простой goto так делать не должен уметь в контексте С-шных функций.
Что не отменяет возможностей стрелять себе в ногу функциями setjmp() и longjmp().
Что не отменяет возможностей стрелять себе в ногу функциями setjmp() и longjmp()

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

тем не менее про goto такие статьи появляются, а про longjmp нет.
setjump/lognjump — это скорее про continuation, а их со времен лиспа принято называть трудными и сложными, а не нежелательными)))
UFO just landed and posted this here
В условиях локальных меток (снова, со времен ассемблера) компилятор скажет аяяй. Если метки не локальные и видны из других функций, тут снова очко не в пользу программиста, если он не понимает что делает, это не проблема языка.

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

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

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

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

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

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

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

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

но практически, если расширять проблему, исключения лишают структурности во многих случаях, т.е. вот у вас не ясно где кинется исключение и не ясно где поймается, и отследить все эти варианты в том же C# (unchecked exception) или Java (RuntimeException как вар unchecked) — сложно для понимания, т.е. структура программы рушится, да это удобно иногда, но иногда — это тот же самый goto, типо обойти исполнение множества методов и прыгнуть в конец к освобождению ресурсов и выходу из программы
Когда-то давным давно, когда Windows был еще 16-битным, в составе Windows SDK компания Microsoft поставляла файлик с исходником функции DefWindowProc. Без тех функций, которые она вызывала и может быть даже исходники были какие-то порезанные, но понять, в первом приближении как обрабатываются разные сообщения было можно.
Так вот. В тексте этой процедуры была строчка:
goto ICantImagineIveUsedGotoStatement;
и соответствующая метка чуть ниже.

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

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

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

вы сами признали что это более универсально
а знаете что самое сложное для понимание — то, у чего не ясно, где лимиты
т.е. я сейчас пространно говорю — вы даже не понимаете о чем я
это пример вашей универсальности
чтобы понять — нужен контекст и лимиты
т.е. взять цикл — из него виден контекст — условие входа и выхода (со счетчиком или boolean или while(true) с break внутри) и лимиты сразу понятней и кол-во тестовых вариантов в голове (и на авто-тестах) очевидней — это плюс
лимиты — это плюс
а не универсальность

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

вот тоже самое с while
зачем усложнять себе жизнь и пихать туда goto, и потом отыскивать контекст
программист уже обозначил контекст — вот он while, вот у него круглые скобочки его границ, вон в нем условие выхода, и\или дополнительные — break \ continue (которые усложняют понимание! если они есть)

избавиться от «флаговых» переменных, единственная задача которых – совершить правильное ветвление после цикла.

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

Без GOTO невозможно эффективно реализовать VM для байт-кода.

большинство программистов не реализуют VM для байт-кода, значит для остальных ваш аргумент не действителен… они пишут структурированные программы с if\while\for и методами и классами с ООП (либо ктото в ФП)
А кто что считает по поводу того корректного применения onError в RxJava?
Лагерь сейчас разделился: кто-то говорит, что бизнес ошибки стоит засовывать в onError, а кто-то решает использовать врапперы типа Result(T data, Exception e).
Автор, имхо, смешал в своем примере две проблемы в одну.

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

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

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

Дайте клиенту возможность самому решать, что делать, если попытка чтения файла завершилось ошибкой!
UFO just landed and posted this here
а давайте всем объектам давать тип 64-битное целое (sint64, со знаком), у нас же все сводится к современным 64-битным процессорам, и все int и boolean и float будет сами в них кодировать, и универсально же, и как удобно

смысл как раз в ограничениях (лимитах) и контексте
когда вы видите int64 и в одном случае в ней кодируется boolean, в другом целочисленный счетчик, а в третьем вещественное число — это затрудняет понимание

а если назначить правильно типы — понимание упрощается

======

еще можно все расчеты писать на ASM и пихать в EAX и EDX, универсально же, как долго надо распутывать логику такой программы? дольше чем если на более высокоуровневом языке

======

еще можно все переменные на СИ называть i и выдавать тип int
int i1
int i2

int i99

прям обфускатор получается :)

универсально же, удобно же, нафиг осмысленные имена?

======

аналогично про goto: если в однмо случае goto используется для перехода от 1 функции к другой, в другом случае этот же самый goto но в другом контексте — для выхода из цикла, в 3м случае им эмулируется if, и т.п. — как быстро вы сообразите читая программу, увидев конркетный goto — что он делает? без перечитывания кучи других строк кода?

дольше чем если структурировано

поэтому для одного — одно, для другого — другое
а не смешивать

Ну и понятно, почему выигрывают языки

что там у кого выигрывает? по читабельности выигрывает? не смешите

… а если вы про перфоманс — пруфы можно?

но даже если и так (гдето по перфомансу Си выиграет чем С++ без goto, или вообще на языке go) — читабельность будет явно хуже, и багов будет завались
Ну так-то стандартные return, break и т.п. тоже есть безусловные переходы и эквивалентны goto по смыслу. В коде их использование повсюду. Более того, даже существуют code style, навязывающие безусловные переходы.
они не есть goto, т.е. у return \ break \ if \ while и т.п. более очерчены лимиты и меньше универсальности, я товарищу выше отвечал
habr.com/ru/post/484840/#comment_21227246
В PHP есть break и continue, но не все знают что у них есть параметр, определяющий на какой из уровней выходить
Такой вот мутировавший GOTO
И не только в php
И слава богу что не все знают…

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

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

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

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

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

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

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

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

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

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

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


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

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

Этот тест сравнивает эксепшн с 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



Вариант 2 − исключение
def t2():
    result = False
    try:
        for i in range(10):
            for j in range(20):
                if i == j == 5:
                    raise Exception
    except:
        result = True
    return result



Вариант 3 − вынос циклов в отдельную функцию
def t3_inner():
    for i in range(10):
        for j in range(20):
            if i == j == 5:
                return True
    return false


def t3():
    result = False
    result = t3_inner()
    return result



Вариант 4 − for… else
def t4():
    result = False
    for i in range(10):
        for j in range(20):
            if i == j == 5:
                break
        else:
            continue
        result = True
        break
    return result


Все варианты выполняются за примерно одинаковое время, ±5%, если верить timeit. Вариант с эксепшном мне кажется наиболее читаемым. Последний вариант − самый быстрый, но и самый нечитаемый. В общем, затраты на обработку эксепшна вполне приемлемы.

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


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

«Вариант 1 − двойная проверка» по сути повторение (копипаст), от чего вы избавились в случае «Вариант 3 − вынос циклов в отдельную функцию» и от этого оно и читается проще чем 1й, и даже проще и лучше чем с exception
(не имхо, по факту минус лишний if — или минс лишний except — это меньше кода читать и понимать быстрей)
raise Exception
    except:
        result = True

заменяется на «return false» или «return variable» (что было default задано в начале)

т.е. вот идеальный вариант (и остальные варианты у вас точно также будут обернуты в функцию — с нормальным говорящим именем, т.е. не надо думать что тут функция это лишнее — нет не лишнее)
def t3_inner():
    for i in range(10):
        for j in range(20):
            if i == j == 5:
                return True
    return false


можно было бы подумать что тут GOTO, но это не совсем так, тут более структурированно, всмысле нет проивзольных rise exception (throw) и catch и нет множества условий, а вполне очевидные 2 ветки исполнения — с 2мя тестовыми вариантами
В Python, например, никакого разворачивания стека не происходит, поэтому исключения стоят столько же, сколько вызовы − пренебрежимо мало в большинстве ситуаций.

"Пренебрежимо мало" в контексте Python надо читать, как "также дорого, как и всё остальное в Python".

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

результат выполнения функции который принадлежит к другому множеству

в современном ООП реализуется классом, если конечно множества не совсем произвольные
т.е. если у вас есть бизнес-задача (я не о проблеме чтения файлов и сети, хотя и ее можно реализовать через то же самое, но там чаще exception бывают и с ними люди свыклись, хотя зря), то у нее должны быть понятные множества вход-выход и понятные результаты, скажем OK и ERROR, причем OK типа int, а ERROR просто как признак любой ошибки

тогда получим
FuncNameResult FuncName(input) { /* implement */}

class FuncNameResult
{
  public final boolean isOk; // или enum если вариантов ошибки много
  public final int result; // нормальное значение (только если OK)
}

FuncNameResult funcNameResult = FuncName(input);
if (funcNameResult .isOk) /* process OK */
else /* process error */

return another value это скорее return Left("Error"), которые вовсю практикуются в современных языках и фреймворках. А исключения плохи тем что у них нелокальное действие на систему.

Эх 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.

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

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

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

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

При рефакторинге забыли ситуацию, когда файл настроек существует, но пустой. Изначально функция не выбрасывала исключение в этом случае, а после рефакторинга этот код его выбросит:
if (loadedProperties.isEmpty()) {
    throw new RuntimeException("Can`t load workspace properties");
}


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

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

И теперь, чтобы восстановить правильное поведение

формально вы правы, но я думаю (я не автор поста, но согласен с вариантов такого рефакторингома от автора, но можно и иначе) что лучше уж так — пусть кидает Exception когда раньше не кидал, потому что это более явный контракт «главный файл существует, а иначе это авария» (а иначе какой смысл кидать exception или вертать null или вертать новый класс LoadFileResult? смысла мало, ведь это реально не-восстановимая ошибка, что вы будете делать при обрабокте? кидать другую ошибку?), и это более правильное поведение а раньше было неправильное :)
Сейчас goto даже в ассемблере почти не используется, т.е. jmp.
Всё в .if .endif, .while .endw заворачиваю.
Хотя в некоторых случаях без goto хуже код получается. Тот же выход из циклов.
Sign up to leave a comment.

Articles