# 有的變數變前面有一個冒號(例如 :name)，是什麼意思？

> 

Published: 2016-04-25
URL: https://kaochenlong.com/string-and-symbol

---

隨便打開一個 Rails 專案，應該都看過類似像這樣的內容：

```ruby
class User &lt; ActiveRecord::Base
  has_many :products
  validates :name, presence: true
end

class Product &lt; ActiveRecord::Base
  belongs_to :user
end
```

或是

```ruby
class ProductController &lt; ApplicationControl
  before_action :find_product

  # .. 中略

  private
  def find_product
    @product = Product.find_by(id: params[:id])
  end
end
```

這在 Rails 專案裡是很常見的寫法，但這裡 `:products`、`:user`、`:name` 以及 `:find_product` 是什麼意思呢? 用一般的字串或變數不行嗎?

&lt;!--more--&gt;

這可能是在大部份的人在學習 Ruby / Rails 的時候覺得困擾的問題。這東西在 Ruby 裡稱之 Symbol，中文翻譯做符號，它的寫法是在前面加上個冒號。

常見的 Symbol 的命名規則跟一般的變數差不多，是以用英文字母或數字的組合，例如 `:name` 或 `:title`，非英文也可以，像是 `:姓名`、`:おはよう`也行，甚至要在中間加上空白也沒問題，如果要使用空白字元的話，會需要用引號包起來，例如 `:&quot;hello world&quot;`。不過以使用頻率來說，大多還是以英文字母的組合為主。

## Symbol 是什麼

Symbol 其實是有點玄的東西，有些人認為它就是的變數，或就是只是個名字，但事實上它不就是變數或名字這麼簡單，你可以想像它是一個「帶有名字的物件」：

```ruby
&gt;&gt; :name.class
=&gt; Symbol
```

Symbol 就是一個 `Symbol` [類別](http://ruby-doc.org/core-2.3.0/Symbol.html)的實體(廢話)，它可用來表示某個狀態，例如我在 Objective-C 如果想要比對訂單的狀態，可能會這樣寫：

```objc
#define OrderStatusPending    0
#define OrderStatusProcessing 1
#define OrderStatusComplete   2
```

或是寫成 Enum，像這樣：

```objc
enum OrderStatus {
    OrderStatusPending    = 0,
    OrderStatusProcessing = 1,
    OrderStatusComplete   = 2
};
```

然後可以用來比對：

```objc
if (order.status == OrderStatusPending) {
    NSLog(@&quot;order is pending&quot;);
}
```

在 Ruby 也可以做類似的事，但不需要特別定義或宣告，因為 Symbol 本身就是一個「帶有名字的物件」，來一段簡單的範例：

```ruby
class Order
  attr_reader :status

  def initialize(items, status = :pending)
    @items = items
    @status = status
  end

  def compete
    @status = :complete
  end
end

order = Order.new([&quot;item A&quot;, &quot;item B&quot;, &quot;item C&quot;])

if order.status == :pending
  puts &quot;order is pending&quot;
end
```

你也許會好奇這裡的 `:pending` 跟 `:complete` 是什麼? 其實它就是代表 pending 跟 complete 這兩個狀態，前面提到 Symbol 是一種「帶有名字的物件」，正如其名，Symbol 就是符號，這個符號表示「已完成」或「未完成」。

那.. 上面這個例子，把 Symbol 改用字串可以嗎? 當然是可以的。

## Symbol 跟變數有什麼不同?

變數是一個指向某個物件的名字，例如：

```ruby
greeting = &quot;Hello Ruby&quot;
```

上面這行語法，是指 `greeting` 這個名字指向 `&quot;Hello Ruby&quot;` 這個字串物件，但如果沒有 `&quot;Hello Ruby&quot;` 這個字串給它指，這個名字本身是沒辦法單獨存在的。

而 Symbol 是一個「帶有名字的物件」，本身不需要指向任何東西也可以拿來用，例如上面的 `:pending` 跟 `:complete` 的例子。

事實上，你也沒辦法直接拿 Symbol 來當變數，像這樣會出現語法錯誤：

```ruby
&gt;&gt; :name = &quot;見龍&quot;
&gt;&gt; SyntaxError: (irb):27: syntax error, unexpected &#39;=&#39;, expecting end-of-input :name = &quot;見龍&quot;
```

其實，當你宣告一個變數，例如：

```ruby
my_name = &quot;見龍&quot;
```

Ruby 會偷偷的新增一個叫做 `:my_name` 的 Symbol，打開 `irb` 來試試：

```ruby
# 使用 all_symbols 方法可以列出目前共有幾個 symbol
# 隨著 Ruby 版本的不同以及載入的模組不同，算出來的數字會不同
&gt;&gt; Symbol.all_symbols.count
=&gt; 3489

# 這裡定義了一個叫做 my_name 的區域變數
&gt;&gt; my_name = &quot;見龍&quot;
=&gt; &quot;見龍&quot;

# 算一下，symbol 的數量多一個了
&gt;&gt; Symbol.all_symbols.count
=&gt; 3490

# 檢查一下，發現它偷偷的出現在 symbol 列表裡了
&gt;&gt; Symbol.all_symbols.map(&amp;:to_s).include?(&quot;my_name&quot;)
=&gt; true
```

事實上，定義一個新的類別或是方法也都會產生新的 Symbol 喔 :)

## Symbol 跟字串有什麼不同?

上課時候最常被問到的問題之一，就是「Symbol 跟字串有什麼不一樣?」

### 字串的內容可以變，但 Symbol 不行

簡單的說，Symbol 跟字串有點像，Symbol 也有一些跟字串長得很像的方法，例如 `length`、`upcase`、`downcase` 等等。不過 Symbol 本身是不能被修改的，但字串可以。一樣開 `irb` 來試試：

```ruby
# 像字串一樣的操作
&gt;&gt; :hello.length
=&gt; 5
&gt;&gt; :hello.upcase
=&gt; :HELLO

# 假設有個 hello 字串，可以使用中括號 + 索引來取得其中某個字元
&gt;&gt; &quot;hello&quot;[0]
=&gt; &quot;h&quot;
&gt;&gt; &quot;hello&quot;[3]
=&gt; &quot;l&quot;

# Symbol 也行
&gt;&gt; :hello[0]
=&gt; &quot;h&quot;
&gt;&gt; :hello[3]
=&gt; &quot;l&quot;

# 這回來試著修改某個字母，例如想把 h 換成 k
&gt;&gt; &quot;hello&quot;[0] = &quot;k&quot;
=&gt; &quot;k&quot;

# 但在 Symbol 上卻行不通，會發生錯誤 (因為 Symbol 類別並沒有 []= 這個方法)
&gt;&gt; :hello[0] = &quot;k&quot;
NoMethodError: undefined method `[]=&#39; for :hello:Symbol&#39;`
```

所以，其實你也可以把 Symbol 看做是一種「不可變(immutable)的字串」。

### 字串的效能稍微差一些

在 Ruby 裡，每次要產生一個新的字串的時候，它會都向去要一塊新的記憶體，來看看這個例子：

```ruby
5.times do
  puts &quot;hello&quot;.object_id
end

# =&gt; 70199659402580
# =&gt; 70199659366640
# =&gt; 70199659366560
# =&gt; 70199659366500
# =&gt; 70199659366420
```

`object_id` 方法會取得該物件在 Ruby 世界裡的唯一的數字編號，在不同的電腦或不同的 Ruby 版本所得到的結果可能會不太一樣。

相同的物件會有相同的 object id，不同的物件則會有不同的 object id。從上面這個例子可以發現，即使一樣都是 `&quot;hello&quot;` 字串，Ruby 每次在產生字串的時候都會產生一個新的 object id，表示它們是在記憶體裡其實是不同的 5 個物件。

但 Symbol 就不同了：

```ruby
5.times do
  puts :hello.object_id
end

# =&gt; 899228
# =&gt; 899228
# =&gt; 899228
# =&gt; 899228
# =&gt; 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](https://bugs.ruby-lang.org/issues/9634)

附帶一提，如果把字串給&quot;冰凍&quot;(freeze)起來，讓它變成不可修改的話，它的 object id 也會是同樣的。

```ruby
5.times do
  puts &quot;hello&quot;.freeze.object_id
end

# =&gt; 70314415546380
# =&gt; 70314415546380
# =&gt; 70314415546380
# =&gt; 70314415546380
# =&gt; 70314415546380
```

### Symbol 的比較(Comparison)比字串快

直接寫一段 benchmark，讓它跑個 1 億次看看結果：

```ruby
require &#39;benchmark&#39;
loop_times = 100000000

str = Benchmark.measure do
  loop_times.times do
    &quot;hello&quot; == &quot;hello&quot;
  end
end.total

sym = Benchmark.measure do
  loop_times.times do
    :hello == :hello
  end
end.total

puts &quot;Benchmark&quot;
puts &quot;String: #{str}&quot;
puts &quot;Symbol: #{sym}&quot;

# =&gt; Benchmark
# =&gt; String: 12.299999999999999
# =&gt; Symbol: 5.750000000000002
```

Symbol 的處理速度明顯比字串快得多，那是因為 Symbol 在做比較的時候，是直接比對這兩顆物件的 object id 是不是相同，讓我們挖 Ruby 的原始碼出來看看。這是 Symbol 在處理比較時候的 function：

```c
// 檔案：object.c

VALUE
rb_obj_equal(VALUE obj1, VALUE obj2)
{
  if (obj1 == obj2) return Qtrue;
  return Qfalse;
}
```

再讓我們挖看看字串是怎麼做比較的：

```c
// 檔案：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 類別都有提供一些方法可以互相轉換，例如：

```ruby
# 使用 to_sym 可把字串轉成 symbol
&gt;&gt; &quot;name&quot;.to_sym
=&gt; :name

# intern 方法比較不常看到其它人用(因為這個方法看起來不怎麼直覺)，但其實跟 to_sym 是一樣的效果
&gt;&gt; &quot;name&quot;.intern
=&gt; :name

# 另外也可用 %s 來做轉換
&gt;&gt; %s(name)
=&gt; :name

# 用 to_s 方法可以把 symbol 轉成字串
&gt;&gt; :name.to_s
=&gt; &quot;name&quot;

# 還有個不常用但功能跟 to_s 一樣的方法
&gt;&gt; :name.id2name
=&gt; &quot;name&quot;
```

## 使用時機

講這麼多，那到底什麼時候該用 Symbol，什麼時候該用字串?

### Hash 裡的 Key

```ruby
&gt;&gt; profile = { name: &quot;見龍&quot;, age: 18 }
=&gt; {:name=&gt;&quot;見龍&quot;, :age=&gt;18}
```

這裡的 `:name` 跟 `:age` 就是 Symbol，詳細內容可參考[這篇](/2016/04/23/different-hash-format/)。

因為 Symbol 不可變(immutable)的特性，以及它的查找、比較的速度比字串還快，它很適合用來當 Hash 的 Key。

### 字串的方法比較多、比較好用

雖然也是可以把 Symbol 當字串用，但畢竟 Symbol 類別內建的方法不像字串類別那麼豐富，所以如果你想要用字串那些好用的功能，就選擇用字串而不要選擇 Symbol。

又，如果你只是想在畫面上把內容印出來，那就選用字串，是因為 Symbol 被輸出的時候，還是會先轉型成字串，所以效能就又差了那麼一點點。

### 要怎麼知道那些方法的參數是要使用字串還是使用 Symbol?

先看個例子：

```ruby
class Cat
  attr_accessor :name
end

kitty = Cat.new
kitty.name = &quot;Nancy&quot;
puts kitty.name       # =&gt; Nancy
```

如果把 `attr_accessor :name` 換成 `attr_accessor &quot;name&quot;` 一樣可以正常運轉。

有的方法的參數是用字串，有的是用 Symbol，有的是兩種都能用，那該怎麼知道該用哪一種?

答案很簡單，就是，看．手．冊!! 就是 [RTFM](https://zh.wikipedia.org/wiki/RTFM) 啦! 遇到不知道怎麼用的，去查它的 API 手冊最直接了

* [Ruby API](http://ruby-doc.org/core/)
* [Rails API](http://api.rubyonrails.org/)

## Ruby 黑魔法

最後來玩一點 Ruby 的黑魔法! 如果我想要挑出 1 到 100 之間所有的單數，可以這樣寫：

```ruby
# 使用陣列的 select 方法搭配 block 可以輕鬆完成
&gt;&gt; [*1..100].select { |i| i.odd? }
=&gt; [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, ... 97, 99]

# 但後面那個 block 也可再簡寫成這樣
&gt;&gt; [*1..100].select(&amp;:odd?)
=&gt; [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, ... 97, 99]
```
那個 `:odd?` 也是個 Symbol 喔!  為什麼可以這樣寫.. 嗯，這又是 Ruby 的黑魔法之一，不過離本次主題有點遠，我們有機會再另外寫一篇來解釋。

## 小結

Symbol 可以說是個簡單但又不太簡單的東西，希望這篇文章可以稍微幫大家稍微釐清一些觀念。如果上述內容有誤，或是還有不清楚的地方，歡迎在底下留言、指教。



