Bash 中是否存在编程的“回调”概念?

21 bash function

有几次,当我阅读有关编程的内容时,我遇到了“回调”概念。

有趣的是,对于“回调函数”这个术语,我从来没有找到我可以称之为“教学”或“清晰”的解释(在我看来,我读到的几乎所有解释都与另一个解释完全不同,我感到困惑)。

Bash 中是否存在编程的“回调”概念?如果是这样,请用一个小的、简单的 Bash 示例来回答。

Ste*_*itt 48

在典型的命令式编程中,您编写指令序列,并通过显式控制流一个接一个地执行它们。例如:

if [ -f file1 ]; then   # If file1 exists ...
    cp file1 file2      # ... create file2 as a copy of a file1
fi
Run Code Online (Sandbox Code Playgroud)

等等。

从示例中可以看出,在命令式编程中,您很容易遵循执行流程,始终从任何给定的代码行开始确定其执行上下文,知道您给出的任何指令都将作为它们的结果执行流中的位置(或它们的调用站点的位置,如果您正在编写函数)。

回调如何改变流程

当您使用回调时,您不是在“地理上”放置一组指令,而是描述应该何时调用它。其他编程环境中的典型例子有“下载此资源,下载完成后调用此回调”等情况。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
Run Code Online (Sandbox Code Playgroud)

如果您想自己尝试一下,请将上述内容保存在一个文件中,例如cleanUpOnExit.sh,使其可执行并运行它:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh
Run Code Online (Sandbox Code Playgroud)

我的代码从未显式调用该cleanup函数;它告诉 Bash 何时调用它,使用trap cleanup EXIT“亲爱的 Bash,请cleanup在退出时运行该命令”(cleanup恰好是我之前定义的一个函数,但它可以是 Bash 理解的任何东西)。Bash 支持所有非致命信号、退出、命令失败和一般调试(您可以指定在每个命令之前运行的回调)。这里的回调是cleanup函数,它在 shell 退出前被 Bash “回调”。

您可以使用 Bash 将 shell 参数作为命令进行评估的能力,以构建面向回调的框架;这有点超出了这个答案的范围,并且可能会因为建议传递函数总是涉及回调而引起更多混乱。有关底层功能的一些示例,请参阅Bash:将函数作为参数传递。与事件处理回调一样,这里的想法是函数可以将数据作为参数,也可以将其他函数作为参数——这允许调用者提供行为和数据。这种方法的一个简单示例可能如下所示

#!/bin/bash

doonall() {
    command="$1"
    shift
    for arg; do
        "${command}" "${arg}"
    done
}

backup() {
    mkdir -p ~/backup
    cp "$1" ~/backup
}

doonall backup "$@"
Run Code Online (Sandbox Code Playgroud)

(我知道这有点没用,因为cp可以处理多个文件,仅用于说明。)

在这里,我们创建了一个函数,doonall,它接受另一个命令,作为参数给出,并将其应用于其余参数;然后我们使用它来调用backup给脚本的所有参数的函数。结果是一个脚本,它将其所有参数一个一个地复制到备份目录中。

这种方法允许编写具有单一职责的函数:doonall的职责是在其所有参数上运行某事,一次一个;backup的职责是在备份目录中制作其(唯一)参数的副本。双方doonallbackup可以在其他情况下,允许更多的代码复用,更好的测试等使用

在这种情况下,回调是backup函数,我们告诉doonall它“回调”它的每个其他参数——我们提供doonall行为(它的第一个参数)以及数据(剩余的参数)。

(请注意,在第二个示例中演示的用例类型中,我自己不会使用术语“回调”,但这可能是我使用的语言导致的习惯。我认为这是传递函数或 lambdas ,而不是在面向事件的系统中注册回调。)


Gil*_*il' 27

首先,重要的是要注意使函数成为回调函数的是它的使用方式,而不是它的作用。回调是指从您未编写的代码中调用您编写的代码。您要求系统在某些特定事件发生时给您回电。

shell 编程中回调的一个例子是陷阱。陷阱是一种回调,它不表示为函数,而是要计算的一段代码。当 shell 接收到特定信号时,您要求 shell 调用您的代码。

回调的另一个示例是命令的-exec操作find。该find命令的工作是递归遍历目录并依次处理每个文件。默认情况下,处理是打印文件名(隐式-print),但-exec处理是运行您指定的命令。这符合回调的定义,尽管在回调中,它不是很灵活,因为回调在单独的进程中运行。

如果你实现了一个类似 find 的函数,你可以让它使用回调函数来调用每个文件。这是一个超简化的类似 find 的函数,它将函数名(或外部命令名)作为参数,并在当前目录及其子目录中的所有常规文件上调用它。该函数用作回调,每次call_on_regular_files找到常规文件时都会调用该回调。

shopt -s globstar
call_on_regular_files () {
  declare callback="$1"
  declare file
  for file in **/*; do
    if [[ -f $file ]]; then
      "$callback" "$file"
    fi
  done
}
Run Code Online (Sandbox Code Playgroud)

回调在 shell 编程中不像在其他一些环境中那样常见,因为 shell 主要是为简单程序设计的。在数据和控制流更有可能在独立编写和分发的代码部分之间来回移动的环境中,回调更为常见:基本系统、各种库、应用程序代码。

  • @mikemaccana当然有可能是同一个人编写了代码的两部分。但这不是普遍情况。我是在解释一个概念的基础知识,而不是给出一个正式的定义。如果您解释所有极端情况,则很难传达基本信息。 (5认同)
  • “回调是指从您未编写的代码中调用您编写的代码。” 简直是错误的。你可以写一个东西来做一些非阻塞的异步工作,然后用一个回调来运行它,它会在完成时运行。与谁写的代码无关, (4认同)

mos*_*svy 7

“回调”只是作为参数传递给其他函数的函数。

在 shell 级别,这仅意味着脚本/函数/命令作为参数传递给其他脚本/函数/命令。

现在,举一个简单的例子,考虑以下脚本:

$ cat ~/w/bin/x
#! /bin/bash
cmd=$1; shift
case $1 in *%*) flt=${1//\%/\'%s\'};; *) flt="$1 '%s'";; esac; shift
q="'\\''"; f=${flt//\\/'\\'}; p=`printf "<($f) " "${@//\'/$q}"`
eval "$cmd" "$p"
Run Code Online (Sandbox Code Playgroud)

有概要

x command filter [file ...]
Run Code Online (Sandbox Code Playgroud)

将应用于filter每个file参数,然后command将过滤器的输出作为参数调用。

例如:

x diff zcat a.gz b.bz   # diff gzipped files
x diff3 zcat a.gz b.gz c.gz   # same with three-way diff
x diff hd a b  # hex diff of binary files
x diff 'zcat % | sort -u' a.gz b.gz  # first uncompress the files, then sort+uniq them, then compare them
x 'comm -12' sort a b  # find common lines in unsorted files
Run Code Online (Sandbox Code Playgroud)

这与你在 lisp 中可以做的非常接近(开个玩笑 ;-))

有些人坚持将“回调”术语限制为“事件处理程序”和/或“闭包”(函数 + 数据/环境元组);这绝不是普遍 接受的意思。狭义上的“回调”在 shell 中没有多大用处的一个原因是,管道 + 并行 + 动态编程功能强大得多,而且您已经在性能方面为它们付出了代价,即使您尝试将 shell 用作perl或的笨拙版本python