Непрозрачное определение
Функция обратного вызова - это функция, которую вы предоставляете другому коду, позволяя вызывать его этим кодом.
Придуманный пример
Зачем тебе это делать? Допустим, есть служба, которую нужно вызвать. Если услуга возвращается немедленно, вы просто:
- Назовите это
- Дождаться результата
- Продолжить, как только появится результат
Например, предположим, что службой была функция factorial
. Если вам нужно значение 5!
, вы вызовете factorial(5)
, и произойдут следующие шаги:
Текущее место выполнения сохраняется (в стеке, но это не важно)
Исполнение передано factorial
Когда factorial
завершается, результат помещается туда, где вы можете его получить
Казнь возвращается туда, где она была в [1]
Теперь предположим, что factorial
заняло очень много времени, потому что вы даете ему огромные цифры и он должен работать на каком-то суперкомпьютерном кластере. Допустим, вы ожидаете, что это займет 5 минут, чтобы вернуть ваш результат. Вы могли бы:
Сохраняйте свой дизайн и запускайте программу ночью, когда вы спите, чтобы не смотреть на экран половину времени
Создайте свою программу для других целей, пока factorial
делает свое дело
Если вы выберете второй вариант, обратные вызовы могут работать для вас.
Сквозной дизайн
Чтобы использовать шаблон обратного вызова, вы должны иметь возможность вызвать factorial
следующим образом:
factorial(really_big_number, what_to_do_with_the_result)
Второй параметр, what_to_do_with_the_result
, - это функция, которую вы отправляете вместе с factorial
, в надежде, что factorial
вызовет его для результата перед возвратом.
Да, это означает, что factorial
должен быть написан для поддержки обратных вызовов.
Теперь предположим, что вы хотите иметь возможность передать параметр в ваш обратный вызов. Теперь вы не можете, потому что вы не собираетесь это называть, factorial
. Поэтому необходимо написать factorial
, чтобы вы могли передать свои параметры, и он просто передаст их вашему обратному вызову, когда вызовет его. Это может выглядеть так:
factorial (number, callback, params)
{
result = number! // i can make up operators in my pseudocode
callback (result, params)
}
Теперь, когда factorial
разрешает этот шаблон, ваш обратный вызов может выглядеть следующим образом:
logIt (number, logger)
{
logger.log(number)
}
и ваш звонок на factorial
будет
factorial(42, logIt, logger)
Что если вы хотите что-то вернуть из logIt
? Ну, вы не можете, потому что factorial
не обращает на это внимания.
Ну, почему factorial
просто не может вернуть то, что возвращает ваш обратный вызов?
Сделать его неблокирующим
Поскольку выполнение предназначено для передачи обратному вызову после завершения factorial
, оно действительно не должно ничего возвращать вызывающей стороне. И в идеале, он каким-то образом запустит свою работу в другом потоке / процессе / машине и сразу же вернется, чтобы вы могли продолжить, возможно, что-то вроде этого:
factorial(param_1, param_2, ...)
{
new factorial_worker_task(param_1, param_2, ...);
return;
}
Теперь это «асинхронный вызов», что означает, что когда вы вызываете его, он немедленно возвращается, но еще не выполнил свою работу. Таким образом, вам нужны механизмы, чтобы проверить его и получить его результат, когда он будет завершен, и ваша программа стала более сложной в этом процессе.
И, кстати, используя этот шаблон, factorial_worker_task
может запустить ваш обратный вызов асинхронно и немедленно вернуться.
Так что ты делаешь?
Ответ - оставаться в рамках схемы обратного вызова. Всякий раз, когда вы хотите написать
a = f()
g(a)
и f
должны вызываться асинхронно, вместо этого вы будете писать
f(g)
, где g
передается как обратный вызов.
Это в корне меняет топологию потока вашей программы и требует некоторого привыкания.
Ваш язык программирования может очень вам помочь, предоставляя вам возможность создавать функции на лету. В приведенном выше коде функция g
может быть такой же маленькой, как print (2*a+1)
. Если ваш язык требует, чтобы вы определили это как отдельную функцию с совершенно ненужным именем и подписью, то ваша жизнь станет неприятной, если вы будете часто использовать этот шаблон.
Если, с другой стороны, ваш язык позволяет вам создавать лямбды, то вы в гораздо лучшей форме. Тогда вы в конечном итоге напишите что-то вроде
f( func(a) { print(2*a+1); })
что намного приятнее.
Как пройти обратный вызов
Как бы вы передали функцию обратного вызова factorial
? Ну, вы могли бы сделать это несколькими способами.
Если вызываемая функция выполняется в том же процессе, вы можете передать указатель на функцию
Или, может быть, вы хотите сохранить словарь fn name --> fn ptr
в вашей программе, в этом случае вы можете передать имя
Возможно, ваш язык позволяет вам определять функцию на месте, возможно, как лямбда! Внутренне это создает какой-то объект и передает указатель, но вам не нужно об этом беспокоиться.
Возможно, вызываемая вами функция выполняется на совершенно отдельной машине, и вы вызываете ее, используя сетевой протокол, такой как HTTP. Вы можете представить свой обратный вызов как функцию, вызываемую по HTTP, и передать ее URL.
Вы поняли.
Недавний рост обратных вызовов
В эту эру Интернета, в которую мы вступили, службы, к которым мы обращаемся, часто находятся в сети. Мы часто не имеем никакого контроля над этими службами, т. Е. Мы их не писали, мы их не обслуживаем, мы не можем гарантировать, что они работают или как они работают.
Но мы не можем ожидать, что наши программы будут блокироваться, пока мы ожидаем ответа этих служб. Осознавая это, поставщики услуг часто разрабатывают API, используя шаблон обратного вызова.
JavaScript очень хорошо поддерживает обратные вызовы, например с лямбдами и пробками. И в мире JavaScript много активности, как в браузере, так и на сервере. Есть даже платформы JavaScript, разрабатываемые для мобильных устройств.
По мере продвижения вперед все больше и больше людей будут писать асинхронный код, для которого это понимание будет необходимо.