Tuesday, October 28, 2008

(2)重载操作符

我们知道Ruby中所有的操作符都定义为方法,所以你可以重载这些操作符以便定义你自己的操作行为:比如+,-,*/,[]。Ruby 把一元减定义为@-,所以我们前面讲的Point类可以定义如下:
class Point
  attr_reader :x, :y   # 定义我们的两个实例变量都有setter和getter方法

  def initialize(x,y)
    @x,@y = x, y
  end

  def +(other)         # 重载加法操作
    Point.new(@x + other.x, @y + other.y)
  end

  def -@               # 重载一元减号操作,返回负数值得对象
    Point.new(-@x, -@y)
  end

  def *(scalar)        # 重载乘号,这样Point对象的x,y可以乘以一个数值
    Point.new(@x*scalar, @y*scalar)
  end
end
注意我们上面定义的+操作,它没有对参数类型进行检查,也就是说如果我们传入的参数对象是一个有.x和y方法的对象,并且这个两个方法都返回数字型,那么就可以正常执行,否则会抛出异常,通常这没有问题,但是如果这个对象没有这个方法或者方法的返回值不是数字,那么就会抛出异常。一个解决的办法是像下面这样进行对象类型检查:
def +(other)
  raise TypeError, "Point argument expected" unless other.is_a? Point
  Point.new(@x + other.x, @y + other.y)
end
上面的方法在传入的对象不符号要求的时候会抛出一个指定的异常,我们用is_a?方法来进行判断,但是这种坚持还不是很严格,比如,如果我传入的是一个Point的子类也是可以正常执行的,但有的时候我们希望进行更严格的检查,即,只允许两个Point类进行相加,那么就要用到方法instance_of?
当然我们有时候要求很宽松,只要参数对象有方法x,y就可以对两个对象进行相加;
def +(other)
  raise TypeError, "Point-like argument expected" unless
    other.respond_to? :x and other.respond_to? :y
  Point.new(@x + other.x, @y + other.y)
end
不过这还是没有解决对参数方法类型判断的问题,如果我们既希望只要有x,y方法就可以相加,但是又不希望这两个方法返回字符串或者其他非数字的情况下出现难于理解的堆栈异常,我们可以使用下面这种通用的做法,通过rescue捕获所有异常:
def +(other)         #传入一个对象
  Point.new(@x + other.x, @y + other.y)
rescue               # 如果出现任何异常
  raise TypeError,   # 抛出我们自定义的异常消息
    "Point addition with an argument that does not quack like a Point!"
end

既然我们可以继承+-*/这样的操作符我们当然也可以继承其他的操作符,比如我们希望给Point对象加上类似数组或者字典对象的功能。可以重载[]操作符:
def [](index)
  case index
  when 0, -2: @x         # 如果索引是0或者-2返回变量x
  when 1, -1: @y         # 如果索引是1或者-1返回变量x
  when :x, "x": @x       # 如果键值是Symbol对象x或者字符串“x”,返回变量x
  when :y, "y": @y       # 如果键值是Symbol对象y或者字符串“y”,返回变量x
  else nil               # 如果索引或者键值不存在就返回nil
  end
end

如果对象可以加上类似数组的功能,我们理所当然的认为他可以实现迭代方法,方法很简单只要用yield就可以了:
def each
  yield @x
  yield @y
end
然后我们就可以这样调用了,是不是很酷。
p = Point.new(1,2)
p.each {|x| print x }   # 打印"12"
下面谈谈对象的比较,如果你曾经是一个Java程序员你一定重载过对象的equals方法,以便实现对象比较的功能,Ruby中定义比较的操作有多种,而且很简单:
def ==(o)               # 判断对象的值是否相同
  if o.is_a? Point      # 如果o是一个Point对象
    @x==o.x && @y==o.y  # 那么比较这两个对象的值.
  elsif                 # I如果不是Point对象
    false               # 返回false.
  end
end

如果现在有另一个需求,我们希望比的o对象可以不是一个Point,只要他们的x,y值相等即可,这种情况下,我们可以不进行类型校验,而是通过异常捕获来实现这个功能:
def ==(o)                  # 传入对象参数
  @x == o.x && @y == o.y   # 比较对象的参数值
rescue                     # 比较失败
  false                    # 返回false
end
记住我们这里比较的是对象的值,而不是对象本身,通常情况下这可以满足我们的需求,你甚至可以把eql?方法也定义成比较值得操作,方法是给它取别名方法为我们已经定义的==操作:
class Point
  alias eql? ==
end
不过有的时候如果你的对象是作为字典对象的键值,那么这样定义是不够的,因为字典对象内部的比较操作是通过eql?方法,而且同时你还需要重载hash方法,下面是一个例子:
def eql?(o)             
  if o.instance_of? Point      
    @x.eql?(o.x) && @y.eql?(o.y)
  elsif
    false
  end
end
def hash
  @x.hash + @y.hash
end
上面的重装hash的方式不是很好,比如Point(1,2)和Point(2,1)就无法区分,这样会带来性能问题,一个推荐的方法是如下所示这样,几乎所有的Ruby类对象比较都可以使用这种方法:
def hash
  code = 17
  code = 37*code + @x.hash
  code = 37*code + @y.hash  
  code  # 返回最终的哈希码
end
你是否想过如果一个数组中有多个相同的Point对象,那么如何利用sort方法进行排序呢?方法是重载<=>
include Comparable   # 必须导入这个包.

def <=>(other)
  return nil unless other.instance_of? Point
  @x**2 + @y**2 <=> other.x**2 + other.y**2
end

我们上面的Point在开始定义的时候是一个不可变的对象,后来我们定义了x=,y=方法来修改对象的属性,Ruby也提供了简单的方法通过attr_accessor :x,:y 来方便的定义可访问方法。除此之外我们还可以定义自己的方法来修改对象属性,还记得以前我们说到的Ruby中以!结尾的方法会修改对象的内部属性,没有叹号的不会,现在我们来给Point也加上这样的方法,不然我们定义一个add放用于将两个Point的值相加:
def add!(p)          # 这个方法将对象p与当前对象相加
  @x += p.x          # 并且更改了当前对象的值
  @y += p.y
  self
end



#下面这个方法也完成了类似的操作,但是返回的是一个对象的拷贝,而不�是修改对象本身

def add(p)           
  q = self.dup       # 通过dup创建一个拷贝对象
  q.add!(p)          # 调用已经定义的add!方法
end
说实话,Ruby语言为我们做了很多工作,它甚至提供了一个简便的方法让我们快速的创建一个可变对象,你可以使用Strut.new的方式来完成:
Struct.new("Point", :x, :y)  # 创建一个新类 Struct::Point
Point = Struct.new(:x, :y)   # 把这个新的类赋给常量Point,现在Point就代表一个新类
通过Strut创建的类和你自己写代码创建的类是一样的:
p = Point.new(1,2)   # => #<struct Point x=1, y=2>
p.x                  # => 1 
p.y                  # => 2
p.x = 3              # => 3
p.x                  # => 3
通过Strut.new的方式定义了Point类后,你可以通过下面的方式给这个类增加新的方法:
Point = Struct.new(:x, :y)   # 创建一个新的类,赋给 Point
class Point                  # “打开”这个类,给他增加新的方法 
def add!(other)            # 定义add!方法
    self.x += other.x
    self.y += other.y
    self
  end

  include Comparable         # 导入必要的包
  def <=>(other)             # 重装 <=> 操作
    return nil unless other.instance_of? Point
    self.x**2 + self.y**2 <=> other.x**2 + other.y**2
  end
end
再说一个有用的方法定义方式,如果你是一个Java程序员你一定使用过类的静态方法,你可以不必创建对象就直接引用这个方法,Ruby也提供了类似的功能,不过这里叫做类方法,定义类方法有3种方式:
你可以在方法前加上类名的前缀和点号:
class Point
  attr_reader :x, :y     

  def Point.sum(*points) # 定义一个类方法,返回多个Point对象的“合计”Point对象
    x = y = 0
    points.each {|p| x += p.x; y += p.y }
    Point.new(x,y)
  end
 
end
或者使用self关键字:
def self.sum(*points)  # 定义类方法的另一种方式
  x = y = 0
  points.each {|p| x += p.x; y += p.y }
  Point.new(x,y)
end
你还可以使用<<定义一个新的类来想Point类中增加方法:
# 向Point类中增加类方法
class << Point      # 使用<<符号来给指定的类增加类方法
  def sum(*points)  # 等同于Point.sum
    x = y = 0
    points.each {|p| x += p.x; y += p.y }
    Point.new(x,y)
  end

  # Other class methods can be defined here
end

再来看另一个问题,如何在类中使用常量,常量是非常有用的,在Ruby中你可以通过以下方式定义和使用常量,使用大写字母来定义常量,使用::来引用类的常量:
class Point
  def initialize(x,y)  # Initialize method
    @x,@y = x, y 
  end
#定义的常量
  ORIGIN = Point.new(0,0) 
  UNIT_X = Point.new(1,0)
  UNIT_Y = Point.new(0,1)
 
end
#引用常量
Point::NEGATIVE_UNIT_X = Point.new(-1,0)
说完了常量我们来看看类变量,类变量一@@开头,它可以在所有的方法之间共享,对所有方法都是可见得,比如下面的例子中,我们给Point类增加了一些类变量;
class Point
  # 初始化类变量
  @@n = 0              # Point个数的计数器
  @@totalX = 0         # x值的合计值
  @@totalY = 0         # y值的合计值

  def initialize(x,y)  # 类初始化方法
    @x,@y = x, y       # 初始化实例变量

    # 使用定义的类变量
    @@n += 1           # 记录创建的Point对象个数
    @@totalX += x      # 计算合计值
    @@totalY += y
  end

  # 打印所有合计值的方法
  def self.report
    # 下面演示了如何使用类变量
    puts "Number of points created: #@@n"
    puts "Average X coordinate: #{@@totalX.to_f/@@n}"
    puts "Average Y coordinate: #{@@totalY.to_f/@@n}"
  end
end
从某些角度来看,类变量和常量有些相似,但还是存在不少差别。类变量与常量的区别如下:
  • 类变量可以重复赋值(但对常量赋值会发出警告)。
  • 类变量默认是protected的,不能在类外部直接引用(在继承类中则可以引用或赋值)。
从某些角度来看,类变量和实例变量也有些相似。类变量与实例变量的区别如下:
  • 在类范围内定义的类变量,可以在该类的方法中访问,但实例变量则不行。
  • 类变量可在子类中引用或者赋值,但实例变量则只可在类范围内直接引用或赋值。

No comments: