Showing posts with label 类与模块. Show all posts
Showing posts with label 类与模块. Show all posts

Wednesday, October 29, 2008

(7)导入模块

我们说类是模块的子集,那么不管你定义一个模块还是定义一个类,在你需要他的时候都必须要导入它。
一个Ruby文件中可以写多个类和方法已经模块,Ruby提供了两个导入模块的方法:require和load。
  • require可以导入源文件和二进制文件,而load不能。
  • require只会导入一次需要的模块,而load在每次调用的时候都会重新导入一次。
  • require是可执行的,这意味着你可以把它放在代码中,比如按条件导入需要的模块
  • require和load关键字都支持相对路径和觉得路径的模块导入。
  • require可以支持导入文件包含扩展名或者不包含扩展名。
  • 如果你要导入多个文件,那么需要每行导入一个文件或者用分号隔开。

(6)模块与名称空间

Ruby中提供了模块的概念,你可以使用module关键字来定义一个模块。模块类似于类,但是模块不能像类那样可以继承,你也不可能想类那样去实例化一个模块。类是模块的子类,这意味着所有的类都是模块,但是模块不是类。
模块可以被用于把一系列相关的方法放到一起,这样你就可以避免重载了Ruby核心库中的方法或者类,就是说你有了自己的名称空间,下面是一个例子,比如我们现在有两个方法用于编码和解码64位字节,我们觉得没必要专门定义两个类来维护着两个方法,那么我们可以把它们放在一个名称空间里:
module Base64
def self.encode
end

def self.decode
end
end

除了用self关键字外,我们还可以像定义类方法那样直接用模块名作为前缀:
module Base64
def Base64.encode
end

def Base64.decode
end
end




# 下面是调用这个模块内方法的示例
text = Base64.encode(data)
data = Base64.decode(text)

当然我们还可以把一组相关的类、方法、常量等都放在一个模块里,这样更加清晰:
module Base64
DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

class Encoder
def encode
DIGITS
end
end

class Decoder
def decode
Base64.helper
end
end

# 为每个类提供一个助手类
def Base64.helper
end
end
对于上面的模块,DIGITS可以在两个类中直接方法,无需加模块名前缀,但是如果方法helper方法就必须要加上模块名前缀了。
模块的另一个重要的作用是“插入”(mixin)功能。你可以在模块中定义一些列的方法,然后在其它模块或类中用到这个模块中方法的时候,通过include 方法来将模块包含进当前的模块或者方法。需要注意的是你不能把一个类插入到另一个类中。比如我们上面定义的模块,你可以在一个类中包含它,实际上include是一个Ruby的核心方法而是不是一个关键字,所以你可以向调用方法那样使用它,如果你有多个模块需要导入,不同,模块之间可以用逗号分开,也可以用括号包含所有要导入的模块,下面的两种方式是一样的:
class Point
include Comparable,OtherModule
end

class Point
include(Comparable,OtherModule)
end

说到这里我想再谈谈继承,前面我们说过Ruby中不允许多重已成,也就是说你的子类只能有一个父类。如果你是一个C++程序员你可能要问为什么Ruby不提供像C++那样的多重继承。实际上你可以通过include多个模块的方式模拟多重继承,因为如果你在类中实现了一个和模块中同名的方法,那么你最终调用的是你自己定义的类方法,而不是模块中的同名方法,这在某种意义上模拟了多重继承。
另一个要注意的地方是,假设我们现在包含了两个模块A和B,这两个模块中存在重名的方法printa,那么如果我们在类C中调用这个方法printa的时候,会调用哪个模块中的方法?答案是取决于你include模块的顺序,如果A在B的前面,那么就调用A中的printa方法而不是b中的。

(5)Ruby中的工厂方法实现

了解设计模式的人一定知道工厂方法,有时候我们希望通过其他的方式创建一个类对象,而不是通过new关键字。不过new是所有类多有的public方法,我们要防止使用这个类的人直接通过new来创建类。Ruby提供了一个对方法访问权限控制的方法:private_class_method :new,这样我们就可以防止别人直接调用new来创建对象,下面是一个例子:
class Point
# 定义初始化方法...
def initialize(x,y) #传入参数
@x,@y = x,y
end

# 我们让new变成私有方法,这样别人就不能从外部调用了
private_class_method :new

def Point.cartesian(x,y) # 定义工厂方法,注意它是一个类方法
new(x,y) # 在方法内部调用new方法
end

def Point.polar(r, theta) # 创建另一种类型的Point对象
new(r*Math.cos(theta), r*Math.sin(theta))
end
end

Tuesday, October 28, 2008

(4)类继承

Ruby和大多数面向对象语言一样,类可以继承。如果你是Java或者C++程序员你一定知道我在说什么。Ruby和Java一样,一个类可以有多个子类,但是一个类只能有一个父类。Ruby中所有的类多是Object类的子类,Ruby1.9中又增加了一个BasicObject,它是Object类的父类。
class Base
attr_accessor :x,:y
def initialize(x,y)
@x,@y=x,y
end

end

class Child <Base
attr_accessor :z
def initialize x,y,z
super(x,y)
@z=z
end

end

c=Child.new(1, 2, 3)


上面的例子中可以看出Ruby使用符号< 来表示类的继承,Ruby中子类会继承父类的所有方法,包括initialize方法也会继承。就像self关键字表示对象本身一样,其中super 关键字表示父类对象。子类会继承父类的所有方法、实例变量、常量和类变量。但是这里有几点区别:
对于类变量来说,子类中如果命名了与父类同名的变量名,那么在调用子类的时候,子类中变量值会覆盖父类中变量值,而对于常量来说,父类中常量不会被子类中的同名常量覆盖,因为他们所在的“域”不同:
class A

@@value=1
def initialize
@x=1
end
def A.vale
@@value
end
def A.x
@x
end
A::LET=1

end
puts A.vale #打印1
class B < A ; @@value=2; B::LET=2; end
puts A.vale #打印2,说明父类的变量值别子类覆盖了。
class C < B;@@value=3;end
puts A.vale #打印3,说明父类的变量值别子类覆盖了。

puts A::LET # 打印1
puts B::LET # 打印2,说明常量没有被覆盖,因为常量之前有类的前缀

(3)类方法的可见性,public,protected,private

如果你使用过其它语言,你一定知道public,protected,private是什么,在Ruby中所有的方法,如果没有特殊的限制都是public的,类之外的方法Ruby解析为Object对象的private方法,在Ruby中定义方法的可见范围可以通过下面的方式,即区块方式,凡是protect关键字后下面的都是protect方法,private关键字下面的是private方法,public方法放在最上面没有任何关键字修饰。
class Med
  def pb
    print "Public"
  end
  protected
  def pt1
      print "pt1"
  end
  def pt2
    print "pt2"
  end
  private
  def pv1
    print "pv1"
  end
  def pv2
    print "pv2"
  end
end

m=Med.new
m.pb  #public方法可以执行
m.pt1 #这里你会得到NameError的异常 
m.pt2 #这里你会得到NameError的异常 
m.pv1 #这里你会得到NameError的异常 
m.pv2 #这里你会得到NameError的异常
你还可以把方法名的Symbold当作protected或者private的参数的方式来定义方法的可见性:
class Test
  def pb
    print "Public"
  end
  def pt
    print "Protected"
  end
  def pv
   print "Private"    
  end
  def pv2
    print "Private"    
  end
  protected :pt #通过Symbol的方式定义protected方法
  private :pv,:pv2
end

t=Test.new
t.pb #这里你会得到NameError的异常 
t.pt #这里你会得到NameError的异常 
t.pv #这里你会得到NameError的异常 
t.pv2 #这里你会得到NameError的异常
有时候我们可能希望让一些public的类方法变成private的那么可以通过private_class_method关键字实现,举个例子,有时候我们希望我们的类实现“单态模式”:
class Test
  @@test=nil  #声明一个类变量
  def pb
    print "Public"
  end
  def pt
    print "Protected"
  end
  def pv
   print "Private"    
  end
  def pv2
    print "Private"    
  end
  def Test.create #自定义一个“工厂方法” 实现“单态模式”
     @@test=new unless @@test
     @@test
  end
  protected :pt #通过Symbol的方式定义protected方法
  private :pv,:pv2
  private_class_method :new #将默认的new方法变成private
  
end

t=Test.create #通过自定义方法创建Test对象
t.pb

(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的,不能在类外部直接引用(在继承类中则可以引用或赋值)。
从某些角度来看,类变量和实例变量也有些相似。类变量与实例变量的区别如下:
  • 在类范围内定义的类变量,可以在该类的方法中访问,但实例变量则不行。
  • 类变量可在子类中引用或者赋值,但实例变量则只可在类范围内直接引用或赋值。

Monday, October 27, 2008

(1)类的定义

Ruby中定义类非常简单,只要在名称前加class关键字并以end结尾就可以了,一个惯例是类名被Ruby视为常量,所以按照命名常量的规则,类名应该以大写字母开头。下面是一个例子:
class Point
  @z=10  #你不应该在方法外部直接定义实例变量
  def initialize(x,y) #初始化方法用initialize关键字
    @x,@y=x,y
    @z=10 
  end
  def pp    
    print @z #通过@z来访问类内部的实例变量
  end
  def to_s #to_s 方法用于定义类对象转换为字符串的信息   
    "#{@x},#{@y}" 
  end
  def x  #定义getter 方法
    @x
  end
  def y #定义getter 方法
    @y
  end
  def x=(x_value) #定义setter 方法
    @x=x_value
  end
  def y=(y_value) #定义setter 方法
    @y=y_value
  end
  def setall #自定义定义setter 方法
    self.x=11
    self.y=22
  end
end
p=Point.new(1,2) #创建对象,这会调用initialize 方法对类进行初始化
p.pp  #调用方法

puts "1:#{p.x},#{p.y};"

p.x=3  # 修改x的值,实际上是调用方法p.x()=3
p.y=4

puts "2:#{p.x},#{p.y};"

p.setall

puts "3:#{p.x},#{p.y};"
就像我在上面代码中加的注释,一样你不能在Ruby的类中直接方法他的内部实例变量,必须通过定义方法来访问这些变量,而且如果你是一个Java程序员,记住类似类中第一行那样的定义是错误的,你不能在方法外直接声明一个实例变量然后给他赋值,这在Ruby中是不允许的。如果你要在类内容调用一个已经存在的实例变量,你可以通过self关键字,但是前提是你已经定义了setter和getter方法。
除此之外,Ruby还提供了一个方便的方法来定义setter和getter 方法,你可以通过attr_reader和attr_accssor来定义,比如:
class Point
  attr_accessor :x, :y # 通过Symbol方式定义实例变量的setter和getter方法
end


class Point
  attr_reader :x, :y  # 通过Symbol方式定义实例变量的getter方法
end


attr_reader "x", "y" #你可以通过给出实例变量的字符串名来定义getter或setter方法
还有一个关键字attr,他用于定义一个单一的实例变量的setter和getter方法:
attr :x        # 为 @x定义一个setter方法
attr :y, true  # 定义getter 和 setter 方法
记住,以上方式只能按照当前变量名的规则来给出setter和getter方法,如果你需要自定义的setter和getter方法,你必须自己手工完成,就像我们开始代码中那样。