# **第 9 章 運算符**
接下來,我們將詳細討論一下 Ruby 的運算符。
在本章的前半部分,我們會補充介紹之前沒介紹的運算符語法、以及邏輯運算符在 Ruby 中的一些習慣用法。
Ruby 的運算符能通過定義方法的方式來改變其原有的含義。在本章的后半部分,我們會討論一下如何定義運算符。
### **9.1 賦值運算符**
正如我們之前所介紹的那樣,Ruby 的變量是在首次賦值的時候創建的。之后,程序可能會對變量引用的對象做各種各樣的處理,甚至再次給變量賦值。例如,對 `a` 變量加 1,對 `b` 變量乘 2,如下所示:
~~~
a = a + 1
b = b * 2
~~~
上面的表達式可被改寫為以下形式:
~~~
a += 1
b *= 2
~~~
大部分的二元運算符 `op` 都可以做如下轉換。
~~~
var op= val
↓
var = var op val
~~~
將二元運算符與賦值組合起來的運算符稱為賦值運算符。表 9.1 為常用的賦值運算符。
**表 9.1 賦值運算符**
| `&&=` | `||=` | `^=` | `&=` | `|=` | `<>` | `>>=` |
|-----|-----|-----|-----|-----|-----|-----|
| `+=` | `-=` | `\*=` | `/=` | `%=` | `\*\*=` | |
除了變量之外,賦值運算符也同樣適用于經由方法的對象操作。下面兩個表達式是等效的。
~~~
$stdin.lineno += 1
$stdin.lineno = $stdin.lineno + 1
~~~
請讀者注意,上面的式子調用的是 `$stdin.lineno` 和 `$stdin.lineno=` 這兩個方法。也就是說,使用賦值運算符的對象必須同時實現 reader 以及 writer 存取方法。
### **9.2 邏輯運算符的應用**
在介紹邏輯運算符的應用例子之前,我們需要先來了解一下邏輯運算符的以下一些特征。
-
**表達式的執行順序是從左到右**
-
**如果邏輯表達式的真假已經可以確定,則不會再判斷剩余的表達式**
-
**最后一個表達式的值為整體邏輯表達式的值**
下面我們來詳細討論一下。首先,請看下面的與 `||` 相關的表達式。
**條件 `1``||` 條件 `2`**
上面的表達式一定會按照條件 1、條件 2 的順序來判斷表達式的值。條件 1 的判斷結果為真時,不需要判斷條件 2 的結果也可以知道整體表達式的結果為真。反過來說,只有當條件 1 的判斷結果為假時,才需要判斷條件 2。也就是說,Ruby 的邏輯運算符會避免做無謂的判斷。下面我們來進一步擴展該邏輯表達式:
**條件 `1``||` 條件 `2``||` 條件 `3`**
這種情況下,只有當條件 1 和條件 2 兩者都為假的時候,才會進行條件 3 的判斷。這里的條件表達式指的是 Ruby 中所有的表達式。
~~~
var || "Ruby"
~~~
在上面的表達式中,首先會判斷 `var` 的真假值,只有當 `var` 為 `nil` 或者 `false` 時,才會判斷后面的字符串 `"Ruby"` 的真假值。之前我們也提到過,邏輯表達式的返回值為最后一個表達式的返回值,因此這個表達式的返回值為:
-
**`var` 引用對象時,`var` 的值**
-
**`var` 為 `nil` 或者 `false` 時,字符串 `"Ruby"`**
接下來,我們再來討論一下 `&&`。基本規則與 `||` 是一樣的。
**條件 `1``&&` 條件 `2`**
與 `||` 剛好相反,只有當條件 1 的判斷結果為真時,才會判斷條件 2。
下面是邏輯運算符的應用例子。假設希望給變量 `name` 賦值,一般我們會這么做:
~~~
name = "Ruby" # 設定 name 的默認值
if var # var 不是 nil 或者 false 時
name = var # 將 var 賦值給 name
end
~~~
使用 `||` 可以將這 4 行代碼濃縮為一行代碼。
~~~
name = var || "Ruby"
~~~
下面我們稍微修改一下程序,假設要將數組的首元素賦值給變量。
~~~
item = nil # 設定 item 的初始值
if ary # ary 不是 nil 或者 false 時
item = ary[0] # 將 ary[0] 賦值給 item
end
~~~
如果 `ary` 為 `nil`,則讀取 `ary[0]` 時就會產生程序錯誤。在這個例子中,預先將 `item` 的值設定為了 `nil`,然后在確認 `ary` 不是 `nil` 后將 `ary[0]` 的值賦值給了 `item`。像這樣的程序,通過使用 `&&`,只要像下面那樣一行代碼就可以搞定了:
~~~
item = ary && ary[0]
~~~
在確定對象存在后再調用方法的時候,使用 `&&` 會使程序的編寫更有效率。從數學的角度上來看,下面的邏輯表達式表達的是一樣的意思,但是從編程語言的角度來看卻并不是一樣的。
~~~
item = ary[0] && ary # 錯誤的寫法
~~~
最后,我們來看看 `||` 的賦值運算符。
~~~
var ||= 1
~~~
和
~~~
var = var || 1
~~~
的運行結果是一樣的。只有在 `var` 為 `nil` 或者 `false` 的時候,才把 1 賦值給它。這是給變量定義默認值的常用寫法。
### **9.3 條件運算符**
條件運算符 `?:` 的用法如下:
**條件 ? 表達式 `1 :` 表達式 `2`**
上面的表達式與下面使用 `if` 語句的表達式是等價的:
**`if` 條件
表達式 `1`
`else`
表達式 `2`
`end`**
例如,對比 `a` 與 `b` 的值,希望將比較大的值賦值給 `v` 時,程序可以像下面這樣寫:
~~~
a = 1
b = 2
v = (a > b) ? a : b
p v #=> 2
~~~
雖然筆者比較喜歡這樣簡潔的寫法,但如果表達式過于復雜就會使程序變得難懂,因此建議不要濫用此寫法。條件運算符也稱為三元運算符。
### **9.4 范圍運算符**
在 Ruby 中有表示數值范圍的范圍(range)對象。例如,我們可以像下面那樣生成表示 1 到 10 的范圍對象。
~~~
Range.new(1, 10)
~~~
用范圍運算符可以簡化范圍對象的定義。以下寫法與上面例子的定義是等價的:
~~~
1..10
~~~
我們在第 6 章 `for` 循環的例子中也使用過這個運算符。
~~~
sum = 0
for i in 1..5
sum += i
end
puts sum
~~~
范圍運算符有 `..` 和 `...` 兩種。*x*`..`*y* 和 *x*`...`*y* 的區別在于,前者的范圍是從 *x* 到 *y*;而后者的范圍則是從*x* 到 *y* 的前一個元素。
對 `Range` 對象使用 `to_a` 方法,就會返回范圍中從開始到結束的值。下面就讓我們使用這個方法來確認一下 `..` 和 `...` 有什么不同。
~~~
p (5..10).to_a #=> [5, 6, 7, 8, 9, 10]
p (5...10).to_a #=> [5, 6, 7, 8, 9]
~~~
如果數值以外的對象也實現了根據當前值生成下一個值的方法,那么通過指定范圍的起點與終點就可以生成 `Range` 對象。例如,我們可以用字符串對象生成 `Range` 對象。
~~~
p ("a".."f").to_a #=> ["a", "b", "c", "d", "e", "f"]
p ("a"..."f").to_a #=> ["a", "b", "c", "d", "e"]
~~~
在 `Range` 對象內部,可以使用 `succ` 方法根據起點值逐個生成接下來的值。具體來說就是,對 `succ` 方法的返回值調用 `succ` 方法,然后對該返回值再調用 `succ` 方法……直到得到的值比終點值大時才結束處理。
> **執行示例**
~~~
> irb --simple-prompt
>> val = "a"
=> "a"
>> val = val.succ
=> "b"
>> val = val.succ
=> "c"
>> val = val.succ
=> "d"
~~~
### **9.5 運算符的優先級**
運算符是有優先級的,表達式中有多個運算符時,優先級高的會被優先執行。例如四則運算中的“先乘除后加減”。表 9.2 是關于運算符的優先級的一些例子。把 Ruby 的運算符按照優先級由高到低的順序進行排列,如圖 9.1 所示。
**表 9.2 運算符的優先級示例**
<table border="1" data-line-num="182 183 184 185 186 187 188 189" width="90%"><thead><tr><th> <p class="表頭單元格">表達式</p> </th> <th> <p class="表頭單元格">含義</p> </th> <th> <p class="表頭單元格">結果</p> </th> </tr></thead><tbody><tr><td> <p class="表格單元格"><code>1 + 2 * 3</code></p> </td> <td> <p class="表格單元格"><code>1 + (2 * 3)</code></p> </td> <td> <p class="表格單元格"><code>7</code></p> </td> </tr><tr><td> <p class="表格單元格"><code>"a" + "b" * 2 + "c"</code></p> </td> <td> <p class="表格單元格"><code>"a" + ("b" * 2) + "c"</code></p> </td> <td> <p class="表格單元格"><code>"abbc"</code></p> </td> </tr><tr><td> <p class="表格單元格"><code>3 * 2 ** 3</code></p> </td> <td> <p class="表格單元格"><code>3 * (2 ** 3)</code></p> </td> <td> <p class="表格單元格"><code>24</code></p> </td> </tr><tr><td> <p class="表格單元格"><code>2 + 3 < 5 + 4</code></p> </td> <td> <p class="表格單元格"><code>(2 + 3) < (5 + 4)</code></p> </td> <td> <p class="表格單元格"><code>true</code></p> </td> </tr><tr><td> <p class="表格單元格"><code>2 < 3 && 5 > 3</code></p> </td> <td> <p class="表格單元格"><code>(2 < 3) && (5 > 3)</code></p> </td> <td> <p class="表格單元格"><code>true</code></p> </td> </tr></tbody></table>

**圖 9.1 運算符的優先級**
如果不想按照優先級的順序進行計算,可以用 `()` 將希望優先計算的部分括起來,當有多個 `()` 時,則從最內側的 `()` 開始算起。因此,如果還未能熟練掌握運算符的優先順序,建議多使用 `()`。
### **9.6 定義運算符**
Ruby 的運算符大多都是作為實例方法提供給我們使用的,因此我們可以很方便地定義或者重定義運算符,改變其原有的含義。但是,表 9.3 中列舉的運算符是不允許修改的。
**表 9.3 不能重定義的運算符**
| `::` | `&&` | `||` | `..` | `...` | `?:` | `not` | `=` | `and` | `or` |
|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
### **9.6.1 二元運算符**
定義四則運算符等二元運算符時,會將運算符名作為方法名,按照定義方法的做法重定義運算符。運算符的左側為接收者,右側被作為方法的參數傳遞。在代碼清單 9.1 的程序中,我們將為表示二元坐標的 `Point` 類定義運算符 `+` 以及 `-`。
**代碼清單 9.1 point.rb**
~~~
class Point
attr_reader :x, :y
def initialize(x=0, y=0)
@x, @y = x, y
end
def inspect # 用于顯示
"(#{x}, #{y})"
end
def +(other) # x、y 分別進行加法運算
self.class.new(x + other.x, y + other.y)
end
def -(other) # x、y 分別進行減法運算
self.class.new(x - other.x, y - other.y)
end
end
point0 = Point.new(3, 6)
point1 = Point.new(1, 8)
p point0 #=> (3, 6)
p point1 #=> (1, 8)
p point0 + point1 #=> (4, 14)
p point0 - point1 #=> (2, -2)
~~~
如上所示,定義二元運算符時,我們常把參數名定義為 `other`。
在定義運算符 `+` 和`-`的程序中,創建新的 `Point` 對象時,我們使用了 `self.class.new`。而像下面這樣,直接使用 `Point.new` 方法也能達到同樣的效果。
~~~
def +(other)
Point.new(x + other.x, y + other.y)
end
~~~
使用上面的寫法時,返回值一定是 `Point` 對象。如果 `Point` 類的子類使用了 `+` 和 `-`,則返回的對象應該屬于 `Point` 類的子類,但是這樣的寫法卻只能返回 `Point` 類的對象。在方法內創建當前類的對象時,不直接寫類名,而是使用 `self.class`,那么創建的類就是實際調用 `new` 方法時的類,這樣就可以靈活地處理繼承與 Mix-in 了。
> **專欄**
> **puts 方法與 p 方法的不同點**
> 代碼清單 9.1 中定義了用于顯示的 `inspect` 方法,在 `p` 方法中把對象轉換為字符串時會用到該方法。另外,使用 `to_s` 方法也可以把對象轉換為字符串,在 `puts`、`print` 方法中都有使用 `to_s` 方法。下面我們來看看兩者的區別。
~~~
> irb --simple-prompt
>> str = "Ruby 基礎教程"
=> "Ruby 基礎教程"
>> str.to_s
=> "Ruby 基礎教程"
>> str.inspect
=> "\"Ruby 基礎教程\""
~~~
> `String#to_s` 的返回結果與原字符串相同,但 `String#inspect` 的返回結果中卻包含了 `\"`。這是因為 `p` 方法在輸出字符串時,為了讓我們更明確地知道輸出的結果就是字符串而進行了相應的處理。這兩個方法的區別在于,作為程序運行結果輸出時用 `to_s` 方法;給程序員確認程序狀態、調查對象內部信息等時用 `inspect` 方法。
> 除了 `puts` 方法、`print` 方法外,`to_s` 方法還被廣泛應用在 `Array#join` 方法等內部需要做字符串處理的方法中。
> `inspect` 方法可以說是主要使用 p 方法進行輸出的方法。例如,irb 命令的各行結果的顯示就用到了 `inspect` 方法。我們在寫程序的時候,如果能根據實際情況選擇適當的方法,就會達到事半功倍的效果。
### **9.6.2 一元運算符**
可定義的一元運算符有 `+`、`-`、`~`、`!` 4 個。它們分別以 +@、-@、~@、!@ 為方法名進行方法的定義。下面就讓我們試試在 `Point` 類中定義這幾個運算符(代碼清單 9.2)。這里需要注意的是,一元運算符都是沒有參數的。
**代碼清單 9.2 point.rb(部分)**
~~~
class Point
┊
def +@
dup # 返回自己的副本
end
def -@
self.class.new(-x, -y) # 顛倒x、y 各自的正負
end
def ~@
self.class.new(-y, x) # 使坐標翻轉90 度
end
end
point = Point.new(3, 6)
p +point #=> (3, 6)
p -point #=> (-3, -6)
p ~point #=> (-6, 3)
~~~
### **9.6.3 下標方法**
數組、散列中的 `obj[`*i*`]` 以及 `obj[`*i*`]=`*x* 這樣的方法,稱為下標方法。定義下標方法時的方法名分別為 `[]` 和 `[]=`。在代碼清單 9.3 中,我們將會定義 `Point` 類實例 `pt` 的下標方法,實現以 `v[0]` 的形式訪問 `pt.x`,以 `v[1]` 的形式訪問 `pt.y`。
**代碼清單 9.3 point.rb(部分)**
~~~
class Point
┊
def [](index)
case index
when 0
x
when 1
y
else
raise ArgumentError, "out of range `#{index}'"
end
end
def []=(index, val)
case index
when 0
self.x = val
when 1
self.y = val
else
raise ArgumentError, "out of range `#{index}'"
end
end
end
point = Point.new(3, 6)
p point[0] #=> 3
p point[1] = 2 #=> 2
p point[1] #=> 2
p point[2] #=> 錯誤(ArgumentError)
~~~
參數 `index` 代表的是數組的下標。由于本例中的類只有兩個元素,因此當索引值指定 2 以上的數值時,程序就會認為是參數錯誤并拋出異常。
- 推薦序
- 譯者序
- 前言
- 本書的讀者對象
- 第 1 部分 Ruby 初體驗
- 第 1 章 Ruby 初探
- 第 2 章 便利的對象
- 第 3 章 創建命令
- 第 2 部分 Ruby 的基礎
- 第 4 章 對象、變量和常量
- 第 5 章 條件判斷
- 第 6 章 循環
- 第 7 章 方法
- 第 8 章 類和模塊
- 第 9 章 運算符
- 第 10 章 錯誤處理與異常
- 第 11 章 塊
- 第 3 部分 Ruby 的類
- 第 12 章 數值類
- 第 13 章 數組類
- 第 14 章 字符串類
- 第 15 章 散列類
- 第 16 章 正則表達式類
- 第 17 章 IO 類
- 第 18 章 File 類與 Dir 類
- 第 19 章 Encoding 類
- 第 20 章 Time 類與 Date 類
- 第 21 章 Proc 類
- 第 4 部分 動手制作工具
- 第 22 章 文本處理
- 第 23 章 檢索郵政編碼
- 附錄
- 附錄 A Ruby 運行環境的構建
- 附錄 B Ruby 參考集
- 后記
- 謝辭