《Ruby元编程》之可调用对象笔记。
写在前面
这是《Ruby元编程》第4章代码块的学习笔记,我看完整章,觉得用可调用对象来概括整章可能更合适,所以, 我把标题改了。
好,进入正题。
可调用对象
四种:
- 代码块
- proc:Proc类的对象,同时也是创建Proc类的一个method
- lambda:Proc类的对象,同时也是创建Proc类的一个method
- 方法
看个例子先。
同一个method,四种调用方式,注意最后一个调用方式有些不同:
# 代码块
class C1
def my_method(x)
yield(x)
end
end
obj = C1.new
obj.my_method("Ruby") {|x| puts "hello, #{x}"} # => hello, Ruby
# proc
class C1
def my_method(x)
yield(x)
end
end
my_proc = Proc.new {|x| puts "hello, #{x}"}
obj = C1.new
obj.my_method("Ruby", &my_proc) # => hello, Ruby
# lambda
class C1
def my_method(x)
yield(x)
end
end
my_lambda = lambda {|x| puts "hello, #{x}"}
obj = C1.new
obj.my_method("Ruby", &my_lambda) # => hello, Ruby
# 方法
class C2
def my_method(x)
puts "hello, #{x}"
end
end
obj = C2.new
m = obj.method :my_method
m.call("Ruby") # => hello, Ruby
来认识下这四位小伙伴。
代码块Blocks
Ruby中绝大多数都是对象,block则是个特例。【判断某个事物是不是对象,可以用x.is_a? Object
来判断】
戳这里Is everything an object in ruby?看stack overflow上人们怎么说,不过对于Amadan的answer,我觉得不是特别准确,他提到“Methods, operators and blocks aren’t, but can be wrapped by objects (Proc).” 而实际上,如果你进irb, 运行puts.is_a? Object
,你会发现它返回true,即puts是method,也是对象。
貌似跑题了,回正题。
代码块的定义
代码块可以用大括号{}定义, 也可以用do … end关键字定义。
代码块可以有参数。
代码块最后一行代码执行的结果会被作为返回值。
只有在调用一个方法时,才可以定义一个块,块会被直接传递给这个方法,该方法会用yield关键字调用这个块。
可以通过内核方法block_given?来判断当前调用的方法中是否包含块。比如:
def my_method return yield if block_given? 'no block' end my_method # => no block my_method {"Here's a block"} # => Here's a block
代码块是闭包closures
运行代码需要一个执行环境:局部变量,实例变量,self等,这些简称为绑定(binding)。
那么,块在哪里获得它的绑定呢?
定义一个块时,它会获取当前环境中的绑定,带着它们四处游荡。当块被传给一个方法时,它会带着这些绑定一块进入该方法。【注意它的这个特性,这也是为什么它被称之为闭包的原因】
作用域scope
ruby中没有嵌套式的作用域,它的作用域是截然分开的。一旦进入一个新的作用域,原先的绑定就会被替换。
作用域门
程序会在三个地方关闭前一个作用域,同时开启一个新的作用域:
- 类定义class
- 模块定义module
- 方法def
这三种情况分别以class,module, def关键字作为标志,每个关键字都对应一个作用域门。
在class/module 与def之间有个小区别:类定义/模块定义中,代码会立即执行,但是在方法中则不会。
扁平化作用域与共享作用域
用Class.new方法代替class关键字, Module.new方法代替module关键字, define_method方法代替def关键字, 就是扁平化作用域。即用方法调用来替代了作用域门,使得一个作用域看到另一个作用域里的变量,好似两个作用域挤压在一起,它们可以共享各自的变量。
看个例子理解下:
my_var = "hello, ruby" class C # 需要在这里打印出my_var def my_method(x) # 在这里也要打印出my_var end end
这里,my_var存在于顶级作用域中,但是一旦进入class/def这个作用域,my_var就不存在了。
你也许会说,把my_var 变成全局变量$my_var, 呃,coding的一个原则就是能不用全局变量就不要用全局变量,存在安全隐患,即使是顶级实例变量,也应当避免少用。
我们来扁平化作用域:
my_var = "hello, ruby" C = Class.new do puts "#{my_var}" # => hello, ruby define_method :my_method do puts "#{my_var}" end end C.new.my_method # => hello, ruby
说完扁平化作用域,来看看共享作用域。
当一个扁平作用域中,定义了多个方法,把这些方法用一个作用域门保护起来,它们就可以共享绑定,这种处理作用域的方法称之为共享作用域。
看个例子:
def shared_banding shared = 1 define_method :my_method do shared end define_method :my_other_method do |x| shared += x end end shared_banding # => 调用方法,切换到def shared_banding的作用域 my_method # => 1 my_other_method(4) # => 5
理解了扁平作用域,来看看instance_eval与instance_exec方法。
instance_eval 与instance_exec
两者都是BasicObject的instance methods,打破封装的杠把子。instance_exec比instance_eval稍微灵活些,可以传递参数,Ruby BasicObject 的doc 里面也有举例。
看段书中的代码理解下:
class C def initialize @v =1 end end obj = C.new obj.instance_eval do self @v end v = 2 obj.instance_eval { @v } #=> 1 obj.instance_eval { @v =v } #=> 2 obj.instance_eval { @v } #=> 2 obj.instance_exec(5) {|x| @v + x} #=> 7
直接改变了obj中的实例变量@v,而针对传递给instance_eval与instance_exec的代码块,它有一个名字,叫做上下文探针(context probe),好似它们可以深入到对象的代码块中,对对象进行操作。
洁净室
洁净室是一个用来执行块的环境,类似一个白板类,比如BasicObject。当你希望多个方法在执行时,不共享实例变量,可以考虑洁净室。
OK,block部分暂时告一段落,我们看Proc。
Proc 与lambda
lambda与Proc很相近,一起说。
由于代码块不是对象,当你想将代码块存起来以后调用时,就需要对象,有需求便有供应,Proc类应运而生。
它是由块转换来的对象,可以与块进行互相切换。建一个Proc的对象,有四种方法:
# Proc.new
p1 = Proc.new {|x| puts "#{x}"}
p1.call("hi,ruby") # => hi,ruby
# proc method
p2 = proc {|x| puts "#{x}"}
p2.call("hi,ruby") # => hi,ruby
# lambda method
p3 = lambda {|x| puts "#{x}"}
p3.call("hi,ruby") # => hi,ruby
# stabby lambda
p4 = -> (x) {puts "#{x}"}
p4.call("hi,ruby") # => hi,ruby
关于Proc,说两点:块与Proc的转换,proc与lambda的区别【这是个频繁被问的问题……】
块与Proc的转换
先从块的传递开始。
在方法中,可以通过yield来直接运行一个代码块,但是当你想把代码块传递给一个方法时,就需要给代码块起一个名字,附加到一个绑定上。怎么破?
解决的方法是给方法添加一个特殊的参数,这个参数位于参数列表最后,且用&表示。【还记得
block_given?
吗?】& 操作符的含义是:这是一个Proc对象,我想把它当作代码块使用,去掉&操作符,就能得到一个Proc对象 【这里暗含着块与Proc的转换】
看例子:
# 去掉& 得到一个Proc对象 def my_method(&my_proc) my_proc end my_proc = my_method {|x| puts "#{x}" } my_proc.class # => Proc my_proc.call("hi") # => hi # & 把Proc转化为块传递给方法 def my_method(a) puts "#{a}, #{yield}" end my_proc = proc {"world"} my_method("hi", &my_proc) # => hi, world
Proc与lambda的对比
先说个区别:用lambda方法(包括 ->)创建的Proc称为lambda,而用其他方法创建的则是Proc。可以用Proc#lambda来检测。
# proc method p2 = proc {|x| puts "#{x}"} p2.call("hi,ruby") # => hi,ruby p2.lambda? # => false # lambda method p3 = lambda {|x| puts "#{x}"} p3.call("hi,ruby") # => hi,ruby p3.lambda? # => true
最重要的区别有两个:
return有不同的含义
在proc中,return不是从proc中返回,而是从定义proc的作用域返回。
觉得书中的例子不够典型,改良了下,可以很明显看出二者的区别:
def my_double a = lambda {return 10} result = a.call return result * 2 end my_double # => 20 def my_other_double my_proc = Proc.new {return 10} result = my_proc.call return result * 2 # => 这段代码不会运行! end my_other_double # => 10
参数问题
在参数问题上,lambda比proc要严格,而proc则宽容很多。
比如:
p = Proc.new {|a, b| puts "#{a}:#{b}"} p.call(1,2,3) # => 1:2 p.call(1) # => 1: p2 = lambda {|a, b| puts "#{a}:#{b}"} p2.call(1,2,3) # => wrong number of arguments (given 3, expected 2) (ArgumentError)
Google下,会发现很多前辈都写过这方面的文章,这里就不多说了,stack overflow上也有一些精彩的answer:What’s the difference between a proc and a lambda in Ruby?, 可加深理解。
方法method
第3章刚刚提到过dynamic methods和ghost methods。不过这里要讲的是方法的另一种定义方式:使用method方法来定义,调用时call。最开始那个class C2例子中的my_method就是这样调用的。
方法这部分没有什么特别的,书中提到了自由方法unbound method,即一个方法从最初定义它的类或者module中脱离出来。
使用Method#unbound可以把一个方法变成自由方法, 使用UnboundMethod#bind可以把它再绑定到一个对象上,不过绑定的对象需要是该类及其子类的对象,而module则不需要。
原谅我不是很理解自由方法存在的意义。书中也提到了它只在极个别场合发挥作用,这里不展开细说了。
结尾
书中最后那个DSL例子,个人感觉很不错,特别是使用共享作用域,消除全局变量的代码重构,很精彩,推荐阅读。