有的變數變前面有一個冒號(例如 :name),是什麼意思?
隨便打開一個 Rails 專案,應該都看過類似像這樣的內容:
class User < ActiveRecord::Base
has_many :products
validates :name, presence: true
end
class Product < ActiveRecord::Base
belongs_to :user
end
或是
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 其實是有點玄的東西,有些人認為它就是的變數,或就是只是個名字,但事實上它不就是變數或名字這麼簡單,你可以想像它是一個「帶有名字的物件」:
>> :name.class
=> Symbol
Symbol 就是一個 Symbol
類別的實體(廢話),它可用來表示某個狀態,例如我在 Objective-C 如果想要比對訂單的狀態,可能會這樣寫:
#define OrderStatusPending 0
#define OrderStatusProcessing 1
#define OrderStatusComplete 2
或是寫成 Enum,像這樣:
enum OrderStatus {
OrderStatusPending = 0,
OrderStatusProcessing = 1,
OrderStatusComplete = 2
};
然後可以用來比對:
if (order.status == OrderStatusPending) {
NSLog(@"order is pending");
}
在 Ruby 也可以做類似的事,但不需要特別定義或宣告,因為 Symbol 本身就是一個「帶有名字的物件」,來一段簡單的範例:
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 跟變數有什麼不同?
變數是一個指向某個物件的名字,例如:
greeting = "Hello Ruby"
上面這行語法,是指 greeting
這個名字指向 "Hello Ruby"
這個字串物件,但如果沒有 "Hello Ruby"
這個字串給它指,這個名字本身是沒辦法單獨存在的。
而 Symbol 是一個「帶有名字的物件」,本身不需要指向任何東西也可以拿來用,例如上面的 :pending
跟 :complete
的例子。
事實上,你也沒辦法直接拿 Symbol 來當變數,像這樣會出現語法錯誤:
>> :name = "見龍"
>> SyntaxError: (irb):27: syntax error, unexpected '=', expecting end-of-input :name = "見龍"
其實,當你宣告一個變數,例如:
my_name = "見龍"
Ruby 會偷偷的新增一個叫做 :my_name
的 Symbol,打開 irb
來試試:
# 使用 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 也有一些跟字串長得很像的方法,例如 length
、upcase
、downcase
等等。不過 Symbol 本身是不能被修改的,但字串可以。一樣開 irb
來試試:
# 像字串一樣的操作
>> :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 裡,每次要產生一個新的字串的時候,它會都向去要一塊新的記憶體,來看看這個例子:
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 就不同了:
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_sym
或 intern
等方法長出來的 Symbol 就可以跟一般物件一樣被回收了。
參考連結:Symbol GC
附帶一提,如果把字串給"冰凍"(freeze)起來,讓它變成不可修改的話,它的 object id 也會是同樣的。
5.times do
puts "hello".freeze.object_id
end
# => 70314415546380
# => 70314415546380
# => 70314415546380
# => 70314415546380
# => 70314415546380
Symbol 的比較(Comparison)比字串快
直接寫一段 benchmark,讓它跑個 1 億次看看結果:
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:
// 檔案:object.c
VALUE
rb_obj_equal(VALUE obj1, VALUE obj2)
{
if (obj1 == obj2) return Qtrue;
return Qfalse;
}
再讓我們挖看看字串是怎麼做比較的:
// 檔案: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 類別都有提供一些方法可以互相轉換,例如:
# 使用 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
>> profile = { name: "見龍", age: 18 }
=> {:name=>"見龍", :age=>18}
這裡的 :name
跟 :age
就是 Symbol,詳細內容可參考這篇。
因為 Symbol 不可變(immutable)的特性,以及它的查找、比較的速度比字串還快,它很適合用來當 Hash 的 Key。
字串的方法比較多、比較好用
雖然也是可以把 Symbol 當字串用,但畢竟 Symbol 類別內建的方法不像字串類別那麼豐富,所以如果你想要用字串那些好用的功能,就選擇用字串而不要選擇 Symbol。
又,如果你只是想在畫面上把內容印出來,那就選用字串,是因為 Symbol 被輸出的時候,還是會先轉型成字串,所以效能就又差了那麼一點點。
要怎麼知道那些方法的參數是要使用字串還是使用 Symbol?
先看個例子:
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 之間所有的單數,可以這樣寫:
# 使用陣列的 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 on Rails 實戰課程
線上課程:五倍學院線上課程