← 上一章:第一個應用程式(使用 Scaffold) 下一章:數字、字串、陣列、範圍、雜湊、符號 →

變數、常數、流程控制、迴圈

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

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

你的第一個 Ruby 程式

執行 Ruby 程式

在開始寫你的第一行 Ruby 程式前,先介紹一下怎麼樣執行 Ruby 的程式。Ruby 的程式很多是在非圖形介面下執行,大多是終端機環境,常見有幾種方式:

單行程式

如果是很簡單的程式碼,可以直接在終端機底下就這樣輸入:

$ ruby -e "puts 'Hello, Ruby'"

這樣應該會在你的終端機環境印出 Hello, Ruby 字樣。

存成檔案

雖然像前面這樣執行單行程式是很簡單,但其實不怎麼實用,因為大部份的程式應該不會只有一行,所以比較常見的是把程式碼存成一個檔案,例如存檔的檔名叫做 hello.rb

# 檔案名稱 hello.rb
puts "Hello, Ruby"

其中,.rb 的附檔名非必須,也不是加上 .rb 才能被 Ruby 執行,加上附檔名僅供識別用。接下來,就是請 Ruby 來執行它:

$ ruby ./hello.rb

沒出錯的話,應該一樣可以在你的終端機環境看到 Hello, Ruby 字樣。

IRB

除了單行或是寫在檔案裡再執行的方式外,Ruby 有內建一個名為 IRB(Interactive Ruby)的互動介面,可以讓你簡單的輸入語法,而且立刻就可以看到結果:

$ irb
>> puts "Hello, Ruby"
Hello, Ruby
=> nil

一樣可以看到 Hello, Ruby 字樣,而最後面的那個 nil 則是「本次執行結果的回傳值」,會出現 nil 是因為 puts 方法本身並沒有回傳值。

像這樣的環境我們又稱之 REPL(Read-Eval-Print Loop)。

跟世界打招呼

大部份的程式語言的第一堂課,都是教大家把「Hello World」印出來,讓我們也來印個「Hello, Ruby」吧!要在 Ruby 做這件事,有以下三種方法:

print "Hello, Ruby"
puts "Hello, Ruby"
p "Hello, Ruby"

這三種方式都可以把 Hello, Ruby 輸出在畫面上,但稍微會有一點差別。

print 方法顧名思義,就是把東西印出來。puts 方法也可以,但跟 puts 的差別是 puts 方法後面會多一個換行符號,print 則沒有,讓我們開 IRB 來實驗一下:

$ irb
>> print "Hello, Ruby"
Hello, Ruby=> nil
>> puts "Hello, Ruby"
Hello, Ruby
=> nil

可以看到在 print 方法印出來之後緊接著 nil,但 puts 方法則是先「換行」再接 nil。而 p 方法同樣也可以把東西輸出在畫面上,也跟 puts 一樣都有換行,但跟 puts 方法有一些不同:

$ irb
>> puts "Hello, Ruby"
Hello, Ruby
=> nil
>> p "Hello, Ruby"
"Hello, Ruby"
=> "Hello, Ruby"
>> puts [1, 2, 3]
1
2
3
=> nil
>> p [1, 2, 3]
[1, 2, 3]
=> [1, 2, 3]
  1. p 方法可以完整的把型態跟結構印出來,例如 "Hello, Ruby" 字串以及 [1, 2, 3] 陣列,而 puts 則是把「內容物」印出來。
  2. p 方法是有回傳值的,但 puts 方法沒有。
  3. 事實上,你可以再加上 inspect 方法,像是 puts "Hello, Ruby".inspectputs [1, 2, 3].inspect 做到類似 p 方法的效果,但依舊沒有回傳值。

變數(variable)與常數(Constant)

變數種類

在 Ruby 裡,依寫法及範圍的不同,變數大概有以下幾種:

種類 範例 預設值 說明
區域變數 (local variable) name 沒有 非大寫字母開頭的名字
全域變數 (global variable) $name nil 前面加了 $ 符號的變數
實體變數 (instance variable) @name nil 前面加了 @ 符號的變數
類別變數 (class variable) @@name 沒有 前面加了 2 個 @ 符號的變數

虛擬變數(Pseudo Variable)

除了上述這幾款變數外,Ruby 還有一種稱之虛擬變數(Pseudo Variable)的東西,這是在 Ruby 內部定義的,例如 nil、self、true、false,虛擬變數通常有特別的用途或意義,內容不能被改變。

self = "123"   # => 發生 Can't change the value of self 錯誤
true = "xyz"   # => 發生 Can't assign to true 錯誤

變數預設值

沒有初始化的全域變數以及實體變數的預設值是 nil,但一般的區域變數是沒有預設值的:

p @name  # => nil
p $name  # => nil
p name   # => 發生 undefined local variable or method 錯誤

有效範圍

這些變數的有效範圍(scope)都有些差別。以全域變數來說,就如同它的名字一樣,到處都可以用,但沒事不要亂用全域變數,可能會造成自己或其它人的困擾。區域變數的作用範圍相對的比較「區域」一點,例如:

def say_hello
  name = "魯夫"
  puts "hi, 我是#{name}"
end

say_hello     # => 印出 "hi, 我是魯夫"
puts name     # => 發生變數找不到的錯誤

定義在 say_hello 方法裡的 name 變數,在離開 say_hello 方法就失效了。再看另一個例子:

name = "魯夫"

def say_hello
  puts "hi, 我是#{name}"
end

puts name     # => 印出 "魯夫"
say_hello     # => 發生變數找不到的錯誤

雖然 name 變數在外面有定義,但在 say_hello 方法裡卻找不到。實體變數跟類別變數會在後面物件導向程式設計章節會再有更詳細的說明。

使用變數

在 Ruby 使用變數,不需要特別宣告或是指定型態,直接拿來用就可以了。在變數命名規則上,常見會使用英文字母、數字或底線的組合。或是非英文字母也可以,例如:

my_name = "eddie"
age = 18
姓名 = "見龍"
なまえ = "Yukihiro Matz"

使用常數

常數用起來其實跟變數差不多,也不需要特別宣告它是常數,但在 Ruby 對常數有特別的命名規定,就是「常數必須要是大寫英文字母開頭」,例如:

ExchangeRate = 0.28
Name = "kitty"

事實上,所有的類別、模組的名字都必須是常數,在後面的物件導向程式設計會有更詳細的說明。要特別注意的是,在 Ruby 的常數的內容是可以修改而且不會發生錯誤:

MyName = "eddie"      # => eddie
MyName << " kao"      # => eddie kao
MyName.prepend "j"    # => jeddie kao

如果是這樣:

MyName = "eddie"
MyName = "hello, ruby"

把常數的內容整個換掉會出現警告訊息,但就僅是警告而已,不是錯誤訊息,程式仍可正常執行,這點跟其它程式語言有很大的不同。

只是個名字,沒有形態

不管是變數或常數,它本身並沒有形態。你可以把它想像成是「一張有寫著名字的標籤,貼在某個東西上面」,被貼的那個東西有形態,但標籤本身沒有,所以這樣寫是不會有任何問題的:

name = "見龍"    # 原本是指向一個字串物件
name = 18       # 這樣會把 name 變數改指向一個數字物件

命名習慣

幫你的變數取個有意義的好名字就跟幫你的小孩取名字一樣是門學問,雖然像這樣寫:

x = "rainy"
a = 1

程式並不會出錯,但用這樣不太具有意義的命名方式,程式碼的可讀性就變差了,建議使用更有意義的方式命名,例如:

weather = "rainy"
age = 1

另外,有些程式語言會使用像是 myShoppingCart 之類的方式來命名,稱之駝峰式命名法(Camel Case),在 Ruby 世界的不成文規定,是使用小寫英文字母以及底線來組合變數名稱,例如像是 my_shopping_cart,稱之蛇形命名法(Snake Case)。

同樣的,這並沒有對錯,即使在 Ruby 使用駝峰式命名法來命名,程式碼一樣可以正常運作,只是大部份的 Ruby 開發者都習慣 Snake Case 這樣的風格,大家就入境隨俗囉!

變數多重指定

當有多個變數要指定值的時候,例如這樣:

x = 1
y = 2
z = 3

利用 Ruby 變數多重指定的特性,上面這三行可以改寫成這樣一行:

x, y, z = 1, 2, 3

如果等號的右邊是一個陣列也可以自動拆解出來:

x, y, z = [1, 2, 3]

如果多重指定的時候數量不一樣多?

讓我們來看看當多重指定時,左右兩邊的數量不一樣多的時候會發生什麼事:

# 右邊比較多的情況:
a, b = 1, 2, 3, 4
# => a = 1, b = 2, 其它的內容被丟掉了

a, *b = 1, 2, 3, 4
# => a = 1, 變數 b 前面的星號會讓 b 接收剩下的數值變成一個陣列 [2, 3, 4]

x, y, z = [1, 2, 3, 4, 5]
# => x = 1, y = 2, z = 3, 剩下的被忽略

x, y, *z = [1, 2, 3, 4, 5]
# => x = 1, y = 2, 同上,z 會接受其它的值變成陣列 [3, 4, 5]

# 左邊比較多的情況
a, b, c = 1, 2
# => a = 1, b = 2, c 因為分不到值而變成 nil

x, y, z = [1, 2]
# => x = 1, y = 2, z 因為分不到值而變成 nil

變數再指定

a = 1
a = a + 1
puts a       # => 印出數字 2

如果你是數學系畢業的,看到 a = a + 1 這樣的寫法大概會覺得很不可思議。在大部份的程式語言中,一個等號通常是「指定」(assign)的意思,所以這行的意思是「把變數 a 的值加 1 之後,再指定回來給 a 這個變數」,變數 a 的值就會由原本的 1 變成 2。

而這裡的 a = a + 1,常也會把後面的加號往前搬,可簡化成 a += 1

註解(Comment)

程式碼裡的註解,在程式執行的時候會被忽略,註解通常有兩個主要的用途:

  1. 說明這段程式碼的用途。
  2. 讓該行程式碼不執行。

Ruby 使用井字符號 # 做為單行註解的符號,多行註解則是使用 =begin=end

# 這行是註解
# 這行也是註解
# 這些都是註解

=begin
在這裡的內容
全部都是
註解
喔
=end

# 或是 =begin..=end 都可以進行註解,在實務上大多會選用 # 符號,使用上比較多行註解的 =begin ... =end 來得方便。

動手做做看

問題:有兩個變數,x = 1, y = 2,請寫一小段程式來交換 x 跟 y 這兩個變數的值。

要交換兩個變數的值,通常會需要額外的暫存變數:

x = 1
y = 2

tmp = x      # 先把 x 的值放在一個暫時的變數 tmp
x = y        # 把變數 y 的值指定給變數 x
y = tmp      # 最後再把暫時變數 tmp 的值指定給 y

這樣就可以把兩個變數的內容換過來了。但在 Ruby 還可以有更簡單的寫法:

x = 1
y = 2

x, y = y, x

很神奇吧!利用 Ruby 可以多重指定變數的特性,一行就搞定了,程式碼簡單又容易閱讀。

流程控制(Flow Controller)

先看看這個有趣的小故事:

回家的時候,太太打電話來交待: 「老公,回家的路上,買 10 個包子回來,如果看到西瓜,買 2 個」 結果先生回到家,只買了 2 顆包子,因為他在路上看到西瓜了…

流程控制看起來是很簡單的事,大概就是「如果這樣..不然就這樣…」,但寫得不好或是語意不清楚,其實也是會出問題的。

另外再看看這個有點極端的例子:

a, b = 1, 2
if a > 0
  c = (a * b) / a
else
  c = b
end

這看起來好像有點複雜,但仔細想一下,你看得出來上面這段範例在寫什麼嗎?(劇透:不管是 if 或 else,結果都是 c = b 喔)

多餘的流程

這是我曾在某個 Rails 專案的 Controller 裡看到的一小段程式碼:

@user = User.find_by(name: params[:name])
if @user.role == "manager"
  @user.update(login_count: @user.login_count + 1)
  redirect_to root_path
else
  redirect_to root_path
end

上面這段有些語法是 Rails 才有的,先不管這樣的語法寫得漂不漂亮,但大概可以猜得出來「如果 user 的角色是〝manager〞,就讓這位 user 的登入次數加 1,然後轉向首頁,不然就什麼都不做,直接轉向首頁」。

看起來是沒什麼問題,但你有注意到不管是 if 或是 else 都有 redirect_to 嗎? 我們可以把多餘的流程抽出來放外面:

@user = User.find_by(name: params[:name])
if @user.role == "manager"
  @user.update(login_count: @user.login_count + 1)
end
redirect_to root_path

在這邊,把重複 redirect_to 拿到外面來,並且刪除不必要的 else,程式碼變得更清楚、易讀了。甚至利用待會就會介紹到的「if 可以放到後面」特性,上面這段再簡化成這樣:

@user = User.find_by(name: params[:name])
@user.update(login_count: @user.login_count + 1) if @user.role == "manager"
redirect_to root_path

真真假假

在各式各樣的程式語言中,有的會把數字 0、數字 -1 或是空陣列當做 false。在 Ruby 裡因為所有的東西都是物件,只有 nilfalse 會被當假的(false),除此這兩個之外,其它都是真的(true),包括數字 0、數字 -1、空陣列、空字串都會被做是 true。

nil 存在嗎?

雖然 nilfalse 在 Ruby 裡會被當成 false 看待,但並不表示他們不存在。在 Ruby 的世界裡,很多東西都是物件,事實上,nil 跟 false 也都是物件,nil 是 NilClass 類別的實體,false 則是 FalseClass 類別的實體:

puts nil.class    # => NilClass
puts false.class  # => FalseClass

nil 跟 false 都是真實存在的物件,只是他們在 Ruby 裡「被當做 false」而已。 特別是 nil,你有想過要怎麼用一個存在的東西來表示一個不存在的東西嗎?其實這有點玄。nil 是一個實實在在存在的物件,只是 Ruby 用它來表示「空的」、「不存在」的概念而已,所以在 Ruby 裡你還是可以對 nil 物件呼叫一些方法而不會出錯,例如:

puts nil.nil?     # => true,因為它就是 nil 沒錯
puts nil.to_a     # => 空的 Array []
puts nil.to_h     # => 空的 Hash {}

如果…不然就…(if .. else ..)

如果… 不然就… 是簡單的二分法,非黑即白、非藍即綠,寫起來大概像這樣:

age = 20

if age >= 18
  puts "你是大人了"
else
  puts "快快長大吧!"
end

如果…或是如果這樣… 不然就…(if .. elsif .. else ..)

如果遇到不只二分法,而是有更多如果的話,可以使用 elsif 來增加分支:

age = 10

if age > 0 && age <= 3
  puts "Baby"
elsif age > 3 && age <= 10
  puts "Kids"
elsif age > 10 && age <= 17
  puts "Teenager"
else
  puts "Adult"
end

要注意的是,這裡不是 elseif 也不是 else if,而是 elsif,少一個 e 喔!

把 if 放到後面去

在英文的文法裡,有時候可以把 if 放到句子最後面的倒裝句寫法。在 Ruby 一樣也可以這樣寫,例如:

if age >= 18
  puts "你是大人了"
end

像這種在 if 區塊裡只有一行內容,就可以把 if 丟到後面去,像這樣:

puts "你是大人了" if age >= 18

語法看起來是不是有點像英文的倒裝句呢。

unless

為了增加程式碼的可讀性,除了 if 之外,Ruby 還有提供 unless 語法,unless 等於 if not 的效果,如果我自己定義了一個叫做 is_adult? 的方法可以判斷是否為成年人(自定方法的寫法會在第七章介紹),原來可能會這樣寫:

if not is_adult?(20)
  puts "你是大人了"
end

unless 改寫後就可以變成:

unless is_adult?(20)
  puts "你是大人了"
end

請注意,不管是使用 unless 或是 if not,請先以程式碼的可讀性為優先考量,不要為了使用而使用。

再精簡一點的三元運算子

如果是簡單的 if … else …,在 Ruby 可考慮使用三元運算子再讓程式更精簡一點。三元運算子是由 ?: 組成,像這樣的寫法:

gender = 1

if gender == 1
  title = "先生"
else
  title = "小姐"
end

puts title   # => 先生

用三元運算子的寫法,可改寫成:

gender = 1
title = (gender == 1) ? "先生" : "小姐"
puts title   # => 先生

雖然這樣在整體的程式碼行數少好幾行,但對某些人來說這樣的程式碼可讀性似乎有降低一些。請以程式碼可讀性為優先考量,不要為了讓程式碼變少而這樣寫。

如果太多的「如果…不然就…(if .. else ..)」

當遇到太多的 if .. elsif … 的時候,可考慮使用 case ... when ... 來改寫,例如原來是這樣:

weather = "下雨"

case weather
when "下雨"
  puts "待在家!"
when "出太陽"
  puts "出去玩!"
else
  puts "在家睡覺!"
end

看起來似乎有比 if ... elsif ... 整齊一點(但其實在底層的實作是一樣的)。除此之外,case ... when ... 還可以搭配使用範圍技(Range):

age = 10

if age >= 0 && age <= 3
  puts "Baby"
elsif age >= 4 && age <= 10
  puts "Kids"
elsif age >= 11 && age <= 17
  puts "Teenager"
else
  puts "Adult"
end

可改寫成:

age = 10

case age
when 0..3
  puts "Baby"
when 4..10
  puts "Kids"
when 11..17
  puts "Teenager"
else
  puts "Adult"
end

這樣程式碼看起來就更清楚、簡潔了。

新手常犯錯誤:一個等號不是等於

看看下面這段程式碼範例:

age = 20

if age = 18
  puts "yes"
else
  puts "no"
end

看起來滿單純的邏輯,因為 age 不等於 18,所以理論上應該會印出〝no〞字樣,但你執行程式會發現總是會印出〝yes〞。

為什麼?

因為在 Ruby 裡(其實在大部份其它程式語言也是),一個等號表示是指定(assign),二個或三個等號才是比較(compare)。在上面這個例子,age = 18 是表示把數字 18 指定給 age 這個變數,而在 Ruby 裡,「指定」這件事的回傳值就是指定的內容本身,這個邏輯判斷式會得到 true,所以不管是 18 或 20,都是會得到〝yes〞字樣。

想想看…

問題:要怎麼計算是不是潤年?

潤年的計算公式是「西元年份可以被 4 整除而不可被 100 整除,但又可以被 400 整除」,根據這個公式可以這樣寫:

year = 2000

if year % 4 == 0 && year % 100 != 0 || year % 400 == 0
  puts "潤年"
else
  puts "不是潤年"
end

迴圈及迭代(Loop and Iteration)

在 Ruby 的迴圈主要有幾種:

  1. while 迴圈
  2. for..in 迴圈
  3. times, upto, downto 方法
  4. 迭代(iteration)

在 Rails 的開發過程中,比較常用到的是第 3 種跟第 4 種,特別是第 4 種。在別的程式語言常見的 for(i = 0; i < 10; i++) 之類的 For 迴圈寫法,在 Ruby 是不存在的。

while 迴圈

counter = 0

while counter < 5
  puts "hi, #{counter}"
  counter += 1
end

# 執行後得到結果:
# hi, 0
# hi, 1
# hi, 2
# hi, 3
# hi, 4

這段程式碼的意思是說「只要 counter 這個變數的值小於 5,就一直執行下去吧!」,所以要注意在迴圈裡的那行 counter += 1 ,如果少了這行,這將會是一個無窮迴圈喔。

就像 if 跟 unless 一樣,while 也有另一個跟它剛好相反意思的死對頭叫做 until,意思等同於 while not,所以上面這段範例也可改寫成:

counter = 0

until counter >= 5
  puts "hi, #{counter}"
  counter += 1
end

for..in 迴圈

假設你有一個陣列(Array,會在下個章節介紹)長得像這樣:

friends = ["eddie", "joanne", "john", "mark"]

如果想要把裡面的元素一個一個印出來,可以使用 for ... in ... 的寫法:

for friend in friends
  puts friend
end

# 執行後得到結果:
# eddie
# joanne
# john
# mark

其中,在變數的命名慣例上,為了增加程式碼的可讀性,通常會讓陣列是複數型態(例如 friends),每個元素則是使用單數型態命名(例如 friend),而盡量不要有像這樣的寫法:

for x in friends
  puts x
end

雖然執行結果是一樣的,但在程式碼的可讀性上就稍微差了一些,光看變數名稱 x,不容易猜到這個變數代表的意義。另外,如果只是想單純的印出 1 ~ 5,不用真的先做出一個含有 1 ~ 5 的陣列,可以利用 Range 的寫法(Range 會在下個章節介紹):

for i in 1..5 do
  puts i
end

# 執行結果
# 1
# 2
# 3
# 4
# 5

times, upto, downto 方法

在 Ruby 的世界裡,幾乎所有的東西都是物件,包括數字也是。而在 Ruby 的數字物件裡有一個 times 方法,可以讓我們指定迴圈要跑幾次:

5.times do
  puts "hello, ruby"
end

# 執行後得到結果:
# hello, ruby
# hello, ruby
# hello, ruby
# hello, ruby
# hello, ruby

語法一開始可能會看不太習慣,但光從字面應該很容易猜到它的意思。後面那個 do..end 在 Ruby 裡稱之 Block,在後面的章節會有更詳細的介紹。除了 times 之外,還有 uptodownto 方法,可以正向或反向的執行迴圈:

正向從 1 數到 5:

1.upto(5) do |i|
  puts "hi, ruby #{i}"
end

# 執行後得到結果:
# hi, ruby 1
# hi, ruby 2
# hi, ruby 3
# hi, ruby 4
# hi, ruby 5

反向從 5 數到 1:

5.downto(1) do |i|
  puts "hi, ruby #{i}"
end

# 執行後得到結果:
# hi, ruby 5
# hi, ruby 4
# hi, ruby 3
# hi, ruby 2
# hi, ruby 1

do..end 裡面那個 |i| 是 Block 裡的區域變數,同樣也會在 Block 單元再做介紹。

迭代(iteration)

承上,除了用 for..in 的方式把陣列裡的東西印出來,更常使用 each 方法來做這件事:

friends = ["eddie", "joanne", "john", "mark"]

friends.each do |friend|
  puts friend
end

在 Rails 專案中,陣列的 each 方法使用頻率也相當高,讓我們來看一下之前使用 Scaffold 產生的檔案(檔案:app/views/users/index.html.erb):

<tbody>
  <% @users.each do |user| %>
    <tr>
      <td><%= user.name %></td>
      <td><%= user.email %></td>
      <td><%= user.tel %></td>
      <td><%= link_to 'Show', user %></td>
      <td><%= link_to 'Edit', edit_user_path(user) %></td>
      <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
    </tr>
  <% end %>
</tbody>

這裡就是使用了 each 方法,一個一個的把資料轉出來印在畫面上。

← 上一章:第一個應用程式(使用 Scaffold) 下一章:數字、字串、陣列、範圍、雜湊、符號 →

Comments