7. 内部库

7.1 instance_methods(true)返回什么?

klass.instance_methods只返回某klass类(或模块)中所定义的实例方法, 而klass.instance_methods(true)则会返回所有的实例方法, 包括从超类中继承来的实例方法。(但仅限于public方法)

private_instance_methodsprotected_instance_methods的参数的意义相同。

7.2 为什么rand总是生成相同的随机数?

ruby 1.4.2以前的版本中, 每次执行程序时rand都会生成相同的随机数。为了生成不同的随机数, 每次都必须使用srand来给出不同的随机数种子。若不带参数地调用srand的话, 它将使用当时的时间作为种子, 因此可以产生出不同的随机数。

7.3 怎样从0到51中选出5个不重复的随机数呢?

下面的方法可以从0到n中选出m个随机数,并以数组的形式返回结果。

def sample(n, m)
  if m.zero?
    []
  else
    s = sample(n-1, m-1)
    t = rand(n+1)
    s.concat s.include?(t) ? [n] : [t]
  end
end

若不使用递归的话, 可以写成这样。

def sample(n, m)
  s = []
  ((n-m)...n).each do |j|
    t = rand(j+2)
    s.concat s.include?(t) ? [j+1] : [t]
  end
  s
end

7.4 FixnumSymboltruenilfalse这些立即值与引用有什么不同?

不能定义特殊方法。

另外, 表示同一个数字的Fixnum的实例通常都是相同的, 所以定义实例变量时, 它们的所指也是相同的。

7.5 nilfalse有什么不同?

nil.methods - false.methodsfalse.methods - nil.methods执行情况的差异就说明了nil和false所带方法的不同。

若某方法返回一个布尔值时,通常会返回truefalse; 除此以外,方法通常返回值或nil

通常情况下, 带?的方法会返回布尔值, 当然也有例外(在标准配置中只有nonzero?)。

7.6 为什么读入文件并修改之后, 原文件依然没有变化?

open("example", "r+").readlines.each_with_index{|l, i|
  l[0,0] = (i+1).to_s + ": "}

上面的代码并不能将行号添加到example中。这是因为该代码并没有修改原文件, 而只是修改了readlines读入的字符串而已。必须将修改后的内容写入文件才行。

io = open("example", "r+")
ary = io.readlines
ary.each_with_index{|l, i| l[0,0] = (i+1).to_s + ": "}
io.rewind
io.print ary
io.close

此时因为文件增大了, 所以没有出现什么问题. 如果文件变小的话,就必须将下列代码

io.flush
io.truncate(io.pos)

嵌入到io.close之前才行.

7.7 怎样覆盖同名文件?

使用命令行选项-i或者将内部变量$-i设为""之后, 就可以覆盖同名文件了。

您可以使用下列代码。

$ ruby -i -ne 'print "#$.: #$_"' example

若想保留原来的文件时, 可以-i.bak这样。

7.8 写文件后拷贝该文件,但所得副本并不完整,请问原因何在?

open('file', 'w').print "This is a file.\n"
system 'cp file copy'

上述代码之所以出错,是因为拷贝文件时有一部分内容尚未被flush进file中所致。因此,请先close然后再进行拷贝。

f = open('file', 'w')
f.print "This is a file.\n"
f.close
system "cp file copy"

7.9 在管道中将字符串传给less后, 为什么看不到结果?

f = open '|less', 'w'
f.print "abc\n"

上述代码会立刻结束,您无法看到less的显示结果。添加一个close,以便等待less完成显示。

f = open '|less', 'w'
f.print "abc\n"
f.close

第一行也可以写成f = IO.popen 'less', 'w'

7.10 无法引用的File对象将会何去何从?

open("file").read中的File是无法引用的, 它会被后来的垃圾回收器所close和销毁.

7.11 怎样手动关闭文件?

GC会自动关闭不再使用的File对象. 若您想自己关闭文件的话, 可以从下面4种方法中任选其一(按照代码长度排列)。

  1. File.foreach('filename') {|line| print line }
    
  2. File.readlines('filename').each {|line| print line }
    
  3. File.open('filename') {|f|
      f.each {|line| print line }
    }
    
  4. begin
      f = File.open('filename')
      f.each {|line| print line }
    ensure
      f.close if f
    end
    

7.12 如何按照更新时间的新旧顺序来排列文件?

Dir.glob("*").collect{|f| [File.mtime(f), f] }.
        sort{|a,b| b[0]<=>a[0] }.collect{|e| e[1] }

上述代码将会按照更新时间的新旧顺序来排列当前目录中(除了"."、".."以外)的文件, 并以数组的形式返回结果. 若想按照相反顺序来排序的话, 就可以去掉sort后面的块。

Dir.glob("*").sort{|a,b| File.mtime(b)<=>File.mtime(a)}

尽管这样可以完成排序, 但是每次都要访问文件来获取更新时间, 因而排序过程较慢。

为解决这个问题, 可以先生成一个对象, 它负责将每个文件的更新时间存入高速缓存, 这样就可以提高处理速度。

cache = {}
def cache.mtime(x)
  self[x] ||= File.mtime(x)
end
Dir.glob("*").sort{|a,b| cache.mtime(b) <=> cache.mtime(a)}
cache = nil

另外,在ruby 1.7中提供了Enumerable#sort_by方法, 使用它可以轻松完成上述任务。

Dir.glob("*").sort_by {|f| File.mtime(f)}.reverse

7.13 如何获取文件中单词的出现频度?

将哈希表的默认值设为0之后, 可以这样处理。

freq = Hash.new(0)
open("file").read.scan(/\w+/){|w| freq[w] += 1}
freq.keys.sort.each {|k| print k, "--", freq[k], "\n"}

7.14 为什么条件表达式中的空字符串表示true呢?

在Ruby中,只有nilfalse表示假, 其他的都表示真。若想判断某字符串是否为空时, 可以将它与""作比较,或者使用empty?, 或者将其length0作比较 即可得出结果。

7.15 如何按照字典顺序来排列英文字符串数组?

您可以这样排序

ary.collect{|f| [f.downcase, f]}.sort.collect{|e| e[1]}

downcase形式相等时, 则按照原来字符串来排序。

7.16 "abcd"[0]会返回什么?

它将返回字符a的代码97(Fixnum)。若想验证它与a是否一致时,可以使用?a进行比较。

7.17 怎么把tab变成space?

常用方法如下。

# 非破坏性的方法
def expand_tab( str )
  str.gsub(/([^\t]{8})|([^\t]*)\t/n) { [$+].pack("A8") }
end

# 破坏性的方法
def expand_tab!( str )
  1 while str.sub!(/(^[^\t]*)\t(\t*)/) { $1 + ' ' * (8-$1.size%8+8*$2.size) }
end

# 破坏性的方法 (2)
def expand_tab!( str )
  1 while str.sub!(/\t(\t*)/) {' ' * (8-$~.begin(0)%8+8*$1.size) }
end

7.18 如何对反斜线进行转义操作?

在正则表达式中, Regexp.quote('\\')就可以实现转义。

若使用gsub的话, 在gsub(/\\/, '\\\\')中,进行句法分析时会将替换字符串变为'\\', 而实际替换还会将其解释成'\', 所以必须写成gsub(/\\/, '\\\\\\')才行。若使用\&来表示匹配字符串的话, 则可以写成gsub(/\\/,'\&\&')

另外, 若使用形如gsub(/\\/){'\\\\'}的块的话, 就只会进行一次转义, 因此可以得到预期的结果。

7.19 subsub!的区别在哪里?

sub不会改变被调的状态。首先,它会拷贝字符串, 然后在副本中进行替换操作,并返回结果(若没有必要替换时,就直接返回副本)。

sub!则会改变被调本身。若没有进行替换的必要时, 将返回nil

sub!这种会修改被调本身的方法叫做破坏性的方法。在Ruby中, 若同时存在同名的破坏性方法和非破坏性方法时, 通常在破坏性方法名后添加!。

def foo(str)
  str = str.sub(/foo/, "baz")
end

obj = "foo"
foo(obj)
print obj
#=> "foo"

def foo(str)
  str = str.sub!(/foo/, "baz")
end

foo(obj)
print obj
#=> "baz"

sub!这样的破坏性的方法可能会带来非预期的效果, 请慎用。

7.20 \Z匹配什么?

当字符串末尾字符不是\n时,\Z匹配于字符串的末尾字符;当字符串的末尾字符是\n时,它匹配于换行之前的部分。

若想让它一直匹配于末尾字符,而不理会\n的问题时,应该使用\z。

7.21 范围对象中的.....有什么不同?

..包含终点,而...不包含终点。

7.22 有函数指针吗?

若使用Proc.newproclambda生成了Proc对象的话,就可以将其用作函数指针。

另外,MethodUnboundMethod对象也很类似于函数指针。

7.23 线程和进程fork有何异同?

线程和进程fork分别具有以下特点:

*系统颠簸是指,当进程或线程过多时,因处理它们的切换问题而消耗了过多的系统时间,所导致的系统无效。

通常不应该混用进程fork和线程。

因为Ruby线程采用分时机制,所以使用进程后并不会加快处理速度。由于Ruby线程是用户线程,因此无法享受多处理机的好处。

7.24 如何使用Marshal?

它可以将对象转变为字节串(即serialize)。您可以将对象存入文件,以后再将其还原,或者通过网络把它发送出去。例如,您可以这样把obj对象转为字节串

Marshal.dump(obj)

该方法返回字符串,因此可以将其存入文件。

File.open('filename', 'w') {|f|
    f.write Marshal.dump(obj)
}

因为经常需要进行上述操作,所以Ruby提供了下列简便方法。

Marshal.dump(obj, io)

io表示可写入的IO对象。另外使用上述方法处理较大的对象时,就省去了生成巨大字符串的麻烦。

另一方面,您可以像下面这样来还原对象。首先是使用字符串进行还原的例子。

obj = Marshal.load(str)

下面的是直接使用IO对象进行还原。

obj = Marshal.load(io)

7.25 Ruby有异常处理语句吗?

有。通常使用下列语句。

begin
  (发生异常之前所处理的事务)
rescue (异常类)
  (发生异常时的处理)
else
  (没有异常时的处理)
ensure
  (必须处理的事务)
end

begin语句中,若发生异常就会执行rescue部分。若没有异常就执行else部分。另外,不管有没有异常,都必须执行ensure部分。rescue,elseensure部分都是可以省略的。若rescue后面没有给出异常类,就使用StandardError作为其默认值。此时将捕捉StandardError的子类。

该语句的值取决于执行ensure部分之前的值。

另外,您可以使用全局变量$!来获得最后发生的异常,使用$!.class来了解该异常的种类。

7.26 如何使用trap

下面的代码在捕捉到SIGPIPE信号时会执行块的内容(随即引发异常)。

trap("PIPE") {raise "SIGPIPE"}

7.27 如何统计文件的行数?

若假定文件末尾也有个换行符的话,下面的方法可能是最简单的。

open("filename").read.count("\n")

7.28 怎样把数组转化为哈希表?

您可以这样把数组array转化为哈希表

h = Hash[*array]

此时array中的奇数元素将成为哈希表h的键,而偶数元素将成为相对应的值。数组array中的元素个数必须是偶数。

array = [1,2,3,4]
h = Hash[*array]
p h

=> {1=>2, 3=>4}

数组array前面的"*"表示展开参数。

7.29 将字符串变为Array时可以使用%w(...),那么将字符串变为Hash时能不能如法炮制呢?

您不能像生成Array那样来直接生成Hash,但您可以这样

p h = Hash[*%w(1 foo 2 bar)]

=> {"1"=>"foo", "2"=>"bar"}

先生成Array,然后将其转化为Hash。(但此时哈希表的键和值都被限定为字符串)

7.30 为何无法捕捉NameError异常呢?

在1.4以前的版本中,下列代码中的rescue部分都可以正常运行,可是到了1.6版本就不行了。

begin
  foo
rescue
  puts "TRAP : #$!"
end

-:2: undefined local variable or method `foo' for #<Object:0x401bece0> (NameError)
ruby 1.6.7 (2002-03-20) [i586-linux]

这是因为NameError异常类不再是StandardError的子类所致(若不指定异常类时,rescue将只负责捕捉StandardError旗下的异常类)。此时,必须显式地指定NameError才行。

begin
  foo
rescue NameError
  puts "**TRAP** : #$!"
end

ruby 1.6.7 (2002-03-20) [i586-linux]
**TRAP** : undefined local variable or method `foo' for #<Object:0x401bece0>

可是到了1.7版本时,NameError类又重新成为StandardError的子类。请您参考异常类中的类层次构造来了解默认情况下所能捕捉的异常的范围。

7.31 为什么有succ却没有prev呢?

虽然定义Integer#prev并不困难,但却没有succ实用。

另一方面,定义String#prev则比较困难。例如

'09'.succ == '9'.succ #=> true

因为有时succ前和后的字符串并不是一一对应的,所以str.prev.succ == str.succ.prev未必成立。