运算符

运算符


  • https://www.kancloud.cn/imxieke/ruby-base/107296


Ruby 的运算符能通过定义方法的方式来改变其原有的含义。

赋值运算符

正如我们之前所介绍的那样,Ruby 的变量是在首次赋值的时候创建的。之后,程序可能会对变量引用的对象做各种各样的处理,甚至再次给变量赋值。例如,对 a 变量加 1,对 b 变量乘 2,如下所示:

a = a + 1
b = b * 2

上面的表达式可被改写为以下形式:

a += 1
b *= 2

大部分的二元运算符 op 都可以做如下转换。

var op= val

var = var op val

将二元运算符与赋值组合起来的运算符称为赋值运算符。

  • &&=

  • ||=

  • ^=

  • &=

  • |=

  • <>

  • >>=

  • +=

  • -=

  • \*=

  • /=

  • %=

  • \*\*=

除了变量之外,赋值运算符也同样适用于经由方法的对象操作。下面两个表达式是等效的。

$stdin.lineno += 1
$stdin.lineno = $stdin.lineno + 1

请读者注意,上面的式子调用的是 $stdin.lineno$stdin.lineno= 这两个方法。也就是说,使用赋值运算符的对象必须同时实现 reader 以及 writer 存取方法。

逻辑运算符的应用

在介绍逻辑运算符的应用例子之前,我们需要先来了解一下逻辑运算符的以下一些特征。

  • 表达式的执行顺序是从左到右

  • 如果逻辑表达式的真假已经可以确定,则不会再判断剩余的表达式

  • 最后一个表达式的值为整体逻辑表达式的值

条件 1``|| 条件 2

上面的表达式一定会按照条件 1、条件 2 的顺序来判断表达式的值。条件 1 的判断结果为真时,不需要判断条件 2 的结果也可以知道整体表达式的结果为真。反过来说,只有当条件 1 的判断结果为假时,才需要判断条件 2。也就是说,Ruby 的逻辑运算符会避免做无谓的判断。下面我们来进一步扩展该逻辑表达式:

条件 1``|| 条件 2``|| 条件 3

这种情况下,只有当条件 1 和条件 2 两者都为假的时候,才会进行条件 3 的判断。这里的条件表达式指的是 Ruby 中所有的表达式。

var || "Ruby"

在上面的表达式中,首先会判断 var 的真假值,只有当 varnil 或者 false 时,才会判断后面的字符串 "Ruby" 的真假值。之前我们也提到过,逻辑表达式的返回值为最后一个表达式的返回值,因此这个表达式的返回值为:

  • var 引用对象时,var 的值

  • varnil 或者 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

如果 arynil,则读取 ary[0] 时就会产生程序错误。在这个例子中,预先将 item 的值设定为了 nil,然后在确认 ary 不是 nil 后将 ary[0] 的值赋值给了 item。像这样的程序,通过使用 &&,只要像下面那样一行代码就可以搞定了:

item = ary && ary[0]

在确定对象存在后再调用方法的时候,使用 && 会使程序的编写更有效率。从数学的角度上来看,下面的逻辑表达式表达的是一样的意思,但是从编程语言的角度来看却并不是一样的。

item = ary[0] && ary    # 错误的写法

最后,我们来看看 || 的赋值运算符。

var ||= 1

var = var || 1

的运行结果是一样的。只有在 varnil 或者 false 的时候,才把 1 赋值给它。这是给变量定义默认值的常用写法。

条件运算符

条件运算符 ?: 的用法如下:

条件 ? 表达式 1 : 表达式 2

上面的表达式与下面使用 if 语句的表达式是等价的:

if 条件
 表达式 1
else
 表达式 2
end

例如,对比 ab 的值,希望将比较大的值赋值给 v 时,程序可以像下面这样写:

a = 1
b = 2
v = (a > b) ? a : b
p v    #=> 2

如果表达式过于复杂就会使程序变得难懂,因此建议不要滥用此写法。条件运算符也称为三元运算符。

范围运算符

在 Ruby 中有表示数值范围的范围(range)对象。例如,我们可以像下面那样生成表示 1 到 10 的范围对象。

Range.new(1, 10)

用范围运算符可以简化范围对象的定义。以下写法与上面例子的定义是等价的:

1..10

范围运算符有 ..... 两种。x..yx...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"

运算符的优先级

运算符是有优先级的,表达式中有多个运算符时,优先级高的会被优先执行。例如四则运算中的“先乘除后加减”。

表达式 | 含义 | 结果

  • | - | - 1 + 2 * 3 | 1 + (2 * 3) | 7 "a" + "b" * 2 + "c" | "a" + ("b" * 2) + "c" | "abbc" 3 * 2 ** 3 | 3 * (2 ** 3) | 24 2 + 3 < 5 + 4 | (2 + 3) < (5 + 4) | true 2 < 3 && 5 > 3 | (2 < 3) && (5 > 3) | true

如果不想按照优先级的顺序进行计算,可以用 () 将希望优先计算的部分括起来,当有多个 () 时,则从最内侧的 () 开始算起。因此,如果还未能熟练掌握运算符的优先顺序,建议多使用 ()

定义运算符

Ruby 的运算符大多都是作为实例方法提供给我们使用的,因此我们可以很方便地定义或者重定义运算符,改变其原有的含义。但是,部分运算符是不允许修改的。

  • ::

  • &&

  • ||

  • ..

  • ...

  • ?:

  • not

  • =

  • and

  • or

二元运算符

定义四则运算符等二元运算符时,会将运算符名作为方法名,按照定义方法的做法重定义运算符。运算符的左侧为接收者,右侧被作为方法的参数传递。在程序中,我们将为表示二元坐标的 Point 类定义运算符 + 以及 -

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 方法的不同点

上面的代码中定义了用于显示的 inspect 方法,在 p 方法中把对象转换为字符串时会用到该方法。另外,使用 to_s 方法也可以把对象转换为字符串,在 putsprint 方法中都有使用 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 方法。我们在写程序的时候,如果能根据实际情况选择适当的方法,就会达到事半功倍的效果。

一元运算符

可定义的一元运算符有 +-~! 4 个。它们分别以 +@、-@、~@、!@ 为方法名进行方法的定义。下面就让我们试试在 Point 类中定义这几个运算符。这里需要注意的是,一元运算符都是没有参数的。

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)

下标方法

数组、散列中的 obj[i] 以及 obj[i]=x 这样的方法,称为下标方法。定义下标方法时的方法名分别为 [][]=

在代码中,定义 Point 类实例 pt 的下标方法,实现以 v[0] 的形式访问 pt.x,以 v[1] 的形式访问 pt.y

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 以上的数值时,程序就会认为是参数错误并抛出异常。