Ruby学习之代码块
代码块在其他的语言中都或多或少接触过一些,如perl中sort{$a<=>$b}keys,传入代码块实现按数值排序,在swift中用到闭包,更加深入学习到training closure、capturing value等代码风格,对代码块有了深入的了解,并且意识到代码块是引用类型(Reference Type),和Value Type有所区别,意识到代码块和类、方法等的相似之处。
在学习Ruby的过程中,对代码块的理解更加加深一步,不仅仅是简化代码的功能,还涉及到作用域、可调用对象等知识。
代码块
代码块定义在大括号或者do...end关键字中,块被传递到方法中时,可以以yield关键字回调,同时能够通过Kernel#block_given?()方法检测是否传入了代码块:
class MyClass
def my_method
return yield if block_given?
"There's no block"
end
end
obj = MyClass.new
puts obj.my_method{"There's a block!"}
puts obj.my_method
块也可以有自己的参数,在||中用逗号隔开,调用时用yield(arg)就能调用:
def my_method
return yield(2,3)
end
puts my_method {|x,y| "The two numbers are #{x} and #{y}”}
以上语句会输出 The two numbers are 2 and 3
绑定
块不仅仅可以通过调用参数的方式和其他语句交流,更加重要的是,块在定义的时候,会从当前作用域获取到绑定:
def my_method
return yield(2,3)
end
z = 4
puts my_method {|x,y| "The there numbers are #{x},#{y} and #{z}"}
这理解起来并不难:在过程语句中,每一条语句都可以访问到当前作用域的其他变量,块也是一样,把可以运行的代码看做两部分组成:代码本身和一组绑定。
如果还是有些困惑的话,是因为表示代码块的{}并不意味着新的作用域,请注意Ruby中是没有{}表示开始结束的,而是用了类似python的风格。
作用域
那么在Ruby中,是什么真正影响了作用域呢?其实只有三个关键字会开启新的作用域,称为“作用域门”:
class、module、def
一旦碰到这三个关键字的其中一个,就意味着进入了新的作用域,并且在end关键字出现时离开,这里可以理解为作用域门就是左大括号{,end可以看成右大括号},如果你愿意这么理解的话。class、module和def有微妙的差别:class和module定义中的代码会被立即执行,而def定义中的代码只有被调用的时候才会被执行。
由于作用域仅仅由这三个关键字来决定,那么可以通过一些方法穿越作用域门,而让代码块获得上方作用域的绑定:用Class.new()、Module.new、Module#define_method()方法来代替class、module和def关键字:
var = "top level obj"
MyClass = Class.new do
define_method :my_method do
puts var
end
end
obj = MyClass.new
obj.my_method
如上述代码,在my_method中任然可以访问到var,而不会受到作用域门的阻隔,这种方法成为扁平作用域,通过利用作用域门和穿越作用域门,可以灵活地共享作用域以及作用域保护。
打破封装
在作用域中,还有一种黑科技:Object#instance_eval()方法。这个方法可以传递一个块,作为上下文探针,该方法调用的块的接收者会成为self,此时,该块可以访问接收者的私有方法和实例变量,甚至可以在不碰其他的绑定情况下,修改self对象:
class MyClass
def init
@v = 1
end
attr_reader :v
end
obj = MyClass.new
obj.instance_eval {
@v = 2
}
puts obj.v
此时,obj的v属性已经被修改了!所以为什么称它为黑科技了,因为他可以打破封装结构,同时它还有另外的作用,制作洁净室。
洁净室是那些只为了执行其中的代码块的类:
class CleanRoom
def complex_calculation
#…
end
def do_something
#…
end
end
clean_room = CleanRoom.new
clean_room.instance_eval{
if complex_calculation > 10
do_something
end
}
以上就是一个洁净室的例子
可调用对象
块的使用过程分为两步:打包和调用。将块打包后方便以后调用的方法有三种:proc、lambda、使用方法。
在Ruby中,绝大多数东西都是对象,但是块除外,如果想要将块打包存储,则需要调用一些类来获取帮助。
Proc就是其中的一种类。在新建Proc类时,会将调用的块存储,并在之后可以通过call来调用:
obj = Proc.new{"This is a block"}
puts obj.call
除了Proc.new之外,Ruby还提供两个内核方法用来将块转换成Proc:proc()、lambda(),其中proc()可以看成是Proc.new的一个别名(Ruby1.9之后),他们的区别是lambda更加像一个方法,他的return会从lambda中返回,而proc仅仅表示从当前作用域中返回,以及其他的一些不是很清楚的区别。
和在swift中类似,代码块可以看做是方法的最后一个隐含参数,也可以显示地命名它,要求使用&符号开头(这里没有任何联系,但是我记忆的时候将他理解为:因为块是引用类型,所以用&符号传入引用):
def my_method(a,b,&operation)
#...
end
my_method(1,2){
#...
}
此时,用&命名也是一种存储块的方式,通过这种方式可以让块的内容在多个方法之间传递,或者将这个块传递给另外一个Proc。
&符号的真实含义:将一个Proc对象作为块来使用,简单地去掉&,就可以再次获得Proc对象。直到了这点,也可以在其他地方,利用&Proc来将Proc转换回一个块。