Ruby 沒有 i++ (increment operator)


Posted by Lyla K on 2021-06-11

最近開始學習 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 這個語言的兩個特性有關:

  1. 整數是在 Ruby 程式一執行時,就存在且不可變的。
  2. 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

Ruby 語法放大鏡

https://kaochenlong.com/2016/04/26/open-class/


#ruby #Programming #rookie







Related Posts

DAY36:Maximum subarray sum

DAY36:Maximum subarray sum

當 React web app 遇上 Progressive web app

當 React web app 遇上 Progressive web app

Android Status bar 深色模式設定

Android Status bar 深色模式設定


Comments