元编程:代码块

1.块的基础知识
代码如下所示:
def a_method(a, b)
  a + yield(a, b)
end

a_method(1, 2){ |x, y|(x+y)*3 } #=>10

通过如上的代码可以总结出代码块基础的如下结论:
1.只有调用一个方法时才可以定义一个块。
2.块会被直接传递给这个方法,然后该方法可以用yield关键词回调这个块。
3.块可以有自己的参数,当回调块时,可以像调用方法那样为块参数提供值,另外,像方法一样,块中最后一行代码执行的结果被作为返回值。
4.可以使用Kernel#block_given?方法来询问当前的方法调用是否包含块,代码如下所示:

def a_method
  return yield if block_given?
  'no block'
end
2.闭包
绑定:局部变量、实例变量、self...这些东西都是绑定到对象上的,可以把他们简称为绑定(binding)
闭包:
1.当创建块时会获取局部绑定(比如上面的x),然后把块连同它自己的绑定传给一个方法,方法中的x对这个块来说是不可见的,见示例代码1
2.也可以在块的内部定义额外的绑定(即块局部变量),但是这些绑定在块结束时就会消失,见示例代码2

示例代码1:
def my_method
  x = 'goodbye'
  yield("cruel")
end

x = "Hello"
my_method { |y| "#{x}, #{y} world "} #=>"Hello,cruel world"

示例代码2:
def my_method
  yield
end

top_level_variable  = 1
my_method do
  top_level_variable += 1
  local_to_block = 1
end

top_level_variable  #=>2  块获得局部绑定,并改变其值
local_to_block  #=> Error!  块局部变量在块外部不能被调用
3.作用域和作用域门(scope gate)
一旦进入一个新的作用域,原先的绑定就会被替换为一组新的绑定。
比如在类外部定义的变量就不能在类内部进行使用,方法外部的变量就不能在方法内部使用。

程序会在三个地方关闭和打开作用域,这三个地方分别是:
1.类定义 class
2.模块定义 module
3.方法 def
这三个标示符class,module,def分别充当了作用域门的作用。

示例代码:
v1 = 1
class MyClass  #作用域门,进入MyClass
  v2 = 2
  local_variables  #=>["v2"]

  def my_method  #进入作用域门,进入def
    v3 = 3
    local_variables
  end  #作用域门,离开def

  local_variables  #=>["v2"]
end  #作用域门,离开class

obj = MyClass.new
obj.my_method
obj.my_method
puts local_variables

注意点:class/module与def之间的差别:
类和模块定义中的代码会被立即执行,方法定义中的代码只有方法被调用时才被执行。
4.全局变量、顶级实例变量和局部变量
全局变量:使用“$”开头表示
顶级实例变量:如果该实例变量的当前对象是main时,该变量是顶级实例变量

顶级实例变量在任何作用域中都可以被访问和修改:
def a_scope
  $var = "some value"
end

def another_scope
  $var
end

a_scope
another_scope  #=>"some value"

可以用顶级实例变量代替全局变量,顶级实例变量是顶级对象main的实例变量:
@var = "the top-level @var"

def my_method
  @var
end

my_method  #=>"the top-level @var"

上面的代码中,只要main对象扮演self的角色,就可以访问顶级实例的变量
如果其他对象成为self时,顶级实例变量就到作用域外部了。

class MyClass
  def my_method
    @var = "this is not the top-level @var"
  end
end

实例变量和局部变量的对比
#实例变量情况
def test
  @demo = "this is the demo"
end

test

puts @demo #=> this is the demo

#局部变量情况
def test
  demo = "this is the demo"
end

test

puts demo #undefined local variable and method test

#总结:局部变量受限于类、块、方法和模块,只在这个范围内有效
5.扁平化作用域和共享作用域
扁平作用域:使用方法来代替作用域们,可以让一个作用域看到另外一个作用域里的变量,即让一个变量穿越作用域。
1、Class.new代替class
如果是继承自Array的类,可以使用Class.new(Array)代替class
2、define_method代替def

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
puts counter #=>0
inc(4)
puts counter #=>4

counter和inc都是Kernel的内核方法
这是Kernel内核方法定义的一种形式,另外一种形式如下:

module Kernel
  def counter
    #do something
  end
end
6.instance_eval、洁净室和instance_exec
#instance_eval可以作为上下文探针,实现如下的作用
1. 改变对象的属性
2. 绑定局部变量,并且改变局部变量的值
3. 执行方法

class MyClass
  def initialize
    @v = 1
  end
end

v = 2
obj.instance_eval{
  @v = v
  puts @v  #=>2
}

洁净室:一个只是为了在其中执行块的对象
class CleanRoom
  def complex_calculation
    11
  end

  def do_something
    puts "do something"
  end
end

clean_room = CleanRoom.new
clean_room.instance_eval do
  if complex_calculation > 10
    do_something
  end
end

instance_eval:该方法允许对块传入参数

class C
  def initialize
    @x = 1
  end
end

class D
  def twisted_method
    @y = 2
    C.new_instance_eval{ "@x: #{@x}, @y: #{@y}"}
  end
end

D.new.twisted_method #=>"@x:1, @y: "
上面的代码说明:
实例变量是依赖于当前对象self的;
因此instance_eval方法把接受者变为当前对象self时,调用者的实例变量就落在作用域范围外了。

class D 
  def twisted_method
    @y=2
    C.new.instance_exec(@y){|y| "@x: #{@x}, @y: #{y}"}
  end
end

D.new.twisted_method #=> "@x:1, @y:2"
可以通过instance_exec方法实现参数传递,把@x和@y放在同一个作用域中。



7.Proc对象
从底层来看,使用块需要经过两个步骤:
第一步是将代码打包备用,第二步调用块来执行代码,这就是“先打包代码,以后调用”机制。
在ruby中,主要有下面五种种方法来来打包代码:
1.Proc.new
2.proc
3.lambda
4.->
5.&

#使用Proc.new
inc = Proc.new{ |x| x+1 }
puts inc.call(2)
puts inc.class #=>Proc

#使用proc
inc = proc { |x| x+1 }
puts inc.call(2)
puts inc.class

#使用lambda
inc = lambda { |x| x+1 }
puts inc.call(2)
puts inc.class

#使用->
p = ->(x){ x+1 }
puts p.class #>Proc
puts p.call(1)

在以上的代码中,一个Proc就是一个转换成对象的块,
可以通过把块传给Proc.new方法来创建一个Proc。
以后就可以用Proc#call()方法来执行这个块转换而来的对象:

inc = Proc.new{ |x| x+1 }
puts inc.call(2)

上面的这种技术叫做延迟技术,同时还要说明的是上面提到的三种方法都是Kernel方法,分别是proc,Proc.new,lambda.

#使用&
作用如下所示:
1.把块传递给另外一个方法
2.块与Proc对象之间的相互转换,其中带&是块,&后面的是Proc对象

把块传递给另外一个方法:
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(2, 3){|x, y| x*y }

在如上代码中,teach_math方法将块传递给math方法,&operation是块参数。
在调用teach_math()方法时如果没有附加一个块,则&operation参数将被赋值为nil,这样在math()方法中的yield操作会失败。

&操作符的真正含义是,这是一个Proc对象,我想把它当做一个块来使用,简单地去掉&操作符,就能再次得到一个Proc对象。
就是说在上面的&operation是一个块参数,而operation是一个块对象。

下面的代码是将一个块参数转化为块对象,代码如下所示:
def my_method(&the_proc)
  the_proc
end

p = my_method{ |name| "Hello, #{name}"}
puts p.class #>Proc
puts p.call("Bill")  #>Hello, Bill

下面的代码是将块对象转换块,具体代码如下:
def my_method(greeting)
  puts "#{greeting}, #{yield}"
end

my_proc = proc{ "Bill" }
my_method("Hello", &my_proc) #> Hello, Bill

当调用my_method()方法时,&操作符会把my_proc转换为块,再把这个块传递给这个方法。
8.pro和lambda的区别
两点区别:
1.return关键词的处理
2.参数检验有关

1.return关键词的处理
在lambda中,return仅仅表示从这个lambda中返回,
而在proc中,return的行为则有所不同,它不是从proc中返回,而是从定义proc的作用域中返回,代码如下所示:
def double(callable_object)
  puts callable_object.call*2
end

l = lambda{ return 10 }
double(l) #=>20
如上的代码表示return从这个lambda中返回。

def another_double
  p = Proc.new{ return 10 }
  resule = p.call
  return result*2
end

puts another_double #>10
上面的代码结果返回的是10,这个结果是从下面这段代码进行返回
p = Proc.new{ return 10 }


而下面的代码其实不会被执行。
resule = p.call
return result*2

2.参数检验有关
proc和lambda相比,lambda对参数要求更严格,如果规定lambda只能接受两个参数,那么参数减少或者增加都会报错,而pro则会自己进行调整,具体代码如下所示:
p = Proc.new{ |a, b| [a, b]}
puts p.arity

puts p.call(1,2,3) #=> [1,2]
puts p.call(1) #=>[1, nil]

上面是Proc类的形式,而下面是lambda的形式:
p = lambda { |a, b| [a, b]}
puts p.arity

puts p.call(1,2,3) #=> [1,2]  #报错,wrong number of arguments
9.对象对象
示例代码:
class MyClass
  def initialize(value)
    @x = value
  end

  def my_method
    puts @x
  end
end

object = MyClass.new(1)
m = object.method :my_method
m.call #=>1

在上面的代码中通过object#method方法可以获得一个用Method对象表示的方法,然后通过call方法进行调用。其中Method对象类似于lambda,但是有一个重要的区别:lambda在定义它的作用域中执行,而Method对象会在它自身所在对象的作用域中执行。

在如上的代码中,m绑定的是object对象,下面的代码可以实现将m绑定到其他对象上面,但是局限于是同一个类中的对象,具体代码如下所示:

class MyClass
  def initialize(value)
    @x = value
  end

  def my_method
    puts @x
  end
end

object = MyClass.new(1)
m = object.method :my_method
m.call #=>1

unbound = m.unbind
another_object = MyClass.new(2)
m = unbound.bind(another_object)
m.call #=>2

在如上的代码中取消的绑定对象是object,而重新绑定的对象是anthoer_object,在another_object这个对象中,实例变量@x已经改变,由原来的1变为2,但是方法还是原来的方法,因此这个方法my_method是存在于MyClass这个类中的。

DSL(domain specific language 领域专属语言)

下面的代码是书中的第一个DSL,文件名为redflag.rb

def event(name)
  puts "ALTER: #{name}" if yield
end

Dir.glob("*events.rb").each{ |file| load file}

下面的代码在test_events.rb这个文件中,并且和上面代码所在的文件在同一个文件夹中,具体代码所示为:

event "an event that always happens" do 
  true
end

event "an event that never happens" do
  false
end

在终端执行第一文件中的redflag,结果如下所示:

ALTER: an event that always happens

上面的代码也可以放在一个文件中,代码如下所示:

def event(name)
  puts "ALTER: #{name}" if yield
end

event "an event that always happens" do 
  true
end

event "an event that never happens" do
  false
end

使用扁平作用域实现共享事件,代码如下:

def monthly_sales
  110
end

target_sales = 100

event "monthly sales are suspiciously high" do
  monthly_sales > target_sales
end

event "monthly sales are abysmally low" do
  monthly_sales < target_sales
end

上面的代码中的两个事件共享了一个方法和一个局部变量,运行最初的执行文件,执行结果如下面代码所示:

ALTER: monthly sales are suspiciously high

增加setup指令,在下面的代码中,要求setup方法会先于其他方法执行,代码如下所示:

event "the sky is failing" do
  @sky_heigth < 300
end

event "it's getting closer" do
  @sky_heigth < @mountains_heigth
end

setup do
  puts "Setting up sky"
  @sky_height = 100
end

setup do
  puts "Setting up mountains"
  @mountains_height = 200
end

执行上面的代码,得到如下结果:

Setting up sky
Setting up mountains
ALTER: the sky is failing
Setting up sky
Setting up mountains
ALTER: it's getting closer

按照书中的要求setup方法是优先执行于event块之前。
代码如下redflag所示:

def event(name, &block)
  @events[name] = block
end

def setup(&block)
  @setups << block
end

Dir.glob("*events.rb").each do |file|
  @events = {}
  @setups = []
  load file
  @events.each_pair do |name, event|
    env = Object.new
    @setups.each do |setup|
      env.instance_eval &setup
    end
    puts "ALTER:#{name}" if env.instance_eval &event
  end
end

在上面的实例变量@events和@setups都是顶级实例变量(因为当前的self对象是main),这些顶级实例变量被event方法和setup方法所共享,&block是块的形式,block则是其对象,然后在对象env的洁净室中进行代码执行。

这里的顶级实例变量相当于是全局变量,为了消除全局变量,这里使用共享作用域,代码如下所示:

lambda {
  setups = []
  events = {}
  Kernel.send :define_method, :event do |name, &block|
    events[name] = block
  end

  Kernel.send :define_method, :setup do |&block|
    setups << block
  end

  Kernel.send :define_method, :each_event do |&block|
    events.each_pair do |name, event|
      block.call name, event
    end
  end

  Kernel.send :define_method, :each_setup do |&block|
    setups.each do |setup|
      block.call setup
    end
  end
}.call

Dir.glob("*events.rb").each do |file|
  load file
  each_event do |name, event|
    env = Object.new
    each_setup do |setup|
      env.instance_eval &setup
    end
    puts "ALTER:#{name}" if env.instance_eval &event
  end
end

你可能感兴趣的:(元编程:代码块)