学习路线
instance_eval
方法来控制作用域def a_method(a,b)
a+ yield(a,b)
end
a_method(1,2) {|x,y| (x+y)*3 }
代码块可以用大括号定义,也可以使用do...end
关键字定义,通常如果一行的块使用大括号,而多行的块使用do...end
块可以有自己的参数,比如上面的例子中的 x 和 y,可以像调用方法那样为块提供参数,
可以通过Kernel#block_given?
查看是否传达快,如果没有,调用 yield 则会报错
代码块可以把变量带出原来的作用域
代码块不能孤立的运行,它需要一个执行环境:局部变量,实例变量,self 等
可以运行的代码由两部分组成:代码本身 和 一组绑定
当我们定义一个 block 时,它将获取到环境中的绑定,当 block 被传给一个方法时,他会带着这些绑定一起进入该方法
def my_method
x = "Good"
yield "cruel"
end
x = "Bad"
my_method {|y| p "#{x} , #{y} World"} # => "Bad , cruel World"
这里我们调用 my_method 时,创建了一个 block,并获取顶层上下文中的 x 变量,而 my_method 中定义的 x 变量对于 block 是不可见的,也可以在 block 中定义额外的绑定,但这些绑定在 block 结束时就消失了。
def just_yield
yield
end
top_leval_var = 1
just_yield do
top_leval_var += 1
local_to_block = 1
end
p top_leval_var # => 2
p local_to_block # => error!
基于这些特性,人们喜欢把代码块称为闭包,换句话说:代码块可以获取局部绑定,并一直带着他们
如何使用闭包呢?
这里我们需要了解绑定寄居的地方——作用域,需要判断程序在哪里切换了作用域,作用域的作用有哪些?
不论是 Java,Python,Ruby 都会有作用域的概念,就好像是单独的一个作用空间,一个领地,在这里有专属的局部变量
v1 = 1 # 顶层作用域
class MyClass # 类作用域
v2 = 2
p local_variables # => [:v2]
def my_method # 方法作用域
v3 = 3
p local_variables # => [:v3]
end
p local_variables # => [:v2]
end
obj = MyClass.new
obj.my_method
obj.my_method
p local_variables # => [:v1, :obj]
最初在顶层作用域 定义 v1 = 1
定义 class MyClass,切换作用域,一旦切换作用域,绑定也会修改,v1 对于 MyClass 内部域是不可见的,在其内部定义了一个方法和变量,当执行到实例方法内部,作用域再次切换
当 MyClass 定义完毕,再次切换回顶级作用域。
全局变量与顶级实例变量
$var = 1
class MyClass
def incre
$var += 1
end
end
p $var
obj = MyClass.new
obj.incre
p $var
这里定义了全局变量 var,发现在所有作用域都可以访问并操作到,所以一旦出现问题,很难排查。
@var = 1
def my_method
@var += 1
end
p @var
my_method
p @var
这里定义一个顶级上下文中的实例变量,当 main 对象扮演 self 的角色,就可以访问到顶级实例变量,但如果进入其他对象作为 self,则无法访问到
@var = 1
class MyClass
def my_method
@var = "this is not top level @var"
end
end
obj = MyClass.new
p obj.my_method
顶级实例变量 要比 全局变量 有限的安全
这里我们想弄清楚作用域是如何切换,绑定是如何切换的,需要了解作用域门
程序一般会在三个地方,关闭之前的作用域,打开新的作用域,分别为:
每个关键字对应一个作用域门,各个作用域中的变量相互隔离
现在看到每个作用域有独立的空间,如果想要变量在作用域之间传递,该如何操作?
var = "Success"
class MyClass
# 这里想访问到var
def my_method
# 这里想访问到var
end
end
一旦切换作用域局部变量就会失效,如何能让 var 穿越两个作用域被访问到?
Ruby 是非常灵活的,它为一种实现提供了多种方法,上面知道了class module def
三个关键字为作用域门,那我们使用其他方式来实现相同的结果,这样就可以避免切换作用域了
var = "Success"
# 使用Class.new 切换 class关键字,避免切换作用域
MyClass = Class.new do
p var
# 使用动态定义方法,替换def关键字,避免切换作用域
define_method :my_method do
p var
end
end
obg = MyClass.new
obg.my_method
如果两个作用域挤压在一起,我们通常简称为 扁平作用域
如果想在一组方法之间共享一个变量,但又不想别的方法访问到这个变量,就可以把这些方法定义在该变量所在的扁平作用域
def my_method
# 这里是一个局部变量,希望仅被几个方法访问到
share = 0
# 使用内核方法,调用 define_method 来定义函数,而又不用切换域,这样这几个方法就可以访问到变量
# 也可以只用 define_method :counter do
Kernel.send :define_method,:counter do
share
end
Kernel.send :define_method,:inc do |x|
share += x
end
end
def other_method
# 这里则无法再访问到share
share
end
my_method
p counter # => 0
inc 4
p counter # => 4
这里我们学习一个新的方法:BasicObject#instance_eval
,它在一个对象的上下文中执行 block,运行时,代码块的接收者将会成为 self
class MyClass
def initialize
@v = 1
end
end
obj = MyClass.new
# 这里self切换为obj
obj.instance_eval do
p self
# 输出的实例变量也是属于self的
p @v
end
# 上下文探针结束后,self又变回了 main
# 这里 下面三行代码都处在扁平作用域,所以Block可以使用局部变量v,并访问到obj中的实例变量
v = 2
obj.instance_eval {@v = v}
obj.instance_eval {p @v}
这里需要注意一个点:instance_eval 会将接收者变为当前对象 self。而调用者的实例变量就落在作用域范围外,如果不了解,就会出现 Bug,例如:
class C
def initialize
@x = 1
end
end
class D
def twisted_method
# 如果这里 @y 改为 y ,即可被访问到
@y = 2
# 在执行下面代码之前,self为D.new,一旦执行下面的代码,C.new为self,@x在C.new中定义了,@y则没有
C.new.instance_eval {"@x : #{@x} , @y : #{@y}"}
end
end
p D.new.twisted_method # => "@x : 1 , @y : "
这里输出发现,@y并未访问到,可是上面的调用代码在同一个扁平作用域,原来是因为 instance_eval 将 C.new 对象变为当前对象 self,调用者的实例变量就落在了作用域外了,所以访问不到为:nil。
这里使用 instance_exec 来解决传递参数的问题
class D
def twisted_method
@y = 2
# 这里主动将 @y传递到块儿中
C.new.instance_exec(@y) {|y|"@x : #{@x} , @y : #{y}"}
end
end
class CleanRoom
def current_temp
18
end
end
clean_room = CleanRoom.new
clean_room.instance_eval do
if current_temp < 20
p "wear a jacket"
end
end
洁净室只是一个用来执行块的环境,它提供若干有用的方法供代码块调用,比如本例中的current_temperature
方法,然而一个理想的洁净室应该是没有实例变量和方法的,因为这可能与 block 从环境中带来的名字冲突,因此 BasicObject 的实例往往用来充当洁净室,因为它是白板类,几乎没什么方法。
目前我们使用的 Block 是直接执行的,我们需要让 Block 变为对象,可以进行打包传递,调用,我们看一下有哪些打包代码的方式。
# 将Block打包为Proc
inc = Proc.new {|x| x + 1}
# 主动调用Block
p inc.call 2
inc_ = proc {|x| x + 1}
p inc.call 2
obj = lambda {|x| x + 1}
p obj.call 2
obj_ = ->(x) {x + 1}
p obj_.call 2
在调用方法时,我们为其传递一个 Block,可通过 yield 进行执行,但是如果我们想将这个 Block 封装起来,延迟调用,该如何操作
# 这里&block将Block封装为Proc
def my_method(name,&block)
p name
# 这里对其调用
block.call
end
# 传递Block时,不需要再参数后面加 ,
my_method "qsc" do p "Hello World" end
如果想把 Proc 再转为 Block 该怎么操作
def my_method(greeting)
p "#{greeting} , #{yield}}"
end
my_proc = proc {"Bill"}
my_method("Hello",&my_proc)
现在就可以将 Block 与 Proc 相互转化了
参数校验是指:Lambda 中定义两个入参,如果你没传递,或者传递多了,则会报错,如果是 Proc 定义两个入参,如果没传递,则变量为 nil,如果传递多了,多余部分也不会使用。Lambda 更严格一些
return 定义不同:Lambda 中使用 return,表示从 Lambda 表达式中返回,而 Proc 表示从定义 Proc 的作用域中返回
def my_method
# 这里block在my_method中定义,一旦执行return就会从my_method中退出
p = Proc.new {return 10}
result = p.call # 这里调用完,就退出定义p的作用域,所以下面执行不到
return result * 2 # 这里实际上是不可到达的
end
p my_method # => 10
def my_method
p = lambda {return 10}
result = p.call
return result * 2
end
p my_method # => 20
整体而言,Lambda 更直观,它更像一个方法,对参数数量校验严格,在调用 return 时,只是从代码中返回。
class MyClass
def initialize
@v = 100
end
def my_method
@v
end
end
obj = MyClass.new
# 通过Kernel#method方法,将obj中的方法转为一个Method对象 !!!
mobj = obj.method :my_method
p mobj.class # => Method
# 主动调用
p mobj.call
mobj.to_proc # 将Method转为Proc
Method 和 Proc 有什么区别?
lambda 在定义它的作用域执行,block 是一个闭包
Method 对象会在自身所在的对象的作用域执行,因为 Method 是所属对象的
听名字感觉是一个脱离类,模块儿的一个方法,可以使用Module#unbind
将一个方法转为自由方法,也可以使用Module#instance_method
获取一个自由方法
module MyModule
def my_method
42
end
end
unbound = MyModule.instance_method(:my_method)
p unbound.class # => UnboundMethod
自由方法并不能脱离对象执行,所以我们可以把他再绑定到一个对象中,使之再次成为 Method 对象,可以使用UnboundMethod#bind
进行绑定,从某个类中分离出来的 UnboundMethod,只能绑定在该类或者子类的对象上,模块儿中分离的自由方法则可以自由处置。
module MyModule
def my_method
42
end
end
unbound = MyModule.instance_method(:my_method)
p unbound.class # => UnboundMethod
# 这里在String中定义新的方法,以前我们会传递一个Block,这里我们直接像参数一眼进行传递
String.send :define_method,:another_method,unbound
p "abc".another_method
编写一个监视工具,如果发生不正常状况,进行消息通知,比如:
# 定义一个事件,传达一个事件描述,如果传递的代码块为true,则进行事件描述通知,如果为false,则不通知
event "we're earning wade of money" do
# 这里从数据库中获取数据
recent_orders = 10000
recent_orders > 5000
end
我们进行第一版的设计,只要让这个程序每隔几分钟运行一次
# 这里需要将加载路径添加,否则load会报错
$LOAD_PATH.unshift(File.dirname(__FILE__))
def event(description)
p description if yield
end
# 加载后,所有事件将会执行
load 'events.rb'
# 这个文件就定义所有的事件
event "i am test can i get the right result: false" do
false
end
event "i am test can i get the right result: true" do
true
end
共享事件:是否能让两个独立的事件访问同一个变量? (使用扁平作用域)
def monthly_sales
100 # 从数据库获取的
end
target_sales = 101
event "monthly sales are suspiciously" do
monthly_sales > target_sales
end
event "monthly sales are abysmally low" do
monthly_sales < target_sales
end
这里我们将方法和临时变量直接定义在顶级作用域中,这是不合适的,应该进行封装与优化,希望是下面的效果
setup do
puts "Setting up sky"
@sky_height = 100
end
setup do
puts "Setting up mountains"
@mountains_height = 200
end
event "the sky is falling" do
@sky_height < 200
end
event "its getting closer" do
@sky_height < @mountains_height
end
event "whoops ... too late" do
@sky_height < 0
end
# 我们可以自由的混合事件和setup代码块,DSL还是会检测事件,在每次执行事件前都会运行所有的setup,我们希望运行后的结果为:
"Setting up sky"
"Setting up mountains"
"the sky is falling"
"Setting up sky"
"Setting up mountains"
"its getting closer"
"Setting up sky"
"Setting up mountains"
setup 应该给@开头的变量赋值,事件可以读取这些变量,这样后面写代码就会干净的多
p "main对象的实例变量:#{self.instance_variables}"
$LOAD_PATH.unshift(File.dirname(__FILE__))
def event(description)
# 每次执行block前,先load数据获取文件,我们在其中定义一些实例变量,当加载后,该实例变量归属于main对象,这里是扁平作用域,所以可以被事件感知到
load 'setup.rb'
p description if yield
end
def setup()
yield
end
load 'events.rb'
# 在 load 后,加载文件中
p "main对象的实例变量:#{self.instance_variables}"
专门在一个文件中定义实例数据
setup do
puts "Setting up sky"
@sky_height = 100
end
setup do
puts "Setting up mountains"
@mountains_height = 200
end
新需求:要求按照特定的顺序执行块和事件
$LOAD_PATH.unshift(File.dirname(__FILE__))
def setup(&block)
@setups << block
end
def event(description,&block)
@events << {:description=>description,:condition=>block}
end
@setups = []
@events = []
# 这里所有的event已经被保存,所有的setup也保存好了
load 'events.rb'
@events.each do |event|
@setups.each do |setup|
setup.call
end
puts "ALERT: #{event[:description]}" if event[:condition].call
end
新需求:@events 和 @setups 为顶级实例变量,但其实也是全局变量的变形,安全性有限,我们能不能消除他们,这里我们使用共享作用域
lambda {
setups = []
events = []
Kernel.send :define_method,:setup do |&block|
setups << block
end
Kernel.send :define_method,:event do |description,&block|
events << {:description=>description,:condition=>block}
end
Kernel.send :define_method,:each_setup do |&block|
setups.each do |setup|
block.call setup
end
end
Kernel.send :define_method,:each_event do |&block|
events.each do |event|
block.call event
end
end
}.call
load "events.rb"
each_event do |event|
each_setup do |setup|
setup.call
end
puts "ALERT: #{event[:description]}" if event[:condition].call
end
这里我们使用 lambda 来定义一个 block,在内部定义 setups 和 events,在 Kernel 中定义方法,这些方法能访问到 block 中的代码块,而其他方法则无法访问到,这样就可以保证,消除全局变量,但代码看起来更复杂了似乎
在目前的版本中,事件可以修改其他事件共享的顶层实例变量,我们需要在 setup 中定义共享变量,而不是在事件中
event "define a shared variable" do
@x = 1
end
event "define a shared variable" do
@x = @x + 1
end
改进方案,使用 Object 作为白板类,使用上线文探针执行 block,这样每个 event 都是独立的,这里我们没有使用 BasicObject 充当白板类,因为里面缺少一些基本的方法:puts
each_event do |event|
env = Object.new
each_setup do |setup|
env.instance_eval &setup
end
puts "ALERT: #{event[:description]}" if env.instance_eval &(event[:condition])
end