Комментарии 191
Сейчас все понимают, что использовать оператор GOTO это не просто плохая, а ужасная практика.
да никто этого не понимает, никто, языки где был тот самый страшной goto пропали ещё в конце 80 вместе с обязательной нумерации строк и пререхода на процедурное программирование
народ по привычке продолжает пересказывать страшилки про goto и приводить, этот единственный пример с выходом из цикла причём в яве для этого можно использовать переход по метке через break или continue
люди, если вы ниразу не писали на языке где используется goto который приводит к тяжелым последствиям, не стоит пересказывать эту страшилку, вы просто не понимаете о чем говорите, в современных языках сам принцип программирования исключает использование goto независимо от вашего понимания 'ужасных практик'
p.s. устриц ел, писал на atari basic
p.p.s. в современных бейсиках goto уже тоже давно нет в том самом опасном виде
языки где был тот самый страшной goto пропали ещё в конце 80PHP, С, C++, 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.
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.
https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
Потом, что интересно, был basic потом c.
Потом был период когда преподавали basic и это позволило увидеть много интересных вещей происходящих с goto.
И вот что можем сказать — goto это не зло, это инструмент (уже ловим гнилые помидоры). Инструмент чертовски опасный, как острый нож — которым легче порезаться, чем что-то им нарезать. В современном программировании, принято делать языки оберегающие программистов от опасных ошибок, поэтому goto там либо нет либо его использование запрещено под страхом увольнения.
Но с этим есть две проблемы
а) Зачастую с водой выплескивают и ребенка. Правильное использование goto в 1% случаев может сделать код проще, быстрее, компактнее и понятнее. Но goto нет, т.к. в 99% случаев его будут использовать неправильно.
б) Народ не понимая сути проблемы с опасными практиками применения goto начинает эмулировать его другими способами, как верно подмечено в статье — например исключениями. В результате goto нет, а опасные практики есть.
Ближайший аналог из чего-то относительно свежего — это register_globals в php. Они влияли только на безопасность неграмотно написанного кода и их отключили, потому что неграмотно написанного кода было слишком много.
На самом деле депрессивная причина, когда удобный инструмент не используют только потому, что куча народа не умеет им правильно пользоваться.
ext_loop: while (...) {
while (...) {
...
if (...) break ext_loop;
...
}
}
Не всегда просто (и бесплатно) переписать часть кода так чтобы можно было без подобной конструкции обойтись, и тот факт что вместо «goto» используется «break» не меняет сути — и первое и второе перепрыгивают через кусок кода (как, собственно, вообще любые «break» и «continue», которые по сути те же самые «goto», хотя называются иначе).
Как выше уже отметили, не сам факт использования «goto» печален, а неадекватное его использование.
а по сути да, return это прыжок в конец тела функции с каким-то значением
например return в начале тела функции,
Так это же наоборот хорошо, так как «только начал читать метод/функцию, как сразу уже понятно, что при определенных условиях, можно уже не продолжать ее анализ».
Раньше старался писать "правильно", чтобы на функцию был только один return
и в конце. Теперь же пишу как удобно и короче, поэтому return
зачастую встречается и в начале. Из-за return
в начале приходится городить блоки if
else
, из-за которых появляется дополнительный уровень вложенности.
return
»?Если функция вида:
f() {
if (!condition) return;
...
if (!condition2) return;
...
}
то всё вполне «правильно», да и несколько return в середине (если уже ясен результат или выполнено всё что нужно) тоже вполне правильно, и совсем не хуже (если не лучше) чем:
f() {
if (condition) {
...
if (condition2) {
....
}
}
}
return
в начале (или перед куском кода) как раз часто позволяет избавится от дополнительной вложенности.Из чего следует что «правильно» это «один return»?
Из каких-то источников в интернете. Слово и обособлено в кавычки, потому что на самом деле не правильно, да и нельзя возводить какой-то принцип в абсолют.
return в начале (или перед куском кода) как раз часто позволяет избавится от дополнительной вложенности.
Я об этом и писал тоже.
Не уверен, насколько это правда, но вроде как с этим правилом такая же ситуация как и с правилом про goto. Изначально имелось ввиду «делайте return в самой функции, а не делайте goto в середину другой функции, где этот return потом есть».Из чего следует что «правильно» это «один return»?Из каких-то источников в интернете. Слово и обособлено в кавычки, потому что на самом деле не правильно, да и нельзя возводить какой-то принцип в абсолют.
Структурированное программирование, в итоге, победило, и правило преобразовалось в «только один return в функции», так как о прыжках в середину других функций уже мало кто вспомнит…
В конечном случае решает понятность кода и эффективность. Может быть субъективным подходом, но в целом консенсус достижим.
Кстати, отличие в сгенерированном коде multiple return от goto обычно чуть менее, чем отсутствует.
Кстати, отличие в сгенерированном коде multiple return от goto обычно чуть менее, чем отсутствует.
ну так язык он для программиста (а не транслятора кода \ компилятора)
goto усложняет понимание т.к. может быть кинут откуда-угодно и куда-угодно, в отличие от for который строго крутит от и до и выходит или сам по условию или строго по break (который внутри него, и четко виден), т.е. вариантов — меньше становится, и все они перечислимые, и часто (не всегда) понятно какой из них в какой ситуации лучше
аналогично с exception — он может быть кинут откуда угодно и пойман где угодно — хоть в этой функции, хоть в другой (или кучей уровней выше), хоть в цикле, хоть вне цикла (если в этой функции), т.е. вариантов много и это замедляет понимание программы, потому что все надо прокрутить в голове и прикинуть а что же здесь выбрано
У нас принято делать ранний return для проверки валилности аргументов и контекста, для edge кейсов алгоритма типа логарифм 1 = 0, а в остальном рекомендуется использовать один, если это не сильно щагромождает код
Забавно, что в любой теософической ветке рано или поздно появится пример с кодом (или с описанием кода) на каком-то языке, без упоминания, какой именно это язык.
Надо в правилах, что ли, обозначить, на заглавной странице: «Дефолтсити: Москва. Дефолтлангадж: Джавапитонскрипт.»
Я это к чему. На языках, на которых в последнее время пишу я — вообще нет такой конструкции «return
».
Мы рады за вас, но какое это имеет отношение к обсуждению?
Если всплыло упоминание, значит контекст был (или стал) про языки, где есть подобные конструкции. У нас это правило применяется на PHP, TypeScript, Java, Go и паре диалектов SQL, если ничего не забыл
В общем можно сформулировать: должен быть один главный return (не обязательно в конце, часто в теле цикла)
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
Использование Goto же, вполне поддаётся контролю.
Бывает и for приводит к тяжелым последствиям, тем более, это слегка пропатченный while. Давайте его тоже запретим?
Все эти гонения на goto — это как корреляция «убийства и хлеб». Проблема в головах, а не в операторе.
Вы, может быть, удивитесь, но в мире есть масса языков, в которых нет циклов (ни for
, ни while
, никаких), и нет return
, и вот на них писать — сплошное наслаждение.
А я думал, ФП уже лет пять, как мейнстрим. Да и платят лучше.
Сомневаюсь.
Обожаю взвешенные пуленепробиваемые аргументы.
Достаточно показать статистику нуждаемости рынка труда в программистах с той или иной специализацией (на хабре были целые статьи про это) и помножить на медианную ЗП (пусть с тех же сайтов, раз более точных данных нет).
Все годы больше всего было нужно джавистов и объем в $$ — самый большой. Причем, голод такой, что уважаемые фирмы, которые раньше стеснялись, теперь чуть ли не открыто пишут, что мол купить условный мяч — нажмите эту кнопку, а если вы джава программист — соседнюю. Т.е. на рекрутеров и сайты поиска работы уже не надеются.
больше всего было нужно джавистов и объем в $$ — самый большой
И как из всего этого следует, что зарплата джависта выше? Функциональщиков ищут прицельно, в обход всех этих агентств, эйчаров, и больших кнопок справа. Хотя уже сейчас видно, что ситуация потихонечку меняется. Не, если надо устроиться в нейпомикуда миддлом — то джавистам проще, конечно, тут спору нет.
Жырные намёки в сторону ФП, где… внезапно, есть и for (map), и while, и прямо документировано в спецификациях языков, что концевая рекурсия реализуется через зацикливание (потому что иначе это смерть для стека).
Масса этих языков, кстати, довольно маленькая. Даже если это какой-нибудь дремучий лисп, базу кода на котором с 1960-х годов наполняли.
есть иfor
(map
), иwhile
[...]
Если вы считаете, что for
— это map
, то вам сто́ит расширить горизонты использования for
. while
есть только в форме reduce_while
.
[...] концевая рекурсия реализуется через зацикливание
Что как реализуется — это вопрос тридцать пятый, потому что понятие «цикл» — тоже высокоуровневое, так-то все реализуется через jmp
.
Масса этих языков, кстати, довольно маленькая.
Ну вот Twitter написан на скале. WhatsApp — на эрланге. Довольно много внутреннего финтеха — на доказательных языках, типа агды / кока. Хаскелл пролезает во все щели. Назовите хоть один язык, не вызывающий рвотного рефлекса одним синтаксисом, по сравению с которым эта масса невелика.
Если вы считаете, что я не знаю, что такое ФП и не умею в него, то давайте на этом месте закончим разговор.
Реализация концевой рекурсии — не тридцать пятый вопрос.
Это либо честная рекурсия и смерть стеку, либо гарантии на размер служебных данных (стека или санок).
А когда у нас есть эти гарантии, то мы наконец можем спокойно выдохнуть и сделать высокоуровневое действие, то есть, например, цикл.
А в языках, где из коробки забыли поддержать линзы, бананы и колючую проволоку, писать циклы рекурсией — это такой же лютый колхоз, как писать циклы на goto.
А называть языки я даже не подумаю, потому что мне надо для этого залезть к вам в глотку и выяснять, с чего лично вас тошнит, а с чего нет.
Меня, например, тошнит со скалы и эрланга. Почему вас с них не тошнит, я не знаю.
С питона вас тошнит или нет?
[...] давайте на этом месте закончим разговор.
А и давайте.
Не посоветуете источник, где описывается реализация функциональных фишек на чём-нибудь низкоуровневом типа С? Колупать Хаскелевский компилятор ранних версий очень лень.
Просто не совсем понимаю, как тот же map реализовать иначе как через цикл или через SIMD-инструкции.
Ну, например, его можно развернуть или вычислить на этапе компиляции.
Я хоть программист не настоящий, но эмбеддед мне сильно мозг поел: не могу осознать высокоуровневые абстракции, в том числе модную функционалку, не разобрав их реализацию на чём-то низкоуровневом. Можете привести пример мапа для произвольных функторов? Я не очень понимаю, в чём сложность, ну обернём в массив указателей или список.
PS Конечно, можно написать мап на С в чистом препроцессоре, но это будет а) ужасно, б) крайне геморно в отладке, в) не С, а скорее всего Рефал.
прямо документировано в спецификациях языков, что концевая рекурсия реализуется через зацикливание (потому что иначе это смерть для стека).
В 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 не является.
void foo() {
try (Res1 res1 = init_resource1())
{
try (Res2 res2 = init_resource2())
{
try (Res3 res3 = init_resource3())
{
// тут код работающий с ресурсами 1, 2, 3
}
}
}
}
что этот код не читаемый чтоли? или не лаконичный? уж полаконичней вашего исходного
другое дело что современная реализация виртуальной машины или транслятора — могут сделать не очень оптимально, ну так это вопрос оптимальности этой машины
т.е. по факту ваш аргумент — потому что в Линуксе сделано так, и там оптимально, а он такой авторитетный этот Линукс, что надо по нему все мерять… не все пишут Линукс, и не потому что дураки или недорасли, а потому что у многих задачи другие (не по-проще, а другие!)
но да это не про перфоманс в текущей имплементации деструкторов или исключений (если вдруг будет оно — в try — все равно деинициализация будет)
!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 эксклюзивный, и передаются права на уничтожение — что сделано для упрощения и избежания от багов
если Вы напишете всю логику (включая деинит) Вы получите что Ваш код суммарно занимает раза в три больше чем мой.
Только если логика в deinit индивидуальна для каждого случая (но и тут помогут лямбды).
выделяем три блока памяти (програмируем же ядро)
один за другим.
удаляя их в обратном порядке — гарантируем что энтропия нас не беспокоит. Проблемы с фрагметнацией минимизированы (пример с памятью — синтетический, но в ядре много подобных оптимизаций)
определитесь всетаки — зависимые или нет
1) в данном случае 3 блока памяти никак не зависимы, и удаляя один блок, у вас остаются другие 2, а значит тут порядок выделения блоков и освобождения — не важен
2) в случае важного порядка (уже не блоки памяти) — деструкторы, и\или вообще передача owner права и удаление всего из одного главного объекта (выше я писал)
если Вы напишете всю логику (включая деинит) Вы получите что Ваш код суммарно занимает раза в три больше чем мой.
программа понимается проще\сложней не от кол-ва кода, можно написать мало кода — но не понятного и багоопасного, а можно много — но каждый блок структурирован так, что там не запутаться и отлаживать и читать это проще
Проблемы с фрагметнацией минимизированы (пример с памятью — синтетический, но в ядре много подобных оптимизаций)
специфичные проблемы, согласен они есть, но очень в малом числе случаев
да и решаются другим подходом — выделяйте большие объекты со старта программы, а для мелких нет проблем фрагментации
если Вы напишете всю логику (включая деинит) Вы получите что Ваш код суммарно занимает раза в три больше чем мой.
ок давайте просуммируем
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 и деструкторы
(больше редактировать не могу, если вам не понравился синтаксис или гдето есть мелкая ошибка — суть от этого не поменяется существенно)
а в случае лямбд код будет такой
(чтоб не придирались, тут 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()
а у меня почти написана
и если вы ее бы выложили — получилось бы что у вас и там простыни, которые вы скрывали
так что суммарно еще посмотреть надо — где более читабельно и меньше копипаста будет
может дать сбой в порядке вызова деструкторов (в зависимости от кода использующего ресурсы 1,2,3).
Каким образом?
Что-то я так и не увидел от вас контрпримера.
мой — влазит в половину.
да вашего кода там 0 — он НЕ ваш!
и даже не удосужились посмотреть сколько экранов занимает init_res и deinit_res — и не приложили их сюда! там наверняка больше в разы! :)
под malloc/free мой паттерн очень даже заточен.
он же взят из кода ядра, а там каждый модуль делает постоянно эти самые malloc/free
вы НЕ написали malloc/free, но используете чужое готовое, и заявляете что ваш код меньше занимает, при том что НЕ считаете кол-во чужого кода, это ваша логика
а я написал ВСЕ включая malloc manager (упрощенный, да), и вы заявили что кода больше, и он в 1 экран не влезает
эпик
кстати ещё хороший паттерн для goto — обработчик со стейтмашиной. Переходы между стейтами красиво оформляются как раз goto.
Странно, я думал, через разбор сумм-типа это выглядит более красиво.
значительно менее
читабельный
Каждый раз когда я вижу слово сlass в коде применённый не по делу на меня нападают тоска и уныние...
научитесь читать для начала классы, просто вы не умеете это делать, очевидно
приведите хоть 1 пример системы, которую вы лично написали, без ООП, ну скажем на СИ, и не пример чужих кодов с goto, а потом мы посмотрим у кого компетенция выше, и кто тут лишнего или не лишнего применяет ООП, пожалуйста, иначе выглядит то что вы якобы профи, дошли что ООП тут хуже, а тут не хуже, с чего вы решили что вам решать, решать мне — и в моем классе все читаемо для меня и для многих других профи, я все сказал
я даже очень сомневаюсь что ваш Manager (смотрите мой ООП пример) будет меньше строк занимать, зуб даю, что на СИ у вас будет больше чем malloc (или вы считаете что если вы юзаете malloc или любую библиотечную функцию — она магией работает и ее код не надо считать? ну давайте ее посчитаем тоже и строки сравним? и вообще кто по строкам судит, давайте по сущностям и цикломатической сложности считать, и интерфейсы соблюдать — тогда будет все понятно — где сложно а где просто)
Ваш пример — из того же разряда.
ставишь задачу перед программистом: надо 2 умножить на 2 и напечатать результат.
ха-ха выделение ресурсов, сравнимое с malloc, это у нас оказывается 2х2, ну удачи написать
а вы попробуйте написать свой менеджер памяти, для задач оптимального распределения (вы же для этого приплели ядро линукса и все такое низкоуровневое)
не напишете, а если и напишете — кода там будет дофига, ну посчитайте его, и скажите сколько тыщ строк занимает ваша программа
и сравните с моим менеджером (да он не полный, но это хотябы интерфейс, у вас же ничего! чисто чужое)
а далее — ещё надо прогарантировать порядок вызова деструкторов. что само по себе тоже не тривиальная задача
А что там нетривиального? При выходе из функции деструкторы будут вызваны в порядке, противоположном тому, в котором объекты создавались. Ничего специально для этого делать не надо, только код в деструкторе прописать ровно один раз на всё приложение.
Вы, конечно же, сможете назвать ещё хотя бы 1 фактор?
мы инстанцировали A, затем B, затем C
затем создали еще 22 объекта имеющих связь с A и B
предсказать в каком порядке удалятся A, B и C — может оказаться непросто
если предсказать сложно — надо сделать чтобы система делала все надежно за вас, используя
— более подходящие структуры и понятие owner (shared_ptr, unique_ptr)
— сборку мусора
— другие алгоритмы
сейчас получается что кода одинаково (деструктор занимает 3 строчки макс, а в случае лямбды — у нас кода еще меньше раза в 2 чем у вас) в нашем и вашем случае с 3мя объектами A, B, C
А каким образом эти 22 объекта повлияют на порядок уничтожения A, B и С?
Какая нафиг зависимость, о чем вы?
Во-первых, вы так и не рассказали каким образом зависимости объектов могут задержать деструктор.
Во-вторых, откуда в объектах A, B и С возьмется ссылка на 22 более поздних объекта, если те на момент создания A, B и С не существовали?
В данном контексте "деструктор" следует читать как метод close, который вызывается конструкцией try-with-resource:
try (Res1 res1 = new Res1()) {
// ...
} // сюда компилятор добавит вызов res1.close()
И никакие 22 объекта никак этот вызов отменить не сумеют, если только не прибьют поток либо процесс средствами ОС или не устроят другое UB.
А финализатор, вызываемый сборщиком мусора — это просто подстраховка.
Можно переписать на языке с RAII (да хоть том же C++) и будет ещё проще.
p.s. я си не знаю
Что не отменяет возможностей стрелять себе в ногу функциями setjmp() и longjmp().
Что не отменяет возможностей стрелять себе в ногу функциями setjmp() и longjmp()
ну фактически это и есть тот самый goto которого все боятся и ненавидят
тем не менее про goto такие статьи появляются, а про longjmp нет.
В статье goto упомянут 7 раз, и лишь один из них — в коде (да и то в комментарии). Можете объяснить смысл претензии?
и лишь один из них — в коде
ну так следуя описываемой страшилке, разработчики языков боятся называть этот оператор — goto чтобы недайбог не вызвать холивар что 'его запрещено использовать'
при том его, правда зачастую урезанный и относительно безопасный вариант этого оператора, есть во многих языках
Лихо вы обозвали исключения goto. Так ведь можно и любую event-based систему заклеймить.
goto
названы не исключения, а конкретный вариант использования исключений для того, чтобы делать на нём бизнес-логику.но практически, если расширять проблему, исключения лишают структурности во многих случаях, т.е. вот у вас не ясно где кинется исключение и не ясно где поймается, и отследить все эти варианты в том же C# (unchecked exception) или Java (RuntimeException как вар unchecked) — сложно для понимания, т.е. структура программы рушится, да это удобно иногда, но иногда — это тот же самый goto, типо обойти исполнение множества методов и прыгнуть в конец к освобождению ресурсов и выходу из программы
Так вот. В тексте этой процедуры была строчка:
goto ICantImagineIveUsedGotoStatement;
и соответствующая метка чуть ниже.К чему это я? Да к тому, что программирование достаточно сложная область, чтобы «простые правила» были применимы безусловно, всегда и во всем. Конечно, использование исключений для реализации бизнес-логики — это порочная практика и в данном случае автор стопроцентно прав. Но опыт показывает, что время от времени происходит нечто такое, что делает вполне оправданным нарушение самых «базовых» правил. Типа исключений в бизнес-логике, или даже использования оператора goto.
Зачастую просто не имеет смысла переписывать здоровенный кусок нормально работающего кода ради того, чтобы обойтись без использования goto в угоду абстрактной «чистоте».
Согласен с DMGarikk. Языки, в которых оператор goto создавал проблемы, давно уже не используются, а то, что в современных языках этот оператор все-таки присутствует, говорит, скорее, о том, что не так уж все с ним страшно.
Зачастую просто не имеет смысла переписывать здоровенный кусок нормально работающего кода ради того, чтобы обойтись без использования goto в угоду абстрактной «чистоте».
скорее сводится к такому «зачастую рефакторить нет смысла, даже если код не идеальный»
(избавление от goto входит в понятие «рефакторить», также как другие костыли)
и да вы правы, но это не говорит о том что рефакторить не надо никогда, и уж темболее что если код рабочий — его не надо трогать
если есть время и код будет многократно перечитываться — его очень желательно рефакторить
GOTO – более универсальный
вы сами признали что это более универсально
а знаете что самое сложное для понимание — то, у чего не ясно, где лимиты
т.е. я сейчас пространно говорю — вы даже не понимаете о чем я
это пример вашей универсальности
чтобы понять — нужен контекст и лимиты
т.е. взять цикл — из него виден контекст — условие входа и выхода (со счетчиком или boolean или while(true) с break внутри) и лимиты сразу понятней и кол-во тестовых вариантов в голове (и на авто-тестах) очевидней — это плюс
лимиты — это плюс
а не универсальность
чтобы не быть голословным, напишу только что ваш мозг постоянно отыскивает контексты, и если вы с кем-то разговариваете — вы без контекста не понимаете человека (или понимаете не так), даже на родном языке, и только после определенного кол-ва «прелюдии», происходит бац — контекст был найден, вы можете чтото переосмыслить, теперь у вас новые ассоциации — о чем говорил собеседник, о чем вы говорили, какоыва вообще проблема (а до этого он может даже не понять «вы о чем?»), можно сузить варианты обсуждения и пойти по более продуктивному пути диалога, а если это переводной язык — так там в зависимости от контекста меняется перевод слова — тоже не просто так
вот тоже самое с while
зачем усложнять себе жизнь и пихать туда goto, и потом отыскивать контекст
программист уже обозначил контекст — вот он while, вот у него круглые скобочки его границ, вон в нем условие выхода, и\или дополнительные — break \ continue (которые усложняют понимание! если они есть)
—
избавиться от «флаговых» переменных, единственная задача которых – совершить правильное ветвление после цикла.
избавляться от них надо разбиением на методы более правильно, и ветвлением после метода, а не ветвлением после цикла
Без GOTO невозможно эффективно реализовать VM для байт-кода.
большинство программистов не реализуют VM для байт-кода, значит для остальных ваш аргумент не действителен… они пишут структурированные программы с if\while\for и методами и классами с ООП (либо ктото в ФП)
воскреснуть в XXI веке под видом исключений
Software exception handling developed inLisp
in the 1960s and 1970s. This originated inLISP 1.5
(1962). — https://en.wikipedia.org/wiki/Exception_handling#History
Мда.
Лагерь сейчас разделился: кто-то говорит, что бизнес ошибки стоит засовывать в onError, а кто-то решает использовать врапперы типа Result(T data, Exception e).
Первая проблема — использование эксепшена для выхода из цикла, которую автор назвал «большой блок логики в catch FileNotFoundException» — это действительно боль, и решение, приведенное автором, вполне разумно и логично.
Вторая проблема — наличие параметра
boolean throwIfNotExists
и тот факт, что метод readPropertiesFromFile()
кидает эксепшн — вообще не является проблемой. Из сигнатуры явно следует, что метод может кинуть ексепшн, и пользователю метода предоставляется выбор, кидать эксепшн или не кидать. Проблемой, однако, является тот факт, что метод кидает RuntimeException
в ситуации, когда файла нет — эта ситуация кажется относительно штатной, и метод должен кидать что-то более внятное. Причем метод кидает RuntimeException
как в первоначальном примере, так и в исправленном. Имхо, наилучшим решением было бы переписать метод как
private Properties readPropertiesFromFile(String filepath) throws IOException
Дайте клиенту возможность самому решать, что делать, если попытка чтения файла завершилось ошибкой!
смысл как раз в ограничениях (лимитах) и контексте
когда вы видите int64 и в одном случае в ней кодируется boolean, в другом целочисленный счетчик, а в третьем вещественное число — это затрудняет понимание
а если назначить правильно типы — понимание упрощается
======
еще можно все расчеты писать на ASM и пихать в EAX и EDX, универсально же, как долго надо распутывать логику такой программы? дольше чем если на более высокоуровневом языке
======
еще можно все переменные на СИ называть i и выдавать тип int
int i1
int i2
…
int i99
прям обфускатор получается :)
универсально же, удобно же, нафиг осмысленные имена?
======
аналогично про goto: если в однмо случае goto используется для перехода от 1 функции к другой, в другом случае этот же самый goto но в другом контексте — для выхода из цикла, в 3м случае им эмулируется if, и т.п. — как быстро вы сообразите читая программу, увидев конркетный goto — что он делает? без перечитывания кучи других строк кода?
дольше чем если структурировано
поэтому для одного — одно, для другого — другое
а не смешивать
Ну и понятно, почему выигрывают языки
что там у кого выигрывает? по читабельности выигрывает? не смешите
… а если вы про перфоманс — пруфы можно?
но даже если и так (гдето по перфомансу Си выиграет чем С++ без goto, или вообще на языке go) — читабельность будет явно хуже, и багов будет завались
habr.com/ru/post/484840/#comment_21227246
Такой вот мутировавший GOTO
Я, кстати, не в первый раз замечаю, что плюсовики и джависты обобщают свой опыт на все ЯП, включая те, для которых он нерелевантен. Интересно, почему это?
Обработка исключений в python сопоставима с другими языками — бросить почти ни чего не стоит, а перехват обходится дорого. Есть буквально несколько оптимизаций для частных случаев (вроде StopIteration), которые в общем случае погоды не делают. В Java можно отключить захват стека при создании объекта-исключения, что увеличивает перформанс в разы. Но использовать исключения для control flow все равно не стоит.
бросить почти ни чего не стоит, а перехват обходится дорого.
Во-первых, перехват эксепшна − это никаким боком не дорого в Python. Да, я читал документацию. Тем не менее, технически перехват эксепшна сводится к поиску в некоем фиксированном количестве словарей. Как и вызов функции/метода. То есть если вы хотите оптимизировать, например, выход из двойного цикла, который в других языках можно было бы оптимизировать с помощью
goto
, то выход с помощью эксепшна в Python будет как минимум так же эффективен, как вынос внутреннего цикла в отдельную функцию. Плюс, выход по эксепшну может быть даже более читаем, чем вариант с объявлением дополнительной функции.Во-вторых, «никакого разворачивания стека» и «не собирать стек трейс» − это совершенно несопоставимые вещи, вам не кажется?
не дорого в Python
А питон тут как самозародился? Или он, как рак, везде?
С другой стороны, тут также самозародились C, Fortran, ассемблер, Go. А на горизонте маячат загадочные языки без
return
. Видимо, не я один проморгал тег “Java”.Просто запустите тест http://gerg.ca/blog/attachments/try-except-speed.py. Если нет исключений, то try ничего не стоит. Если кидать часто, как в типичных сценариях для control flow, то разница с проверкой кода возврата оказывается значительной.
if
. if
, конечно, менее затратен, чем поимка эксепшна, но не способствует читаемости кода. Я сравниваю поимку эксепшна с вызовом функции.Например, выход из двойного цикла.
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
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
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
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.
(не имхо, по факту минус лишний 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
А любые значения/структуры которые мы хотим получим передаются в функцию по ссылке.
То, что весь публичный API работал одинаковым образом и был глобальный список возможных ошибок делало программирование очень структурированным и легко читаемым. Неважно какую часть огромного win API используешь, все легко читается и понятно.
а сейчас с зоопарком технологий и фреймворков — кто во что горазд, каждый упражняется в своем подходе писать более правильный код
в этом плане Win32 API мне нравится — если функция сработала успешно — то она всегда вернет 0. Если была ошибка (исключение) то возвратится номер ошибки которые сравнивают с константой.
Справедливости ради следует отметить, что это соглашение появилось задолго до Win32 API…
если функция сработала успешно — то она всегда вернет 0
Обычно под функциями подразумевают что-то возвращающее полезное, а не служебное значение.
Я программировал и знаю. И это не только в 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")
, которые вовсю практикуются в современных языках и фреймворках. А исключения плохи тем что у них нелокальное действие на систему.
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.
Странное название статьи. Я уж думал, что в новых версиях языков, таких как golang, например, собираются оператор 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? смысла мало, ведь это реально не-восстановимая ошибка, что вы будете делать при обрабокте? кидать другую ошибку?), и это более правильное поведение а раньше было неправильное :)
Всё в .if .endif, .while .endw заворачиваю.
Хотя в некоторых случаях без goto хуже код получается. Тот же выход из циклов.
Возвращение GOTO