块
- 块可以定义在大括号中, 也可以放在do…end关键字中, 一般来说,只有一行的块使用{},而对多行的块使用do…end
- 只有在调用一个方法时才可以定义一个块,块会被直接传递给这个方法,然后该方法可以通过yield关键字回调这个块.
- 块可以有自己的参数,当回调块时,可以像调用方法一样为块提供参数,并且块中最后一行代码执行的结果会被作为返回值.
- 通过Kernel#block_given?()方法可以询问当前方法调用是否包含块
def my_method(a, b)
return a + yield(a, b) if block_given?
'no block'
end
my_method(1, 2) # => "no block"
my_method(1, 2) {|x, y| (x + y) * 3} # => 10
对于在块中出现异常的情况,可以使用ensure关键字保证在出错的情况下,还能执行代码块,该方法常用于释放连接的情况,即使块出错也会保证连接释放.使用ruby模仿C#的关键字using.
def using(connect)
begin
yield(connect) # 执行块中内容
ensure
puts "dispose connection" # 确保该段代码执行
end
end
using(1) {|connect| connect / 0} # => "dispose connection"
# => 报告异常
闭包
代码运行时需要一个执行环境:局部变量,实例变量,self等, 这些绑定在对象上的名字我们简称为绑定.
块包含代码和一组绑定,当创建块时会获得到局部绑定,然后连同自己的绑定传递给方法,而方法中的变量对于块是不可见的.
def my_method
var = "my_method()"
yield("block")
end
my_method { |text| "#{var}, #{text}" } # => 报错,没有var
var = "Hello"
my_method { |text| "#{var}, #{text}" } # => Hello, block
在上述例子中,第一次块的绑定中无var变量, 导致出错,第二次块绑定中包含var变量,虽然my_method中包含了var但对于块不可见, 这种特性称之为闭包.
作用域
在C++和Java等语言中,有内部作用域和外部作用域的概念,内部作用域可以看到外部作用域的变量, 但在Ruby中,没有这种嵌套式的作用域,它们的作用域之间是分开的: 一但进入一个新的作用域,原先的绑定就会被替换为一组新的绑定.
v1 = 1
class MyClass
v2 = 2
local_variables # => [:v2]
def my_method
v3 = 3
local_variables
end
local_variables # => [:v2]
end
obj = MyClass.new
obj.my_method # => [:v3]
local_variables # => [:v1, :obj]
从上述代码可以看出在程序切换作用域时,绑定会发生变化.
作用域门
程序会在三个地方关闭前一个作用域,同时打开新的作用域:
- 类定义
- 模块定义
- 方法
这三个边界分别用class, module和def关键字作为区分标识, 每个关键字都充当了一个作用域门.
全局变量和顶级实例变量
全局变量在任何作用域中都可以访问,正因如此,为了安全尽量少使用全局变量.
有时可以使用顶级实例变量来代替全局变量, 它们是顶级对象main的实例变量.
@var = "The top-level @var"
def my_method
@var
end
my_method # => "The top-level @var"
如上述代码所示,只要main对象扮演self的角色,就可以访问到顶级实例变量, 但当其它对象成为self时,顶级实例变量就退出作用域了,一般情况下,顶级实例变量比全局变量更安全.
扁平化作用域
既然了解了作用域的功能,那么如何让绑定穿过作用域门就成了一大问题.
Ruby中的类是Class类的实例,所以使用Class.new可以避免class关键字的出现,这样可以让变量穿过该作用域,同样对于def关键字,我们可以使用define_method()方法.
my_var = "Success"
MyClass = Class.new do
puts "#{my_var} in the class definition!"
define_method :my_method do
puts "#{my_var} in the method!"
end
end
MyClass.new.my_method
=> Success in the class definition!
Success in the method!
这样可以让一个作用域看到另一个作用域, 我么称这种技巧为嵌套文法作用域,也称之为扁平作用域.
共享作用域
但是对于想让某些变量在特定的方法中共享,而不希望其它方法访问,这样便需要另一种技巧共享作用域.
def define_methods
shared = 0
Kernel.send :define_method, :counter do
shared
end
Kernel.send :define_method, :inc do |x|
shared += x
end
end
define_methods
counter # => 0
inc(4)
counter # => 4
上述代码中,通过send给Kernel模块定义counter方法和inc方法, 这样只有Kernel的counter和inc方法可以访问shared变量,而其它方法则无法访问,因为这个变量被def作用域门保护这, 这种用于共享变量的技巧称为共享作用域.
instance_eval()
class MyClass
def initialize
@v = 1
end
end
obj = MyClass.new
obj.instance_eval do
self # => #<MyClass:0x007f65373c0b38 @v=1>
@v # => 1
end
使用instance_eval方法时,该块的接受者会成为self, 因此他可以访问接收者的私有方法和实例变量, instance_eval()方法可以在不碰其它绑定的情况下修改self对象.
v = 2
obj.instance_eval {@v = v}
obj.instance_eval {@v} # => 2
这三行代码在同一扁平作用域中执行,故都能访问局部变量v, 而且由于块把运行它的对象作为self,所以它们也能访问obj的实例变量@v.
在这些情况中,可以把传递给instance_eval()方法的块称为一个上下文探针.
洁净室
仅仅为了在其中执行块而创建的对象称之为洁净室
class CleanRoom
def complex_calc
# ...
end
def do_something
# ...
end
end
clean_room = CleanRoom.new
clean_room.instance_eval do
if complex_calc > 10
do_something
end
end
# 洁净室仅仅是一个用来执行块的环境,它通常会暴漏若干有用的方法提供调用.
可调用对象
从实质上看,使用块有两个步骤:1)将代码打包备用;2)调用块来执行代码.在ruby中,有常用三种情况来打包代码.
- 使用proc, proc实质上是由一个块转换成的对象.
- 使用lambda, 它与proc类似
- 使用方法’
Proc对象
Ruby在标准库中提供了Proc类,可以通过Proc.new方法创建一个Proc,之后可以使用Proc#call()方法进行调用,称之为延迟执行.
Ruby还提供了两个内核方法,用于转换块,lambda()和proc().
inc = Proc.new {|x| x + 1}
inc.call(2) # => 3
dec = lambda {|x| x - 1}
dec.call(2) # => 1
块就类似与方法的额外参数名,大多情况下,方法可以通过yield语句直接运行一个块,但是对与把块传递给另一个方法或者把该块转换为Proc,yield将力不从心. 这时,我们可以使用&操作符,&块名即可.
注:要将块附加到一个绑定上,可以给这个方法添加一个特殊参数,该参数必须是参数列表最后一个,如&block, block为块名.
def math(a, b)
yield(a, b)
end
def teach_math(a, b, &operation)
puts "Let's do the math:"
puts math(a, b, &operation)
end
teach_math(1, 2) {|x, y| x + y}
=> Let's do the math:
3
&操作符,就像一个转换, 它可以将块转换为Proc对象,相反也可以将Proc对象转换为一个块.
def my_method(&the_proc) # 通过&操作符,将块转换为Proc对象
the_proc
end
p = my_method {|name| "Hello, #{name}!"}
p.class # => Proc
p.call("Bill") # => Hello, Bill!
def my_method(greeting)
puts "#{greeting}, #{yield}!"
end
my_proc = proc { "Bill" }
my_method("Hello", &my_proc) # 通过&操作符将proc对象转换为块
=> Hello, Bill!
proc与lambda对比
- proc与lambda的return不同
- 在lambda中,return表示仅从该lambda中返回
- 在proc中,return表示从定义proc的作用域中返回
- 在lambda中,return表示仅从该lambda中返回
def my_method(name)
name.call # 调用Proc对象
end
l = lambda {return "lambda"}
my_method(l) # => lambda
p = proc {return "proc"}
my_method(p) # => 出错:LocalJumpError
p.call # => 出错:LocalJumpError
由于proc的返回是从它定义的作用域中返回,而上述代码定义proc对象的作用域为顶级作用域main, 故而返回错误.
def my_method
p = proc { return "proc" }
p.call
return "Finial my_method"
end
my_method # => "proc"
# 由于proc会在定义的作用域中返回,所以并没有执行my_method最后一行.
- proc和lambda检查参数的方式不同
- proc中,对于多余的参数,proc则会忽略掉
- lambda中, 对于多余的参数,则会失败,同时抛出ArgumentError
方法
通过调用Objec#method()方法可以获得一个用Method对象表示的方法,之后可以使用Method#call()对其进行调用.
class MyClass
def initialize(value)
@x = value
end
def my_method
@x
end
end
obj = MyClass.new(1)
m = obj.method :my_method
m.call # => 1
unbound = m.unbind
another_obj = MyClass.new(2)
m = unbound.bind(another_obj)
m.call # => 2
Method方法类似lambda, 但有一个重要的区别:lambda在定义它的作用域中执行,而Method对象在它自身所在的对象的作用域中执行.
Method#unbind()方法可以把一个方法跟它所绑定的对象分离,该方法再返回一个UnboundMethod对象,虽然无法直接执行UnboundMethod对象,但可以把它绑定到一个对象上,使其再次成为一个Method对象.