← 上一章:變數、常數、流程控制、迴圈 下一章:方法與程式碼區塊(block) →

數字、字串、陣列、範圍、雜湊、符號

Rails 不是一種程式語言,它是使用 Ruby 這個程式語言所開發出來的網頁開發框架(Web Framework)。

這個章節的目的並不是要詳細的介紹 Ruby 這個程式語言所有的功能,而是希望讓大家對 Ruby 有足夠的基本認識,之後大家在閱讀或撰寫 Rails 專案的時候,會比較知道 Rails 在寫些什麼。

數字(Number)

在 Ruby 的世界裡,幾乎什麼東西都是物件,即使是看起來很單純的數字,事實上它也是一個數字物件。在這個數字物件身上有一些好用的方法,像是在上個章節提到的 timesuptodownto 方法:

# 印出 5 次的 Ruby
5.times do
  puts "Ruby"
end

# 從 1 印到 10
1.upto(10) { |i|
  puts i
}

# 從 10 印到 1
10.downto(1) { |i|
  puts i
}

數字類別?

事實上,你可以在 irb 裡面做一下這個試驗:

$ irb
>> 1.class
=> Integer
>> 1000000000000000000000000000000000000000000000000000000.class
=> Integer

你會發現在 Ruby 裡,數字其實就只是 Integer 這個類別的一個實體(instance)而已。要稍微注意的是,Ruby 2.4 版之後,數字類別統一成 Integer,在 2.4 版之前,數字會根據它值的大小,有分 FixnumBignum

$ irb
>> 1.class
=> Fixnum
>> 1000000000000000000000000000000000000000000000000000000.class
=> Bignum

不過還好這部份的改變對大多數的 Rails 專案應該沒影響。

也許你會好奇 Fixnum 跟 Bignum 的分界點在哪裡? 小於 2 的 62 次方的數字是 Fixnum,超過的話則會是 Bignum:

$ irb
>> 4611686018427387903.class
=> Fixnum
>> 4611686018427387904.class
=> Bignum

整數除法

在進行除法的時候,如果除數跟被除數都是整數的話,結果也會是整數,除不盡的小數會被無條件捨去:

puts 10 / 3   # => 得到 3

這個結果不見得是你想要的,要解決這個問題的話,只要讓除數或被除數其中一個加上小數點就可以解決了:

puts 10 / 3.0   # => 3.3333333333333335
puts 10.0 / 3   # => 3.3333333333333335

其實四則運算跟你想的不太一樣

在 Ruby 裡,很多東西都不是它看起來的樣子,其中一個就是四則運算

puts 1 + 2     # => 3

這看起來簡單到不行的加法運算,但其實那個 + 號其實並不是普通的加號,它在 Ruby 只是一個一般的方法(method),上面這行的原來的樣子應該是:

puts 1.+(2)    # => 3

這個加號,事實上是「數字物件 1」呼叫了 + 這個方法,並且把「數字物件 2」當做參數傳進去。也因為它是一個方法,所以就有機會可以重新改寫它原來的功能,讓 1 + 1 不等於 2 都是有可能的,這個在後面物件導向程式設計會有更詳細的說明。

浮點數是不太準確的

浮點數就是帶有小數點的數字,讓我們來看一段簡單的運算式:

puts 4.51212 == (3.51212 + 1)

我們光用肉眼就看得出來這應該是相等的,但執行之後會發現結果是 false,表示這兩個值不相等。這是因為浮點數本身就是沒辦法非常精準,如果真的需要精準的計算,可使用 Ruby 的標準函式庫 BigDecimal 做個轉換:

require 'bigdecimal'
puts BigDecimal("4.51212") == BigDecimal("3.51212") + BigDecimal("1")    # => true

問題:要怎麼知道某個數字是不是奇數?

如果數字可以被 2 整除,表示它是偶數,反之則是奇數,程式可以這樣寫:

num = 20

if num % 2 == 0
  puts "偶數"
else
  puts "奇數"
end

這個 % 是表示「取餘數」的意思,如果餘數為 0 表示為偶數,不然就是奇數。事實上,在數字物件上已經有有內建的判斷方法:

puts 20.odd?    # => false
puts 20.even?   # => true

問題:要怎麼做「四捨五入到小數點第二位」?

使用數字物件的 round 方法,可以對數字進行四捨五入計算:

puts 3.333.round      # => 3
puts 3.834.round      # => 4

如果想要四捨五入到小數第 2 位,就加個參數給它:

puts 3.333.round(2)   # => 3.33
puts 4.518.round(2)   # => 4.52

字串(String)

使用方法

字串大概是所有程式語言最常用的東西了。在 Ruby 要建立字串,可以這樣寫:

name = String.new("鳴人")

不過實務上我們大部份會使用單引號(Single Quote)或雙引號(Double Quote):

name = "鳴人"
name = '鳴人'

單引號跟雙引號做出來的字串本質上沒有差別,最主要的差別就是雙引號的字串可以處理字串安插,但單引號字串則不會處理。

字串安插(String Interpolation)

字串可以使用加號來進行組合、串接,像這樣:

name = "eddie"
age = 18

puts "你好,我是 " + name
# => 印出「你好,我是 eddie」

puts "你好,我是 " + name + " 我今年 " + age + " 歲"
# => 字串跟數字無法直接串接,會出現 TypeErro

# 需要先把 age 轉換成字串(使用 to_s 方法)
puts "你好,我是 " + name + " 我今年 " + age.to_s + " 歲"
# => 印出「你好,我是 eddie 我今年 18 歲」

以上面這個例子來說,有時候需要先把數字轉換成字串才能串接,寫起來語法也較不好看。Ruby 的字串有提供了字串安插的寫法,寫起來更簡單也更清楚一些:

name = "eddie"
age = 18

puts "你好,我是 #{name},我今年 #{age} 歲"
# => 印出「你好,我是 eddie,我今年 18 歲」

要注意的是,單引號組成的字串沒有這個效果,它將會完整呈現 #{ ... } 裡的東西。

name = "eddie"
age = 18

puts '你好,我是 #{name},我今年 #{age} 歲'
# => 印出「你好,我是 #{name},我今年 #{age} 歲」

引號裡的引號

有時候可能會在引號裡面再放別的引號,例如:

puts "Hello, I'm 悟空"

這是在雙引號裡放了一個單引號,這樣沒什麼問題,但如果想要在雙引號裡放雙引號,或是單引號裡放單引號,就得做一些額外的工作。為了可以正常呈現字串,需要使用反斜線來跳脫(escape)這個引號,告訴 Ruby 說「這是一個普通的引號,不是用來包字串的那個引號喔」

puts "我說\"雙引號需要使用反斜線來處理!\""
# => 印出「我說"雙引號需要使用反斜線來處理!"」

在 Ruby 有提供另一種字串的表現方式,分別是 %Q%q,各代表雙引號跟單引號,而且不太需要使用跳脫方式來處理多餘的引號:

name = "紅寶石"

puts %Q(你好,#{name})                   # 跟雙引號一樣,可以使用字串安插
# => 印出「你好,紅寶石」

puts %Q(你好,紅寶石"'"'"'"'"''"'"'")    # 要放幾個引號都可以
# => 印出「你好,紅寶石"'"'"'"'"''"'"'"」

puts %q(你好,#{name})                   # 跟單引號一樣,不會處理字串安插
# => 印出「你好,#{name}」

把字串當陣列玩

字串,其實可以看成是許多字元的組合,在 Ruby 的字串可以這樣玩:

name = "This is a book"
title = "紅寶石鑑定商"

puts name[0]        # => T
puts title[1]       # => 寶

title[0..1] = "鑽"
puts title          # => 鑽石鑑定商

感覺就像是把字串當做陣列在操作。

問題:字串的長度怎麼算?

如果都是英文字母,字串的長度容易算的,就是直接使用 size 方法就可以算得出來這個字串有幾個字:

puts "hello, ruby".size        # => 11

但如果遇到的是非英文字,例如中文或日文字:

puts "五倍紅寶石".size         # => 5
puts "ありがとう".size         # => 5
puts "五倍紅寶石".bytesize     # => 15
puts "ありがとう".bytesize     # => 15

size 方法會算出這個字串有幾個字,而 bytesize 則會算出該字串實際的 byte 數。

問題:如何計算一段文字中共有幾個字(Word)?

這裡指的字,是指英文的 Word,例如〝hello ruby〞這樣算 2 個字。如果想要計算一段文字中共有幾個字,可以這樣寫:

words = "Ut in aliquam mauris. Donec dolor quam, sagittis id efficitur vel, convallis vitae tortor"
puts words.split.count    # => 14

字串的 split 方法可把字串依照所輸入的參數拆成陣列(若未輸入將以空白字元做為拆解字元),然後再使用 count 方法計算陣列的數量即可得知共總有幾個字。

問題:字串的大小寫轉換?

使用 downcase 方法可讓字母全部變小寫、使用 upcase 方法可讓方法全部變大寫,swapcase 則是讓大小寫互相轉換:

puts "hello, ruby".upcase     # => HELLO, RUBY
puts "HELLO, RUBY".downcase   # => hello, ruby
puts "Hello, Ruby".swapcase   # => hELLO, rUBY

如果只是想首字大寫的話,可使用 capitalize 方法:

puts "eddie".capitalize     # => Eddie

問題:檢查是否為空字串?

想要檢查是否為空字串,最直覺的方式可能是這樣:

if name == ''
  # ...
else
  # ...
end

但 Ruby 有內建了一個 empty? 方法,可檢查該字串是否為空字串,程式碼看起來更容易懂:

puts "hello".empty?    # => false
puts " ".empty?        # => 裡面有一個空白字元,所以得到 false
puts "".empty?         # => true

注意,空白字元(space)不算是空字串喔!

問題:想要知道某個字母在字串中共出現幾次?

這個題目雖然可以把字串的所有字母拆開再跑 for 迴圈或 each 方法來計算,但使用 Ruby 字串類別內建的 count 方法即可簡單的算出字串中某些字母出現的次數:

words = "Lorem Ipsum Dolor Sit Amet, Consectetur Adipiscing Elit."

puts words.count("i")    # => 算出有幾個 i,共有 5 個
puts words.count("A-Z")  # => 算出所有大寫字母,共有 8 個
puts words.count("a-z")  # => 算出所有小寫字母,共有 39 個

問題:想知道字串是不是特定字元開題或結尾?或是包含指定字串或字元?

使用 start_with?end_with? 方法可檢查該字串是否某字元(或字串)開頭及結尾:

puts "Hello, Ruby".start_with?("H")  # => true
puts "Hello, Ruby".start_with?("h")  # => false
puts "Hello, Ruby".end_with?("y")    # => true

使用 include? 方法則可檢查是否包含指定的字串或字元:

puts "Hello, Ruby".include?("r")     # => false
puts "Hello, Ruby".include?("R")     # => true
puts "Hello, Ruby".include?("Ruby")  # => true
puts "Hello, Ruby".include?("Rudy")  # => false

注意,在方法名稱後面的問號 ? 不是不小心打錯字,問號本身也是方法名稱的一部份,在使用的時候別忘了一併加上去。

問題:想把字串裡的某些字換成其它字?

使用 subgsub 方法,可換掉一個或全部符合條件的字串:

puts "PHP is good, and I love PHP".sub(/PHP/, "Ruby")
# sub 只會換掉最先遇到的那個字串
# => 印出「Ruby is good, and I love PHP」

puts "PHP is good, and I love PHP".gsub(/PHP/, "Ruby")
# gsub 會換掉全部符合的字串
# => 印出「Ruby is good, and I love Ruby」

這樣就可以把 PHP 換成 Ruby 了!(咦)

陣列(Array)

使用方法

在 Ruby 要建立一個陣列有幾個方式:

  1. 使用 Array.new 方法
  2. 使用中括號 [ ]
p Array.new
# 產生一個空的陣列
# => []

p Array.new(5)
# 產生一個擁有 5 個元素並放滿 nil 的陣列
# => [nil, nil, nil, nil, nil]

p Array.new(5, "Ruby")
# 產生一個擁有 5 個元素並放滿 "Ruby" 字串的陣列
# => ["Ruby", "Ruby", "Ruby", "Ruby", "Ruby"]

比起上面這樣的寫法,我們更常使用中括號 [ ... ] 來定義、操作陣列,而且元素不限定只能存放同一種類型,想要把數字跟字串混著放也沒問題。

friends = ["魯夫", "孫悟空", "黑崎一護", "旋渦嗚人"]
secret_number = [4, 8, 15, 16, 23, 42]

除了放一般的元素之外,陣列裡面也可以再放陣列,像是這樣:

list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

陣列在寫的時候,如果為了方便檢視也不一定要全部寫在同一行,像這樣寫也是可以的:

friends = [["魯夫", "娜美", "羅賓"],
            ["孫悟空", "克林", "天津飯"],
            ["黑崎一護", "朽木露琪亞", "阿散井戀次"],
            ["旋渦嗚人", "我愛羅", "宇智波佐助"]]

陣列取用

要取用陣列的內容,可使用陣列的索引值(Index)來取用,但要注意,Ruby 的陣列索引是從 0 開始算的,所以 0 表示是第 1 個元素,負的索引值則是表示從後面開始算:

friends = ["魯夫", "孫悟空", "黑崎一護", "旋渦嗚人"]
puts friends[0]     # => 魯夫
puts friends[1]     # => 孫悟空
puts friends[-1]    # => 旋渦嗚人

不過在 Ruby 要存取陣列裡的第一個或最後一個元素的話,可以有更簡單、優雅的寫法:

friends = ["魯夫", "孫悟空", "黑崎一護", "旋渦嗚人"]
puts friends.first     # => 魯夫
puts friends.last      # => 旋渦嗚人

陣列取用方法冷知識

也許你會好奇,既然有 firstlast 方法可以拿到第一個跟最後一個元素,那有 secondthird 嗎? Ruby 本身沒有,但 Rails 有幫 Array 做了一些額外的擴充功能喔。讓我們打開 rails console 來看看:

$ bin/rails console
Running via Spring preloader in process 44994
Loading development environment (Rails 5.1.0.rc1)
2.4.1 :001 > ["a", "b", "c", "d", "e", "f"].second
 => "b"
2.4.1 :002 > ["a", "b", "c", "d", "e", "f"].third
 => "c"
2.4.1 :003 > ["a", "b", "c", "d", "e", "f"].fourth
 => "d"
2.4.1 :004 > ["a", "b", "c", "d", "e", "f"].fifth
 => "e"
2.4.1 :005 > ["a", "b", "c", "d", "e", "f"].forty_two
 => nil

Rails 幫原本的陣列方法擴充了可取得 2nd ~ 5th 元素的方法,如果去翻 Rails 原始碼,還會發現 DHH(Rails 的作者)還很有哏的擴充了 forty_two 這個方法。但這些有趣的方法僅在 Rails 中才能使用,在純 Ruby 的環境是沒有的。

問題:請把陣列 [1, 2, 3, 4, 5] 變成 [1, 3, 5, 7, 9]

如果是剛從別的程式語言轉過來的話,可能會這樣寫:

list = [1, 2, 3, 4, 5]

result = []
list.each do |i|
  result << i * 2 - 1
end

p result    # [1, 3, 5, 7, 9]

就是先做一個空的陣列(result),然後在 each 迴圈裡面把元素計算過之後再丟回那個空陣列裡。雖然這樣執行結果是正確的,但如果你看到「在迴圈外面定義某個變數,然後在迴圈裡面一直存取它」這樣的情況,在 Ruby 八成都有更好的寫法。以這個情況來說,你可直接使用 map 方法來改寫:

list = [1, 2, 3, 4, 5]
p list.map { |i| i * 2 - 1 }    # [1, 3, 5, 7, 9]

簡單的說,就是這個 map 方法會「對 list 這個陣列裡的每一個元素做某件事之後再收集成一個新的陣列」,所以也不需要另外再建立一個空的陣列。

這一開始會有點不習慣,但用習慣之後就會覺得很好用了 :)

問題:請計算從 1 加到 100 的總和。

你可能會這樣寫:

total = 0

(1..100).to_a.each do |i|
  total = total + i
end

puts total    # 得到 5050

其中,前面的 (1..100) 會做出一個 1 到 100 的範圍(Range),to_a 則是把這個範圍轉換成陣列。 (1..100).to_a 也可以改用星號 * 方式展開,連 to_a 都可以省略,寫起來會再更短一點:

total = 0

[*1..100].each do |i|
  total = total + i
end

puts total    # 得到 5050

這樣寫沒什麼問題,但「在迴圈外面定義某個變數,然後在迴圈裡面一直存取它」這個模式又出現了,所以在 Ruby 可以使用 reduce 方法讓程式碼更精簡:

puts [*1..100].reduce(0) { |total, i| total + i }

這個 reduce 方法會把「裡面每個元素不斷的〝互動〞(在這個例子是相加),然後再把最後結果收集起來」。其實 reduce 方法只在意傳進去的 totali 這兩個之間的「互動」,以這個範例來說,它只在意「相加」這件事,所以如果想要再更精簡的話還可以這樣寫:

puts [*1..100].reduce(:+)

只是這可能程式碼可讀性上有稍微差一點點了。

參考資料:https://ruby-doc.org/core/Enumerable.html#method-i-reduce

問題:請印出 1 ~ 100 數字中所有的單數

你可能會這樣寫:

result = []

[*1..100].each do |i|
  result << i if i % 2 == 1
end

p result    # [1, 3, 5, ... 95, 97, 99]

「在迴圈外面定義某個變數,然後在迴圈裡面一直存取它」模式再度出現,在 Ruby 有個 select 方法,可以挑選出「符合條件」的元素,並收集成陣列:

p [*1..100].select { |i| i % 2 == 1 }

再配合數字物件內建檢查奇數、偶數的方法:

p [*1..100].select { |i| i.odd? }

再精簡一點:

p [*1..100].select(&:odd?)

問題:如果想要從 1 到 100 中隨便取 5 個不重複的亂數該怎麼做?

這有幾種做法,一種是不斷的跑迴圈,一次隨機抽一個顆球,連續抽 5 次,如果抽到重複的再重新抽一次。但其實可換個方式想,如果題目改成「你有一副牌,共有 52 張,請你隨便發 5 張不重複的牌給我」,你會怎麼做?

大部份的人應該都是先洗牌(shuffle),然後再從最上面直接發 5 張出來。用程式寫的話大概會長這樣:

puts [*1..52].shuffle.first(5)

陣列可透過 shuffle 洗牌,然後直接用 first 方法取前 5 張。其實還可以再更短一點:

puts [*1..52].sample(5)

所以原題目想要取出 1 到 100 不重複亂數的話,就是:

puts [*1..100].sample(5)

範圍(Range)

使用方法

Ruby 的範圍有「二個點」 .. 跟「三個點」 ... 這兩種寫法。二個點的 1..5 會做出 1 ~ 5 的範圍,而 1...5 則是產生 1 ~ 4 的範圍,不包含 5。通常會使用 to_a 方法把範圍轉換成陣列:

$ irb
>> (1..5).to_a
=> [1, 2, 3, 4, 5]
>> (1...5).to_a
=> [1, 2, 3, 4]

範圍不只可以用數字,用字串也可以:

$ irb
>> ('a'..'z').to_a
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
>> ('A'..'Z').to_a
=> ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]

另外,把範圍轉換成陣列除了使用 to_a 方法外,也可使用 * 來把範圍展開成陣列:

$ irb
>> [*1..5]
=> [1, 2, 3, 4, 5]

但這個寫法如果忘了前面的那個 * 號,會變成一個「只包含一個範圍的陣列」:

$ irb
>> [1..5].first
=> 1..5

範圍也可以像陣列一樣使用 eachmap 之類的方法來處理裡面的元素,而且不需要先轉成陣列:

$ irb
>> (1..5).each { |x| puts x }
1
2
3
4
5
=> 1..5
>> (1..5).map { |x| x * 2 }
=> [2, 4, 6, 8, 10]

取陣列元素也可使用範圍:

$ irb
>> languages = ["ruby", "rails", "elixir", "phoenix", "php", "java", "python", "perl"]
=> ["ruby", "rails", "elixir", "phoenix", "php", "java", "python", "perl"]
>> languages[1..3]
=> ["rails", "elixir", "phoenix"]

這樣就可取出陣列索引值 1 到 3 的元素,也就是第 2 到第 4 個元素。

雜湊(Hash)

Hash 有兩種寫法,一種是「箭頭式」的寫法:

old_hash = {:title => "Ruby", :price => 350}

在 Ruby 1.9 版之後,加入了類似 JSON 格式的寫法:

new_hash = {title: "Ruby", price: 350 }

但不管是新式或是舊式的,其實本質上都是一樣的。

old_hash = {:title => "Ruby", :price => 350}
new_hash = {title: "Ruby", price: 350 }

puts old_hash == new_hash      # => true

在這個 Hash 裡的 titleprice 稱之 key,而字串 "Ruby" 及數字 350 則稱之 value。

要拿對的鑰匙才能拿到對的資料

假設我有個 Hash 長這樣:

profile = {name: "5xRuby", age: 18, tel: "02-28825252" }

如果想要取得這個 Hash 的 name 的資料,從別的程式語言剛轉過來的人,可能會這樣寫:

puts profile["name"]

你會發現什麼都沒有(其實是會得到 nil),要改成這樣寫才會拿得到:

puts profile[:name]    # => 5xRuby

因為,在這個 Hash 中,它的 key 是 :name 而不是字串 "name":name"name" 是不一樣的東西,所以拿不到你想要的東西,這點要特別注意。:name 是一個符號(Symbol)。至於符號是什麼,在下一個段落會有詳細說明。

問題:要怎麼把 Hash 裡的東西一個一個印出來?

假設我的 Hash 是這個樣子:

profile = {name: "5xRuby", age: 18, tel: "02-28825252" }

如果想要取得所有的 key 或是所有的 value,可以直接使用 keysvalues 方法取得:

p profile.keys     # => [:name, :age, :tel]
p profile.values   # => ["5xRuby", 18, "02-28825252"]

如果是想要一個一個東西印出來,可以這樣寫:

profile.each do |element|
  p element
end

# 執行後得到
# [:name, "5xRuby"]
# [:age, 18]
# [:tel, "02-28825252"]

但如果後面的 block 多加一個變數:

profile.each do |key, value|
  puts key
  puts value
end

這樣就可各別取得 key 跟 value 了。

符號(Symbol)

在 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 是個有點玄的東西,有些人認為它就只是變數,或就只是個名字,但事實上它不只是變數或名字這麼簡單,你可以想像它是一個「帶有名字的物件」:

Symbol 是一個 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 的錯誤訊息

Symbol 跟字串有什麼不同?

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

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

簡單的說,Symbol 跟字串有點像,Symbol 也有一些跟字串長得很像的方法,例如 lengthupcasedowncase 等等。不過 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,表示這些 Symbol 都是同一顆東西,當 Ruby 第二次要取用同一個 Symbol 的時候,它會直接從記憶體裡拿,而不用重新產生一份,所以 Symbol 相對的較節省記憶體。

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 是不是相同,而字串比較則是一個字母一個字母逐一比對。所以在效能上來說,字串在做比較的時間複雜度會隨著字母的數量(N)而增加,但 Symbol 的比較因為只比較是不是同一顆物件,所以它的時間複雜度是固定的。

字串跟 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,有的是兩種都能用,那該怎麼知道該用哪一種?

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

← 上一章:變數、常數、流程控制、迴圈 下一章:方法與程式碼區塊(block) →

Comments