這是我們從兩年專案經驗中獲得的ruby小技巧的第二部分.。擊這裡檢視第一部分,第一部分包含程式碼塊(Blocks)和區間(Ranges)。
拆分和重構(Destructuring)
你或許會對ruby的合成/拆分符印象深刻,例子如下:
1 2 3 4 5 6 7 |
attrs = [:data, :cache] attr_accessor *attrs # destructure array into an arguments list private *attrs def hyphenate(*words) # restructure arguments list into an array words.join("-") end |
同樣,可以在賦值時進行這樣的操作,下一個例子中,將拆分一個區間(Range),同時,將拆分後中間的元素收集到body中合成一個陣列。
1 2 3 4 |
head, *body, tail = *(1..10) head #=> 1 body #=> [2, 3, 4, 5, 6, 7, 8, 9] tail #=> 10 |
當進行並行賦值時,如果右值是一個陣列,則會被自動拆分。當你有一個方法返回一個陣列時,這種方式很有效。
1 |
family, port, host, address = socket.peeraddr |
你同樣可以利用這種方式獲取陣列的第一個元素。
1 |
family, = socket.peeraddr |
然而,可以使用例項方式first以更優雅的方式來獲。
1 |
family = socket.peeraddr.first |
你可以利用雜湊(Hash)的values_at例項方法來返回一個陣列。
1 |
first, last = params.values_at(:first_name, :last_name) |
在傳遞程式碼塊引數的時候將發生隱式拆分。
1 2 3 4 5 6 |
names = ["Arthur", "Ford", "Trillian"] ids = [42, 43, 44] id_names = ids.zip(names) #=> [[42, "Arthur"], [43, "Ford"], [44, "Trillian"]] id_names.each do |id, name| puts "user #{id} is #{name}" end |
使用括號你可以更深的層次拆分。
1 2 3 4 |
id_names = [[42, ["Arthur", "Dent"]], [43, ["Ford", "Prefect"]], [44, ["Tricia", "McMillan"]]] id_names.each do |id, (first_name, last_name)| puts "#{id}\t#{last_name}, #{first_name[0]}." end |
方法在傳遞引數的時候這種方式同樣可行!
1 2 3 4 5 |
def euclidean_distance((ax, ay), (bx, by)) Math.sqrt((ax - bx)**2 + (ay - by)**2) end euclidean_distance([1, 5], [4, 2]) #=> 4.242640687119285 |
在Ruby中,你可以通過兩種方式實現這種拆分的功能.第一種方式,你可以通過‘*’作用與一個物件。呼叫物件的to_a例項方法進行拆分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Point attr accessor :x, :y def initialize(x, y) @x, @y = x, y end def to_a [x, y] end end point = Point.new(6, 3) x, y = *point x #=> 6 y #=> 3 |
第二種方式,對於隱式拆分,需要使用to_ary例項方法。你可以選擇性的實現這個方法,由於它會使你的物件在你沒有預料到的地方突然表現出像陣列(Array)一樣的行為。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Point attr_accessor :x, :y def initialize(x, y) @x, @y = x, y end def to_ary [x, y] end end point = Point.new(6, 3) x, y = point x #=> 6 y #=> 3 points = [Point.new(1, 5), Point.new(4, 2)] points.each do |x, y| ... end # using our distance method from an earlier example euclidean_distance(Point.new(1, 5), Point.new(4, 2)) #=> 4.242640687119285 |
最後一種使用‘*’符的特殊情形,是在子類覆寫的方法中使用super呼叫父類的同名方法。
事實上,你沒有必要寫一大堆為了增加附加行為的引數,你只需要傳遞一個‘*’,在方法中僅僅使用super,就可以把所有引數傳遞給父類方法。
1 2 3 4 5 6 7 8 9 10 11 12 |
class LoggedReader < Reader def initialize(*) super @logger = Logger.new(STDOUT) end def read(*) result = super @logger.info(result) result end end |
轉換方法(Conversion methods)
我們剛剛看到,可以通過顯式或隱式的方法將一個物件轉換為一個陣列(Array).。這些方法可以被認為是嚴格或非嚴格的轉化方法,其他的型別同樣也有一些這樣的方法。
非嚴格的轉換方法你或許知道一些,,例如#to_a, #to_i,#to_s以及Ruby2.0中的#to_h和少數的其他方法。大多數型別都具有這些方法,包括nil。
1 2 3 4 5 6 7 8 |
{:foo => 1, :bar => 2}.to_a # => [[:foo, 1], [:bar, 1]] nil.to_a # => [] "3".to_i # => 3 "foo".to_i # => 0 nil.to_i # => 0 [1, 2, 3].to_s # => "[1, 2, 3]" nil.to_s # => "" nil.to_h # => {} |
有時,這些非嚴格的轉換方法會使你犯某些錯誤,例如,傳遞一些非法的資料使系統崩潰,返回一些錯誤的結果,或者產生一些意想不到的錯誤。
1 2 3 4 5 6 7 |
USERS = ["Arthur", "Ford", "Trillian"] def user(id) USERS[id.to_i] end user(nil) # => "Arthur" # oops! |
對於一些型別Ruby提供了更加嚴格的轉化方法,例如,#to_ary, #to_int, 以及#to_str。
這些方法只在對嚴格轉換敏感的特定類中實現,例如,例項方法#to_int只對於數字類可用。
1 2 3 4 5 |
def user(id) USERS[id.to_int] end user(nil) # => NoMethodError: undefined method 'to_int' for nil:NilClass |
這種方式更好一點,我們在合適的地方獲得了一個異常,但是這個異常並不能很好表達我們的意圖。
另外,由於String類沒有實現例項方法#to_int,對於String類我們需要覆寫原始的方法。
但是Ruby擁有另一系列轉換方法,這些方法更加智慧。這些方法中,最有用的方法是Array()和Integer()(同時還有其他轉換方法例如Float())。其他一些方法例如String()和Hash()使用情況較少,他們僅僅是代理到例項方法#to_s和例項方法#to_h。
1 2 3 4 5 |
def user(id) USERS[Integer(id)] end user(nil) # => TypeError: can't convert nil into Integer |
現在我們的例子將丟擲一個異常可以很好展示我們的意圖,另外我們也可以使用字串。
這也是Integer()方法更加優雅的地方。
1 2 3 4 5 |
"1".to_i # => 1 Integer("1") # => 1 "foo".to_i # => 0 Integer("foo") # => ArgumentError: invalid value for Integer(): "foo" |
Array()同樣非常有用,當它不能將引數轉換成一個陣列時,它將把引數放到一個空陣列中。
1 2 |
Array([1, 2]) Array(1) |
偶爾,你需要定義一個方法接受一個物件或者一個物件陣列,你可以使用Array()而避免明確的型別檢查。
1 2 3 4 5 |
def project_cost(hours, developer) developers = Array(developer) avg_rate = developers.inject(0) {|acc, d| acc + d.rate } / developers.length hours * avg_rate end |
正如上面提到的例項方法#to_ary,這類轉換方法中的一些被Ruby直譯器內部使用,這些方法是嚴格轉換方法,同樣也可以作為隱式轉換方法。由於它們是嚴格的所以可以被作為隱式轉換方法所使用。
這些方法在Ruby中被廣泛的使用,例如例項方法#to_int用來將傳遞給Array#[]的引數轉換為int,當raise的引數不是一個Exception物件時,例項方法#to_str用來將被用來轉換。
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 |
class Line def initialize(id) @id = id end def to_int @id end end line = Line.new(2) names = ["Central", "Circle", "District"] names[line] # => "District" class Response def initialize(status, message, body) @status, @message, @body = status, message, body end def to_str "#{@status} #{@message}" end end res = Response.new(404, "Not Found", "") raise res # => RuntimeError: 404 Not Found |
最後一個要提到的轉換方法是#to_proc。這個方法在一個方法的引數中使用一元符‘&’時將被呼叫。
在一個方法呼叫中,‘&’將一個Proc物件轉換為一個block引數。
1 2 3 |
sum = Proc.new {|a, b| a + b } (1..10).inject(&block) # => 55 |
如果‘&’直接作用於一個引數,則會隱式呼叫例項方法#to_proc將運算元轉變為一個block。
在Ruby程式碼中,一種非常常見的做法是將其作用於一個Symbol物件。
1 |
["foo", "bar", "baz"].map(&:upcase) #=> ["FOO", "BAR", "BAZ"] |
這中方式內建於Ruby1.9以後的版本,對於以前的版本你可以像這樣自己實現:
1 2 3 4 5 6 |
class Symbol def to_proc # call the method named by this Symbol on the supplied object Proc.new {|obj| obj.send(self) } end end |
下面的例子使用例項方法#to_proc和‘&’來從一個陣列(Array)初始化Point物件。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Point attr_accessor :x, :y def initialize(x, y) @x, @y = x, y end def self.to_proc Proc.new {|ary| new(*ary)} end end [[1, 5], [4, 2]].map(&Point) #=> [#<Point:0x007f87e983af40 @x=1, @y=5>, #<Point:0x007f87e983ace8 @x=4, @y=2>] |
接下來進入第三部分…
伯樂線上注:@geekerzp 正在翻譯第 3 部分和第 4 部分,各位敬請期待 :)