递归函数的参数传递问题
时间:2008-03-21 来源:剑心通明
参数传递问题
在设计函数时,除了返回值之外,我们可能还希望所调用的函数还能够返回其他一些信息。例如,在上面的阶乘递归函数中,我们除了希望计算最后的结果之外,还 希望了解这个函数一共被调用了多少次。熟悉 c 语言之类的读者都会清楚,这可以通过传递一个指针类型的参数实现。然而,在 bash 中并不支持指针,它提供了另外一种在解释性语言中常见的设计:间接变量引用(indirect variable reference)。让我们看一下下面这个例子:
var2=$var3
var1=$var2
其中变量 var2 的存在实际上就是为了让 var1 能够访问 var3,实际上也可以通过 var1 直接引用 var3 的值,方法是 var1=\$$var3(请注意转义字符是必须的,否则 $$ 符号会被解释为当前进程的进程 ID 号),这种方式就称为间接变量引用。从 bash2 开始,对间接变量引入了一种更为清晰的语法,方法是 var1=${!var3}。
清单 10 中给出了使用间接变量引用来统计阶乘函数被调用次数的实现。
清单10. 利用间接变量引用统计递归函数的调用次数
[root@localhost shell]# cat -n depth.sh
1 #!/bin/bash
2
3 factorial()
4 {
5 local i=$1
6 local l=$2
7
8 if [ $i -eq 0 ]
9 then
10 eval ${l}=1
11 rtn=1
12 else
13 factorial `expr $i - 1` ${l}
14 rtn=`expr $i \* $rtn `
15
16 local k=${!l}
17 eval ${l}=`expr ${k} + 1`
18 fi
19
20 return $rtn
21 }
22
23 if [ -z $1 ]
24 then
25 echo "Need one parameter."
26 exit 1
27 fi
28
29 level=0
30 factorial $1 level
31
32 echo "The factorial of $1 is : $rtn"
33 echo " the function of factorial is invoked $level times."
[root@localhost shell]# ./depth.sh 6
The factorial of 6 is : 720
the function of factorial is invoked 7 times.
在上面我们曾经介绍过,为了解决变量作用域和函数返回值的问题,在递归函数中我们使用 local 声明局部变量,并采用全局变量来传递返回值。但是随着调用关系变得更加复杂,全局变量的值有可能在其他地方被错误地修改。实际上,使用局部变量也存在一个 问题,下面让我们来看一下清单 11 中给出的例子。
清单11. 查找字符串在文件中是否存在,并计算所在行数和出现次数
[root@localhost shell]# cat -n getline1.sh
1 #!/bin/bash
2
3 GetLine()
4 {
5 string=$1
6 file=$2
7
8 line=`grep -n $string $file`
9 if [ $? -eq 0 ]
10 then
11 printf "$string is found as the %drd line in $file \n" `echo $line \
| cut -f1 -d:`
12 num=`grep $string $file | wc -l`
13 rtn=0
14 else
15 printf "$string is not found in $file \n"
16 num=0
17 rtn=1
18 fi
19
20 return $rtn;
21 }
22
23 if [ ! -f testfile.$$ ]
24 then
25 cat >> testfile.$$ <<EOF
26 first line .
27 second line ..
28 third line ...
29 EOF
30 fi
31
32 num=0
33 rtn=0
34 for i in "second" "six" "line"
35 do
36 echo
37 GetLine $i testfile.$$
38 echo "return value: $rtn"
39
40 if [ $num -gt 0 ]
41 then
42 echo "$num occurences found totally."
43 fi
44 done
[root@localhost shell]# ./getline1.sh
second is found as the 2rd line in testfile.4280
return value: 0
1 occurences found totally.
six is not found in testfile.4280
return value: 1
line is found as the 1rd line in testfile.4280
return value: 0
3 occurences found totally.
[root@localhost shell]#
这段程序的目的是查找某个字符串在指定文件中是否存在,如果存在,就计算第一次出现的行数和总共出现的次数。为了说明局部变量和后面提到的子函数的问题, 我们故意将对出现次数的打印也放到了 GetLine 函数之外进行处理。清单 11 中全部使用全局变量,并没有出现什么问题。下面让我们来看一下将 GetLine 中使用的局部变量改用 local 声明后会出现什么问题,修改后的代码和执行结果如清单 12 所示。
清单12. 使用 local 声明局部变量需要注意的问题
[root@localhost shell]# cat -n getline2.sh
1 #!/bin/bash
2
3 GetLine()
4 {
5 local string=$1
6 local file=$2
7
8 local line=`grep -n $string $file`
9 if [ $? -eq 0 ]
10 then
11 printf "$string is found as the %drd line in $file \n" `echo $line \
| cut -f1 -d:`
12 num=`grep $string $file | wc -l`
13 rtn=0
14 else
15 printf "$string is not found in $file \n"
16 num=0
17 rtn=1
18 fi
19
20 return $rtn;
21 }
22
23 if [ ! -f testfile.$$ ]
24 then
25 cat >> testfile.$$ <<EOF
26 first line .
27 second line ..
28 third line ...
29 EOF
30 fi
31
32 num=0
33 rtn=0
34 for i in "second" "six" "line"
35 do
36 echo
37 GetLine $i testfile.$$
38 echo "return value: $rtn"
39
40 if [ $num -gt 0 ]
41 then
42 echo "$num occurences found totally."
43 fi
44 done
[root@localhost shell]# ./getline2.sh
second is found as the 2rd line in testfile.4300
return value: 0
1 occurences found totally.
six is found as the 0rd line in testfile.4300
return value: 0
line is found as the 1rd line in testfile.4300
return value: 0
3 occurences found totally.
清单 12 的运行结果显示,在文件中搜索 six 关键字时的结果是错误的,调试会发现,问题的原因在于:第 8 行使用 local 将 line 声明为局部变量,并将 grep 命令的执行结果赋值给 line 变量。然而不论 grep 是否成功在文件中找到匹配项(grep 程序找到匹配项返回值为 0,否则返回值为 1),第 9 行中 $? 的值总是 0。实际上,第 8 行相当于执行了两条语句:第一条语句使用 grep 在文件中查找匹配项,第二条语句将 grep 命令的结果赋值给变量 line,并设定其作用域只对于本函数及其子进程可见。因此第 9 行命令中 $? 的值实际上是执行 local 命令的返回值,不管 grep 命令的结果如何,它总是 0。
要解决这个问题,可以将第 8 行的命令拆分开,首先使用单独一行将变量 line 声明为 local的,然后再执行这条 grep 命令,并将结果赋值给变量 line(此时前面不能加上 local)。
解决变量作用域的另外一种方法是使用子 shell。所谓子 shell 是在当前 shell 环境中启动一个子 shell 来执行所调用的命令或函数,这个函数中所声明的所有变量都是局部变量,它们不会污染原有 shell 的名字空间。清单 13 给出了使用子 shell 修改后的例子。
清单13. 利用子 shell 实现局部变量
[root@localhost shell]# cat -n getline3.sh
1 #!/bin/bash
2
3 GetLine()
4 {
5 string=$1
6 file=$2
7
8 line=`grep -n $string $file`
9 if [ $? -eq 0 ]
10 then
11 printf "$string is found as the %drd line in $file \n" `echo $line \
| cut -f1 -d:`
12 num=`grep $string $file | wc -l`
13 rtn=0
14 else
15 printf "$string is not found in $file \n"
16 num=0
17 rtn=1
18 fi
19
20 return $rtn;
21 }
22
23 if [ ! -f testfile.$$ ]
24 then
25 cat >> testfile.$$ <<EOF
26 first line .
27 second line ..
28 third line ...
29 EOF
30 fi
31
32 num=0
33 rtn=0
34 for i in "second" "six" "line"
35 do
36 echo
37 (GetLine $i testfile.$$)
38 echo "return value: $? (rtn = $rtn)"
39
40 if [ $num -gt 0 ]
41 then
42 echo "$num occurences found totally."
43 fi
44 done
[root@localhost shell]# ./getline3.sh
second is found as the 2rd line in testfile.4534
return value: 0 (rtn = 0)
six is not found in testfile.4534
return value: 1 (rtn = 0)
line is found as the 1rd line in testfile.4534
return value: 0 (rtn = 0)
在清单 13 中,GetLine 函数并不需要任何变化,变量定义和程序调用都沿用正常方式。唯一的区别在于调用该函数时,要将其作为一个子 shell 来调用(请注意第 37 行两边的圆括号)。另外一个问题是在子 shell 中修改的所有变量对于原有 shell 来说都是不可见的,这也就是为什么在第 38 行要通过 $? 来检查返回值,而 rtn 变量的值却是错误的。另外由于 num 在 GetLine 函数中也被当作是局部变量,同样无法将修改后的值传出来,因此也并没有打印所匹配到的 line 的数目是 3 行的信息。
解决上面这个问题就只能使用前面提到的利用标准输入输出设备的方法了,否则即使使用间接变量引用也无法正常工作。清单 14 给出了一个使用间接变量引用的例子,尽管我们使用不同的名字来命名全局变量和局部变量,从而确保不会引起同名混淆,但是依然无法正常工作。原因同样在于 GetLine 函数是在另外一个子进程中运行的,它对变量所做的更新随着子 shell 的退出就消失了。
清单14. 利用间接变量索引也无法解决子 shell 通过变量回传值的问题
[root@localhost shell]# cat -n getline4.sh
1 #!/bin/bash
2
3 GetLine()
4 {
5 string=$1
6 file=$2
7 num=$3
8 rtn=$4
9
10 line=`grep -n $string $file`
11 if [ $? -eq 0 ]
12 then
13 printf "$string is found as the %drd line in $file \n" \
`echo $line | cut -f1 -d:`
14 eval ${num}=`grep $string $file | wc -l`
15 eval ${rtn}=0
16 else
17 printf "$string is not found in $file \n"
18 eval ${num}=0
19 eval ${rtn}=1
20 fi
21
22 return ${!rtn};
23 }
24
25 if [ ! -f testfile.$$ ]
26 then
27 cat >> testfile.$$ <<EOF
28 first line .
29 second line ..
30 third line ...
31 EOF
32 fi
33
34 g_num=0
35 g_rtn=0
36 for i in "second" "six" "line"
37 do
38 echo
39 (GetLine $i testfile.$$ g_num g_rtn)
40 echo "return value: $? (g_rtn = $g_rtn)"
41
42 if [ $g_num -gt 0 ]
43 then
44 echo "$g_num occurence(s) found totally."
45 fi
46 done
[root@localhost shell]# ./getline4.sh
second is found as the 2rd line in testfile.4576
return value: 0 (g_rtn = 0)
six is not found in testfile.4576
return value: 1 (g_rtn = 0)
line is found as the 1rd line in testfile.4576
return value: 0 (g_rtn = 0)