Ruby元编程 星期三
@ Shen Jianan · Monday, Oct 26, 2015 · 9 minute read · Update at Oct 26, 2015

代码块

块可以用来控制作用域,它只是Ruby中“可调用对象”的一种,还有其他的可调用对象,比如proc和lambda。在这一章将会学习怎样利用这些对象来进行元编程。

前面两章的内容跟普通的面向对象没有很大的区别,但是代码块源于函数式编程语言,所以会带有函数式编程独特的思考角度~

块基础

在Ruby中块是指大括号{}或者do…end之间的代码块,只有在调用一个方法时,才可以定义一个块。块会被传递给这个方法,该方法可以使用yield调用这个块。

使用yield调用代码块的例子:

def a_method
  return yield if block_given?
  'no block'
end


p a_method      #=> no block
p a_method{'The block !'}  #=> the block!

在Java中,有内部作用域的概念,在内部作用域可以看到外部作用域的变量。但是Ruby没有这种嵌套式的作用域,一旦进入新的作用域,原来的绑定就会被替换成一组新的绑定。这意味着在如下的代码中,v1在MyClass的作用域之外,一旦进入MyClass之后v1就不可见了。但是全局变量是可以跨越作用域的~~

v1 = 1
$v2 = 2


class MyClass
  v3 = 3
  def print_v1
    p v1
  end
  
  def print_v2
  	 p v2
  end
end


MyClass.new.print_v1    #=> Error!
MyClass.new.print_v2    #=>2

如果觉得全局变量无法控制,那么可以使用顶级实例变量,这样只要main对象扮演self对象就可以访问,而当其他对象成为self时,就不能够看见顶级实例变量了。但是这种安全性也是有限的。

@var = "The top-level @var"

def my_method
	@var
end

my_method		# => "The top-level @var",因为这时的self还是main

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

作用域门与扁平化作用域

Ruby中程序会在三个地方关闭前一个作用域,同时打开一个新作用域:

  • 类定义
  • 模块定义
  • 方法

有时候会想要知道怎么才能让绑定穿越作用域门:

my_var = "Success"

class MyClass
	# 希望在这里打印 my_var
	
	def my_method
		# ...还有这里
	end
	
end

class是作用域门,我们能想的就是用非作用域门,比如方法调用来代替作用域门。从Ruby文档可以知道(书上写的,我就查看了一下文档)

Creates a new anonymous (unnamed) class with the given superclass (or Object if no parameter is given). You can give a class a name by assigning the class object to a constant. If a block is given, it is passed the class object, and the block is evaluated in the context of this class using class_eval.

也就是说Class.new可以用来达到跟class关键字一样的效果,但是它是方法调用,不会造成作用域分隔。类似的,可以使用动态方法来代替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

这种使用方法调用来代替作用域门的方法,称为扁平化作用域。

共享作用域

有了扁平作用域之后,就拥有了控制作用域的能力。假如想在一组方法之间共享一个变量而不希望其他方法能访问这个变量,就可以把这个方法定义在那个变量所在的扁平作用域中。

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   

这个例子定义了两个内核方法,还使用动态派发来访问Kernel的私有方法define_method。现在counter和inc方法都能看到shared变量,但是其他方法却看不到它。

通过作用域门、扁平作用域和共享作用域之后,就可以在任何地方看到希望看到的变量了。

instance_eval方法

instance_eval是BasicObject类的方法,它将在一个对象的上下文中执行块,也可以看到在代码块中变量定义时的绑定,就像下面代码中的v一样。

class MyClass
  def initialize
    @v = 1
  end
end

obj = MyClass.new

obj.instance_eval do
  @v      # => 1
end

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

传递给instance_eval的代码块被称为上下文探针,因为这些代码块对对象进行操作。

instance_exec方法

instance_exec方法与instance_eval方法类似,但是它还允许传入参数。 实例变量是依赖于当前对象self的,所以即使使用上下文探针,当instance_eval方法把接收者变为当前对象self的时候,调用者的实例变量就落在作用域范围外面了。

class A
  def initialize
    @x =1
  end
end

class B
  def twisted_method
    @y = 2
    A.new.instance_eval { "@x: #{@x}, @y: #{@y}" }
  end
end

B.new.twisted_method      # => "@x: 1, @y: "

在这里@y就因为在作用域外而被当成未定义的nil了。为了把@y放在一个作用域里面,可以使用instance_exec方法传递值:

class A
  def initialize
    @x =1
  end
end

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

p B.new.twisted_method      # => "@x: 1, @y: 2"

洁净室

如果只想要一个用来执行块的环境,那么就不应该有任何方法或变量,以免和块中的方法/变量产生冲突,因此,BasicObject类的实例通常充当洁净室,几乎不带有任何方法。在这样的洁净室中,一些像String的常量都必须用到绝对路径::String。

#可调用对象 代码块的机制是:“打包,调用”,而在Ruby中至少还有三种方法可以打包代码:

  • 使用proc,它是由块转换而来的对象
  • 使用lambda,它是proc的变种
  • 使用方法

Proc对象

如果我们想要存储一段代码怎么办呢?代码块并不是对象,所以不能赋给变量。为了解决这个问题,Ruby提供了名为Proc的类。我们通过可以把代码传给Proc.new来创建一个Proc,以后就可以用Proc#call来执行这个代码块了。

inc = Proc.new {|x| x+1 }
inc.call(2)			# => 3

这样就可以达到延迟执行的效果

其他创建proc的方法:

# 使用lambda
dec = lambda { |x| x+1 }
dec.class			# => Proc
dec.call(2)

# 使用lambda操作符
p = ->(x) { x+1 }

#使用&操作符,在下一段讲解

&操作符

在大多数情况下,传递给一个方法的代码块可以通过yield直接运行,就像一个匿名参数一样。但是在下面两种情况下就力不从心了,因为要实现这两个需求势必要求代码块有自己的名字:

  • 把代码块传递给另外一个方法/代码块
  • 把代码块转换成Proc

要将代码块附加到一个绑定上,可以通过给这个方法添加一个特殊的参数,它必须是参数列表的最后一个并且以&开头。

def math(a, b)
  yield(a, b)
end

def do_math(a, b, &operation)
  math(a, b, &operation)	#将代码块传递给了另一个方法
end

do_math(2, 3){|x,y| x*y}  # => 6,这里的代码块就是&operation参数

&符号的意思就是:这是一个Proc对象,我想把它当成代码块来使用。去掉&符号之后,就能再次得到一个Proc对象。

def math(a, b)
  yield(a, b)
end

def do_math(a, b, &operation)
  math(a, b, &operation)
  operation.class         # => Proc
  operation.call(1,2)     # => 2
end

do_math(2, 3){|x,y| x*y}  # => 6

Proc与Lambda的对比

用lambda方法创建的Proc与其他方式创建的Proc有一些细微却重要的差别。用lambda方法(包括”->"操作符)创建的Proc称为lambda,而其他方式创建的称为proc。这里面有很多特例以及规则,下面是一些大概:

不同的return含义

在lambda中,return仅仅表示从lambda中返回,而在proc中则表示从定义proc的作用域中返回。

#lambda的return 
def double(callable_obj)
  callable_obj.call() * 2
end

l = lambda{ return 10 }
double(l)   # => 20


def another_double()
  l = Proc.new{ return 10 }
  result = l.call()
  result * 2
end


another_double  # => 10

而像下面的代码:

def another_double(callable_obj)
  callable_obj.call *2
end

p = Proc.new { return 10 }
another_double(p)	# => LocalJumpError

因为上面的程序试图从定义p的作用域,也就是顶级作用域返回,而顶级作用域不能返回,所以就失败了,如果不使用return就不会出现这样的问题。

Proc、Lambda和参数数量

在参数数量的问题上,lambda的适应能力比proc以及普通的代码块差。如果调用lambda时参数数量不对,就会抛出ArgumentError错误,而proc则会把传来的参数调整成期望的参数形式。

p = Proc.new {|a,b| [a, b]}
p.call(1,2,3) 	# => [1, 2]
p.call(1)			# => [1, nil]

Method对象

通过Kernel.method方法,可以获得一个用Method对象表示的方法,并在以后用Method#call进行调用。还可以使用Kernel#sinleton_method把单例方法名转换成Method对象

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

  def my_method
    @x
  end
end

obj = MyClass.new(1)
m = obj.method :my_method

m.call    # => 1

Method对象类似于Proc和lambda对象,可以通过Method#to_proc方法转换成Proc对象,也可以用define_method方法把代码块转换成方法

define_method(:print_m, Proc.new {|x| p x})

print_m 'aaa'

Method对象和lambda/Proc之间有一个重要的区别:lambda可以看到定义它的作用域,而Method对象只能看到自身的定义域。

aa = 'aaa'


def my_method(str)
  aa = str
end
my_m = method :my_method

l = lambda{|str| aa = str}

proc = Proc.new {|str| aa = str}


my_m.call 'bbb'
p aa    # => aaa

l.call 'ccc'
p aa    # => ccc


proc.call 'ddd'
p aa    # => ddd

自由方法

自由方法是从类/模块中脱离的方法,可以通过调用Method#unbind或者Module#instance_method方法来获得一个自由方法(类名是UnboundMethod)。

UnboundMethod不能调用,但是可以通过UnboundMethod#bind或者Module#define_method(需要动态派发,因为是私有方法)把它自己绑定到一个对象上。从某一个类中分离出来的UnboundMethod对象只能被绑定到该类及其子类对象上,不过从某个模块中分离出来的UnboundMethod在Ruby2.0之后不受这个限制。

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

  def my_method
    12
  end

end

class MyClassSon < MyClass

end

m = MyClass.new(1).method :my_method
m.unbind

MyClassSon.send(:define_method,:another_method,m)


module MyModule
  def my_method
    42
  end
end

unbound = MyModule.instance_method :my_method
unbound.class     # => UnboundMethod

String.send :define_method, :another_method, unbound

"abc".another_method  # => 42

自由方法在极个别的场景下可以发挥特殊的作用,下面是书上的例子:

Active Support 例子

在有些类库中实现了这样的功能:如果我们想在引用定义在某个文件中的常量时,自动加载该文件,这个自动加载系统中包含了一个Loadable模块,重新定义了标准的Kernel#load方法。有时我们希望消除自动加载功能,也就是使用标准的Kernel#load方法。但是Ruby中没办法将Loadable从祖先链中去除,但是却可以通过自由方法解决这个问题:

module Loadable
	def self.exclude(base)
		base.class_eval {define_method(:load, Kernel.instance_method(:load))}
	end
end


class MyClass
	include Loadable
	exclude(MyClass)
end

这样就会把Kernel#load拷贝到MyClass类中,从而让这个拷贝在祖先链中先于Loadable#load。

这是一个相当冷门的情况,其实大多数情况下自由方法都是用不着的+_+

理解下面代码

下面的一段代码是编写DSL的小测验中的一段代码,理解了它应该就差不多能理解那段代码了~

lambda{
  setups = []
  Kernel.send :define_method, :setup do |&block|
    setups << block
  end

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

}.call


setup do
  puts "initialize sky"
end

each_setup do |setup|
  setup.call
end

这里的定义的each_setup代码段中有参数&block,而代码段内部又嵌套了一个带参数的代码段。

下面来抽丝剥茧,其实最后的

define_method(:each_method, Proc.new {|setup| block.call setup})

就是把setup当做block的参数来调用block方法。 那么再把最后的调用处代码代进去,就是把

define_method(:m, Proc.new{|x| x.call})

代入each_setup,效果就是:

def each_setup
	setups.each do |setup|
		m.call setup
	end
end

#把setup放进m的内部展开也就是
def each_setup
	setups.each do |setup|
		setup.call
	end
end

About Me

2018.02至今 杭州嘉云数据 算法引擎

2017.6-2017.12 菜⻦网络-⼈工智能部-算法引擎

2016.09-2018.06 南京大学研究生

2015.07-2015.09 阿里巴巴-ICBU-实习

2012.09-2016.06 南京大学本科