元編程:代碼塊

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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,765評論 18 399
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,768評論 0 9
  • 一:java概述:1,JDK:Java Development Kit,java的開發和運行環境,java的開發工...
    ZaneInTheSun閱讀 2,687評論 0 11
  • 前言 有的時候我們只要按條處理,追求實時性而非吞吐量的時候,類似Storm的模式就比較好了。Spark 在流式處理...
    祝威廉閱讀 2,227評論 2 6