一、心得体会
1、今天完成了什么?
- 今天看了20页的镐头书(139-160)
- 看了10个controller
2、今天收获了什么?
- Ruby中的循环有哪些?
- 捕获、异常和抛出是怎么工作的?
- 含有异常信息的数据包(package)是Exception类或其子类的一个对象。
- Ruby将相关的Exception对象的引用放在全局变量$!中,这与任何随后的异常处理不相干。不带任何参数的调用raise,它会重新引发$!中的异常。举个栗子:
op_file = File.open(opfile_name, "w")
begin
# 这段代码引发异常会被下面的rescue语句捕获
while data = socket.read(512)
op_file.write(data)
end
rescue SystemCallError
$stderr.print "IO failed: " + $!
op_file.close
File.delete(opfile_name)
raise
end
- 10个controller:取送、取送日志、取送跟进、取送跟进、疑难原因、区县(districts)、礼物(gifts)、催单(hastens)、盘点(inventories)、盘点订单
3、今天犯了哪些错误?
4、明天需要做哪些工作?
二、读书笔记
昨天我学到的最重要的是什么?
Ruby有几种循环:
- while
- until
- 迭代器循环,如times、upto(递增)、downto(递减)、each、step
- For..in
7.6.3 Break、Redo和Next
循环控制结构break,redo和next可以让你改变循环或者迭代的正常流程。
- break终止最接近的封闭循环体,然后继续执行block后面的语句。
- redo从循环头重新执行循环,但不重计算循环条件表达式或者获得迭代中的下一个元素。
- next跳到本次循环的末尾,并开始下一次迭代
while line = gets
next if line =~ /^\s*#/ # skip comments
break if line =~/^END/ # stop at end
redo if line.gsub!(/'(.*?)'/) { eval($1) }
# process line ...
end
这些关键字还可以和任何基于迭代的循环机制一起使用。
i = 0
loop do
i += 1
next if i <3
print i
break if i > 4
end
输出结果:
345
7.6.4 Retry
redo语句使得一个循环重新执行当前的迭代,但是有时你需要从头重新执行一个循环,retry语句就是做这件事的,它重新执行任意类型的迭代式循环。
for i in 1..100
print "Now at #{i}. Restart? "
retry if gets =~ /^y/i
end
交互式地运行这段代码,你会看到:
Now at 1. Restart? n
Now at 2. Restart? y
Now at 1. Restart? n
retry在重新执行之前会重新计算传递给迭代的所有参数。下面是一个DIY的until循环的例子。
def do_until(cond)
break if cond
yield
retry
end
i = 0
do_until(i>10) do
print i, " "
i += 1
end
7.7 变量作用域、循环和Blocks
while、until和for循环内建到了RUby语言中,但没有引入新的作用域,前面已经存在的局部变量可以在循环中使用,而循环中新创建的局部变量也可以在循环后使用。
被迭代器使用的block(比如loop和each)与此略有不同,通常,在这些block中创建的局部变量无法在block外访问。
[1, 2, 3 ].each do |x|
y = x + 1
end
[x, y]
输出结果:
NameError: undefined local variable or method `y' for main:Object
from (irb#1):126
然而,如果执行block的时候,一个局部变量已经存在且与block中的变量同名,那么block将使用此已有的局部变量,因而,它的值在块后面仍然可以使用。
如下面的例子所示,此即适用于block中的普通变量也适用于block的参数。
x = nil
y = nil
[1, 2, 3].each do |x|
y = x + 1
end
[x, y] -> [3, 4]
为什么终端结果是这样的:
[nil, 4]
注意在外部作用域中变量不必有值:Ruby解释器只需要看到它即可。
if false
a = 1
end
3.times {|i| a = i}
第8章
异常、捕获和抛出(Exceptions,Catch and Throw)
到目前为止,我们都是在欢乐谷中开发代码,每个程序库的调用都是成功的,用户从来没有输入不准确的数据。但是,在现实世界中,错误会发生,好的程序可以预见它们的发生,然后优雅地处理这些错误,但这并非总像听起来那么简单,通常检测到错误出现的那部分代码,缺少有关如何处理它的上下文信息。
举个栗子:
试图去打开一个并不存在的文件,在一些情况下是可行的的,但在别的情况下却是致命的错误,文件处理模块要如何做呢?
传统的做法是使用返回码。open方法在失败时会返回一些特定值,然后这个值会沿着调用例程的层次往回传播,直到有函数想要处理它。
这种做法问题是,管理所有这些错误码是一件痛苦的事情。如果函数首先调用open,然后调用read,最后调用close方法,而且每个方法都有可能返回错误标识,那么当函数将返回码返回给调用者时,该如何区分这些错误码呢?
异常在很大程度上解决了这个问题,异常允许把错误信息打包到一个对象中,然后该异常对象被自动传播调用栈(calling stack),直到运行系统找到明确声明直到如何处理这类异常的代码为止。
8.1 异常类(The Ecxeption Class)
含有异常信息的数据包(package)是Exception类、或其子类的一个对象。Ruby预定义了一个简洁的异常层次结构,这个层次结构使得处理异常变得相当简单。
当需要引发(raise)异常时, 可以使用某个内建的Exception类,或者创建自己异常类。如果创建自己的异常类,可能你希望它从StandardError类或其子类派生,否则,你的异常在默认情况下不会被捕获。
每个Exception都关联有一个消息字符串和栈回溯信息(backtrace)。如果定义自己的异常,可以添加额外的信息。
8.2 处理异常(Handing Exception)
我们的点唱机用TCP套接字从互联网下载歌曲。它的基本代码很简单(假设文件名个套接字都已经创建好)。
op_file = File.open(opfile_name, "w")
while data = socket.read(512)
op_file.write(data)
end
如果下载过程中得到一个致命错误,会发生什么呢?我们肯定不想在歌曲列表中存储一首不完整的歌曲。“I Did it My...”
让我们添加一些处理异常的代码,看看它是如何帮助处理异常的。在一个beigin/end块中,使用一个或多个rescue语句告诉Ruby希望处理的异常类型。
在这个特定的例子中,我们感兴趣的是捕获SystemCallError异常(同时暗含着任何SystemCallError子类的异常),所以它就是出现在resuce行的异常类型,在这个错误处理block中,我们报告了错误,关闭和删除了输出文件,同时重新引起异常。
op_file = File.open(opfile_name, "w")
begin
#这段代码引发异常会被
#下面的rescue语句捕获
while data = socket.read(512)
op_file.write(data)
end
rescue SystemCallError
$stderr.print "IO failed: " + $!
op_file.close
File.delete(opfile_name)
raise
end
当异常被引发时,Ruby将关于Exception对象的引用放在全局变量$!中,这与任何随后的异常处理不相干。
Exception
fatal(used internally by Ruby)
NoMemoryError
-
ScriptError
- LoadError
- NotImplementError
- SyntaxError
-
SignalException
- Interrupt
-
StandardError
- ArgumentError
- IOError
- EOFError
- IndexError
- LocalJumpError
- NameError
- NoMethodError
- RangeError
- FloatDomainError
- RegexpError
- RuntimeError
- SecurityError
- SystemCallError
- system-dependent exception(Errno::XXX)
- ThreadError
- TypeError
- ZeroDivisionError
SystemExit
SystemStackError
这个感叹号大概反映出了我们的惊讶,我们的代码竟然会导致错误!在前面的例子中,我们用$!变量去格式化错误信息。
关闭和删除文件后,我们可以不带任何参数来调用raise,它会重新引导$!中的异常,这是一个有用的技术,它允许我们先编写代码过滤掉一些异常,再把不能处理的异常传递到更高的层次。这几乎就像实现了一个错误处理的继承层次结构。
在begin块中可以有多个rescue子句(clause),每个rescue子句可以指示捕获多个异常,在rescue子句的结束处,你可以提供一个Ruby的局部变量名来接收匹配的异常,许多人发现这比导出使用$!有更好的可读性。
begin
eval string
rescue SyntaxError, NameError => boom
print "String doesn't compileL: " + boom
rescue Standard => bang
print "Error running script: " + bang
end
RUby如何决定执行哪个rescue子句呢?
这个处理非常类似于对case语句的处理,RUby用引发的异常依次比较begin块中每个rescue子句的每个参数。如果引发的异常匹配了一个参数,Ruby就执行rescue的程序体,同时停止比较。
匹配是用parameter ===$!完成的。对于大多数异常来说,如果rescue子句给出的类型,与当前引发的异常的类型相同,或者它是引发异常的超类(superclass),这意味着匹配是成功的。如果编写一个不带参数表的rescue子句,它的默认参数是StandardError。
注意:可以这样进行比较的原因是:异常是类,而类进而是某种Module。===方法是为模块定义的,如果操作数的类与接收者相同或者接收者的祖先,这个方法返回true。
如果没有任何rescue子句与之匹配,或者异常在begin/end块外面被引发,Ruby就沿着调用栈向上查找,在调用者上寻找异常的处理者,接着在调用者的调用者上寻找,依次类推。
尽管,rescue子句的参数通常是Exception类的名称,实际上它们可以是任何返回的Exception类的表达式(包括方法调用)。
8.2.1 系统错误(System Errors)
当对操作系统的调用返回错误码时,会引发系统错误,在POSIX系统上,这些错误名称有诸如EAGAIN和EPERM等(在Unix机器上,键入man error,你会得到这些错误的列表)。
Ruby得到这些错误,把每个错误装(wrap)到特定的对象中,每个错误都是SystemCallError的子类,定义在Errno模块中,这意味着,你会发现类名如Errno::EAGIN, ERRno::EIO和Errno::EPERM等的异常,如果想得到底层的系统错误码,则把每个Errno异常对象有一个Errno(有点令人疑惑)的类常量(class constant),它包含相应的系统错误。
Errno::EAGAIN::Errno -> 35
Errno::EPERM::Errno -> 1
注意到EWOULDBLOCK和EAGAIN有相同的错误码,这是我电脑上的操作系统的一个特性——两个常量映射到相同的错误码。为了处理这种情况,Ruby做出了安排,让Errno::EAGAIN和Errno::EWOULDBOCK在rescue子句中被等同对待,如果你要求rescue其中一个错误,那么另一个也会被rescue。通过定义SystemCallError#===可以做到这点,因此,如果要比较SystemCallError的两个子类,是比较它们的错误码而不是它们在层次结构中的位置。
8..2.2 善后(Tidying Up)
有时候你需要宝恒一些处理在block结束时能够被执行,而不管是否有异常引发。比如,也许在block的入口处(entry)打开一个文件,需要确保当block退出时它会被关闭。
ensure子句就是做这个的。ensure跟在最后的rescue子句后面,它包含一段block退出时总是被执行的代码。不管block是否正常退出,是否引发并rescue异常,或者是否被捕获的异常终止——这个ensure块总会得到运行。
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
ensure
f.close unless f.nil?
end
尽管不是那么有用,else子句是一个类似ensure子句的构造,如果存在的话,它会出现在rescue子句之后和任何一个ensure子句之前。else子句的程序体,只有当主代码体没有引发任何异常时才会被执行。
f = File.open("testfile")
begin
# ..
rescue
# ..
else
# ..
puts "congratulations -- no errors!"
ensure
f.close unless f.nil
end
8.2.3 再次执行(Play It Again)
有时候也许可以纠正异常的原因。在这些例子中,你可以在rescue子句中使用retry语句去重复执行整个begin/end区块。显然这很可能导致无线循环,所以使用这个特性应该倍加小心(同时把一根手指轻轻放在键盘的中断键上,随时准备着)。
下面的例子再出现异常时会重新执行,它来自Minero Aoki的net/stmp.rb。
@esmtp = true
begin
#首先尝试扩展登录,如果因为服务器不支持而失败
#则使用正常登陆
if @esmtp then
@command.ehlo(helodom)
else
@command.helo(helodom)
end
rescue ProtocolError
if @esmtp then
@esmtp = false
retry
else
raise
end
end
这段代码首先使用EHLO命令试图连接SMTP服务器,而这个命令并没有被广泛支持,如果连接尝试失败了,则设置@esmtp变量为false,同时重试连接。如果第二次连接也失败了,则引发异常给它的调用者。
8.3 引发异常(Raising Exceptions)
到目前为止,我们一直都处于守势,处理那些被别人引发的异常,该是轮到我们进攻的时候了(有些人说本书这些温和的作者总是咄咄逼人,但那是另外一本书)。
可是使用Kernel.raise方法在代码中引发异常(或者它有点判决意味的同义词,Kernel.fail)
raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller
第一种形式只是简单地重新引发当前异常(如果没有当前异常的话,已发RuntimeError)。这种形式用于首先截获异常再将其继续传递的异常处理方法中。
第二种形式创建新的RuntimeError异常,把它的消息设置为指定的字符串,然后异常随着调用栈向上引发。
第三种形式使用第一个参数创建异常,然后把相关联的消息设置给第二个参数,同时把栈信息(trace)设置给第三个参数。通常,第一个参数是Exception层次结构中某个类的名称,或者是某个异常类的对象实例的引用,通常使用Kernel.caller方法产生栈信息。
下面是使用raise的典型例子。
raise
raise "Missing name" if name.nil?
if i >= names.size
raise IndexError, "#{i} >= size (#{names.size})"
end
raise ArgumentError, "Name too big", caller
最后这个例子从栈回溯信息删除当前函数,这在程序库模块中十分有用。可以更进一步:下面的代码通过只将调用栈的子集传递给新异常,从而达到从栈回溯信息中删除两个函数的目的。
raise ArgumentError, "name too big", caller[1..-1]
8.3.1 添加信息到异常(Adding Information to Exceptions)
你可以定义自己的异常,保存任何需要从错误发生地传递出去的信息。
例如,取决于外部环境,某种类型的网络错误可能是暂时的。如果这种错误发生了,而环境是适宜的,则可以在异常中设置一个标志,告诉异常处理程序重试这个操作可能是值得。
class RetryExceptipn < RuntimeError
attr :ok_to_retry
def initialize(ok_to_retry)
@ok_to_retry = ok_to_retry
end
end
注意:从技术层面讲,这个参数可以是任何对象,只要它能响应消息Exception,且这个消息返回一个能够满足object.kind_of?(Exception)为真的对象。
在下面的代码里面,发生了一个暂时的错误。
def read_data(socket)
data = socket.read(512)
if data.nil?
raise RetryException.new(true), "transient read error"
end
# .. 正常处理
end
在上一级的调用栈处理了异常。
begin
stuff = read_data(socket)
# .. process stuff
resuce RetryException => detail
retry of detail.ok_to_retry
raise
end
8.4 捕获和抛出(Catch and Throw)
尽管raise和rescue的异常机制对程序出错时终止执行已经够用了,但是如果在正常处理过程期间能够从一些深度嵌套的结构中跳转出来,则是很棒的,catch和throw应运而生,可以方便地做到这点。
catch (:done) do
while line = gets
throw :done unless fields = line.split(/\t/)
songlist.add(Song.new(*fields))
end
songlist.play
end
catch定义了以给定名称(可能符号或字符串)为标签的block,这个block会正常执行直到晕倒throw为止。
当Ruby碰到throw,它迅速回溯(zip back)调用栈,用匹配的符号寻找catch代码块,当发现它之后,Ruby将栈清退(unwind)到这个为止并终止该block。所以,在前面的例子中,如果输入没有包含正确格式化的行,throw会跳到相应的catch代码块的结束处,不仅终止了while循环,而且跳出了歌曲列表的播放。
如果调用throw时制定了可选的第二个参数,这个值会作为catch的值返回。
在下面的例子中,如果响应任意提示符时键入!,使用throw终止与用户的交互。
def prompt_and_get(prompt)
print prompt
res = readline.chomp
throw :quit_requested if res == "!"
res
end
catch :quit_requested do
name = prompt_and_get("Name: ")
age = prompt_and_get("Age:")
sex = prompt_and_get("Sex:")
end
这个例子说明了throw没必要出现在catch的静态作用域中。
第9章 Modules
模块一种将方法、类与常量组织在一起的方式,模块给你提供了两个主要的好处:
1.模块提供了命名空间(namespace)来防止命令冲突
2.模块实现了mixin功能
9.1 命名空间(Namespace)
当你开始编写越来越大的Ruby程序时,你自然会发现自己编写了许多可重用的代码——将先关的例程(routine)组成一个库通常是合适的。你会希望将这些代码分解不同的文件,使其内容可以被其他不同的Ruby程序共享。
通常代码会被组织为类,你可能会让一个类(或一组相关的类)对应一个文件。
不过,有时你想要把那些无法自然构成类的部分集合到一起。
一种初步的方法是将所有内容放到一个文件中,然后简单地在任何需要它的程序中加载(load)它,这是C语言工作的方式,不过,这种方式有一个问题。假设你要编写一组三角函数sin、cos等等,你将它们全部塞到一个文件trig.rb中,为后世享用。
同时,Sally想要模拟善良和邪恶,并且她编写了一组对自己有用的例程,包括be_good和sin,并将它们放到moral.rb中,Joe想要编写一个程序找出针尖上有多少跳舞的天使,想要在他的程序中加载。
答案是使用模块机制,模块定义了一个namespace(命名空间),它是一个沙箱(sandbox),你的方法和常量可以在其中任意发挥,而无需担心被其他方法或常量干扰,三角函数可以放到一个模块中。
module Trig
PI = 3.1415926
def Trig.sin(x)
end
def Trig.cos(x)
end
end
而品行好坏的方法可以放到另一个模块中。
module Moral
VARY_BAD = 0
BAD = 1
def Moral.sin(badness)
end
end
模块常量的命名和类常量一样,都以大写字母开头,方法定义同样看起来很相似:这些模块方法就类似于类方法的定义。
如果第三方的程序想要使用这些模块,它可以简单地加载两个文件(使用Ruby的require语句)并引用它们的完整名称(qualified name)。
require 'trig'
require 'moral'
y = Trig.sin(Trig::PI/4)
wrongdoing = Moral.sin(Moral::VERY_BAD)
同类方法一样,你可以用模块名和句点来调用模块方法, 使用模块名和两个冒号来引用常量。
9.2 Mixin
模块有另一个妙用,它提供了一种称为mixin的功能,以雷霆之势,极大地的消除了对多重继承的需要。
在上一节的示例中,我们定义了模块方法,它们的名字都以模块名为前缀,如果这让你想到类方法,你接下来的想法可能是“如果我在模块内定义实例方法会怎么样?”
好问题,模块并没有实例,因为模块并不是类,不过,你可以在类的定义中,include一个模块,当包含发生时,模块所有的实例方法瞬间在类中也可以使用了。它们被混入(mixin)了,实际上,所混入的模块其实际行为就像是一个超类。
module Debug
def who_am_i?
"#{self.class.name} (\##{self.object_id}): #{self.to_s}"
end
end
class Phonograph
include Debug
# ...
end
class EightTrack
include Debug
# ..
end
ph = Phonograph.new("West End Blues")
et = EightTrack.new("Surrealistic Pillow")
ph.who_am_i?
et.who_am_i?
通过包含Debug模块,Phonograph和EightTrack都得以访问who_am_i?这个实例方法。
在继续前行之前,我们将探讨关于include语句的几点问题,首先,include与文件无关,C程序员使用叫做#include的预处理器指令,在编译期将一个文件的内容插入到另一个文件中,Ruby语句只是简单地产生一个指向指定模块的引用。如果模块位于另一个文件中,在使用include之前,你必须使用require(或者不那么常用的旁系,load)将文件加载进来。第二点,Ruby的include并非简单地将模块的实例方法拷贝到类中,相反,它建立一个由类到所包含模块的引用。
如果多个类包含这个模块,它们都指向相同的内容,即使当程序正在运行时,如果你改变模块中一个方法的定义,所有包含这个模块的类都会表现出新的行为。
Mixin为你向类中添加功能,提供了一种控制精巧的方式,不过,它们真正的力量的,当mixin的代码和使用它的类中的代码开始交互时,它们会一起迸发出来,让我们以标准的Ruby mixin——Comparable为例。
你可以使用Comparable mixin向类中添加比较操作符(<, <=, ==, >=和>)以及between?方法。为了使其能够工作,Comparable假定任何使用它的类都定义了<=>操作符,这样,作为类的一个编写者,你定义一个方法<=>,再包含Comparable,然后就可以免费得到6个比较函数,让我们用Song类来尝试一下,令它们基于时长来进行比较,我们所要做的就是包含Comparable模块并实现比较操作符<=>。
class Song
include Comparable
def initialize(name, artist, duration)
@name = name
@artist = artist
@duration = duration
end
def <=>(other)
self.duration <=> other.duration
end
end
我们可以用几首测试歌曲来检查一下结果。
song1 = Song.new("My Way", "Sinatra", 225)
song2 = Song.new("Bicyclops", "Fleck", 260)
song1 <=> song2
song2 < song2
song1 == song1
song1 > song2
9.3 迭代器与可枚举模块(lterators and the Enumerable Module)
你可能已经注意到Ruby收集(collection)类支持大量针对收集的各种操作:遍历、排序等等,你可能会想,“哎呀,如果我自己的类也能支持这些出色的特性,那就太好了!”
当然,你的类可以支持所有这些出色的特性,感谢mixin和Enumerable模块的魔力,你要做的就是编写一个称为each的迭代器,由它依次返回收集中的元素。包含Enumerable,然后你的类瞬间支持诸如map、include?和find_all等操作。
如果在你收集的对象中使用<=>方法是想了有意义的排序语义,你还会得到诸如min、max和sort等方法。
9.4 组合模块(Composing Modules)
我们讨论了Enumerable的inject方法,Enumerable是另一个标准的mixin,它基于宿主类(host class)中的each实现了许多方法,因此,我们可以在任何包括了Enumerable模块并定义了each方法的类中使用了inject。许多内建的类都是如此。
[1, 2, 3, 4, 5 ].inject {|v, n| v+n }
('a'..'m').inject {|v,n| v+n}
我们还可以定义自己的类以包含Enumerable,继而得到inject支持。
class VowelFinder
include Enumerable
def initialize(string)
@string = string
end
def each
@string.scan(/[aeiou]/) do |vowel|
yield vowel
end
end
end
vf = VowelFinder.new("the quick brown fox jumped")
vf.inject {|v,n| v+n}
注意,我们使用了和前面实例中调用inject的相同模式——使用它来求和,当作用于数字时,它返回算术和,当作用于字符串时,返回串联的字符串。我们也可以使用一个模块来封装这个功能。
module Summable
def sum
inject {|v, n| v+n}
end
end
class Array
include Summable
end
class Range
include Summable
end
class VomelFinder
include Summable
end
[1, 2, 3, 4, 5].sum
vf = VowelFinder.new("the quick brown fox jumped")
vf.sum
9.4.1 Mixin中的实例变量(Instance variables in Mixins)
从C++转向Ruby的人经常问我们,“mixin中的实例变量会如何呢?在C++中,我必须兜好几个圈子才能控制如何在多重继承中共享变量。Ruby是如何处理的呢?”
我们告诉他们,好吧,对初学者来说,这根本不是什么问题,回忆一下Ruby中实例变量是如何工作的:当前缀为@的变量第一次出现时,即在当前对象(也就是self)中创建实例变量。
对mixin来说,这意味着你要混入客户类中的模块,可能会在客户对象中创建实例变量,并可能使用attr_reader或类似方法,定义这些实例变量的访问方法。例如,下面实例中的Observable模块,会向包含它类中添加实例变量@observer_list。
module Observable
def observers
@observer_list ||= []
end
def add_observer(obj)
observers << obj
end
def notify_observers
observers.each {|o| o.update}
end
end
多数时候,mixin模块并不带有它们自己的实例数据——它们只是使用访问访问方法从客户对象中取得数据。但是,如果你要创建的mixin不得不持有它们自己的状态。确保这个实例变量具有唯一的名字,可以对系统中其他的mixin区别开来(也许使用模块名作为变量名的一部分)。或者,模块可以使用模块一级的散列表,以当前对象的ID作为索引,来保存特定于实例的数据,而不必使用Ruby的实例变量。
module Test
State = {}
def state = (value)
State[object_id] = value
end
def state
State[object_id]
end
end
class Client
include Test
end
c1 = Client.new
9.4.2 解析有歧义的方法名(Resolving Ambiguous Method Names)
关于mixin,人们经常问到的另一个问题是,方法查找是如何处理的?特别的是,如果类、父类以及类所包含的mixin中,都定义有相同名字的方法时,会发生什么?
答案是,Ruby首先会从对象的直属类(immediate class)中查找,然后是类所包含的mixin,之后是超类以及超类的mixin。如果一个类有多个混入的模块,最后一个包含的模块将会被第一个搜索。
9.5 包含其他文件(including other Files)
因为Ruby可以使我们轻松地编写良好的、模块化的代码,你经常会发现自己编写了含有大量自包含功能的小文件——比如x的接口、完成y的算法,等等。典型地,你会将这些文件组织为类或库。
产生这些文件之后,你希望在新的程序中结合使用它们,Ruby有两个语句来完成这一点,每次当load方法执行时,都会将制定的Ruby源文件包含进来。
load "filename.rb"
更常见的是使用require方法来加载指定的文件,且只加载一次。
require 'filename'
被加载文件中的局部变量不会蔓延到加载它们所在的范围中。例如,这里有一个名为include.rb。
a = 1
def b
2
end
下面是当我们把它包含到另一个文件中时,将会发生什么?
a = "cat"
b = "dog"
require "included"
require有额外的功能:它可以加载共享的二进制库,两者都可以接受相对或者绝对路径,如果指定了一个相对路径(或者只是一个简单的名字),它们将会在当前加载路径中(load path——$:)的每个目录下搜索这个文件。
使用load或者require所加载的文件,当然也可以包含其他文件,而这些文件又包含别的文件,依次类推,可能不那么明显的是,require是一个可执行的语句——它可能在一个if语句内、或者可能包含一个刚刚拼合的字符串。搜索路径也可以在运行时更改,只需将你希望的目录加入$:数组中。
因为load会无条件地包含源文件,你可以使用它来重新加载一个在程序开始执行厚可能更改的源文件,下面是一个认为设计的示例。
这并不是严格为真的,Ruby在数组$中保存了被require所加载的文件列表,不过,这个列表只包括了调用require时所指定的文件名,欺骗Ruby让它多次加载同一个文件,是有可能。
5.times do |i|
File.open("temp.rb", "w")
f.puts "module Temp"
f.puts " def Temp.var"
end
load "temp.rb"
puts Temp.var
end
对于这个功能,可以考虑一个不太实际的例子:Web应用重新加载正在运行的模块,这让它能动态地更新自己;它不需要重新启动来集成软件的新版本,这是使用例如Ruby等动态语言的众多好处之一。