Введение
Именная функция, определенная внутри функции
В компьютерном программировании вложенная функция (или вложенная процедура, подпрограмма) — это именная функция, которая определяется внутри другого, охватывающего блока и имеет лексическую область видимости в пределах охватывающего блока, что означает, что она может быть вызвана только по имени внутри тела охватывающего блока и может использовать идентификаторы, объявленные во внешних блоках, включая внешние функции. Охватывающий блок обычно, но не всегда, является другой функцией. Поддержка вложенных функций в языках программирования различается. Для структурированных языков программирования она поддерживается в некоторых устаревших языках, таких как ALGOL, Simula 67 и Pascal, а также в широко используемом JavaScript. Она обычно поддерживается в динамических и функциональных языках. Однако она не поддерживается в некоторых широко используемых языках, включая стандартные C и C++. Другие технологии программирования предоставляют аналогичные возможности. Например, лямбда-функция также позволяет определять функцию внутри функции (а также в других местах) и обеспечивает аналогичное сокрытие данных и инкапсуляцию. Важно отметить, что лямбда-функция не имеет имени (является анонимной) и, следовательно, не может быть вызвана по имени и не имеет аспекта видимости.
Атрибуты
Область видимости вложенной функции — это блок, который её содержит, будь то блок функции или блок внутри тела функции. Она не видна (не может быть вызвана по имени) за пределами содержащего её блока. Вложенная функция может использовать идентификаторы (то есть имена функций, переменных, типов, классов), объявленные в любом охватывающем блоке, за исключением случаев, когда они перекрываются внутренними объявлениями с теми же именами. Вложенную функцию можно объявлять внутри другой вложенной функции рекурсивно, формируя глубоко вложенную структуру. Глубоко вложенная функция может обращаться к идентификаторам, объявленным во всех своих охватывающих блоках, включая охватывающие функции. Вложенные функции в определенных ситуациях могут приводить к созданию замыкания. Если вложенная функция может "вырваться" из охватывающей функции, например, если функции являются объектами первого класса и вложенная функция передается другой функции или возвращается из охватывающей функции, то создается замыкание, и вызовы этой функции могут обращаться к окружению исходной функции. Фрейм непосредственно охватывающей функции должен оставаться активным до тех пор, пока не умрет последнее замыкание, ссылающееся на него, и поэтому нелокальные автоматические переменные, на которые ссылаются замыкания, не могут быть выделены в стеке в языках, которые позволяют замыканию сохраняться после окончания срока жизни охватывающего блока. Это известно как проблема funarg и является ключевой причиной, по которой вложенные функции не были реализованы в некоторых более простых языках, поскольку это значительно усложняет генерацию кода и анализ, особенно когда функции вложены на разные уровни, совместно используя различные части своего окружения.
Стоимость
Технология вложенных функций позволяет программисту писать исходный код, обладающий полезными свойствами, такими как сокрытие информации, инкапсуляция и декомпозиция. Программист может разбить задачу на подзадачи, имеющие смысл только в контексте исходной задачи, таким образом скрывая функции подзадач от вызывающих сторон, не предназначенных для их использования. Область видимости блоков позволяет функциям совместно использовать состояние окружающих блоков (включая окружающие функции) без передачи параметров или использования глобальных переменных.
Помощник
Вложенная функция обычно выступает в роли вспомогательной или рекурсивной функции (как в примере быстрой сортировки, приведенном выше).
Контрольный поток
Вложенные функции могут использоваться для реализации неструктурированного управления потоком выполнения, применяя оператор return для общего неструктурированного управления. Это позволяет добиться более детального контроля, чем предоставляют другие встроенные возможности языка – например, можно обеспечить досрочное завершение цикла for, если оператор break недоступен, или досрочное завершение вложенного цикла for, если нет возможности использовать многоуровневый break или исключения.
Функции высшего порядка
В некоторых языках программирования можно создать вложенную функцию, которая обращается к набору параметров внешней функции, образуя замыкание, и возвращать эту функцию как результат работы внешней функции. Таким образом, можно вернуть функцию, настроенную на выполнение определенной задачи, требующую минимального количества или отсутствия дополнительных параметров, что может существенно повысить производительность.
Функциональные языки
В большинстве функциональных языков программирования, таких как Scheme, вложенные функции — распространённый способ реализации алгоритмов, содержащих циклы. Создаётся простая (хвостовая) рекурсивная внутренняя функция, которая выполняет роль основного цикла алгоритма, а внешняя функция выполняет действия по инициализации, которые необходимо выполнить только один раз. В более сложных случаях может быть создано несколько взаимно рекурсивных функций в качестве внутренних функций.
Альтернативы
Для достижения аналогичных результатов программирования, как при использовании вложенных функций, можно применять различные альтернативные методы.
Модульность
Общая альтернатива — использование технологии модульности языка. Некоторые функции предоставляются для использования вне модуля, а другие видны только внутри модуля. В языке C это можно реализовать, объявляя функции и переменные как `static`, чтобы скрыть их от кода, находящегося за пределами файла. Это обеспечивает сокрытие данных, инкапсуляцию и декомпозицию, но на другом уровне детализации, чем при использовании вложенных функций. Эта модульность не поддерживает более одного уровня вложенности. В объектно-ориентированных языках класс обычно предоставляет область видимости, в которой функции и состояние могут быть скрыты от пользователей класса, но доступны внутри самого класса. Некоторые языки допускают вложение классов друг в друга.
Реализация
Реализация вложенных функций может оказаться более сложной, чем кажется на первый взгляд, поскольку ссылка на вложенную функцию, использующую нелокальные переменные, создает замыкание. По этой причине вложенные функции не поддерживаются в некоторых языках, таких как C, C++ или Java, так как это усложняет реализацию компиляторов. Однако некоторые компиляторы поддерживают их как расширение, специфичное для данного компилятора. Хорошо известный пример – реализация GNU C, которая использует общий код с компиляторами для языков, таких как Pascal, Ada и Modula.
Функции как значения
Для того чтобы локальные функции с лексически ограниченными нелокальными переменными могли передаваться в качестве результатов, код времени выполнения языка должен также неявно передавать окружение (данные), которое функция видит внутри заключающей её функции, чтобы оно оставалось доступным даже после завершения текущего вызова заключающей функции. Это означает, что окружение должно храниться в области памяти, отличной от (последующего освобождения частей) стека вызовов, основанного на хронологии, что, в свою очередь, требует некоторой формы свободного динамического выделения памяти. Поэтому многие старые языки на основе Algol (или их диалекты) не разрешают передавать локальные функции, обращающиеся к нелокальным переменным, в качестве возвращаемых значений, или вообще не разрешают передавать функции в качестве возвращаемых значений, хотя передача таких функций в качестве аргументов все еще может быть возможна.
Неисполняемые стеки
Реализация вложенных функций в GCC приводит к потере защиты стека от исполнения (NX-стеков). Эта реализация вызывает вложенные функции посредством инструкции перехода, размещаемой в стеке во время выполнения. Это требует, чтобы стек был исполняемым. Таким образом, NX-стеки и вложенные функции являются взаимоисключающими в GCC. Если в разработке программы используется вложенная функция, то защита NX-стека теряется незаметно, если только GCC не будет вызван с опцией `-Wtrampoline` для уведомления об этой ситуации. Программное обеспечение, разработанное с применением методологии безопасной разработки, часто запрещает использование вложенных функций в этом компиляторе из-за потери защиты NX-стеков.