Каждый раз когда я вижу слово сlass в коде применённый не по делу на меня нападают тоска и уныние...
научитесь читать для начала классы, просто вы не умеете это делать, очевидно
приведите хоть 1 пример системы, которую вы лично написали, без ООП, ну скажем на СИ, и не пример чужих кодов с goto, а потом мы посмотрим у кого компетенция выше, и кто тут лишнего или не лишнего применяет ООП, пожалуйста, иначе выглядит то что вы якобы профи, дошли что ООП тут хуже, а тут не хуже, с чего вы решили что вам решать, решать мне — и в моем классе все читаемо для меня и для многих других профи, я все сказал
я даже очень сомневаюсь что ваш Manager (смотрите мой ООП пример) будет меньше строк занимать, зуб даю, что на СИ у вас будет больше чем malloc (или вы считаете что если вы юзаете malloc или любую библиотечную функцию — она магией работает и ее код не надо считать? ну давайте ее посчитаем тоже и строки сравним? и вообще кто по строкам судит, давайте по сущностям и цикломатической сложности считать, и интерфейсы соблюдать — тогда будет все понятно — где сложно а где просто)
я отредактировал комментарий, теперь все что вам хотелось там есть?
(больше редактировать не могу, если вам не понравился синтаксис или гдето есть мелкая ошибка — суть от этого не поменяется существенно)
а в случае лямбд код будет такой
(чтоб не придирались, тут 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()
а у меня почти написана
и если вы ее бы выложили — получилось бы что у вас и там простыни, которые вы скрывали
так что суммарно еще посмотреть надо — где более читабельно и меньше копипаста будет
вариант с лямбдами = 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 и деструкторы
во 1х в примере выше нет 22 объектов, и вы сравниваете несравнимое, вот покажите нам портянку где эти 22 объекта будут, и циклические зависимости (вы далее пишете), и мы покажем как это срефакторить на C++ \ Java \ C# с применением современных техник, без goto, с меньшим кол-во кода
сейчас получается что кода одинаково (деструктор занимает 3 строчки макс, а в случае лямбды — у нас кода еще меньше раза в 2 чем у вас) в нашем и вашем случае с 3мя объектами A, B, C
выделяем три блока памяти (програмируем же ядро)
один за другим.
удаляя их в обратном порядке — гарантируем что энтропия нас не беспокоит. Проблемы с фрагметнацией минимизированы (пример с памятью — синтетический, но в ядре много подобных оптимизаций)
определитесь всетаки — зависимые или нет
1) в данном случае 3 блока памяти никак не зависимы, и удаляя один блок, у вас остаются другие 2, а значит тут порядок выделения блоков и освобождения — не важен
2) в случае важного порядка (уже не блоки памяти) — деструкторы, и\или вообще передача owner права и удаление всего из одного главного объекта (выше я писал)
если Вы напишете всю логику (включая деинит) Вы получите что Ваш код суммарно занимает раза в три больше чем мой.
программа понимается проще\сложней не от кол-ва кода, можно написать мало кода — но не понятного и багоопасного, а можно много — но каждый блок структурирован так, что там не запутаться и отлаживать и читать это проще
Проблемы с фрагметнацией минимизированы (пример с памятью — синтетический, но в ядре много подобных оптимизаций)
специфичные проблемы, согласен они есть, но очень в малом числе случаев
да и решаются другим подходом — выделяйте большие объекты со старта программы, а для мелких нет проблем фрагментации
затем создали еще 22 объекта имеющих связь с A и B
предсказать в каком порядке удалятся A, B и C — может оказаться непросто
если предсказать сложно — надо сделать чтобы система делала все надежно за вас, используя
— более подходящие структуры и понятие owner (shared_ptr, unique_ptr)
— сборку мусора
— другие алгоритмы
если в деструкторе своя логика деструкции (что тоже плюс — логика скрыта, это не усложнение, это инкапсуляция), и такие объекты постоянно создаются и уничтожаются *в разных местах кода*, вам не придется оборачивать это в одинаковые паттерны в разных местах кода
!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 эксклюзивный, и передаются права на уничтожение — что сделано для упрощения и избежания от багов
я подразумевал деструктор, и порядок деструкторов при желании сможет обеспечить порядок деинициализации
но да это не про перфоманс в текущей имплементации деструкторов или исключений (если вдруг будет оно — в try — все равно деинициализация будет)
Да два тут цикла, два. Оператор «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.
а давайте всем объектам давать тип 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) — читабельность будет явно хуже, и багов будет завались
результат выполнения функции который принадлежит к другому множеству
в современном ООП реализуется классом, если конечно множества не совсем произвольные
т.е. если у вас есть бизнес-задача (я не о проблеме чтения файлов и сети, хотя и ее можно реализовать через то же самое, но там чаще 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 */
Зачастую просто не имеет смысла переписывать здоровенный кусок нормально работающего кода ради того, чтобы обойтись без использования goto в угоду абстрактной «чистоте».
скорее сводится к такому «зачастую рефакторить нет смысла, даже если код не идеальный»
(избавление от goto входит в понятие «рефакторить», также как другие костыли)
и да вы правы, но это не говорит о том что рефакторить не надо никогда, и уж темболее что если код рабочий — его не надо трогать
если есть время и код будет многократно перечитываться — его очень желательно рефакторить
ext_loop: while (...) {
while (...) {
…
if (...) break ext_loop;
…
}
}
вы привели какойто очень специфичный слуай, что у вас 2 штуки while и еще break на метку что по сути третий цикл, но замаскированный… эта маскировка она только усложняет понимание что тут по факту 3 while а не 2… и с какой семантикой? без контекста не оправдано, может быть ради оптимизации какого-то алгоритма перебора… ну наверняка есть другой вариант того же алгоритма со стеком или списком и одним циклом (или 2мя) а не 3мя, т.е. кидать в стек или в список и делать break из 2го цикла например
хотя я могу путать… какова семантика «break ext_loop;» это выход на метку или выход из обоих циклов за раз? в последнем случае — решается return при if
они не есть goto, т.е. у return \ break \ if \ while и т.п. более очерчены лимиты и меньше универсальности, я товарищу выше отвечал habr.com/ru/post/484840/#comment_21227246
вы сами признали что это более универсально
а знаете что самое сложное для понимание — то, у чего не ясно, где лимиты
т.е. я сейчас пространно говорю — вы даже не понимаете о чем я
это пример вашей универсальности
чтобы понять — нужен контекст и лимиты
т.е. взять цикл — из него виден контекст — условие входа и выхода (со счетчиком или boolean или while(true) с break внутри) и лимиты сразу понятней и кол-во тестовых вариантов в голове (и на авто-тестах) очевидней — это плюс
лимиты — это плюс
а не универсальность
чтобы не быть голословным, напишу только что ваш мозг постоянно отыскивает контексты, и если вы с кем-то разговариваете — вы без контекста не понимаете человека (или понимаете не так), даже на родном языке, и только после определенного кол-ва «прелюдии», происходит бац — контекст был найден, вы можете чтото переосмыслить, теперь у вас новые ассоциации — о чем говорил собеседник, о чем вы говорили, какоыва вообще проблема (а до этого он может даже не понять «вы о чем?»), можно сузить варианты обсуждения и пойти по более продуктивному пути диалога, а если это переводной язык — так там в зависимости от контекста меняется перевод слова — тоже не просто так
вот тоже самое с while
зачем усложнять себе жизнь и пихать туда goto, и потом отыскивать контекст
программист уже обозначил контекст — вот он while, вот у него круглые скобочки его границ, вон в нем условие выхода, и\или дополнительные — break \ continue (которые усложняют понимание! если они есть)
—
избавиться от «флаговых» переменных, единственная задача которых – совершить правильное ветвление после цикла.
избавляться от них надо разбиением на методы более правильно, и ветвлением после метода, а не ветвлением после цикла
Без GOTO невозможно эффективно реализовать VM для байт-кода.
большинство программистов не реализуют VM для байт-кода, значит для остальных ваш аргумент не действителен… они пишут структурированные программы с if\while\for и методами и классами с ООП (либо ктото в ФП)
отчасти это то что вы имели ввиду, т.е. если использовать исключение внутри одного метода (т.е. бросать и ловить в нем) то это тоже самое что goto, только другой инструкцией
но практически, если расширять проблему, исключения лишают структурности во многих случаях, т.е. вот у вас не ясно где кинется исключение и не ясно где поймается, и отследить все эти варианты в том же C# (unchecked exception) или Java (RuntimeException как вар unchecked) — сложно для понимания, т.е. структура программы рушится, да это удобно иногда, но иногда — это тот же самый goto, типо обойти исполнение множества методов и прыгнуть в конец к освобождению ресурсов и выходу из программы
что этот код не читаемый чтоли? или не лаконичный? уж полаконичней вашего исходного
другое дело что современная реализация виртуальной машины или транслятора — могут сделать не очень оптимально, ну так это вопрос оптимальности этой машины
т.е. по факту ваш аргумент — потому что в Линуксе сделано так, и там оптимально, а он такой авторитетный этот Линукс, что надо по нему все мерять… не все пишут Линукс, и не потому что дураки или недорасли, а потому что у многих задачи другие (не по-проще, а другие!)
Кстати, отличие в сгенерированном коде multiple return от goto обычно чуть менее, чем отсутствует.
ну так язык он для программиста (а не транслятора кода \ компилятора)
goto усложняет понимание т.к. может быть кинут откуда-угодно и куда-угодно, в отличие от for который строго крутит от и до и выходит или сам по условию или строго по break (который внутри него, и четко виден), т.е. вариантов — меньше становится, и все они перечислимые, и часто (не всегда) понятно какой из них в какой ситуации лучше
аналогично с exception — он может быть кинут откуда угодно и пойман где угодно — хоть в этой функции, хоть в другой (или кучей уровней выше), хоть в цикле, хоть вне цикла (если в этой функции), т.е. вариантов много и это замедляет понимание программы, потому что все надо прокрутить в голове и прикинуть а что же здесь выбрано
Изначально функция не выбрасывала исключение в этом случае, а после рефакторинга этот код его выбросит:
И теперь, чтобы восстановить правильное поведение
формально вы правы, но я думаю (я не автор поста, но согласен с вариантов такого рефакторингома от автора, но можно и иначе) что лучше уж так — пусть кидает Exception когда раньше не кидал, потому что это более явный контракт «главный файл существует, а иначе это авария» (а иначе какой смысл кидать exception или вертать null или вертать новый класс LoadFileResult? смысла мало, ведь это реально не-восстановимая ошибка, что вы будете делать при обрабокте? кидать другую ошибку?), и это более правильное поведение а раньше было неправильное :)
научитесь читать для начала классы, просто вы не умеете это делать, очевидно
приведите хоть 1 пример системы, которую вы лично написали, без ООП, ну скажем на СИ, и не пример чужих кодов с goto, а потом мы посмотрим у кого компетенция выше, и кто тут лишнего или не лишнего применяет ООП, пожалуйста, иначе выглядит то что вы якобы профи, дошли что ООП тут хуже, а тут не хуже, с чего вы решили что вам решать, решать мне — и в моем классе все читаемо для меня и для многих других профи, я все сказал
я даже очень сомневаюсь что ваш Manager (смотрите мой ООП пример) будет меньше строк занимать, зуб даю, что на СИ у вас будет больше чем malloc (или вы считаете что если вы юзаете malloc или любую библиотечную функцию — она магией работает и ее код не надо считать? ну давайте ее посчитаем тоже и строки сравним? и вообще кто по строкам судит, давайте по сущностям и цикломатической сложности считать, и интерфейсы соблюдать — тогда будет все понятно — где сложно а где просто)
(больше редактировать не могу, если вам не понравился синтаксис или гдето есть мелкая ошибка — суть от этого не поменяется существенно)
а в случае лямбд код будет такой
(чтоб не придирались, тут 3 раза 1 и тот же класс с параметром — что вызывать для получения ресурса, и что в деструкторе)
и если у вас задача — блоки памяти выделять
можно 1 класс сделать и передавать туда просто размер блока
или один общий менеджер, с методом allocate(size) как malloc только со своим пулом например
т.е. вообще более ООП-шно даже с памятью
да тут кода больше чем в 1м случае — но структура тут еще более понятная и более поддерживаемая и переиспользовать теперь этот менеджер можно в куче мест, а не копипастит эти init_resource1(), deinit_resource1() тыщу раз
пусть даже 38 строк кода, плюс еще что комментом обозначено не заполнено
зато у вас — не написана начинка init_resource1(), deinit_resource1()
а у меня почти написана
и если вы ее бы выложили — получилось бы что у вас и там простыни, которые вы скрывали
так что суммарно еще посмотреть надо — где более читабельно и меньше копипаста будет
ок давайте просуммируем
вариант с лямбдами = 10 строк основной блок (work = 1 строка)
плюс системное:
если это java, то try как try-with-resources кол-ва кода сокращает
можно 1 раз написать Res (вместо 3 классов Res1, Res2, Res3, каждый просто со своим деструктором например, или если нужны private свойства то отнаследовать, или нет — не суть, всеравно кода не будет больше, будут просто скобки а они не код а структура)
7 класс + 10 основной = 17 в случае лямбды (не 3 класса, а 1 достаточен тут)
считаем что без лямбд, каждый класс такой
ну скажем 5 строк на класс лишних, причем куча скобов (они не усложняют)
на 3 класса будет 5*3 = 15
+10 основное
суммарно 25
а если сравнивать не с лямбдой, а с деструктором
и на C++ например
ну те же 5 строк на класс
и те же 25 строк суммарно
vs ваш вариант = 11 строк
причем у вас хитро метки стоят, а у нас лишние { и } скобочки, но пусть так
в крайнем случае в 2 раза больше кода, а не в 3
хотя и это можно написать по-другому
и скобки за усложнения не считаются в случае ООП
и в случае СИ и GOTO можно пробелов понаставить и их тоже считать, и метки вот так сделать
ваш код станет 18 строк, он сложней стал? или проще? 11 vs 18, но тут просто по-другому оформлено
так что даже тезис «меньше кода — воспринимается проще» не верный в общем случае, а в данном он противоположный т.е. «добавленные скобки и пробелы и переводы строк упрощают понимание хоть кода и больше становится»
так что тоже не в 3 раза, а в самом крайнем случае плюс проценты, ну на системные структуры
… а когда кода work в 10-100 раз больше чем 1 строка, то системные try \ деструкторы \ или goto или все такое — не заметны будут в общей массе кода, и кода будет примерно столько же \ на проценты больше в случае ООП, и то не всегда
а теперь если мы развеяли миф что кода в 3 раза больше у нас, чем у вас с goto, получается что и восприятие нашего — проще — потому что более структурировано — есть нормальные try и деструкторы
сейчас получается что кода одинаково (деструктор занимает 3 строчки макс, а в случае лямбды — у нас кода еще меньше раза в 2 чем у вас) в нашем и вашем случае с 3мя объектами A, B, C
определитесь всетаки — зависимые или нет
1) в данном случае 3 блока памяти никак не зависимы, и удаляя один блок, у вас остаются другие 2, а значит тут порядок выделения блоков и освобождения — не важен
2) в случае важного порядка (уже не блоки памяти) — деструкторы, и\или вообще передача owner права и удаление всего из одного главного объекта (выше я писал)
программа понимается проще\сложней не от кол-ва кода, можно написать мало кода — но не понятного и багоопасного, а можно много — но каждый блок структурирован так, что там не запутаться и отлаживать и читать это проще
специфичные проблемы, согласен они есть, но очень в малом числе случаев
да и решаются другим подходом — выделяйте большие объекты со старта программы, а для мелких нет проблем фрагментации
если предсказать сложно — надо сделать чтобы система делала все надежно за вас, используя
— более подходящие структуры и понятие owner (shared_ptr, unique_ptr)
— сборку мусора
— другие алгоритмы
и от такого копипаста вы избавитесь, потому что паттерн вот таких вот goto будет скрыт в высокоуровневой конструкции
try (Res1 res1 = new Res1())
{
}
а порядок деструкторов, *при желании* в подходящем языке есть
более того, если объекты зависимые, то при конструировании 2го объекта — можно передавать ему обязанность по уничтожении и вложенного
т.е.
тут Res2 внутри себя может и подчистить за res1
обычно это тоже плюс, т.к. хозяин один, и никакого порядка тут не надо соблюдать уже т.е. 2й почистит за собой и уничтожит 1го
в cpp на сколько я знаю тоже есть owner эксклюзивный, и передаются права на уничтожение — что сделано для упрощения и избежания от багов
но да это не про перфоманс в текущей имплементации деструкторов или исключений (если вдруг будет оно — в try — все равно деинициализация будет)
«break с меткой» это *почти* тот же самый goto
p.s. некоторая структурность все же есть — т.к. *обычно* выход не в произвольное место, а например для выхода из всех циклов (как в вашем примере)
но понимание от этого всеравно усложняется — в более сложном случае, и написанном как goto — например
Выполнение этой программы генерирует следующий вывод:
Перед оператором break.
Данный оператор размещен после блока second.
© javarush.ru/groups/posts/1389-operatorih-perekhoda
смысл как раз в ограничениях (лимитах) и контексте
когда вы видите int64 и в одном случае в ней кодируется boolean, в другом целочисленный счетчик, а в третьем вещественное число — это затрудняет понимание
а если назначить правильно типы — понимание упрощается
======
еще можно все расчеты писать на ASM и пихать в EAX и EDX, универсально же, как долго надо распутывать логику такой программы? дольше чем если на более высокоуровневом языке
======
еще можно все переменные на СИ называть i и выдавать тип int
int i1
int i2
…
int i99
прям обфускатор получается :)
универсально же, удобно же, нафиг осмысленные имена?
======
аналогично про goto: если в однмо случае goto используется для перехода от 1 функции к другой, в другом случае этот же самый goto но в другом контексте — для выхода из цикла, в 3м случае им эмулируется if, и т.п. — как быстро вы сообразите читая программу, увидев конркетный goto — что он делает? без перечитывания кучи других строк кода?
дольше чем если структурировано
поэтому для одного — одно, для другого — другое
а не смешивать
что там у кого выигрывает? по читабельности выигрывает? не смешите
… а если вы про перфоманс — пруфы можно?
но даже если и так (гдето по перфомансу Си выиграет чем С++ без goto, или вообще на языке go) — читабельность будет явно хуже, и багов будет завались
в современном ООП реализуется классом, если конечно множества не совсем произвольные
т.е. если у вас есть бизнес-задача (я не о проблеме чтения файлов и сети, хотя и ее можно реализовать через то же самое, но там чаще exception бывают и с ними люди свыклись, хотя зря), то у нее должны быть понятные множества вход-выход и понятные результаты, скажем OK и ERROR, причем OK типа int, а ERROR просто как признак любой ошибки
тогда получим
скорее сводится к такому «зачастую рефакторить нет смысла, даже если код не идеальный»
(избавление от goto входит в понятие «рефакторить», также как другие костыли)
и да вы правы, но это не говорит о том что рефакторить не надо никогда, и уж темболее что если код рабочий — его не надо трогать
если есть время и код будет многократно перечитываться — его очень желательно рефакторить
вы привели какойто очень специфичный слуай, что у вас 2 штуки while и еще break на метку что по сути третий цикл, но замаскированный… эта маскировка она только усложняет понимание что тут по факту 3 while а не 2… и с какой семантикой? без контекста не оправдано, может быть ради оптимизации какого-то алгоритма перебора… ну наверняка есть другой вариант того же алгоритма со стеком или списком и одним циклом (или 2мя) а не 3мя, т.е. кидать в стек или в список и делать break из 2го цикла например
хотя я могу путать… какова семантика «break ext_loop;» это выход на метку или выход из обоих циклов за раз? в последнем случае — решается return при if
habr.com/ru/post/484840/#comment_21227246
вы сами признали что это более универсально
а знаете что самое сложное для понимание — то, у чего не ясно, где лимиты
т.е. я сейчас пространно говорю — вы даже не понимаете о чем я
это пример вашей универсальности
чтобы понять — нужен контекст и лимиты
т.е. взять цикл — из него виден контекст — условие входа и выхода (со счетчиком или boolean или while(true) с break внутри) и лимиты сразу понятней и кол-во тестовых вариантов в голове (и на авто-тестах) очевидней — это плюс
лимиты — это плюс
а не универсальность
чтобы не быть голословным, напишу только что ваш мозг постоянно отыскивает контексты, и если вы с кем-то разговариваете — вы без контекста не понимаете человека (или понимаете не так), даже на родном языке, и только после определенного кол-ва «прелюдии», происходит бац — контекст был найден, вы можете чтото переосмыслить, теперь у вас новые ассоциации — о чем говорил собеседник, о чем вы говорили, какоыва вообще проблема (а до этого он может даже не понять «вы о чем?»), можно сузить варианты обсуждения и пойти по более продуктивному пути диалога, а если это переводной язык — так там в зависимости от контекста меняется перевод слова — тоже не просто так
вот тоже самое с while
зачем усложнять себе жизнь и пихать туда goto, и потом отыскивать контекст
программист уже обозначил контекст — вот он while, вот у него круглые скобочки его границ, вон в нем условие выхода, и\или дополнительные — break \ continue (которые усложняют понимание! если они есть)
—
избавляться от них надо разбиением на методы более правильно, и ветвлением после метода, а не ветвлением после цикла
большинство программистов не реализуют VM для байт-кода, значит для остальных ваш аргумент не действителен… они пишут структурированные программы с if\while\for и методами и классами с ООП (либо ктото в ФП)
но практически, если расширять проблему, исключения лишают структурности во многих случаях, т.е. вот у вас не ясно где кинется исключение и не ясно где поймается, и отследить все эти варианты в том же C# (unchecked exception) или Java (RuntimeException как вар unchecked) — сложно для понимания, т.е. структура программы рушится, да это удобно иногда, но иногда — это тот же самый goto, типо обойти исполнение множества методов и прыгнуть в конец к освобождению ресурсов и выходу из программы
что этот код не читаемый чтоли? или не лаконичный? уж полаконичней вашего исходного
другое дело что современная реализация виртуальной машины или транслятора — могут сделать не очень оптимально, ну так это вопрос оптимальности этой машины
т.е. по факту ваш аргумент — потому что в Линуксе сделано так, и там оптимально, а он такой авторитетный этот Линукс, что надо по нему все мерять… не все пишут Линукс, и не потому что дураки или недорасли, а потому что у многих задачи другие (не по-проще, а другие!)
ну так язык он для программиста (а не транслятора кода \ компилятора)
goto усложняет понимание т.к. может быть кинут откуда-угодно и куда-угодно, в отличие от for который строго крутит от и до и выходит или сам по условию или строго по break (который внутри него, и четко виден), т.е. вариантов — меньше становится, и все они перечислимые, и часто (не всегда) понятно какой из них в какой ситуации лучше
аналогично с exception — он может быть кинут откуда угодно и пойман где угодно — хоть в этой функции, хоть в другой (или кучей уровней выше), хоть в цикле, хоть вне цикла (если в этой функции), т.е. вариантов много и это замедляет понимание программы, потому что все надо прокрутить в голове и прикинуть а что же здесь выбрано
формально вы правы, но я думаю (я не автор поста, но согласен с вариантов такого рефакторингома от автора, но можно и иначе) что лучше уж так — пусть кидает Exception когда раньше не кидал, потому что это более явный контракт «главный файл существует, а иначе это авария» (а иначе какой смысл кидать exception или вертать null или вертать новый класс LoadFileResult? смысла мало, ведь это реально не-восстановимая ошибка, что вы будете делать при обрабокте? кидать другую ошибку?), и это более правильное поведение а раньше было неправильное :)