最近開始學習 Ruby 這個語言,在嘗試寫迴圈印出數字的時候,想要用 i++ 在每次跑迴圈時,印出遞增數字,結果跳出 Syntax error:
i = 0
3.times do |x|
puts i++
end
# SyntaxError: syntax error, unexpected `end`
遇到這個錯誤的時候,覺得很奇怪,為什麼這麼常用的語法,在 Ruby 卻不能使用呢?
在 stackoverflow 可以看到也有其他人提問,結果 Ruby 的作者 Matz 有出來回覆:
(1) ++ and -- are NOT reserved operator in Ruby.
(2) C's increment/decrement operators are in fact hidden assignment. They affect variables, not objects. You cannot accomplish assignment via method. Ruby uses +=/-= operator instead.
(3) self cannot be a target of assignment. In addition, altering the value of integer 1 might cause severe confusion throughout the program.
看起來主要是跟 Ruby 這個語言的兩個特性有關:
- 整數是在 Ruby 程式一執行時,就存在且不可變的。
- method 中不能重新設定自己的值。
整數不可變
在 Ruby 中,有一部分的物件是程式一執行就被分配好記憶體的。其中就包含了整數、true、false、nil 這些,而我們用將變數賦值為整數的時候,並不會在創建新的物件,而只是把變數指向給已經存在的物件。
舉例來說,假設整數是下圖的水杯,Ruby 一執行就準備好了,當我們宣告變數 n = 1
的時候,就是把一個名字是 n 的標籤,貼到 1 的水杯上,方便以後使用。
也因為一開始就把這些數字水杯建立好了,去改變這些水杯的內容是不可能的。
如果說我們把水杯 1 的內容換成 2,就會變成有兩杯 2 的內容,卻再也找不到 1 了,這可能會造成整個系統的錯誤。
如果想要驗證數字是一開始就建立好的,可以在終端機裡面輸入 irb (Interactive Ruby) 測試以下的程式碼:
1.object_id #=> 3
1.object_id #=> 3
a = 1
a.object_id #=> 3
b = 1
b.object_id #=> 3
a = 1 是把 a 的標籤,貼到 1 的水杯上。
b = 1 是把 b 的標籤,貼到 1 的水杯上。
因此這兩個變數,印出來的 object_id 就會和數字 1 的 object_id 相同,因為他們目前都是指向同一個物件(水杯)。
接著設定 a = 2,會發現它印出的 object_id 改變,表示 a 指向不同的物件了。
a = 2
a.object_id #=> 5
Method 中不能重新設定自己
在 Ruby 中,加減乘除這些看似簡單的四則運算
puts 1 + 2 # => 3
其實是數字 1 呼叫一個名字叫 + 的 method:
puts 1.+(2) # => 3
既然如此,我們可以嘗試去改寫這個 method,讓他在回傳結果前,先印出 "hello world"
class Integer
alias :oroginal_plus :+
def +(other)
puts "hello world"
original_plus(other)
end
end
puts 1 + 2
# output:
# hello world
# 3
這樣我們要改寫成 i++ 好像很簡單呀,立刻就來試試看!
class Integer
alias :oroginal_plus :+
def +(other)
original_plus(other)
self += 1
end
end
puts 1 + 2
# Error=> Can't chagne value of self
結果 Ruby 直接跳出錯誤訊息,說他沒辦法接受XD
這是為什麼呢? 我們來看一個比較清楚的例子
def foo(bar)
bar = bar + 2
end
baz = 1
foo(baz)
puts "baz: #{baz}"
# baz: 1
一開始 baz = 1,代表我們把 baz 這個標籤貼到 1 的水杯上。
接下來呼叫 foo(baz),進到 foo method 中的時候,其實隱含了把 bar 這個標籤貼到 baz 所在的水杯上的意思。
最後把 bar = bar + 2 是把 bar 的標籤往旁邊移兩格,可以發現 baz 的標籤還在原本的位置上。
所以我們在 method 中,是沒有辦法把 method 外的變數標籤貼到別的位置上的,而 Ruby 的編譯器就是貼心地告訴我們,想要改變 self 是不行的。
這些就是為什麼 Ruby 沒有 i++。
補充
Ruby 不可能把整台電腦的記憶體都拿去分配給無限大的數字,那分配的極限在哪裡呢?
在 64bit 的機器上是 2 的 62 次方:
$-2^{62}$ < FixNum < $2^{62}$ -1
這個也可以用 irb 來驗證:
# 2 的 62 次方 -1 (object_id 相同)
4611686018427387903.object_id #=> 9223372036854775807
4611686018427387903.object_id #=> 9223372036854775807
# 2 的 62 次方 (object_id 改變)
4611686018427387904.object_id #=> 70261819186720
4611686018427387904.object_id #=> 7026181979900
Reference
StackOverflow
https://stackoverflow.com/questions/3717519/no-increment-operator-in-ruby
https://stackoverflow.com/questions/1872110/is-ruby-pass-by-reference-or-by-value
Matz response
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/2710
Why post increment is tricky (C的範例有誤)
https://avdi.codes/ruby-or-why-post-increment-is-tricky/
為你自己學 Ruby (數字...)
https://railsbook.tw/chapters/06-ruby-basic-2.html