读《Ruby元编程》之可调用对象

《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_evalinstance_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例子,个人感觉很不错,特别是使用共享作用域,消除全局变量的代码重构,很精彩,推荐阅读。