今天看到了這篇文章--Five Ruby Methods You Should Be Using,感覺收獲頗豐,先簡單翻譯一下先。
作者寫這篇文章的契機是在Exercism上看到了很多ruby代碼可以用更好的方式去重構,因此他分享了一些冷門的但是非常有用的ruby方法。
Object#tap
你是否曾發現在某個對象上調用方法時返回值不是你所預期?你想返回這個對象,但是返回的時候又想對這個對象進行一些修改。比方說,你想給hash對象增加1個key value,這時候你需要調用Hash.[]方法,但是你想返回的是整個hash對象,而不是具體的某個value值,因此你需要顯示的返回該對象。
def update_params(params)
params[:foo] = 'bar'
params
end
最后一行的那個params顯得有些多余了。
我們可以用Object#tap
方法來優化這個方案。
tap方法用起來非常簡單,直接在某個對象上調用tap方法,然后就可以在代碼塊里yielded這個對象,最后這個對象本身會被返回。下面的代碼演示了如何使用tap方法來重構剛才的實現。
def update_params(params)
params.tap {|p| p[:foo] = 'bar' }
end
有很多地方都可以使用到Object#tap
方法,一般的規律是對那些在對象上調用,希望返回對象,但是卻沒返回該對象本身的方法都適用。
Array#bsearch
我不清楚你的情況,但我經常在數組里去查找數據。ruby的enumerable模塊提供了很多簡單好用的方法select, reject, find
。不過當數據源很龐大的時候,我開始對這些查找的性能表示憂桑。
如果你正在使用ActiveRecord和非NO SQL的數據庫,查詢的算法復雜度是經過優化了的。但是有時候你需要從數據庫里把所有的數據拉出來進行處理,比方說如果你加密了數據庫,那就不能好好的寫sql做查詢了。
這時候我會冥思苦想以找到一個最小的算法復雜度來篩選數據。如果你不了解算法復雜度,也就是這個O,請閱讀 Big-O Notation Explained By A Self-Taught Programmer或[Big-O Complexity Cheat Sheet](http://bigocheatsheet.com/)。
一般來說,算法復雜度越低,程序運行的速度就越快。 O(1), O(log n), O(n), O(n log(n)), O(n^2), O(2^n), O(n!)
,在這個例子里,越往右算法復雜度是越高的。所以我們要讓我們的算法接近左邊的復雜度。
當我們搜索數組的時候,一般第一個想到的方法便是Enumerable#find
,也就是select方法。不過這個方法會搜索整個數組直到找到預期的結果。如果要找的元素在數組的開始部分,那么搜索的效率倒不會太低,但如果是在數據的末尾,那么搜索時間將是很可觀的。find方法的算法復雜度是O(n)。
更好的辦法是使用(Array#bsearch)[http://www.ruby-doc.org/core-2.1.5/Array.html#method-i-bsearch]方法。該方法的算法復雜度是O(log n)。你可以查看Building A Binary Search這篇文章來該算法的原理。
下面的代碼顯示了搜索50000000個數字時不同算法之間的性能差異。
require 'benchmark'
data = (0..50_000_000)
Benchmark.bm do |x|
x.report(:find) { data.find {|number| number > 40_000_000 } }
x.report(:bsearch) { data.bsearch {|number| number > 40_000_000 } }
end
user system total real
find 3.020000 0.010000 3.030000 (3.028417)
bsearch 0.000000 0.000000 0.000000 (0.000006)
如你所見,bsearch
要快的多。不過要注意的是bsearch要求搜索的數組是排序過的。盡管這個限制bsearch的使用場景,bsearch在顯示生活中確實是有用武之地的。比如通過created_at
字段來查找從數據庫中取出的數據。
Enumerable#flat_map
考慮這種情況,你有個blog應用,你希望找到上個月有過評論的所有作者,你可以會這樣做:
module CommentFinder
def self.find_for_users(user_ids)
users = User.where(id: user_ids)
user.posts.map do |post|
post.comments.map |comment|
comment.author.username
end
end
end
end
得到的結果看起來會是這樣的
[[['Ben', 'Sam', 'David'], ['Keith']], [[], [nil]], [['Chris'], []]]
不過你想得到的是所有作者,這時候你大概會使用flatten
方法。
module CommentFinder
def self.find_for_users(user_ids)
users = User.where(id: user_ids)
user.posts.map { |post|
post.comments.map { |comment|
comment.author.username
}.flatten
}.flatten
end
end
另一個選擇是使用flat_map
方法。
module CommentFinder
def self.find_for_users(user_ids)
users = User.where(id: user_ids)
user.posts.flat_map { |post|
post.comments.flat_map { |comment|
comment.author.username
}
}
end
end
這跟使用flatten方法沒什么太大的不同,不過看起來會優雅一點,畢竟不需要反復調用flatten了。
Array.new with a Block
想當年我在一個技術訓練營,我們的導師Jeff Casimir同志(Turing School的創始人)讓我們在一小時內寫個Battleship游戲。這是極好的進行面向對象編程的練習,我們需要Rules,Players, Games和Boards類。
創建代表Board的數據結構是一件非常有意思的事情。經過幾次迭代我發現下面的方法是初始化8x8格子的最好方式:
class Board
def board
@board ||= Array.new(8) { Array.new(8) { '0' } }
end
end
上面的代碼是什么意思?當我們調用Array.new
並傳入了參數length,1個長度為length的數組將會被創建。
Array.new(8)
#=> [nil, nil, nil, nil, nil, nil, nil, nil]
當你傳入一個block,這時候block的返回值會被當成是數組的每個元素。
Array.new(8) { 'O' }
#=> ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']
因此,當你向block傳入1個具有8個元素的數組時,你會得到8x8個元素的嵌套數組了。
用Array#new加block的方式可以創建很多有趣和任意嵌套層級的數組。
<=>
這個方法就很常見了。簡單來說這方法是判斷左值和右值的關系的。如果左值大於右值返回1,相等返回0,否則返回-1。
實際上Enumerable#sort, Enumerable#max
方法都是基於<=>的。另外如果你定義了<=>,然后再include Comparable,你將免費得到<=, <, >=, >以及between方法。
這是作者的在現實生活中所用到的例子:
def fix_minutes
until (0...60).member? minutes
@hours -= 60 <=> minutes
@minutes += 60 * (60 <=> minutes)
end
@hours %= 24
self
end
這個方法不是很好理解,大概的意思就是如果minutes超過60的話,小時數+1,等於60小時數不變,否則-1。
討論
會的方法越多寫出來的代碼可能會更有表現力,邊寫代碼邊改進,另外多讀rubydoc。