24.2 局部变量

怎样使一个变量变成“局部”变量?

局部变量
如果变量用local来声明,那么它就只能够在该变量被声明的代码块中可见。 这个代码块就是局部范围。 在一个函数中,一个局部变量只有在函数代码中才有意义.[1]

例子 24-12. 局部变量的可见范围

  1. #!/bin/bash
  2. # ex62.sh: 函数内部的局部变量与全局变量。
  3. func () {
  4. local loc_var=23 # 声明为局部变量。
  5. echo # 使用'local'内建命令
  6. echo "\"loc_var\" in function = $loc_var"
  7. global_var=999 # 没有声明为局部变量。
  8. # 默认为全局变量。
  9. echo "\"global_var\" in function = $global_var"
  10. }
  11. func
  12. # 现在,来看看局部变量“loc_var”在函数外部是否可见。
  13. echo
  14. echo "\"loc_var\" outside function = $loc_var"
  15. # $loc_var outside function =
  16. # 不行, $loc_var 不是全局可见的.
  17. echo "\"global_var\" outside function = $global_var"
  18. # $在函数外部global_var = 999
  19. # $global_var 是全局可见的.
  20. echo
  21. exit 0
  22. # 与C语言相比,在函数内声明的Bash变量
  23. #+ 除非它被明确声明为local时,它才是局部的。

notice 在函数被调用之前,所有在函数中声明的变量,在函数外部都是不可见的,当然也包括那些被明确声明为local的变量。

  1. #!/bin/bash
  2. func ()
  3. {
  4. global_var=37 # 变量只在函数体内可见
  5. #+ 在函数被调用之前。
  6. } # 函数结束
  7. echo "global_var = $global_var" # global_var =
  8. # 函数 "func" 还没被调用,
  9. #+ 所以$global_var 在这里还不是可见的.
  10. func
  11. echo "global_var = $global_var" # global_var = 37
  12. # 已经在函数调用的时候设置。

extra 正如Evgeniy Ivanov指出的那样,当在一条命令中定义和给一个局部变量赋值时,显然操作的顺序首先是给变量赋值,之后限定变量的局部范围。这可以通过返回值来反应。

  1. #!/bin/bash
  2. echo "==OUTSIDE Function (global)=="
  3. t=$(exit 1)
  4. echo $? # 1
  5. # 如预期一样.
  6. echo
  7. function0 ()
  8. {
  9. echo "==INSIDE Function=="
  10. echo "Global"
  11. t0=$(exit 1)
  12. echo $? # 1
  13. # 如预期一样.
  14. echo
  15. echo "Local declared & assigned in same command."
  16. local t1=$(exit 1)
  17. echo $? # 0
  18. # 意料之外!
  19. # 显然,变量赋值发生在Apparently,
  20. #+ 局部声明之前。
  21. #+ 返回值是为了latter.
  22. echo
  23. echo "Local declared, then assigned (separate commands)."
  24. local t2
  25. t2=$(exit 1)
  26. echo $?
  27. }
  28. function0

24.2.1 局部变量和递归

递归是一个有趣并且有时候非常有用的自己调用自己的形式。 Herbert Mayer 是这样定义递归的,“。。。表示一个算法通过使用一个简单的相同算法版本。。。”

想象一下,一个定义是从自身考虑的,[2] 一个表达包含了自身的表达, [3] 一条蛇吞下自己的尾巴, [4] 或者 。。。 一个函数调用自身。[5]

例子 24-13. 一个简单的递归函数表示

  1. #!/bin/bash
  2. # recursion-demo.sh
  3. # 递归演示.
  4. RECURSIONS=9 # 递归的次数.
  5. r_count=0 # 必须是全局变量,为什么?
  6. recurse ()
  7. {
  8. var="$1"
  9. while [ "$var" -ge 0 ]
  10. do
  11. echo "Recursion count = "$r_count" +-+ \$var = "$var""
  12. (( var-- )); (( r_count++ ))
  13. recurse "$var" # 函数调用自身(递归)
  14. done #+ 直到遇到什么样的终止条件?
  15. }
  16. recurse $RECURSIONS
  17. exit $?
  18. `

例子 24-14. 另一个简单的例子

  1. #!/bin/bash
  2. # recursion-def.sh
  3. # 另外一个描述递归的比较生动的脚本。
  4. RECURSIONS=10
  5. r_count=0
  6. sp=" "
  7. define_recursion ()
  8. {
  9. ((r_count++))
  10. sp="$sp"" "
  11. echo -n "$sp"
  12. echo "\"The act of recurring ... \"" # Per 1913 Webster's dictionary.
  13. while [ $r_count -le $RECURSIONS ]
  14. do
  15. define_recursion
  16. done
  17. }
  18. echo
  19. echo "Recursion: "
  20. define_recursion
  21. echo
  22. exit $?

局部变量是一个写递归代码有效的工具,但是这种方法一般会包含大量的计算负载,显然在shell脚本中并不推荐递归.[6]

例子24-15. 使用局部变量进行递归

  1. #!/bin/bash
  2. # 阶乘
  3. # ---------
  4. # Bash允许递归么?
  5. # 恩,允许,但是...
  6. # 他太慢了,所以恐怕你难以忍受。
  7. MAX_ARG=5
  8. E_WRONG_ARGS=85
  9. E_RANGE_ERR=86
  10. if [ -z "$1" ]
  11. then
  12. echo "Usage: `basename $0` number"
  13. exit $E_WRONG_ARGS
  14. fi
  15. if [ "$1" -gt $MAX_ARG ]
  16. then
  17. echo "Out of range ($MAX_ARG is maximum)."
  18. # 现在让我们来了解一些实际情况。
  19. # 如果你想计算比这个更大的范围的阶乘,
  20. #+ 应该用真正的编程语言来重写它。
  21. exit $E_RANGE_ERR
  22. fi
  23. fact ()
  24. {
  25. local number=$1
  26. # 变量"number" 必须被定义为局部变量,
  27. #+ 否则不能正常工作。
  28. if [ "$number" -eq 0 ]
  29. then
  30. factorial=1 # 0的阶乘为1.
  31. else
  32. let "decrnum = number - 1"
  33. fact $decrnum # 递归的函数调用 (就是函数调用自己).
  34. let "factorial = $number * $?"
  35. fi
  36. return $factorial
  37. }
  38. fact $1
  39. echo "Factorial of $1 is $?."
  40. exit 0

也可以参考例子 A-15,一个包含递归例子的脚本。我们意识到递归同时也意味着巨大的资源消耗和缓慢的运行速度,因此它并不适合在脚本中使用。

注释

[1] 然而,如Thomas Braunberger 指出的那样,一个函数里定义的局部变量对于调用它的父函数也是可见的。

  1. #!/bin/bash
  2. function1 ()
  3. {
  4. local func1var=20
  5. echo "Within function1, \$func1var = $func1var."
  6. function2
  7. }
  8. function2 ()
  9. {
  10. echo "Within function2, \$func1var = $func1var."
  11. }
  12. function1
  13. exit 0
  14. # 脚本的输出:
  15. # Within function1, $func1var = 20.
  16. # Within function2, $func1var = 20.

在Bash手册里是这样描述的:

“局部变量只能在函数内部使用; 它让变量名的可见范围限制在了函数内部以及它的孩子里” [emphasis added]
The ABS Guide的作者认为这个行为一个bug.

[2] 被熟知为冗余。

[3] 被熟知为同义反复。

[4] 被熟知为暗喻。

[5] 被熟知为递归函数。

[6] 太多的递归层次可能会引发一个脚本的段错误。

  1. #!/bin/bash
  2. # 提醒: 运行这个脚本可能会让你的系统卡死。
  3. # 如果你够好运的话,在耗尽可用内存之前,它会发生一个段错误。
  4. recursive_function ()
  5. {
  6. echo "$1" # 让函数做一些事情,加快发生段错误。
  7. (( $1 < $2 )) && recursive_function $(( $1 + 1 )) $2;
  8. # 只要第一个参数小于第二个参数,
  9. #+ 让第一个参数加1,然后递归。
  10. }
  11. recursive_function 1 50000 # 递归 50,000层!
  12. # 很可能发生段错误(依赖于栈的大小,通过ulimit -m可以设置栈的大小)
  13. # 即使是C语言,递归调用这么多层也会发生段错误,
  14. #+ 通过分配栈耗尽所有的内存。
  15. echo "This will probably not print."
  16. exit 0 # 这个脚本可能不会正常退出。
  17. # 感谢, Stéphane Chazelas.