Обновить
-12
0
Алексей Поляков@AlexHell

Разработчик

Отправить сообщение
значительно менее
читабельный

Каждый раз когда я вижу слово с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()
а у меня почти написана
и если вы ее бы выложили — получилось бы что у вас и там простыни, которые вы скрывали
так что суммарно еще посмотреть надо — где более читабельно и меньше копипаста будет
если Вы напишете всю логику (включая деинит) Вы получите что Ваш код суммарно занимает раза в три больше чем мой.

ок давайте просуммируем
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 и деструкторы
с goto и эти 22 объекта — у вас нет примера, и написать его даже если напишете — там будет лапша
во 1х в примере выше нет 22 объектов, и вы сравниваете несравнимое, вот покажите нам портянку где эти 22 объекта будут, и циклические зависимости (вы далее пишете), и мы покажем как это срефакторить на C++ \ Java \ C# с применением современных техник, без goto, с меньшим кол-во кода

сейчас получается что кода одинаково (деструктор занимает 3 строчки макс, а в случае лямбды — у нас кода еще меньше раза в 2 чем у вас) в нашем и вашем случае с 3мя объектами A, B, C
выделяем три блока памяти (програмируем же ядро)
один за другим.
удаляя их в обратном порядке — гарантируем что энтропия нас не беспокоит. Проблемы с фрагметнацией минимизированы (пример с памятью — синтетический, но в ядре много подобных оптимизаций)

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

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

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

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

специфичные проблемы, согласен они есть, но очень в малом числе случаев
да и решаются другим подходом — выделяйте большие объекты со старта программы, а для мелких нет проблем фрагментации
мы инстанцировали A, затем B, затем C

затем создали еще 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.

© javarush.ru/groups/posts/1389-operatorih-perekhoda
а давайте всем объектам давать тип 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
GOTO – более универсальный

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

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

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

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

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

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

большинство программистов не реализуют VM для байт-кода, значит для остальных ваш аргумент не действителен… они пишут структурированные программы с if\while\for и методами и классами с ООП (либо ктото в ФП)
отчасти это то что вы имели ввиду, т.е. если использовать исключение внутри одного метода (т.е. бросать и ловить в нем) то это тоже самое что goto, только другой инструкцией

но практически, если расширять проблему, исключения лишают структурности во многих случаях, т.е. вот у вас не ясно где кинется исключение и не ясно где поймается, и отследить все эти варианты в том же C# (unchecked exception) или Java (RuntimeException как вар unchecked) — сложно для понимания, т.е. структура программы рушится, да это удобно иногда, но иногда — это тот же самый goto, типо обойти исполнение множества методов и прыгнуть в конец к освобождению ресурсов и выходу из программы
вполне структурированный код на 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
            }
        }
    }
}


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

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

т.е. по факту ваш аргумент — потому что в Линуксе сделано так, и там оптимально, а он такой авторитетный этот Линукс, что надо по нему все мерять… не все пишут Линукс, и не потому что дураки или недорасли, а потому что у многих задачи другие (не по-проще, а другие!)
Кстати, отличие в сгенерированном коде multiple return от goto обычно чуть менее, чем отсутствует.

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

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

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

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

формально вы правы, но я думаю (я не автор поста, но согласен с вариантов такого рефакторингома от автора, но можно и иначе) что лучше уж так — пусть кидает Exception когда раньше не кидал, потому что это более явный контракт «главный файл существует, а иначе это авария» (а иначе какой смысл кидать exception или вертать null или вертать новый класс LoadFileResult? смысла мало, ведь это реально не-восстановимая ошибка, что вы будете делать при обрабокте? кидать другую ошибку?), и это более правильное поведение а раньше было неправильное :)

Информация

В рейтинге
Не участвует
Откуда
Россия
Зарегистрирован
Активность