高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

Ruby 語法放大鏡之「為什麼 Hash 好像有不同的寫法?」

Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。

在 Ruby 裡的 Hash 的寫法,是用一個大括號,裡面是一堆 key 跟 value 的配對組合,一個蘿蔔一個坑,就像這樣:

1
profile = { :name => "見龍", :age => 18, :title => "紅寶石鑑定商" }

在 Ruby 1.9 之後,Ruby 提供了另一款類似 JSON 格式的 Hash 寫法,上面這行可以改寫成這樣:

1
profile = { name: "見龍", age: 18, title: "紅寶石鑑定商" }

少了 =>,改用 : 並且把 key 前面的冒號也移掉了,看起來好像比較清爽一點。但不管是舊式或是新式的 Hash 寫法,本質上並沒有改變,新式寫法其實只是語法糖衣而已,不信可試著把它印出來看看:

1
puts profile       # => {:name=>"見龍", :age=>18, :title=>"紅寶石鑑定商"}

會得到跟舊式寫法一樣的結果。 所以,如果想要取得 profile 這個 Hash 的內容,還是得用 Symbol 來存取,使用字串會取得不正確結果:

1
2
3
4
5
puts profile[:name]      # => 見龍
puts profile["name"]     # => nil

puts profile[:title]     # => 紅寶石鑑定商
puts profile["title"]    # => nil

但我在寫 Rails 的時候好像都可以耶…

隨便打開一個 Rails 專案的 Controller,你可能會看過像這樣的程式碼:

1
2
3
4
5
class ProductionController < ApplicationController
  def show
    @product = Product.find(params[:id])
  end
end

這裡不管你是用 params[:id] 或是 params["id"],都可以拿到你要的結果。

怎麼辦到的? 我們直接在 show action 裡放個中斷點看看,順便來練習怎麼追程式碼:

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
36
[9, 19] in /private/tmp/blog/app/controllers/products_controller.rb
    9:
   10:  # GET /products/1
   11:  # GET /products/1.json
   12:  def show
   13:    @product = Product.find(params[:id])
=> 14:    debugger
   15:  end
   16:
   17:  # GET /products/new
   18:  def new
   19:    @product = Product.new

# 先看一下 params 的內容
(byebug) params
<ActionController::Parameters {"controller"=>"products", "action"=>"show", "id"=>"1"} permitted: false>

 # 很好,這個 params 看起來像是一個 Hash
 # 而且它是用字串當做 key,讓我們試著用字串來取值...
(byebug) params["id"]
"1"

# 用 Symbol 也可以
(byebug) params[:id]
"1"

# 來看看這個 params 是什麼東西...
(byebug) params.class
ActionController::Parameters

# 繼續來挖看看這個取值的 [] 方法放在哪兒...
(byebug) params.method("[]".to_sym)
#<Method: ActionController::Parameters#[]>

(byebug) params.method("[]".to_sym).source_location
["/Users/user/.rvm/gems/ruby-2.3.0/gems/actionpack-5.0.0.beta3/lib/action_controller/metal/strong_parameters.rb", 400]

抓到了! 就讓我們打開這個 strong_parameters.rb 檔案的第 400 行,繼續追:

1
2
3
def [](key)
  convert_hashes_to_parameters(key, @parameters[key])
end

嗯…這個 @parameters 是怎麼來的? 發現在 initialize 方法裡有寫到:

1
2
3
4
def initialize(parameters = {})
  @parameters = parameters.with_indifferent_access
  @permitted = self.class.permit_all_parameters
end

原來,@parameters 是呼叫了 Hash 類別的 with_indifferent_access 方法所做出來的。但這個方法並不是 Ruby 內建的,而是 Rails 幫 Hash 做的擴充功能。

Rails 幫 Ruby 的 Hash 加了一個 ActiveSupport::HashWithIndifferentAccess 類別,讓開發者在取存 Hash 的時候可以使用 Symbol (例如:params[:id]) 也可以使用字串 (例如:params[“id”]),畢竟在其它程式語言不見得有 Symbol 的設計,這樣的擴充可以讓從別的程式語言轉過來的新朋友比較能習慣。

如果對 ActiveSupport::HashWithIndifferentAccess 的內容有興趣,可以點這裡看看它是怎麼做到的。

我們開 irb 來玩一下:

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
>> require "active_support/core_ext/hash/indifferent_access"
=> true

# profile 是一般的 Hash
>> profile = {name: "見龍", age: 18, title: "紅寶石鑑定商"}
=> {:name=>"見龍", :age=>18, :title=>"紅寶石鑑定商"}

# 用字串拿不到
>> profile["name"]
=> nil

# 要用 Symbol 才可以
>> profile[:name]
=> "見龍"

# 用 with_indifferent_access 做一個新的 new_profile
>> new_profile = profile.with_indifferent_access
=> {"name"=>"見龍", "age"=>18, "title"=>"紅寶石鑑定商"}

# 用字串可以
>> new_profile["name"]
=> "見龍"

# 用 Symbol 也可以
>> new_profile[:name]
=> "見龍"

所以,下回在存取 Hash 裡的東西的時候,要記得要拿正確的 key 喔 :)

新網址,連載再開!

好可怕,時間過得好快,一年一下子就過去了。這一年來忙公司、社群、校園推廣的事,結果一整年都沒更新 Blog! (藉口)

給自己買了個新的 domain,重新繼續連載,希望不會再富堅了!

Ruby 語法放大鏡之「有時候會看到有兩個冒號寫法是什麼意思?」

Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。

隨便打開你 Rails 專案裡的 model 檔案,它可能會長得像這樣:

1
2
3
4
class Post < ActiveRecord::Base
  belongs_to :user
  belongs_to :category
end

第一行後面那個 ActiveRecord::Base 裡面的 2 個冒號 ::,各位知道是什麼意思嗎?

菜市場名

也許你也叫金城武(或自己覺得自己長得像金城武),然後我也剛好也很巧叫金城武,這樣在大街上大叫一聲帥哥就一堆人回頭了,不太好。

寫程式偶爾也會遇到這樣的狀況,舉例來說,如果大家剛好都有一個叫做 Animal 的類別或是 Flyable 的模組,放在同一個專案裡就會打架了。所以 Ruby 有設計 namespace 來解決這個問題。

在 Ruby 裡,因為 moduleclass 可以有巢狀結構(類別裡可以包模組,模組裡面可以包類別,就是大腸包小腸的概念啦),剛好可以拿來做 namespace 的包裝,例如:

1
2
3
4
5
6
7
module Utility
  module Flyable
    def fly
      # ....
    end
  end
end

這樣一來,如果要使用 Flyable 這個類別的話:

1
2
3
class Cat
  include Utility::Flyable
end

要 include 那個 Flyable 模組,就必須連名帶姓的 include,也就是「樹林::金城武」、「三重::劉德華」的概念。

除此之外,其實你也可把常數包在 namespace 裡:

1
2
3
4
5
6
7
8
9
10
11
12
module Utility
  TaxRate = 10
  module Flyable
    Speed = 20
    def fly
      # ....
    end
  end
end

puts Utility::TaxRate          # => 10
puts Utility::Flyable::Speed   # => 20

要使用這些常數,也是一樣要提供完整的路徑給才行。(如果你知道的話,其實不管是類別或模組,它們的名字基本上也就是常數而已)

所以,原來這段程式範例:

1
2
3
4
class Post < ActiveRecord::Base
  belongs_to :user
  belongs_to :category
end

它的意思就是說有一個叫做 Post 的類別,它是繼承自某個放在 ActiveRecord 的模組(或類別)裡的 Base 類別。如果去 github 上翻 Rails 的原始碼的話(連結),應該可以看到這段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module ActiveRecord
  # .....
  # 中略
  # .....
  class Base
    extend ActiveModel::Naming
    extend ActiveSupport::Benchmarkable
    extend ActiveSupport::DescendantsTracker

    extend ConnectionHandling
    extend QueryCache::ClassMethods
    # ... 略
  end
end

的確是有一個 ActiveRecord 的模組,裡面包了一個 Base 的類別沒錯 :)

Ruby 語法放大鏡之「類別跟模組有什麼不一樣?」

Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。

在網路上常看到一些介紹 Ruby 裡類別(class)跟模組(module)的差別,有人說他們兩個差別很大,是完全不同的東西。事實上,類別跟模組是很像的。可能很多人不知道,類別跟模組是有血緣關係的,如果你試著這樣做:

1
puts Class.superclass

你會得到 Module 這個類別。也就是說,在 Ruby 裡,「類別」其實就是「模組」的後代,類別根本就是繼承自模組來的。即然是同一個體系,自然沒有太多的差異。

那差別是什麼?

最明顯的差別,當然就是一個是用 class 來定義,一個是用 module 來定義。至於前、後代功能上的差別,我們只要寫幾行簡單的程式就可以看得出來:

1
2
3
4
5
# 前、後代的類別方法差異:
p Class.methods - Module.methods

# 或直接這樣寫:
p Class.methods(false)

上面這段會得到 [] (空陣列),表示他們在類別方法上並沒有差別。再來看看實體方法的差別:

1
2
3
4
5
# 前、後代的實體方法差異:
p Class.instance_methods - Module.instance_methods

# 或直接這樣寫:
p Class.instance_methods(false)

會得到以下結果:

1
[:allocate, :new, :superclass]

其中,allocatenew 跟產生實體(instance)有關,superclass 跟繼承有關。

也就是說,類別比模組多了可以「產生實體」的能力以及「繼承」的能力。

那用途上有什麼差別?

請大家先思考一個問題:「如果我有一個小貓類別,我希望這個小貓會飛,你會怎麼做?」

  1. 直接寫一個有飛行功能的小鳥類別,然後再叫小貓類別去繼承它?

  2. 直接把飛行功能寫在小貓類別裡?

第 1 種做法的設計有點怪怪的,好好的貓不當,為什麼要去當鳥? 為了想要有飛行功能就去當別人家的小孩…

第 2 種做法看來似乎可行,但如果之後又有個「我希望我的這個小狗類別也會飛!」的需求,那這樣又得在小狗類別裡寫一段飛行功能,程式碼沒辦法共同。

這時候,模組就可以派上用場了。

1
2
3
4
5
6
7
8
9
10
11
12
module Flyable
  def fly
    puts "I can fly!"
  end
end

class Cat
  include Flyable
end

kitty = Cat.new
kitty.fly        # => I can fly!

在上面這段範例中,我做了一個飛行模組(Flyable),然後小貓類別不用特別寫什麼功能,就只要把這個飛行模組掛上去(include)就搞定了。

如果之後小狗類別也想要會飛的話,只要這樣:

1
2
3
class Dog
  include Flyable
end

小狗也會飛了。

要用繼承還是要用模組?

基本上,如果你發現你要做的這個功能,它可能在很多不同體系的類別裡都會用得到,那你可以考慮把功能包在模組裡,然後在必要的時候再 include 進來即可。

如果你還是不知道到底類別跟模組有什麼差別,我再舉二個例子。

不知道大家有沒看過火影忍者這部漫畫,漫畫裡的主人公之一,宇智波佐助,因為他們家族血統的關係,他寫輪眼這個功能是天生就有的,這個功能是從他的家族「繼承」來的。而佐助的老師,旗木卡卡西,他雖然也有寫輪眼功能,但他的寫輪眼並非繼承來的,事實上是他在年輕時候 include 了某個寫輪眼模組,所以才有這個效果。

另一個例子,海賊王漫畫裡,魯夫本來是普通人,但在偶然的機會下,他 include 了橡膠果實之後,他就有了橡膠人的能力了,並不是因為他老爸是橡膠人所以他才是橡膠人。

以上,希望能讓大家在使用類別跟模組時有更進一步的認識 :)

Ruby 語法放大鏡之「Ruby 的參數」

Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。

大家在寫 Rails 專案的時候,一定都有用過 link_to 這個 view helper。問大家一個簡單的問題:

1
link_to '刪除', user, method: :delete, data: { confirm: 'sure?' }, class:'btn'

請問,上面這段範例中,link_to 方法共有幾個參數?

如果你的答案是 5 個,那你就需要繼續往下看了 :)

這篇文章中提到,Ruby 很愛省略東西,像是方法的小括號,所以原來上面的 link_to 語法:

1
link_to '刪除', user, method: :delete, data: { confirm: 'sure?' }, class:'btn'

原本應該長這樣:

1
link_to('刪除', user, method: :delete, data: { confirm: 'sure?' }, class:'btn')

最後一個參數如果是 Hash,大括號可省略

在 Ruby 省略小括號大家都知道,不過逗點分開的參數不是 5 個嗎?

事實上,在 Ruby 中如果最後一個參數是 Hash 的話,它的大括號是可以省略的。舉個例子來說:

1
2
3
def say_hello_to(name, options = {})
  # do something
end

如果要使用這個方法,可以這樣寫:

1
say_hello_to "eddie", {age: 18, favorite: 'ruby'}

又,因為最後一個參數是 Hash,所以 Hash 的大括號也可省略:

1
say_hello_to "eddie", age: 18, favorite: 'ruby'

如果你了解有什麼東西被省略的話,一開始的那段 link_to 的範例:

1
link_to '刪除', user, method: :delete, data: { confirm: 'sure?' }, class:'btn'

還原之後會變成:

1
link_to('刪除', user, {method: :delete, data: { confirm: 'sure?' }, class:'btn'})

所以,其實參數個數只有 3 個,最後一個參數是一個 Hash。

小結

Ruby 的語法可以適時的省略小括號、大括號或 return,程式碼寫起來的確會更像文章,對新手來說可能會容易混淆,不過看久了應該也會慢慢習慣了。

以上,希望對大家有幫助 :)