ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# **第 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 &lt; 5 + 4</code></p> </td> <td> <p class="表格单元格"><code>(2 + 3) &lt; (5 + 4)</code></p> </td> <td> <p class="表格单元格"><code>true</code></p> </td> </tr><tr><td> <p class="表格单元格"><code>2 &lt; 3 &amp;&amp; 5 &gt; 3</code></p> </td> <td> <p class="表格单元格"><code>(2 &lt; 3) &amp;&amp; (5 &gt; 3)</code></p> </td> <td> <p class="表格单元格"><code>true</code></p> </td> </tr></tbody></table> ![{%}](https://box.kancloud.cn/2015-10-26_562e01e76e521.png) **图 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 以上的数值时,程序就会认为是参数错误并抛出异常。