高見龍

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

Ruby 語法放大鏡之「常在 controller 裡看到 before_action,它是一個方法嗎? 跟一般用 def 定義的有何不同?」

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

在 Rails 專案中,常會在 Controller 裡看到這樣的寫法:

1
2
3
4
5
6
7
8
9
10
class ProductController < ApplicationController
  before_action :find_product, only: [:show, :edit, :update, :destroy]

  # .. 中略

  private
  def find_product
    @product = Product.find_by(id: params[:id])
  end
end

這是指在執行特定 Action 之前,先去執行 find_product 方法。

在 Model 裡也常看到類似的寫法:

1
2
3
4
5
6
7
8
9
class Article < ApplicationRecord
  before_save :encrypt_password
  validates :title, presence: true

  private
  def encrypt_password
    # ....
  end
end

這是說在資料儲存之前,先執行 encrypt_password 方法對密碼進行加密。

在一般的 Ruby 專案裡也常會看到這樣的寫法:

1
2
3
4
5
6
7
8
9
10
class Cat
  attr_accessor :age, :name
end

kitty = Cat.new
kitty.name = "nancy"
kitty.age = 10

puts kitty.name   # => nancy
puts kitty.age    # => 10

所以,在 Controller 裡的 before_action 或是 Model 裡 before_savevalidates,或是那個 attr_accessor 到底是什麼來歷? 感覺好像是什麼設定還是屬性,或是關鍵字之類的東西?

類別方法

其實,這些看起來像設定或是屬性的東西,它們不過就是類別方法罷了。

1
2
3
4
5
6
7
class Cat
  def self.my_attr_accessor(*field_name)
    # ... 實作
  end

  my_attr_accessor :age, :name
end

跟寫一般的方法沒什麼不同,一樣是使用 def 這個關鍵字來定義,只是在定義類別方法的時候,需要在方法名字前面加個 self ,這樣就可以定義出類別方法(事實上是 Singleton Method),然後就可以直接在類別裡面使用。

至於這些類別方法裡面的實作稍微有些複雜,有的需要知道怎麼操作 Block,有的還需要知道怎麼動態的定義方法(例如使用 define_method),我們另外再開一篇來介紹。

關鍵字?

有些人以為 privateprotected 這些語法是關鍵字,事實上這些都只是方法罷了(Ruby 什麼東西沒有,方法最多了)。

Ruby 裡的關鍵字不算太多,有興趣可翻 Ruby 的原始碼出來看看,Ruby 大概定義了 40 個左右的關鍵字,大概就是 ifelsedefclass 之類的。參考連結

但即使是關鍵字也不表示全部不能用,你要拿來定義方法也是可以的,例如:

1
2
3
def if
  puts "this is a if"
end

在定義的時候不會出錯,但使用的時候就沒辦法了:

1
if()

會得到錯誤訊息如下:

1
2
-:51: syntax error, unexpected end-of-input
shell returned 1

因為 Ruby 在判讀語法的時候,期待 if 後面還有一些東西,所以 Ruby 認為你還沒寫完然後被判定成語法錯誤。但如果是包在類別裡的話:

1
2
3
4
5
6
7
class Joke
  def if
    puts "this is a if"
  end
end

Joke.new.if

這樣就能正常執行了 :)

Ruby 語法放大鏡之「如果想要找某個方法的定義,該怎麼找?」

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

在寫程式的過程中,如果遇到某個方法不會用或不知道該傳什麼參數怎麼辦? 不要急著翻 Google 或 Stackoverflow 網站,先翻官方的使用說明手冊吧,RTFM 是最好的方法!

除了翻手冊之外,像 Ruby 或 Rails 這樣 open source 的工具,另一個好方法就是直接翻它的原始碼出來看,看看它是怎麼實作的。

原始碼挖挖挖!

在開發 Ruby / Rails 專案時,通常我遇到一些很有趣或很有點厲害的寫法,就會想挖出來看看它是怎麼做的。直接開 Rails Console 來試一下:

1
2
3
4
5
$ rails console
Loading development environment (Rails 5.0.0.beta3)

>> 3.days.ago
=> Sun, 24 Apr 2016 09:02:02 UTC +00:00

3.days.ago 這樣就能印出三天前的時間,超方便! 但它怎麼做的…?

你可以使用 method 這個方法來回傳一個 Method 類別的實體,並且準備對它做一些操作:

1
2
3
# 我先用 method 方法取得 `days` 這個方法物件
>> 3.method(:days)
=> #<Method: Fixnum(Numeric)#days>

雖然「用 method 這個 method 回傳一個 Method 的實體」這句話唸起來超饒舌,但這個方法取得的物件還滿好用的,例如:

1
2
3
# 使用 owner 方法取得定義這個方法的類別或模組
>> 3.method(:days).owner
=> Numeric

看來是定義在 Numeric 這個類別或模組裡。使用 source_location 可以挖出檔案在哪:

1
2
>> 3.method(:days).source_location
=> ["/Users/user/.rvm/gems/ruby-2.3.0/gems/activesupport-5.0.0.beta3/lib/active_support/core_ext/numeric/time.rb", 45]

即然檔案路徑都有了,就可以去打開來看看是怎麼寫的了。但如果是 Ruby 內建的方法,就沒辦法直接取得原始碼的路徑,source_location 方法會回傳 nil:

1
2
>> method(:puts).source_location
=> nil

那怎辦? 例如想要知道陣列內建的 sort 是怎麼做排序的話,就去翻翻官方手冊吧。

連到官方手冊,跳轉到關於 Array 類別的 sort 章節:

image

除了介紹以及使用範例外,如果對原始檔有興趣,也可以點一下右上角的那個「放大鏡」,看看這個方法是怎麼用 C 語言實作的:

image

然後就可以再繼續追下去了。如果對 Ruby 的原始碼有興趣,也可在 Github 上取得完整的原始碼。連結

小結

曾有朋友問,要怎麼樣才能從新手變成比較資深的開發者。

除了官網的手冊要看之外,有空挖挖原始碼、讀原始碼,看看這些大大是怎麼做出這些功能進而吸收變成自己的經驗值,都是讓你可以更快變強的好方法 :)

Ruby 語法放大鏡之「為什麼 2.days.ago 在內建的 irb 會找不到這個方法? 這不是 Ruby 語法嗎?」

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

在公開演講或是校園推廣 Rails 的時候,我常會開 rails console 出來秀一下 Rails 快速開發的威力:

1
2
3
4
5
6
7
8
$ rails console
# 看,這樣寫就可以印出 2 天前的時間
>> 2.days.ago
=> Sun, 24 Apr 2016 00:56:51 UTC +00:00

# 這樣可以印出 10 Megabyte 是多大
>> 10.megabyte
=> 10485760

這真的很驚人,不只程式碼 2.days.ago 本身看起來好寫,而且就算連不懂程式語言的人看了也大概猜得出來是什麼意思,台下觀眾看到這裡,有的就會開始發出「喔喔喔」的讚嘆聲了。

但打開標準的 irb 來試試:

1
2
3
4
5
6
7
8
>> 2.days.ago
NoMethodError: undefined method `days' for 2:Fixnum
 from (irb):1
 from /Users/user/.rvm/rubies/ruby-2.3.0/bin/irb:11:in `<main>'
>> 10.megabyte
NoMethodError: undefined method `megabyte' for 10:Fixnum
  from (irb):2
  from /Users/user/.rvm/rubies/ruby-2.3.0/bin/irb:11:in `<main>'

一樣的語法在 Ruby 竟然不會動了!

原來,這些看起來很厲害的語法是 Rails 幫 Ruby 做的擴充功能,讓開發者在開發網站應用程式的時候可以很快的把功能完成。

怎麼做的?

先來看一段範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Cat
  def sleep
    puts "ZZZZZZ"
  end
end

class Cat
  def eat
    puts "Yammy!"
  end
end

kitty = Cat.new
kitty.eat         # => Yammy!
kitty.sleep       # => ???

這裡我不小心定義了兩次 Cat 類別,如果這是你剛接觸 Ruby 不久,你也許會認為後面定義的類別會蓋掉前面的類別,所以執行 eat 方法沒問題,但執行 sleep 就會壞掉了。

但實際執行的時候發現其實兩個方法都可以正常運作! 所以,上面這段範例到底發生了什麼事?

事實上,當 Ruby 遇到兩個類別重複定義的時候,後面的定義並不會「覆蓋」掉前面的,反而比較像是「合併」。這個設計在 Ruby 裡面稱之「Open Class」,可以「打開」已經存在的類別,並再加料進去,甚至連內建的類別(像是數字、字串、陣列之類的)也可以。

我們來試試看內建的字串類別:

1
2
3
4
5
class String
  def say_hello
    "hello, #{self}"
  end
end

在這個定義之後,所有的字串就都會 say_hello

1
2
3
puts "kitty".say_hello  # => "hello, kitty"
puts "snoopy".say_hello  # => "hello, snoopy"
puts "くまモン".say_hello  # => "hello, くまモン"

這在其它程式語言是很少見的設計,特別是連內建的類別也可以這樣玩。讓我們再來玩一個!

1
puts 1 + 2    # => 3

1 加 2,不就等於 3 啊,這小學生都會!

但你知道其實這個簡單的加法可能跟你想的不太一樣,Ruby 的四則運算,事實上並不是一般的運算元(operator)。以上面這個 1 + 2 的例子來說,在 Ruby 裡實際上是:

數字物件 1 呼叫了 `+` 這個方法,並且把數字物件 2 傳進去當做參數

所以 1 + 2 事實上是長這樣:

1
puts 1.+(2)  # => 3

執行之後會得到一樣的結果。即然知道加法其實也是一個 Ruby 的 method 的話,那就可以來試著這樣玩:

1
2
3
4
5
6
7
class Fixnum
  def +(n)
    1000
  end
end

puts 1 + 2

這樣一來,不管是 1 + 2 或是 3 + 4,得到的結果都會是 1000。

不過這樣做有點風險,因為這樣等於是改寫了加法的行為,如果有其它方法有用到這個方法的話也會跟著得到不正確的結果。上面這個例子可以再改寫成:

1
2
3
4
5
6
7
8
9
10
class Fixnum
  alias :original_plus :+

  def +(n)
    puts "hey hey hey"
    original_plus(n)
  end
end

puts 1 + 2

執行上面這段範例可以發現,1 + 2 還是等於 3,但除此之外還會默默的輸出 hey hey hey 字樣,表示除了原來的加法之外,還可以做一些事。至於可以做什麼,這就靠大家自己發揮想像力了。

所以,Rails 是怎麼做的?

Rails 使用了 Open Class 的手法來幫 Ruby 加了不少功能,就以 2.days.ago 來說:

1
2
3
4
5
6
7
8
9
10
class Numeric
  # .. 略

  def days
    ActiveSupport::Duration.new(self * 24.hours, [[:days, self]])
  end
  alias :day :days

  # .. 略
end

這裡打開了 Numeric 類別(它是 Fixnum 的上層類別),幫它加上了 days 方法,而且還很貼心的幫 days 做了單數的 alias day,萬一只有 1 天也可以寫出 1.day.ago 這樣更像英文的語法。

實作的程式碼請看這裡

安全嗎?

這種「猴子補丁 (Monkey Patching)」的手法,有些人覺得很不嚴謹 (但我自己個人覺得還好),在 Ruby 2.0 之後推出了一個叫做 Refinement 的設計,可以稍微控制一下 Open Class 影響範圍。以前面那個 "kitty".hello 的範例,重新用 Refinement 改寫會長像這樣:

1
2
3
4
5
6
7
module MyHelloString
  refine String do
    def say_hello
      "hello, #{self}"
    end
  end
end

上面這段範例建立了一個叫 MyHelloString 的模組,在裡面使用 refine 方法在 String 類別上面進行「提煉」。但這並不會像之前 Open Class 的一樣寫完馬上有效果,要使用這個提煉過的方法,則是使用 using 方法:

1
2
3
using MyHelloString

puts "kitty".say_hello  # => "hello, kitty"

小結

各位看官會覺得 Ruby 的 Open Class 很隨便嗎? 我自己是還滿愛這樣的設計的 :)

Ruby 是一款非常有彈性的程式語言,但彈性本身也是雙面刃,你得知道學會怎麼運用這個彈性,而且你最好知道自己在做什麼。

Ruby 語法放大鏡之「有的變數變前面有一個冒號(例如 :name),是什麼意思?」

English version is available.

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

隨便打開一個 Rails 專案,應該都看過類似像這樣的內容:

1
2
3
4
5
6
7
8
class User < ActiveRecord::Base
  has_many :products
  validates :name, presence: true
end

class Product < ActiveRecord::Base
  belongs_to :user
end

或是

1
2
3
4
5
6
7
8
9
10
class ProductController < ApplicationControl
  before_action :find_product

  # .. 中略

  private
  def find_product
    @product = Product.find_by(id: params[:id])
  end
end

這在 Rails 專案裡是很常見的寫法,但這裡 :products:user:name 以及 :find_product 是什麼意思呢? 用一般的字串或變數不行嗎?

這可能是在大部份的人在學習 Ruby / Rails 的時候覺得困擾的問題。這東西在 Ruby 裡稱之 Symbol,中文翻譯做符號,它的寫法是在前面加上個冒號。

常見的 Symbol 的命名規則跟一般的變數差不多,是以用英文字母或數字的組合,例如 :name:title,非英文也可以,像是 :姓名:おはよう也行,甚至要在中間加上空白也沒問題,如果要使用空白字元的話,會需要用引號包起來,例如 :"hello world"。不過以使用頻率來說,大多還是以英文字母的組合為主。

Symbol 是什麼

Symbol 其實是有點玄的東西,有些人認為它就是的變數,或就是只是個名字,但事實上它不就是變數或名字這麼簡單,你可以想像它是一個「帶有名字的物件」:

1
2
>> :name.class
=> Symbol

Symbol 就是一個 Symbol 類別的實體(廢話),它可用來表示某個狀態,例如我在 Objective-C 如果想要比對訂單的狀態,可能會這樣寫:

1
2
3
#define OrderStatusPending    0
#define OrderStatusProcessing 1
#define OrderStatusComplete   2

或是寫成 Enum,像這樣:

1
2
3
4
5
enum OrderStatus {
    OrderStatusPending    = 0,
    OrderStatusProcessing = 1,
    OrderStatusComplete   = 2
};

然後可以用來比對:

1
2
3
if (order.status == OrderStatusPending) {
    NSLog(@"order is pending");
}

在 Ruby 也可以做類似的事,但不需要特別定義或宣告,因為 Symbol 本身就是一個「帶有名字的物件」,來一段簡單的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Order
  attr_reader :status

  def initialize(items, status = :pending)
    @items = items
    @status = status
  end

  def compete
    @status = :complete
  end
end

order = Order.new(["item A", "item B", "item C"])

if order.status == :pending
  puts "order is pending"
end

你也許會好奇這裡的 :pending:complete 是什麼? 其實它就是代表 pending 跟 complete 這兩個狀態,前面提到 Symbol 是一種「帶有名字的物件」,正如其名,Symbol 就是符號,這個符號表示「已完成」或「未完成」。

那.. 上面這個例子,把 Symbol 改用字串可以嗎? 當然是可以的。

Symbol 跟變數有什麼不同?

變數是一個指向某個物件的名字,例如:

1
greeting = "Hello Ruby"

上面這行語法,是指 greeting 這個名字指向 "Hello Ruby" 這個字串物件,但如果沒有 "Hello Ruby" 這個字串給它指,這個名字本身是沒辦法單獨存在的。

而 Symbol 是一個「帶有名字的物件」,本身不需要指向任何東西也可以拿來用,例如上面的 :pending:complete 的例子。

事實上,你也沒辦法直接拿 Symbol 來當變數,像這樣會出現語法錯誤:

1
2
>> :name = "見龍"
>> SyntaxError: (irb):27: syntax error, unexpected '=', expecting end-of-input :name = "見龍"

其實,當你宣告一個變數,例如:

1
my_name = "見龍"

Ruby 會偷偷的新增一個叫做 :my_name 的 Symbol,打開 irb 來試試:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 使用 all_symbols 方法可以列出目前共有幾個 symbol
# 隨著 Ruby 版本的不同以及載入的模組不同,算出來的數字會不同
>> Symbol.all_symbols.count
=> 3489

# 這裡定義了一個叫做 my_name 的區域變數
>> my_name = "見龍"
=> "見龍"

# 算一下,symbol 的數量多一個了
>> Symbol.all_symbols.count
=> 3490

# 檢查一下,發現它偷偷的出現在 symbol 列表裡了
>> Symbol.all_symbols.map(&:to_s).include?("my_name")
=> true

事實上,定義一個新的類別或是方法也都會產生新的 Symbol 喔 :)

Symbol 跟字串有什麼不同?

上課時候最常被問到的問題之一,就是「Symbol 跟字串有什麼不一樣?」

字串的內容可以變,但 Symbol 不行

簡單的說,Symbol 跟字串有點像,Symbol 也有一些跟字串長得很像的方法,例如 lengthupcasedowncase 等等。不過 Symbol 本身是不能被修改的,但字串可以。一樣開 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
# 像字串一樣的操作
>> :hello.length
=> 5
>> :hello.upcase
=> :HELLO

# 假設有個 hello 字串,可以使用中括號 + 索引來取得其中某個字元
>> "hello"[0]
=> "h"
>> "hello"[3]
=> "l"

# Symbol 也行
>> :hello[0]
=> "h"
>> :hello[3]
=> "l"

# 這回來試著修改某個字母,例如想把 h 換成 k
>> "hello"[0] = "k"
=> "k"

# 但在 Symbol 上卻行不通,會發生錯誤 (因為 Symbol 類別並沒有 []= 這個方法)
>> :hello[0] = "k"
NoMethodError: undefined method `[]=' for :hello:Symbol'`

所以,其實你也可以把 Symbol 看做是一種「不可變(immutable)的字串」。

字串的效能稍微差一些

在 Ruby 裡,每次要產生一個新的字串的時候,它會都向去要一塊新的記憶體,來看看這個例子:

1
2
3
4
5
6
7
8
9
5.times do
  puts "hello".object_id
end

# => 70199659402580
# => 70199659366640
# => 70199659366560
# => 70199659366500
# => 70199659366420

object_id 方法會取得該物件在 Ruby 世界裡的唯一的數字編號,在不同的電腦或不同的 Ruby 版本所得到的結果可能會不太一樣。

相同的物件會有相同的 object id,不同的物件則會有不同的 object id。從上面這個例子可以發現,即使一樣都是 "hello" 字串,Ruby 每次在產生字串的時候都會產生一個新的 object id,表示它們是在記憶體裡其實是不同的 5 個物件。

但 Symbol 就不同了:

1
2
3
4
5
6
7
8
9
5.times do
  puts :hello.object_id
end

# => 899228
# => 899228
# => 899228
# => 899228
# => 899228

只要是一樣的 Symbol,就會有相同的 object id,表示他們是同一顆東西,當 Ruby 第二次要取用同一個 Symbol 的時候,它會直接從記憶體裡拿,而不用重新產生一份,所以 Symbol 相對的較節省記憶體。

雖然說相對的節省記憶體,但在 Ruby 2.2 之前,Symbol 所佔用的記憶體沒辦法被自動回收,要釋放掉 Symbol 所佔用的記憶體只能重新啟動應用程式,所以如果大量的產生 Symbol 的話有可能會造成 memory leak 的問題。Ruby 2.2 之後加入了 Symbol GC(Garbage Collection) ,那些動態用 to_symintern 等方法長出來的 Symbol 就可以跟一般物件一樣被回收了。

參考連結:Symbol GC

附帶一提,如果把字串給"冰凍"(freeze)起來,讓它變成不可修改的話,它的 object id 也會是同樣的。

1
2
3
4
5
6
7
8
9
5.times do
  puts "hello".freeze.object_id
end

# => 70314415546380
# => 70314415546380
# => 70314415546380
# => 70314415546380
# => 70314415546380

Symbol 的比較(Comparison)比字串快

直接寫一段 benchmark,讓它跑個 1 億次看看結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require 'benchmark'
loop_times = 100000000

str = Benchmark.measure do
  loop_times.times do
    "hello" == "hello"
  end
end.total

sym = Benchmark.measure do
  loop_times.times do
    :hello == :hello
  end
end.total

puts "Benchmark"
puts "String: #{str}"
puts "Symbol: #{sym}"

# => Benchmark
# => String: 12.299999999999999
# => Symbol: 5.750000000000002

Symbol 的處理速度明顯比字串快得多,那是因為 Symbol 在做比較的時候,是直接比對這兩顆物件的 object id 是不是相同,讓我們挖 Ruby 的原始碼出來看看。這是 Symbol 在處理比較時候的 function:

1
2
3
4
5
6
7
8
// 檔案:object.c

VALUE
rb_obj_equal(VALUE obj1, VALUE obj2)
{
  if (obj1 == obj2) return Qtrue;
  return Qfalse;
}

再讓我們挖看看字串是怎麼做比較的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 檔案:string.c

static VALUE
str_eql(const VALUE str1, const VALUE str2)
{
  const long len = RSTRING_LEN(str1);
  const char *ptr1, *ptr2;

  if (len != RSTRING_LEN(str2)) return Qfalse;
  if (!rb_str_comparable(str1, str2)) return Qfalse;
  if ((ptr1 = RSTRING_PTR(str1)) == (ptr2 = RSTRING_PTR(str2)))
    return Qtrue;
  if (memcmp(ptr1, ptr2, len) == 0)
    return Qtrue;
  return Qfalse;
}

猜得出來它的比較是一個字母一個字母逐一比對。

所以在效能上來說,字串在做比較的時間複雜度會隨著字母的數量(N)而增加,所以它的時間複雜度是 O(N),但 Symbol 的比較因為只比較是不是同一顆物件,所以它的複雜度是固定的 O(1)。

字串跟 Symbol 是可以互相轉換的

字串及 Symbol 類別都有提供一些方法可以互相轉換,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 使用 to_sym 可把字串轉成 symbol
>> "name".to_sym
=> :name

# intern 方法比較不常看到其它人用(因為這個方法看起來不怎麼直覺),但其實跟 to_sym 是一樣的效果
>> "name".intern
=> :name

# 另外也可用 %s 來做轉換
>> %s(name)
=> :name

# 用 to_s 方法可以把 symbol 轉成字串
>> :name.to_s
=> "name"

# 還有個不常用但功能跟 to_s 一樣的方法
>> :name.id2name
=> "name"

使用時機

講這麼多,那到底什麼時候該用 Symbol,什麼時候該用字串?

Hash 裡的 Key

1
2
>> profile = { name: "見龍", age: 18 }
=> {:name=>"見龍", :age=>18}

這裡的 :name:age 就是 Symbol,詳細內容可參考這篇

因為 Symbol 不可變(immutable)的特性,以及它的查找、比較的速度比字串還快,它很適合用來當 Hash 的 Key。

字串的方法比較多、比較好用

雖然也是可以把 Symbol 當字串用,但畢竟 Symbol 類別內建的方法不像字串類別那麼豐富,所以如果你想要用字串那些好用的功能,就選擇用字串而不要選擇 Symbol。

又,如果你只是想在畫面上把內容印出來,那就選用字串,是因為 Symbol 被輸出的時候,還是會先轉型成字串,所以效能就又差了那麼一點點。

要怎麼知道那些方法的參數是要使用字串還是使用 Symbol?

先看個例子:

1
2
3
4
5
6
7
class Cat
  attr_accessor :name
end

kitty = Cat.new
kitty.name = "Nancy"
puts kitty.name       # => Nancy

如果把 attr_accessor :name 換成 attr_accessor "name" 一樣可以正常運轉。

有的方法的參數是用字串,有的是用 Symbol,有的是兩種都能用,那該怎麼知道該用哪一種?

答案很簡單,就是,看.手.冊!! 就是 RTFM 啦! 遇到不知道怎麼用的,去查它的 API 手冊最直接了

Ruby 黑魔法

最後來玩一點 Ruby 的黑魔法! 如果我想要挑出 1 到 100 之間所有的單數,可以這樣寫:

1
2
3
4
5
6
7
# 使用陣列的 select 方法搭配 block 可以輕鬆完成
>> [*1..100].select { |i| i.odd? }
=> [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, ... 97, 99]

# 但後面那個 block 也可再簡寫成這樣
>> [*1..100].select(&:odd?)
=> [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, ... 97, 99]

那個 :odd? 也是個 Symbol 喔! 為什麼可以這樣寫.. 嗯,這又是 Ruby 的黑魔法之一,不過離本次主題有點遠,我們有機會再另外寫一篇來解釋。

小結

Symbol 可以說是個簡單但又不太簡單的東西,希望這篇文章可以稍微幫大家稍微釐清一些觀念。如果上述內容有誤,或是還有不清楚的地方,歡迎在底下留言、指教。

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 喔 :)