大话Ruby Block

Published on:
Tags: ruby block

这是Ruby中「方法和块(block)之间」的夺魂战!

注:本帖,不讨论Ruby中块(block)的函数式语言特性, 在「诱人的Ruby」视频中已有相关论述。


一、 块(block)是一段「半自由」的代码,其形在方法外,而魂归方法里。

块(block)代码由{...}或do...end界定而成。

   [1, 2, 3].each{|i| i + 1}

块(block)代码不能被单独调用,只能依附于方法,所以称其为魂归方法里。

二、 块(block)魂归自身,获得完全自由的唯一方法就是,让其对象化。

魂归自身,意味着,它完全自由了,可被单独调用了

  Proc.new{ puts "hello"}
  proc{ puts  "hello"}
  proc{|msg| puts "hello #{msg}"}
  
  lambda{puts "hello"}
  lambda{|msg = "world!"| puts "hello #{msg}"}
  
  ->{puts "hello"}
  ->(msg="world!"){puts "hello #{msg}"}

以上就是Ruby提供的所有块(block)对象, 都是Proc类的对象。

三、 用不用块(block),是你的自由,但是方法天生就是块的魂归之地。

Ruby中任何一个方法你都可以传递一个块。

   def test;end
   test{ puts i} 

上面的代码,甚至都不会报错,因为在test方法内部没有去使用这个块(block)。
显然,你已经不能把块(block)当成那个常规的方法参数了,虽然它的行为像参数。

四、 如何使用块(block), 直接决定了它的命运!

A. 在方法内部使用yield关键字去引block的“魂”。

  def test
    yield
  end
  test{puts "hello test!"}
  
  def test(x)
    yield(x)
  end
  test('world!'){|x| puts "hello #{x}"} 

yield关键字不仅可以挂载块(block)代码,而且可以给块传递参数。

B. 魂归自身式:使用「&」让一个传递给方法的块(block)魂归自身。

  def test(&block)
    block.call("world")
  end
  
  test{|msg| puts "hello #{msg}"}

block到了方法内部,已经被&转化为了一个Proc对象。
但是在方法内部,也可以使用「&」给块对象(Proc对象)抽魂:

  def test(&block)
    inner_test(&block)
  end
  
  def inner_test
    yield("haha!")
  end
  
  test{|msg| puts "hello #{msg}"}

test方法传进去的block被转化为了Proc对象,而其内部的inner_test又利用「&」把这个Proc对象转化为了半自由的块(block)
test方法真阴险!有木有?

五、 争风吃醋的block兄弟。

{...}和do ... end,虽然是块(block)孪生兄弟,但是也有个谁先谁后的,使用的时候注意,否则它们会让你知道谁才是老大!

  def test(block)
    block.call
  end
  
  test lambda do 
     puts "hello"
  end

你给test方法传递了一个自由的Proc对象,但是稍有不同,这个Proc对象是用do...end,但是,你执行代码后会发现,这段代码会抛出异常:ArgumentError: tried to create Proc object without a block

  test( lambda do
          puts "hello"
        end
      )

必须要这么修改才可以正常执行。或者使用:

  test lambda{puts "hello"}

这样也可以正常工作。

可以看得出来, do...end 不如 {...}优先级高,{...}亲近离它最近的那个lambda, 而do...end却亲近最远的那个test。
感觉do...end有点混,所以类似于这种把Proc对象传递给方法的时候,一定要注意用括号,或者用{...}

不过由于这个抛出的异常,ArgumentError: tried to create Proc object without a block,我们倒是可以得到一个隐式传递block给方法的写法:

  def test
    proc_obj = Proc.new
    proc_obj.call
  end
  
  test{puts "hello"}

六、 块(block)也有自尊。

  def test
    x = 1
    yield(x)
  end
  
  x = 2
  test{ puts x }

块(block)有个超能力,穿透作用域。 上面例子里,块(block)并没有使用test方法里的变量x=1, 而是把块(block)作用域外的变量x=2抓在了手里。

七、 对于块(block)的自尊, 方法向你(Rubyist)求助!

  def test
    x = 1
    yield(x)
  end
  
  x = 2
  test{ |x| puts x }

你(Rubyist)往块(block)里插入了个参数,顿时霸气侧漏。方法对你感激不尽,你骄傲的昂起了头 。。。

八、 块(block)还是不服!向你(Rubyist)求助,恢复其自由身!

你(Rubyist)帮块(block)实现了自由身,使用lambda对象化了块

  def test
    x = 1
    lambda{ puts x}
  end
  
  lambda_proc_obj = test
  lambda_proc_obj.call

这下块对象(lambda_proc_obj)出气了, lambda_proc_obj想啥时候调用就啥时候调用(lambda_proc_obj.call)。
lambda_proc_obj心想:「哥把你这方法打包了,掌握着你的数据(x=1),再也不看你脸色了。」
噢, 原来如此, 让块(block)完全自由以后变成Proc对象, 它就有闭包的超能力了,好可怕!

九、 两种块对象(Proc对象), 两种命运,可悲可叹!

A. 被方法所奴役 : 必须要执行的代码段

  proc_obj = Proc.new{ return "from proc return"}
  # proc_obj = proc {return  "from proc return"}

  
  def test(block)
    msg = block.call
    puts "hello! #{msg}!"
  end
  
  test(proc_obj)

这段代码会抛出LocalJumpError异常, 用Proc.new和proc{}定义的Proc对象,是不能使用return的。
这就意味着,这个Proc对象,一旦被call,就必须要执行完。

注意这里test方法传递一个本身就是Porc的对象,并没有使用&。

再看看这段代码,看看这俩Proc对象对arity的关心程度(是否检查块对象的参数个数)

def call_with_too_many_args(closure)
  begin
      puts "closure arity: #{closure.arity}"
      closure.call(1,2,3,4,5,6)
      puts "Too many args worked"
  rescue Exception => e
      puts "Too many args threw exception #{e.class}: #{e}"
  end
end
 
def two_arg_method(x,y)
end

puts "Proc.new:"; call_with_too_many_args(Proc.new {|x,y|})
puts "proc:"    ; call_with_too_many_args(proc {|x,y|})

这段代码可以看出来, Proc.new或proc{}定义的proc对象,完全是方法的奴隶啊!
工作必须执行完不说(无法return),参数个数给传多少都不能发出半点怨言啊!僵尸? 奴隶?

B. 一段匿名的方法体

  lambda_proc_obj = lambda{return "return from lambda proc"}
  # lambda_proc_obj = ->{return "return from lambda proc"}


  def test(block)
    msg = block.call
    puts "hello! #{msg}!"
  end
  
  test(lambda_proc_obj)

可以看得出来,lambda proc对象是可以正常返回的.

  puts "lambda:"  ; call_with_too_many_args(lambda {|x,y|})

这段代码,就明白了,lambda{}方式创建的Proc对象,才是真正的自由块对象啊!
工作想休息的时候就休息(可以return), 给传的参数多了,也可以发出怨言。
自由的空气,真好。

看来lambda proc和Method对象,有点类似:

  puts "Method:"  ; call_with_too_many_args(method(:two_arg_method))

十、 你(Rubyist)有四种方式对可怜的块对象呼来喝去。

  lm_proc_obj = lambda{puts  "hello world!"}
  
  lm_proc_obj.call
  lm_proc_obj.() #无参数必须给一个空的括号

  lm_proc_obj[] #无参数必须给一个空的中括号

  
  lm_proc_obj = lambda{|msg| puts  "hello #{msg}!"}
  
  lm_proc_obj.call("world")
  lm_proc_obj.("world") 
  lm_proc_obj["world"]
  lm_proc_obj === "world"

看到第四种方式,你应该想到,你可以在case语句里使用proc对象了吧?

  def response_code?(code)
    ->(response) { response.code == code }
  end

  case response
  when response_code?(200) then 'OK'
  when response_code?(404) then 'Not found'
  else 'Unknown code'
  end

总结:

可怜的块(block)和块对象(Proc对象),就这样被你(Rubyist)和方法欺负着。如果你能善心大发,在开发中尽量还块对象以自由身(lambda proc 对象),你会得到好报的。


相关视频:

「诱人的Ruby」 入门篇 - 「Ruby中的Block」
「诱人的Ruby」基础提高篇 - 「块与闭包」。

参考

closures-in-ruby
lambdas-slash-procs-in-case-expressions

如果你喜欢本文,想看更多内容,请关注微信公众平台:RubyStudy

Comments

comments powered by Disqus