В типичном императивном программировании вы пишете последовательности инструкций, и они выполняются одна за другой с явным потоком управления. Например:
if [ -f file1 ]; then # If file1 exists ...
cp file1 file2 # ... create file2 as a copy of a file1
fi
и т.п.
Как видно из примера, в императивном программировании вы достаточно легко следите за потоком выполнения, всегда пробираясь вверх от любой строки кода, чтобы определить его контекст выполнения, зная, что любые инструкции, которые вы дадите, будут выполнены в результате их выполнения. местоположение в потоке (или местоположение их сайтов вызовов, если вы пишете функции).
Как обратные вызовы меняют поток
Когда вы используете обратные вызовы, вместо того, чтобы использовать набор инструкций «географически», вы описываете, когда он должен быть вызван. Типичными примерами в других средах программирования являются такие случаи, как «загрузить этот ресурс, а когда загрузка будет завершена, вызвать этот обратный вызов». Bash не имеет общей конструкции обратного вызова такого рода, но он имеет обратные вызовы для обработки ошибок и некоторых других ситуаций; например (чтобы понять пример, нужно сначала понять подстановку команд и режимы выхода Bash ):
#!/bin/bash
scripttmp=$(mktemp -d) # Create a temporary directory (these will usually be created under /tmp or /var/tmp/)
cleanup() { # Declare a cleanup function
rm -rf "${scripttmp}" # ... which deletes the temporary directory we just created
}
trap cleanup EXIT # Ask Bash to call cleanup on exit
Если вы хотите попробовать это сами, сохраните вышеупомянутое в файле, скажем cleanUpOnExit.sh
, сделайте его исполняемым и запустите его:
chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh
Мой код здесь никогда явно не вызывает cleanup
функцию; он сообщает Bash, когда его вызывать, используя trap cleanup EXIT
, например, «дорогой Bash, пожалуйста, запустите cleanup
команду при выходе» (и cleanup
это функция, которую я определил ранее, но это может быть все, что понимает Bash). Bash поддерживает это для всех нефатальных сигналов, выходов, сбоев команд и общей отладки (вы можете указать обратный вызов, который запускается перед каждой командой). Обратный вызов здесь - это cleanup
функция, которую Bash «вызывает» перед выходом из оболочки.
Вы можете использовать способность Bash оценивать параметры оболочки в виде команд, чтобы создать ориентированную на обратный вызов структуру; это несколько выходит за рамки этого ответа, и, возможно, приведет к еще большей путанице, если предположить, что передача функций всегда связана с обратными вызовами. См. Bash: передать функцию в качестве параметра для некоторых примеров базовой функциональности. Идея здесь, как и в случае обратных вызовов обработки событий, заключается в том, что функции могут принимать данные в качестве параметров, но также и другие функции - это позволяет вызывающим сторонам предоставлять поведение, а также данные. Простой пример такого подхода может выглядеть так
#!/bin/bash
doonall() {
command="$1"
shift
for arg; do
"${command}" "${arg}"
done
}
backup() {
mkdir -p ~/backup
cp "$1" ~/backup
}
doonall backup "$@"
(Я знаю, что это немного бесполезно, поскольку cp
может работать с несколькими файлами, это только для иллюстрации.)
Здесь мы создаем функцию, doonall
которая принимает другую команду, заданную в качестве параметра, и применяет ее к остальным ее параметрам; затем мы используем это для вызова backup
функции по всем параметрам, переданным скрипту. В результате получается скрипт, который копирует все свои аргументы, один за другим, в каталог резервных копий.
Такой подход позволяет писать функции с единственной ответственностью: doonall
ответственность состоит в том, чтобы запускать что-то по всем своим аргументам, по одному за раз; backup
ответственность заключается в том, чтобы сделать копию своего (единственного) аргумента в резервном каталоге. И то, doonall
и другое backup
можно использовать в других контекстах, что позволяет больше повторного использования кода, лучшие тесты и т. Д.
В этом случае обратный вызов - это backup
функция, которую мы говорим doonall
«перезванивать» по каждому из ее других аргументов - мы предоставляем doonall
поведение (его первый аргумент), а также данные (остальные аргументы).
(Обратите внимание, что в случае использования, продемонстрированном во втором примере, я бы сам не использовал термин «обратный вызов», но, возможно, это привычка, вытекающая из языков, которые я использую. Я думаю об этом как о передаче функций или лямбд вокруг вместо регистрации обратных вызовов в ориентированной на события системе.)