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

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

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

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

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

答案1

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

if [ -f file1 ]; then   # If file1 exists ...
    cp file1 file2      # ... create file2 as a copy of a file1
fi

ETC。

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

回调如何改变流程

当您使用回调时,您不是“按地理位置”放置一组指令的使用,而是描述何时应该调用它。其他编程环境中的典型例子是“下载这个资源,下载完成后调用这个回调”之类的情况。 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,IE“亲爱的 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 "$@"

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

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

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

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

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

答案2

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

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

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

如果您实现了类似查找的函数,则可以使其使用回调函数来调用每个文件。这是一个极其简化的类似 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
}

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

答案3

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

在 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"

有概要

x command filter [file ...]

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

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

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

答案4

只是为了在其他答案中添加几句话。函数回调对回调函数外部的函数进行操作。为了实现这一点,要么需要将要回调的函数的完整定义传递给回调函数,要么其代码应该可供回调函数使用。

前者(将代码传递给另一个函数)是可能的,尽管我将跳过一个示例,因为这会涉及复杂性。后者(按名称传递函数)是一种常见做法,因为在一个函数作用域之外声明的变量和函数在该函数中可用,只要它们的定义先于对对其进行操作的函数的调用(反过来, ,在调用之前声明)。

另请注意,导出函数时也会发生类似的情况。导入函数的 shell 可能已经准备好框架,并且只是等待函数定义将其付诸实践。 Bash 中存在函数导出,并且导致了之前的严重问题,顺便说一句(称为 Shellshock):

我将用另一种将函数传递给另一个函数的方法来完成这个答案,这种方法在 Bash 中没有明确提供。这种方法是通过地址传递,而不是通过名称传递。例如,可以在 Perl 中找到这种方法。Bash 既不为函数提供这种方法,也不为变量提供这种方法。但是,如果如您所说,您想以 Bash 为例来更全面地了解情况,那么您应该知道,函数代码可能驻留在内存中的某个位置,并且可以通过该内存位置(称为其地址)访问该代码。

相关内容