在老東家寫(xiě)了一段時(shí)間Python后,我被告知Python里面有個(gè)很經(jīng)典的被稱之為生成器的特性,而這個(gè)特性至今讓我難以忘懷。后來(lái)我接觸了Ruby,我就納悶了Ruby怎么就沒(méi)看到這個(gè)特性?
直到最近在讀《松本行弘的程序員世界》的時(shí)候我才意識(shí)到我錯(cuò)了,這個(gè)特性早在Ruby1.9就已經(jīng)具備,只不過(guò)是我資歷尚淺,察覺(jué)不到它的存在罷了。

1. Python的yield語(yǔ)句可用于構(gòu)造生成器
下面是一個(gè)簡(jiǎn)單的Python生成器例子,它的功能是遍歷0-max范圍內(nèi)的所有數(shù)字
def range(max):
n = 0
while n < max:
yield n
n += 1
if __name__ == '__main__':
for i in range(100):
print(i)
PS: 結(jié)果就是打印0~99這里就不貼出來(lái)了。
有沒(méi)有感覺(jué)這個(gè)函數(shù)很有梯度感?(這是我放棄Python的理由之一 )T_T。如果我們直接在REPL環(huán)境里面運(yùn)行range函數(shù),會(huì)得到下面的結(jié)果
In [8]: range(100)
Out[8]: <generator object range at 0x102f41460>
從字面上可以看出它是一個(gè)generator,也就是我們所說(shuō)的生成器。
當(dāng)然,我們也可以直接構(gòu)造一個(gè)數(shù)組,通過(guò)遍歷數(shù)組來(lái)完成以上的打印過(guò)程。但生成器的好處就是它是惰性的,也就是說(shuō)當(dāng)你訪問(wèn)某個(gè)元素的時(shí)候?qū)?yīng)元素才會(huì)被生成,而不是先生成一個(gè)完整的數(shù)組,然后我們?cè)偃ケ闅v它。
2. Ruby中的yield
現(xiàn)在我們看看Ruby的yield語(yǔ)句是用來(lái)做什么的,舉個(gè)簡(jiǎn)單的例子就能夠看出來(lái)
def handler(a)
yield(a)
end
result = handler(100) { |i| i * 20 }
puts result
程序的運(yùn)行結(jié)果是
> 2000
簡(jiǎn)單來(lái)說(shuō),Ruby里面的yield可以用來(lái)接收方法塊,調(diào)用它就相當(dāng)于調(diào)用方法塊。上面的例子里我們的方法塊是{ |i| i * 20 },它接收一個(gè)參數(shù)i并返回i * 20的值,結(jié)果就是 100 * 20 = 2000。
3. Ruby中的生成器
那問(wèn)題來(lái)了,yield這個(gè)語(yǔ)句已經(jīng)被用作方法塊的代理了,那我們?cè)赗uby里面要怎么實(shí)現(xiàn)如Python般可以返回生成器的函數(shù)?還是說(shuō)Ruby中壓根沒(méi)有生成器這種機(jī)制?
怎么可能呢?其實(shí)生成器就在我們身邊,只不過(guò)我們很多時(shí)候都察覺(jué)不到它的存在,具體我們可以參考一下這篇文章。在Ruby1.9之后就實(shí)現(xiàn)了Enumerator這個(gè)類,它的機(jī)制跟Python里面的生成器類似,可以用作惰性求值。使用方式如下
> a = (1..100000000000000000).each
=> #<Enumerator: 1..100000000000000000:each>
> a.next
=> 1
> a.next
=> 2
> a.next
=> 3
> a.next
=> 4
> a.next
=> 5
有點(diǎn)編程經(jīng)驗(yàn)的朋友都知道,我們不怎么可能活著看到我們的計(jì)算機(jī)生成長(zhǎng)度為100000000000000000的數(shù)組,所以這里面一定有什么黑魔法T_T。
本質(zhì)上它是通過(guò)Range#each方法返回了Enumerator的對(duì)象,我們可以通過(guò)依次調(diào)用Enumerator#next方法來(lái)獲取下一個(gè)元素,而這個(gè)Enumerator的對(duì)象就相當(dāng)于一個(gè)生成器。
4. 用Ruby實(shí)現(xiàn)可以返回生成器的函數(shù)
?
Ruby1.9之后Ruby提供了名為Fiber的類,可以用于構(gòu)建輕量級(jí)的協(xié)程,它讓我們可以手動(dòng)地去控制我們的線程是否被喚醒。那么這個(gè)玩意到底有什么用呢?畢竟,我們現(xiàn)在來(lái)談協(xié)程這些多線程的東西似乎有點(diǎn)早。
下面是一個(gè)簡(jiǎn)單的例子
fiber = Fiber.new do
Fiber.yield 1
2
end
puts fiber.resume
puts fiber.resume
puts fiber.resume
可見(jiàn)Fiber::yield的作用跟Python中的yield語(yǔ)法有點(diǎn)類似。這里我們通過(guò)Fiber#new語(yǔ)句來(lái)創(chuàng)建Fiber實(shí)例,并傳遞一個(gè)代碼塊。我們可以把Fiber::yield想象成類似于byebug或者binding.pry等調(diào)試語(yǔ)句,程序運(yùn)行到這里就會(huì)暫停,然后把控制權(quán)讓出去。
當(dāng)我們第一次運(yùn)行Fiber#resume方法的時(shí)候就會(huì)返回Fiber::yield語(yǔ)句后面的數(shù)值,而第二次執(zhí)行Fiber#resume就會(huì)繼續(xù)運(yùn)行代碼塊余下的代碼并返回2,第三次運(yùn)行Fiber#resume的時(shí)候由于我們塊已經(jīng)執(zhí)行完畢所以會(huì)報(bào)錯(cuò)
> fiber.resume
=> 1
> fiber.resume
=> 2
> fiber.resume
FiberError: dead fiber called
from (irb):7:in `resume'
from (irb):7
感覺(jué)這個(gè)Fiber類就是我們要找的生成器的類。下面是我YY出來(lái)的用Ruby的生成器特性來(lái)實(shí)現(xiàn)的函數(shù),它的功能類似文章開(kāi)頭用Python實(shí)現(xiàn)的函數(shù)。
def range(n)
Fiber.new do
i = 0
while i < n
Fiber.yield i
i += 1
end
end
end
fiber = range(10)
p fiber.resume
p fiber.resume
p fiber.resume
p fiber.resume
上面腳本的輸出是
0
1
2
3
在Python里面含有yield語(yǔ)句的方法被調(diào)用之后會(huì)返回一個(gè)生成器對(duì)象,然后我們可以利用這個(gè)生成器來(lái)進(jìn)行惰性求值(個(gè)人感覺(jué)這種語(yǔ)法不夠清爽,沒(méi)有一個(gè)顯式的返回語(yǔ)句,把生成器對(duì)象返回)。另外,Python的版本里使用了for語(yǔ)句來(lái)迭代生成器,我這里只是簡(jiǎn)單地手動(dòng)去迭代。
Ruby的range方法返回了一個(gè)Fiber的對(duì)象,它就相當(dāng)于一個(gè)生成器(或者也可以說(shuō)是一個(gè)外部的迭代器),通過(guò)反復(fù)調(diào)用Fiber#resume方法就可以獲取下一個(gè)值,如果反復(fù)調(diào)用超出限制就會(huì)報(bào)錯(cuò)。我Fiber#resume10次之后就會(huì)得到下面的結(jié)果。
dead fiber called (FiberError)
5. 總結(jié)
瞎扯了一下Ruby的生成器特性。作為一個(gè)特性,它并不像Python那么明顯,但是它確實(shí)存在著。得益于Fiber這種類庫(kù)的出現(xiàn),已經(jīng)更多Ruby多線程相關(guān)的庫(kù)被開(kāi)發(fā)出來(lái)了。據(jù)說(shuō)Fiber性能方面的表現(xiàn)比Ruby原來(lái)內(nèi)置的Thread要好一些,具體的比較我想放在后面的文章再詳細(xì)討論,很感謝你能夠看到這里。