從實戰(zhàn)項目總結(jié)的Ruby小技巧(第一部分)
從我們在Global Personals項目中使用Github并且following Github Flow開始到現(xiàn)在已經(jīng)將近兩年的時間。在這段時間中,我們以很高的頻率提交了上千次的pull請求,雖然沒有太多如何改善或提高程序的建議和想法,但是 我仍獲得了如此廣泛和珍貴的經(jīng)驗。其中,有一些建議是和項目相關(guān)的,同時,也包含了大量可以在團(tuán)隊內(nèi)分享的Ruby開發(fā)小技巧。
由于我擔(dān)心將從這個項目中獲得和學(xué)習(xí)到的如此珍貴的技巧和經(jīng)驗所遺忘,于是我挑出了其中***的最有價值的部分和大家分享,同時進(jìn)行了一點小小的擴 展。每個人都有自己的工作方式和風(fēng)格,所以我會簡潔明了地和大家闡述。并不是每部分內(nèi)容對每個人來說都是新的,但是希望你在這里可以或多或少都有所收獲。
我將文章分為了幾塊內(nèi)容,以免你一口氣讀完5000個字,同時將它們歸類為幾個部分以便于參考。
- 代碼塊(Blocks) 和 區(qū)間(Ranges)
- 重構(gòu)(Destructuring) 和 轉(zhuǎn)換方法(onversion Methods)
- 異常(Exceptions)和模塊(Modules)
- 調(diào)試(Debugging),項目結(jié)構(gòu)(Project Layout)和文檔(Documentation)
- 其他(Odds and Ends)
讓我們進(jìn)入***部分。
代碼塊(Blocks)
代碼塊是Ruby非常重要的一部分,你隨處都見到它們被廣泛使用。如果你沒有使用,那么你將發(fā)現(xiàn)許多人使用方法關(guān)聯(lián)代碼塊,甚至僅僅是讓代碼結(jié)構(gòu)變得清晰而已。
代碼塊有三種主要的作用:循環(huán)(looping),初始化和銷毀(setup and teardown),以及回調(diào)和延遲執(zhí)行(callbacks or deferred action)。
下面這個例子演示了如何使用代碼塊循環(huán)輸出菲波那切數(shù)列。它使用block_given?方法判斷是否關(guān)聯(lián)了一個代碼塊,否則將從當(dāng)前方法返回一個枚舉器。
yield關(guān)鍵字用來在方法中執(zhí)行一個代碼塊,它的參數(shù)將傳遞給代碼塊。當(dāng)代碼塊執(zhí)行完畢,將返回調(diào)用方法,并執(zhí)行下一行代碼。方法返回值為在***數(shù)(max)之前的***一個菲波那切數(shù)。
- def fibonacci(max=Float::INFINITY)
- return to_enum(__method__, max) unless block_given?
- yield previous = 0
- while (i ||= 1) < max
- yield i
- i, previous = previous + i, i
- end
- previous
- end
- fibonacci(100) {|i| puts i }
下一個例子將把設(shè)置、銷毀以及錯誤處理等操作代碼放到方法中,將方法的主要邏輯放到代碼塊中。通過這種方式,樣板代碼就不需要在多個地方重復(fù),另外當(dāng)你需要改變錯誤處理代碼時,只需要做少量的修改。
yield語句的返回結(jié)果,即代碼塊的返回值,將保存到一個局部變量中。這樣,可以將代碼塊的執(zhí)行結(jié)果作為方法的返回值。
- require "socket"
- module SocketClient
- def self.connect(host, port)
- sock = TCPSocket.new(host, port)
- begin
- result = yield sock
- ensure
- sock.close
- end
- result
- rescue Errno::ECONNREFUSED
- end
- end
- # connect to echo server, see next example
- SocketClient.connect("localhost", 55555) do |sock|
- sock.write("hello")
- puts sock.readline
- end
下一個例子不會使用yield關(guān)鍵字。這里有另外一種使用代碼塊的方法:將‘&’作為方法***一個參數(shù)的前綴,將把關(guān)聯(lián)代碼塊作為一個Proc對 象保存到此參數(shù)當(dāng)中。Proc對象擁有一個call實例方法,可以用來執(zhí)行代碼塊,傳遞給call方法的參數(shù)將作為代碼塊的參數(shù)。在這個例子中,你可以保 存代碼塊最為一個回調(diào)稍后執(zhí)行,或者在你需要的時候執(zhí)行延遲的操作。
- require "socket"
- class SimpleServer
- def initialize(port, host="0.0.0.0")
- @port, @host = port, host
- end
- def on_connection(&block)
- @connection_handler = block
- end
- def start
- tcp_server = TCPServer.new(@host, @port)
- while connection = tcp_server.accept
- @connection_handler.call(connection)
- end
- end
- end
- server = SimpleServer.new(5555)
- server.on_connection do |socket|
- socket.write(socket.readline)
- socket.close
- end
- server.start
區(qū)間(Ranges)
區(qū)間在Ruby代碼中也很常見,通常區(qū)間的形式是(0..9),包含0到9之間的所有數(shù)字,包括9。也有另一種形式(0…10),包含0到10之間的所有數(shù)字,不包括10,即和前一種形式都包含0到9之間的所有數(shù)字,包括9。這種形式并不常見,但有時卻非常有用。
有時候你會看到像這樣的代碼:
- random_index = rand(0..array.length-1)
使用不包含結(jié)尾的區(qū)間將更加整潔:
- random_index = rand(0...array.length)
#p#
有時候,使用不包含結(jié)尾的區(qū)間將更加簡潔和直觀。例如,eb_first…march_first可以更加簡單的計算出今年二月的天數(shù),同時,1…Float::INFINITY可以更加直觀的表示出所有正整數(shù),由于無窮大infinity不是一個數(shù)字。
區(qū)間是優(yōu)秀的數(shù)據(jù)結(jié)構(gòu),因為它允許你定義一個巨大的集合而不需要在內(nèi)存中實實在在的創(chuàng)造出整個集合。你必須小心你所使用的方法,因為某些區(qū)間操作可能導(dǎo)致整個集合被創(chuàng)建。
實例方法each顯而易見會創(chuàng)造出整個區(qū)間,但這通常只會發(fā)生在每次使用***個對象的時候,例如 (1..Float::INFINITY).each {|i| puts i },在沒有輸出任何信息之前,事實上不可能用盡所有可用內(nèi)存。區(qū)間對象中,mixin Enumerable所獲得的方法依賴于each方法,所以它們也具有相同的行為。
區(qū)間有include?和cover?兩個實例方法來測試一個值是否屬于區(qū)間。include?方法使用each方法迭代整個區(qū)間來檢測值是否存在 于區(qū)間中,cover?方法只是簡單比較值是否大于區(qū)間的開頭,并且小于等于區(qū)間的結(jié)尾(對于不包含結(jié)尾元素的區(qū)間是小于區(qū)間的結(jié)尾)。這兩個方法是不可 以等價互換的,可能由于區(qū)間建立方式的不同和排序方式的不同,而導(dǎo)致意象不到的結(jié)果。
- ("a".."z").include?("ab") # => false
- ("a".."z").cover?("ab") # => true
Ruby中許多類都可以進(jìn)行區(qū)間操作,同樣的,你也可以很容易地讓自定義的類進(jìn)行區(qū)間操作。
首先,你需要在類中實現(xiàn)稱之為‘太空船’<=>操作符的方法。在這個方法中,如果other參數(shù)大于self返回-1,如果小于返回1,如果相等則返回0。一般情況下,如果比較是不合法的則返回nil。
下面的例子中,簡單的代理了String#casecmp方法,這個實例方式是一個大小寫不敏感的字符串比較方法,返回上面敘述的格式。
- class Word
- def initialize(string)
- @string = string
- end
- def <=>(other)
- return nil unless other.is_a?(self.class)
- @string.casecmp(other.to_s)
- end
- def to_s
- @string
- end
- end
這樣的話,你便可以創(chuàng)建一個區(qū)間,通過實例方法cover?測試成員關(guān)系,但這通常不是一種非常好的做法。
- dictionary = (Word.new("aardvark)..Word.new("xylophone")
- dictionary.cover?(Word.new("derp")) # => true
如果你想要迭代整個區(qū)間,生成一個數(shù)組,或者是使用實例方法include?測試成員關(guān)系,你需要實現(xiàn)succ方法,這個方法產(chǎn)生序列中的下一個對象。
- class Word
- DICTIONARY = File.read("/usr/share/dict/words").each_line.map(&:chomp)
- DICTIONARY_INDEX = (0...DICTIONARY.length)
- include Comparable
- def initialize(string, i=nil)
- @string, @index = string, i
- end
- def <=>(other)
- return nil unless other.is_a?(self.class)
- @string.casecmp(other.to_s)
- end
- def succ
- i = index + 1
- string = DICTIONARY[i]
- self.class.new(string, i) if string
- end
- def to_s
- @string
- end
- private
- def index
- return @index if @index
- if DICTIONARY_INDEX.respond_to?(:bsearch) # ruby >= 2.0.0
- @index = DICTIONARY_INDEX.bsearch {|i| @string.casecmp(DICTIONARY[i])}
- else
- @index = DICTIONARY.index(@string)
- end
- end
- end
你會注意到我同時也mixin了Comparable模塊,這樣在定義了實例方法<=>之后,類也擁有了實例方法==(同時也擁有了實例方法>,<等),而不是繼承自O(shè)bject類。
現(xiàn)在我們的區(qū)間將變得更加強大
- dictionary = (Word.new("aardvark")..Word.new("xylophone"))
- dictionary.include?(Word.new("derp")) #=> false
- (Word.new("town")..Word.new("townfolk")).map(&:to_s) #=> ["town", "towned", "townee", "towner", "townet", "townfaring", "townfolk"]
下一部分內(nèi)容將會很快和大家見面,在Twitter上follow我們將會即時收到***的消息……