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