從我們在Global Personals專案中使用Github並且following Github Flow開始到現在已經將近兩年的時間。在這段時間中,我們以很高的頻率提交了上千次的pull請求,雖然沒有太多如何改善或提高程式的建議和想法,但是我仍獲得瞭如此廣泛和珍貴的經驗。其中,有一些建議是和專案相關的,同時,也包含了大量可以在團隊內分享的Ruby開發小技巧。
由於我擔心將從這個專案中獲得和學習到的如此珍貴的技巧和經驗所遺忘,於是我挑出了其中最好的最有價值的部分和大家分享,同時進行了一點小小的擴充套件。每個人都有自己的工作方式和風格,所以我會簡潔明瞭地和大家闡述。並不是每部分內容對每個人來說都是新的,但是希望你在這裡可以或多或少都有所收穫。
我將文章分為了幾塊內容,以免你一口氣讀完5000個字,同時將它們歸類為幾個部分以便於參考。
- 1. 程式碼塊(Blocks) 和 區間(Ranges)
- 2. 重構(Destructuring) 和 轉換方法(Conversion Methods)
- 3. 異常(Exceptions)和模組(Modules)
- 4. 除錯(Debugging),專案結構(Project Layout)和文件(Documentation)
- 5. 其他(Odds and Ends)
讓我們進入第一部分。
程式碼塊(Blocks)
程式碼塊是Ruby非常重要的一部分,你隨處都見到它們被廣泛使用。如果你沒有使用,那麼你將發現許多人使用方法關聯程式碼塊,甚至僅僅是讓程式碼結構變得清晰而已。
程式碼塊有三種主要的作用:迴圈(looping),初始化和銷燬(setup and teardown),以及回撥和延遲執行(callbacks or deferred action)。
下面這個例子演示瞭如何使用程式碼塊迴圈輸出菲波那切數列。它使用block_given?方法判斷是否關聯了一個程式碼塊,否則將從當前方法返回一個列舉器。
yield關鍵字用來在方法中執行一個程式碼塊,它的引數將傳遞給程式碼塊。當程式碼塊執行完畢,將返回撥用方法,並執行下一行程式碼。方法返回值為在最大數(max)之前的最後一個菲波那切數。
1 2 3 4 5 6 7 8 9 10 11 |
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 } |
下一個例子將把設定、銷燬以及錯誤處理等操作程式碼放到方法中,將方法的主要邏輯放到程式碼塊中。通過這種方式,樣板程式碼就不需要在多個地方重複,另外當你需要改變錯誤處理程式碼時,只需要做少量的修改。
yield語句的返回結果,即程式碼塊的返回值,將儲存到一個區域性變數中。這樣,可以將程式碼塊的執行結果作為方法的返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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關鍵字。這裡有另外一種使用程式碼塊的方法:將‘&’作為方法最後一個引數的字首,將把關聯程式碼塊作為一個Proc物件儲存到此引數當中。Proc物件擁有一個call例項方法,可以用來執行程式碼塊,傳遞給call方法的引數將作為程式碼塊的引數。在這個例子中,你可以儲存程式碼塊最為一個回撥稍後執行,或者在你需要的時候執行延遲的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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 |
區間(Ranges)
區間在Ruby程式碼中也很常見,通常區間的形式是(0..9),包含0到9之間的所有數字,包括9。也有另一種形式(0…10),包含0到10之間的所有數字,不包括10,即和前一種形式都包含0到9之間的所有數字,包括9。這種形式並不常見,但有時卻非常有用。
有時候你會看到像這樣的程式碼:
1 |
random_index = rand(0..array.length-1) |
使用不包含結尾的區間將更加整潔:
1 |
random_index = rand(0...array.length) |
有時候,使用不包含結尾的區間將更加簡潔和直觀。例如,eb_first…march_first可以更加簡單的計算出今年二月的天數,同時,1…Float::INFINITY可以更加直觀的表示出所有正整數,由於無窮大infinity不是一個數字。
區間是優秀的資料結構,因為它允許你定義一個巨大的集合而不需要在記憶體中實實在在的創造出整個集合。你必須小心你所使用的方法,因為某些區間操作可能導致整個集合被建立。
例項方法each顯而易見會創造出整個區間,但這通常只會發生在每次使用第一個物件的時候,例如(1..Float::INFINITY).each {|i| puts i },在沒有輸出任何資訊之前,事實上不可能用盡所有可用記憶體。區間物件中,mixin Enumerable所獲得的方法依賴於each方法,所以它們也具有相同的行為。
區間有include?和cover?兩個例項方法來測試一個值是否屬於區間。include?方法使用each方法迭代整個區間來檢測值是否存在於區間中,cover?方法只是簡單比較值是否大於區間的開頭,並且小於等於區間的結尾(對於不包含結尾元素的區間是小於區間的結尾)。這兩個方法是不可以等價互換的,可能由於區間建立方式的不同和排序方式的不同,而導致意象不到的結果。
1 2 |
("a".."z").include?("ab") # => false ("a".."z").cover?("ab") # => true |
Ruby中許多類都可以進行區間操作,同樣的,你也可以很容易地讓自定義的類進行區間操作。
首先,你需要在類中實現稱之為‘太空船’<=>操作符的方法。在這個方法中,如果other引數大於self返回-1,如果小於返回1,如果相等則返回0。一般情況下,如果比較是不合法的則返回nil。
下面的例子中,簡單的代理了String#casecmp方法,這個例項方式是一個大小寫不敏感的字串比較方法,返回上面敘述的格式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 |
這樣的話,你便可以建立一個區間,通過例項方法cover?測試成員關係,但這通常不是一種非常好的做法。
1 2 |
dictionary = (Word.new("aardvark)..Word.new("xylophone") dictionary.cover?(Word.new("derp")) # => true |
如果你想要迭代整個區間,生成一個陣列,或者是使用例項方法include?測試成員關係,你需要實現succ方法,這個方法產生序列中的下一個物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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模組,這樣在定義了例項方法<=>之後,類也擁有了例項方法==(同時也擁有了例項方法>,<等),而不是繼承自Object類。
現在我們的區間將變得更加強大
1 2 3 4 |
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"] |
下一部分內容將會很快和大家見面,在Twitter上follow我們將會即時收到最新的訊息……