Является ли «наконец» часть конструкции «попробуй ... поймай ... наконец» даже необходимой?

25

Некоторые языки (такие как C ++ и ранние версии PHP) не поддерживают finallyчасть try ... catch ... finallyконструкции. Когда- finallyлибо необходимо? Поскольку код в нем всегда выполняется, почему бы не разместить этот код после try ... catchблока без finallyпредложения? Зачем использовать один? (Я ищу причину / мотивацию для использования / неиспользования finally, а не причину покончить с «уловом» или почему это допустимо.)

Agi Hammerthief
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
maple_shaft

Ответы:

36

В дополнение к тому, что сказали другие, также возможно исключение, которое будет добавлено в условие catch. Учти это:

try { 
    throw new SomeException();
} catch {
    DoSomethingWhichUnexpectedlyThrows();
}
Cleanup();

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

Erik
источник
4
Спасибо за краткий и прямой ответ, который не касается теории и «язык X лучше, чем Y».
Agi Hammerthief
56

Как уже упоминали другие, нет гарантии, что код после tryоператора будет выполняться, если вы не поймаете все возможные исключения. Тем не менее, это:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   handleError();
} finally {
   cleanUp();
}

можно переписать 1 как:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   try {
       handleError();
   } catch (Throwable e2) {
       cleanUp();
       throw e2;
   }
} catch (Throwable e) {
   cleanUp();
   throw e;
}
cleanUp();

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

C ++ не имеет, finallyпотому что Бьярн Страуструп считает, что RAII лучше или, по крайней мере, достаточно для большинства случаев:

Почему C ++ не предоставляет конструкцию "finally"?

Потому что C ++ поддерживает альтернативу, которая почти всегда лучше: техника «получение ресурсов - инициализация» (TC ++ PL3 раздел 14.4). Основная идея заключается в представлении ресурса локальным объектом, чтобы деструктор локального объекта освободил ресурс. Таким образом, программист не может забыть освободить ресурс.


1 Конкретный код для перехвата всех исключений и переброса без потери информации трассировки стека зависит от языка. Я использовал Java, где трассировка стека фиксируется при создании исключения. В C # вы бы просто использовали throw;.

Doval
источник
8
Вы также должны поймать исключения во handleError()втором случае, нет?
Юрий Робл
1
Вы также можете выдать ошибку. Я бы перефразировал это catch (Throwable t) {}с помощью блока try .. catch вокруг всего начального блока (также для отлова handleError
бросков
1
Я бы на самом деле добавил дополнительный трик-трик, который вы пропустили при вызове, handleErro();что сделает его еще лучшим аргументом относительно того, почему наконец полезны блоки (даже если это не был первоначальный вопрос).
Алекс
1
Этот ответ на самом деле не отвечает на вопрос, почему C ++ не имеет finally, что намного более нюансировано.
DeadMG
1
@AgiHammerthief Вложенное tryнаходится внутри catchдля конкретного исключения. Во-вторых, возможно, вы не знаете, сможете ли вы успешно обработать ошибку, пока не изучите исключение, или что причина исключения также не позволяет вам обработать ошибку (по крайней мере, на этом уровне). Это довольно часто при вводе / выводе. Перебрасывание происходит потому, что единственный способ гарантировать выполнение cleanUp- перехватывать все , но исходный код позволит исключениям, происходящим в catch (SpecificException e)блоке, распространяться вверх.
Довал
22

finally блоки обычно используются для очистки ресурсов, которые могут помочь с удобочитаемостью при использовании нескольких операторов возврата:

int DoSomething() {
    try {
        open_connection();
        return get_result();
    }
    catch {
        return 2;
    }
    finally {
        close_connection();
    }
}

против

int DoSomething() {
    int result;
    try {
        open_connection();
        result = get_result();
    }
    catch {
        result = 2;
    }
    close_connection();
    return result;
}
AlexFoxGill
источник
2
Я думаю, что это лучший ответ. Использование finally в качестве замены для общего исключения кажется просто дерьмовым. Правильный вариант использования - для очистки ресурсов или аналогичных операций.
Кик
3
Возможно, еще более распространенным является возврат внутри блока try, а не внутри блока catch.
Майкл Андерсон
На мой взгляд, код недостаточно объясняет использование finally. (Я бы использовал код, как во втором блоке, так как несколько операторов возврата не приветствуются там, где я работаю.)
Agi Hammerthief
15

Как вы, очевидно, уже догадались, да, C ++ предоставляет те же возможности без этого механизма. Как таковой, строго говоря, механизм try/ finallyне является действительно необходимым.

Тем не менее, обход без него накладывает некоторые требования на то, как устроен остальной язык. В C ++ тот же набор действий воплощен в деструкторе класса. Это работает в основном (исключительно?), Потому что вызов деструктора в C ++ является детерминированным. Это, в свою очередь, приводит к некоторым довольно сложным правилам жизни объектов, некоторые из которых явно не интуитивны.

Большинство других языков предоставляют некоторую форму сборки мусора. Хотя в сборке мусора есть противоречивые вещи (например, его эффективность по сравнению с другими методами управления памятью), в общем-то, это не так: точное время, когда объект будет «очищен» сборщиком мусора, не связано напрямую к объему объекта. Это предотвращает его использование, когда очистка должна быть детерминированной, или когда она просто необходима для правильной работы, или когда имеешь дело с ресурсами, настолько ценными, что их очистка не будет задерживаться произвольно. try/ finallyпозволяет таким языкам справляться с ситуациями, требующими детерминированной очистки.

Я думаю, что те, кто заявляет, что синтаксис C ++ для этой возможности «менее дружественен», чем Java, скорее упускают суть. Хуже того, они упускают гораздо более важный момент в разделении ответственности, который выходит далеко за рамки синтаксиса и имеет гораздо больше общего с тем, как разрабатывается код.

В C ++ эта детерминированная очистка происходит в деструкторе объекта. Это означает, что объект может быть (и обычно должен быть) предназначен для очистки после себя. Это относится к сути объектно-ориентированного проектирования - класс должен быть спроектирован так, чтобы обеспечивать абстракцию и реализовывать свои собственные инварианты. В C ++ это делается именно так - и один из инвариантов, для которых он предусмотрен, заключается в том, что при уничтожении объекта ресурсы, контролируемые этим объектом (все они, а не только память), будут уничтожены правильно.

Java (и аналогичные) несколько отличаются. Хотя они поддерживают (своего рода) функцию, finalizeкоторая теоретически может обеспечить аналогичные возможности, поддержка настолько слаба, что в принципе ее невозможно использовать (и фактически она практически никогда не используется).

В результате, а не класс сам в состоянии сделать необходимую очистку, то клиент класса должен предпринять шаги , чтобы сделать это. Если мы проведем достаточно близорукое сравнение, на первый взгляд может показаться, что это различие довольно незначительное, и в этом отношении Java вполне конкурентоспособна с C ++. В итоге получается что-то подобное. В C ++ класс выглядит примерно так:

class Foo {
    // ...
public:
    void do_whatever() { if (xyz) throw something; }
    ~Foo() { /* handle cleanup */ }
};

... и код клиента выглядит примерно так:

void f() { 
    Foo f;
    f.do_whatever();
    // possibly more code that might throw here
}

В Java мы обмениваемся немного больше кода, где объект используется для немного меньше в классе. Это изначально выглядит довольно даже компромисс. На самом деле это далеко не так, потому что в большинстве типичных программ мы определяем класс только в одном месте, но используем его во многих местах. Подход C ++ означает, что мы пишем этот код только для обработки очистки в одном месте. Подход Java означает, что мы должны написать этот код для многократной очистки, во многих местах - в каждом месте, где мы используем объект этого класса.

Короче говоря, подход Java в основном гарантирует, что многие абстракции, которые мы пытаемся предоставить, являются «утечками» - любой и каждый класс, который требует детерминированной очистки, обязывает клиента класса знать о деталях того, что нужно очистить и как выполнить очистку , а не те детали, которые скрыты в самом классе.

Хотя я назвал это «подходом к Java» выше, try/ finallyи подобные механизмы под другими именами не полностью ограничены Java. Для одного яркого примера большинство (все?) Языков .NET (например, C #) предоставляют то же самое.

Недавние итерации как Java, так и C # также обеспечивают нечто среднее между «классической» Java и C ++ в этом отношении. В C # объект, который хочет автоматизировать свою очистку, может реализовать IDisposableинтерфейс, который предоставляет Disposeметод, который (по крайней мере, неопределенно) похож на деструктор C ++. Хотя это может быть использовано через try/ finallyкак в Java, C # немного автоматизирует задачу с помощью usingоператора, который позволяет вам определять ресурсы, которые будут создаваться при входе в область и уничтожаться при выходе из области. Хотя уровень автоматизации и определенности, предоставляемый C ++, все еще значительно ниже, это все же является существенным улучшением по сравнению с Java. В частности, дизайнер класса может централизовать детали того, какраспоряжаться классом при его реализации IDisposable. Все, что остается клиентскому программисту, - это меньшее бремя написания usingоператора, чтобы гарантировать, что IDisposableинтерфейс будет использоваться тогда, когда он должен быть. В Java 7 и новее имена были изменены, чтобы защитить виновных, но основная идея в основном идентична.

Джерри Гроб
источник
1
Идеальный ответ. Деструкторы являются функция незаменима в C ++.
Томас Эдинг
13

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

Это совершенно разумно:

try 
{
   AcquireManyResources(); 
   DoSomethingThatMightFail(); 
}
finally 
{
   CleanUpThoseResources(); 
}

Никакое предложение catch нигде не является зрением, потому что этот метод не может сделать ничего полезного с этими исключениями; их оставляют для передачи обратно вверх по стеку вызовов в обработчик, который может . Поймать и перебросить исключения в каждом методе - плохая идея, особенно если вы просто перебрасываете одно и то же исключение. Это полностью идет вразрез с тем, как должна работать структурированная обработка исключений (и довольно близка к возвращению «кода ошибки» из каждого метода, просто в «форме» исключения).

Что этот метод действительно нужно сделать, хотя, убирать за собой, так что «внешний мир» никогда не должен ничего знать о беспорядке , что он попал в себя. Предложение finally делает именно это - независимо от того, как ведут себя вызываемые методы, предложение finally будет выполнено «по пути» метода (и то же самое верно для каждого предложения finally между точкой, в которой выбрасывается исключение, и возможное предложение catch, которое его обрабатывает); каждый запускается как стек вызовов "раскручивается".

Фил В.
источник
9

Что произойдет, если и будет выброшено исключение, которое вы не ожидали. Попытка завершится в середине, и предложение catch не будет выполнено.

Последний блок должен помочь с этим и гарантировать, что независимо от исключения произойдет очистка.

чокнутый урод
источник
4
Это не достаточная причина для finally, так как вы можете предотвратить "неожиданные" исключения catch(Object)или catch(...)перехватить все.
MSalters
1
Что звучит как работа вокруг. Концептуально, наконец, чище. Хотя я должен признаться, что редко использую это.
fast_now
7

Некоторые языки предлагают как конструкторы, так и деструкторы для своих объектов (например, C ++, я считаю). С этими языками вы можете делать большинство (возможно, все) того, что обычно делается в finallyдеструкторе. Таким образом, в этих языках finallyпункт может быть излишним.

В языке без деструкторов (например, Java) трудно (возможно, даже невозможно) добиться правильной очистки без finallyпредложения. NB. В Java есть finaliseметод, но нет гарантии, что он когда-либо будет вызван.

OldCurmudgeon
источник
Может быть полезно отметить, что деструкторы помогают в очистке ресурсов, когда разрушение является детерминированным . Если мы не знаем, когда объект будет разрушен и / или собран мусором, то деструкторы недостаточно безопасны.
Morwenn
@ Морвенн - Хороший вопрос. Я намекнул на это со своей ссылкой на Java, finaliseно я бы предпочел не вдаваться в политические аргументы вокруг деструкторов / финалов в настоящее время.
OldCurmudgeon
В C ++ разрушение детерминировано. Когда выходит область действия, содержащая автоматический объект (например, он извлекается из стека), вызывается его деструктор. (C ++ позволяет размещать объекты в стеке, а не только в куче.)
Роб К
@RobK - И это точная функциональность, finaliseно с расширяемым вкусом и механизмом, похожим на упс - очень выразительный и сравнимый с finaliseмеханизмом других языков.
OldCurmudgeon
1

Попробуй наконец и попробуй catch - это две разные вещи, которые имеют только ключевое слово: "try". Лично я хотел бы видеть это по-другому. Причина, по которой вы видите их вместе, в том, что исключения создают «скачок».

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

Питер Б
источник
3
В .NET они реализованы с использованием отдельных механизмов; в Java, однако, единственная конструкция, распознаваемая JVM, семантически эквивалентна «при переходе к ошибке», паттерну, который напрямую поддерживает, try catchно не поддерживает try finally; код, использующий последнее, преобразуется в код, использующий только первое, путем копирования содержимого finallyблока во всех местах кода, где его может потребоваться выполнить.
суперкат
@supercat хорошо, спасибо за дополнительную информацию о Java.
Питер Б
1

Поскольку этот вопрос не определяет язык C ++ как язык, я рассмотрю сочетание C ++ и Java, поскольку они используют другой подход к уничтожению объектов, который предлагается в качестве одной из альтернатив.

Причины, по которым вы можете использовать блок finally, а не код после блока try-catch

  • вы рано возвращаетесь из блока try: учтите это

    Database db = null;
    try {
     db = open_database();
     if(db.isSomething()) {
       return 7;
     }
     return db.someThingElse();
    } finally {
      if(db!=null)
        db.close();
    }
    

    по сравнению с:

    Database db = null;
    int returnValue = 0;
    try {
     db = open_database();
     if(db.isSomething()) {
       returnValue = 7;
     } else {
       returnValue = db.someThingElse();
     }
    } catch(Exception e) {
      if(db!=null)
        db.close();
    }
    return returnValue;
    
  • Вы возвращаетесь рано из блока (ов) вылова: Сравнить

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      return 7;
    } catch (DBIsADonkeyException e ) {
      return 11;
    } finally {
      if(db!=null)
        db.close();
    }
    

    против:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null) 
        db.close();
      return 7;
    } catch (DBIsADonkeyException e ) {
      if(db!=null)
        db.close();
      return 11;
    }           
    db.close();
    
  • Вы отбрасываете исключения. Для сравнения:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      throw convertToRuntimeException(e,"DB was wonkey");
    } finally {
      if(db!=null)
        db.close();
    }
    

    против:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null)
        db.close();
      throw convertToRuntimeException(e,"DB was wonkey");
    } 
    if(db!=null)
      db.close();
    

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

Теперь в C ++ они могут быть обработаны объектами, основанными на области видимости. Но у ИМО есть два недостатка в этом подходе: 1. синтаксис менее дружественный. 2. Порядок строительства, противоположный порядку разрушения, может сделать вещи менее ясными.

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

Майкл Андерсон
источник
1

Все, что логично «необходимо» в языке программирования, это инструкции:

assignment a = b
subtract a from b
goto label
test a = 0
if true goto label

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

Смотрите Oldie Worldy Computer для реального оборудования, используя такой минимальный набор инструкций.

Джеймс Андерсон
источник
1
Ваш ответ, безусловно, верен, но я не пишу код на ассемблере; это слишком больно. Я спрашиваю, зачем использовать функцию, в которой я не вижу смысла в языках, которые ее поддерживают, а не в том, каков минимальный набор инструкций языка.
Agi Hammerthief
1
Дело в том, что любой язык, реализующий только эти 5 операций, может реализовать любой алгоритм - хотя и довольно извилистый. Большинство верс / операторов в языках высокого уровня не являются «необходимыми», если целью является просто реализация алгоритма. Если целью является быстрое развитие читаемого обслуживаемого кода, то большинство из них необходимы, но «читаемые» и «обслуживаемые» не поддаются измерению и чрезвычайно субъективны. Хорошие разработчики языка добавили множество функций: если у вас нет некоторых из них, не используйте их.
Джеймс Андерсон
0

На самом деле больший пробел для меня обычно в языках, которые поддерживают, finallyно не имеют деструкторов, потому что вы можете смоделировать всю логику, связанную с «очисткой» (которую я разделю на две категории), с помощью деструкторов на центральном уровне, не занимаясь очисткой вручную. логика в каждой соответствующей функции. Когда я вижу, что код на C # или Java выполняет такие вещи, как ручное разблокирование мьютексов и закрытие файлов в finallyблоках, это выглядит как устаревший код, похожий на код C, когда все это автоматизировано в C ++ с помощью деструкторов, что освобождает людей от этой ответственности.

Тем не менее, я все равно нашел бы небольшое удобство, если бы C ++ был включен, finallyи это потому, что есть два типа очистки:

  1. Уничтожение / освобождение / разблокировка / закрытие / и т.д. локальных ресурсов (деструкторы идеально подходят для этого).
  2. Отмена / откат внешних побочных эффектов (для этого достаточно деструкторов).

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

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

try
{
    // Cause external side effects. These side effects should
    // be undone if we don't finish successfully.
}
rollback
{
    // Reverse external side effects. This block is *only* executed 
    // if the 'try' block above faced a premature return out 
    // of the function. It is different from 'finally' which 
    // gets executed regardless of whether or not the function 
    // exited prematurely. This block *only* gets executed if we 
    // exited prematurely from  the try block so that we can undo 
    // whatever side effects it failed to finish making. If the try 
    // block succeeded and didn't face a premature exit, then we 
    // don't want this block to execute.
}

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

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


источник
-9

Как и многие другие необычные вещи в языке C ++, отсутствие try/finallyконструкции является недостатком дизайна, если вы даже можете назвать его так, что в языке, который, как часто кажется, вообще не было выполнено никакой реальной работы по дизайну .

RAII (использование детерминистского вызова деструктора на основе области действия для объектов на основе стека для очистки) имеет два серьезных недостатка. Во-первых, это требует использования стековых объектов , которые являются мерзостью, нарушающей принцип подстановки Лискова. Есть много веских причин, почему ни один другой язык ОО до или после C ++ не использовал их - в рамках epsilon; D не считается, поскольку он в значительной степени основан на C ++ и в любом случае не имеет доли на рынке - и объяснение проблем, которые они вызывают, выходит за рамки этого ответа.

Во-вторых, что finallyможет сделать, так это надстройка уничтожения объектов. Многое из того, что делается с RAII в C ++, будет описано на языке Delphi, который не имеет сборки мусора, по следующей схеме:

myObject := MyClass.Create(arguments);
try
   doSomething(myObject);
finally
   myObject.Free();
end;

Это шаблон RAII, сделанный явным; если бы вы создали подпрограмму C ++, которая содержала бы только эквивалент первой и третьей строк выше, то, что сгенерировал бы компилятор, в конечном итоге выглядело бы так, как я написал в его базовой структуре. И поскольку это единственный доступ к try/finallyконструкции, предоставляемой C ++, разработчики C ++ в конечном итоге получают довольно близорукое представление try/finally: когда все, что у вас есть, это молоток, все начинает выглядеть, так сказать, деструктором.

Но есть и другие вещи, которые опытный разработчик может сделать с finallyконструкцией. Речь идет не о детерминированном разрушении, даже перед лицом возникающего исключения; речь идет о детерминированном выполнении кода , даже при возникновении исключения.

Вот еще одна вещь, которую вы обычно можете увидеть в коде Delphi: объект набора данных с привязанными к нему пользовательскими элементами управления. Набор данных содержит данные из внешнего источника, а элементы управления отражают состояние данных. Если вы собираетесь загрузить кучу данных в свой набор данных, вам нужно временно отключить привязку данных, чтобы она не вызывала странных вещей в вашем пользовательском интерфейсе, пытаясь обновлять ее снова и снова с каждой новой записанной записью. , так что вы бы закодировали это так:

dataset.DisableControls();
try
   LoadData(dataset);
finally
   dataset.EnableControls();
end;

Понятно, что здесь нет разрушаемого объекта, и он не нужен. Код прост, сжат, явен и эффективен.

Как это будет сделано в C ++? Ну, во-первых, вы должны написать весь класс . Наверное, это называется DatasetEnablerили что-то в этом роде. Все его существование будет в качестве помощника RAII. Тогда вам нужно будет сделать что-то вроде этого:

dataset.DisableControls();
{
   raiiGuard = DatasetEnabler(dataset);
   LoadData(dataset);
}

Да, эти явно лишние фигурные скобки необходимы для управления надлежащей областью видимости и обеспечения немедленного включения набора данных, а не в конце метода. То, что вы в итоге получите, не займет меньше строк кода (если вы не используете египетские скобки). Это требует создания лишнего объекта, который имеет накладные расходы. (Разве код C ++ не должен быть быстрым?) Он не является явным, а использует магию компилятора. Выполненный код нигде не описывается в этом методе, но вместо этого находится в совершенно другом классе, возможно, в совершенно другом файле . Короче говоря, это ни в коем случае не лучшее решение, чем возможность написать try/finallyблок самостоятельно.

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

Мейсон Уилер
источник
Комментарии предназначены для уточнения или улучшения вопроса и ответа. Если вы хотите обсудить этот ответ, перейдите в чат. Спасибо.
maple_shaft