关于递归的讨论

我们的程序需要能够将整个子目录树向下导航到任意数量的级别。为了能够做到这一点,我们必须使用递归。

什么是递归(Recursion)?

简单的说,递归方法就是调用它自己的。如果你不熟悉递归编程,请参阅本章末尾的“深入探索”部分中的“简单递归”。
file_info.rb

在程序 file_info.rb 中,processfiles 方法是递归的:

  1. def processfiles( aDir )
  2. totalbytes = 0
  3. Dir.foreach( aDir ){
  4. |f|
  5. mypath = "#{aDir}\\#{f}"
  6. s = ""
  7. if File.directory?(mypath) then
  8. if f != '.' and f != '..' then
  9. bytes_in_dir = processfiles(mypath) # <==== recurse!
  10. puts( "<DIR> ---> #{mypath} contains [#{bytes_in_dir/1024}] KB" )
  11. end
  12. else
  13. filesize = File.size(mypath)
  14. totalbytes += filesize
  15. puts ( "#{mypath} : #{filesize/1024}K" )
  16. end
  17. }
  18. $dirsize += totalbytes
  19. return totalbytes
  20. end

你将看到,当首次调用该方法时,向下到源代码的底部,它将在变量 dirname 中传递一个目录的名称:

  1. processfiles( dirname )

我已经将当前目录的父级(由两个点给出,"..")分配给 dirname。如果你在其原始位置运行此程序(即,从本书的源代码存档中提取其位置),则将引用包含所有示例代码文件的子目录的目录。或者,你可以将硬盘上某个目录的名称分配给代码中指定的变量 dirname。如果你这样做,不要指定包含大量文件和目录的目录(“C:\ Program Files” 不是一个好的选择!),因为程序需要一些时间来执行。

让我们仔细看看 processfiles 方法中的代码。再次,我使用 Dir.foreach 查找当前目录中的所有文件,并一次传递一个文件 f,由花括号之间的块中的代码处理。如果 f 是一个目录但不是当前目录(".")或其父目录(".."),那么我将目录的完整路径传递回 processfiles 方法:

  1. if File.directory?(mypath) then
  2. if f != '.' and f != '..' then
  3. bytes_in_dir = processfiles(mypath)

如果 f 不是目录,而只是一个普通的数据文件,我用 File.size 计算它的大小(以字节为单位)并将其分配给变量 filesize

  1. filesize = File.size(mypath)

由于每个连续文件 f 由代码块处理,因此计算其大小并将此值添加到变量 totalbytes

  1. totalbytes += filesize

将当前目录中的每个文件传递到块后,totalbytes 将等于目录中所有文件的总大小。

但是,我还需要计算所有子目录中的字节数。由于该方法是递归的,因此这是自动完成的。请记住,当 processfiles 方法中大括号之间的代码确定当前文件f是一个目录时,它会将此目录名称传递回自身 - processfiles 方法。

让我们假设首先使用 C:\test 目录调用 processfiles。在某些时候,变量 f 被赋予其子目录之一的名称 - 比如 C:\test\dir_a。现在这个子目录被传递回 processfiles。在 C:\test\dir_a 中找不到更多目录,因此 processfiles 只计算该子目录中所有文件的大小。当它完成计算这些文件时,processfiles 方法结束并将当前目录中的字节数 totalbytes 返回到首先调用该方法的代码位置:

  1. return totalbytes

在这种情况下,processfiles 方法本身内部的这段代码以递归方式调用 processfiles 方法:

  1. bytes_in_dir = processfiles(mypath)

因此,当 processfiles 完成处理子目录 C:\test\dir_a 中的文件时,它返回在那里找到的所有文件的总大小,并将其分配给 bytes_in_dir 变量。processfiles 方法现在从它停止的地方继续(也就是说,它从它自己处理子目录的地方继续)以处理原始目录 C:\test 中的文件。

无论此方法遇到多少级别的子目录,每当它找到目录时都会调用它自己的事实确保它会自动沿着它找到的每个目录路径向下移动,计算每个子目录中的总字节数。

最后要注意的是,在每个递归级别完成时,分配给 processfiles 方法内部声明的变量的值将更改回其“之前”的值。因此,totalbytes 变量首先包含 C:\test\test_a\test_b 的大小,然后是 C:\test\test_a 的大小,最后是 C:\test 的大小。为了保证运行结果总和是所有目录的组合大小,我们需要将值分配给在方法外部声明的变量。为此,我使用全局变量 $dirsize 来实现这个目的,将处理的每个子目录计算的 totalbytes 值增加到该变量:

  1. $dirsize += totalbytes

顺便提一下,虽然字节(byte)对于非常小的文件来说可能是很方便的测量单位,但通常更好的是以千字节(kilobyte)描述更大的文件,以兆字节(megabytes)描述非常大的文件或目录。要将字节转换为千字节或将千字节转换为兆字节,你需要除以 1024。要将字节转换为兆字节,除以 1048576。

我程序中的最后一行代码执行这些计算,并使用 Ruby 的 printf 方法以格式化字符串显示结果:

  1. printf( "Size of this directory and subdirectories is #{$dirsize} bytes, #{$dirsize/1024}K, %0.02fMB", "#{$dirsize/1048576.0}" )

请注意,我在第一个字符串中嵌入了格式化占位符 “%0.02fMB”,并在逗号后面添加了第二个字符串:

  1. "#{$dirsize/1048576.0}".

第二个字符串计算目录大小(以兆字节为单位),然后将该值替换为第一个字符串中的占位符。占位符的格式选项 "%0.02f" 确保兆字节值显示为浮点数 "f",带有两个小数位,"0.02"