第三章 类与对象
Ruby是一种真正的面向对象程序设计语言,面向对象指以对象为中心的理论体系。
l 封装(Encapsulation)
将内部结构和算法隐藏起来,以确保只有特定的过程(也叫方法)才能直接操作数据,其结果是不能从外部直接使用数据构造,同时一旦内部构造发生变化也不会对外界造成不良影响。这种隔离方法就叫做封装。
l 继承
l 多态(Polymorphism)
根据对象的不同选择合适的操作。在Ruby中的实现方法是,根据被调的对象的不同来选择不同的方法。
虽然有很多语言都宣称自己是面向对象的,但是他们往往对面向对象的解释都一样,大多是以自己特有的方式来解释什么是面向对象,而在实际情况中,这些面向对象语言又采用了很多非面向对象的做法。
以 Java 为例:如果你想取一个数字取绝对值,java 的做法是:
int num = Math.abs(-99);
也就是将一个数值传递给 Math 类的一个静态函数 abs 处理。为什么这么做?因为在 java 中,数值是基本类型不是类。
而在 Ruby 中,任何事物都是对象,也就是说,数字–99就是对象,取绝对值这样的操作应该属于数字本身,所以Ruby的做法就是:
c = -99.abs
在Ruby中,你所操作的一切都是对象,操作的结果也是对象。
§3.1 类的定义
类是对具有同样属性和同样行为的对象的抽象,Ruby中类的声明使用class关键字。定义类的语法如下,
class ClassName
def method_name(variables)
#some code
end
end
类的定义要在class…end之间,在上面的格式中,ClassName是类名,类名必须以大写字母开始,也就是说类名要是个常量。
看下面的例子:
class Person
def initialize(name, gender, age)
@name = name
@gender = gender
@age = age
end
end
若某个类已经被定义过,此时又用相同的类名进行类定义的话,就意味着对原有的类的定义进行追加。
class Test
def meth1
puts "This is meth1"
end
end
class Test
def meth2
puts "This is meth2"
end
end
在Test类中,原有meth1方法,我们又追加了meth2方法,这时候,对于Test类的对象,meth1和meth2同样可用。
§3.2 对象,属性和方法
类在实例化后生成对象,在强调对象归属于某类时,有时候我们也使用实例对象一词。
方法(Method)是对对象进行的操作。操作对象(被调)以self来表示。在Ruby中,除去内部类的对象以外,通常对象的构造都是动态确定的。某对象的性质由其内部定义的方法所决定。
看下面的例子,我们使用new方法构造一个新的对象,
class Person
def initialize(name, gender, age)
@name = name
@gender = gender
@age = age
end
end
people = Person.new('Tom', 'male', 15)
我们可以使用Person.new方法来创建一个Person类的实例对象。以@打头的变量是实例变量,他们从属于某一实例对象,Ruby中实例变量的命名规则是变量名以@开始,您只能在方法内部使用它。
initialize方法使对象变为“就绪”状态,initialize方法是一个特殊的方法,这个方法在构造实例对象时会被自动调用。
对实例进行初始化操作时,需要重定义initialize方法。类方法new的默认的行为就是对新生成的实例执行initialize方法,传给new方法的参数会被原封不动地传给initialize方法。另外,若带块调用时,该块会被传给initialize方法。因此,不必对new方法进行重定义。
在Ruby中,只有方法可以操作实例变量,因此可以说Ruby中的封装是强制性的。在对象外部不可以直接访问,只能通过接口方法访问。
class Person
def name
@name
end
def gender
@gender
end
def age
@age
end
end
people = Person.new('Tom', 'male', 15)
puts people.name
puts people.gender
puts people.age
输出结果为:
Tom
male
15
在Ruby中,一个对象的内部属性都是私有的。上面的代码中,我们定义了方法name,gender,age三个方法用来访问Person类实例对象的实例变量。注意name,gender,age访问只能读取相应实例变量,而不能改变它们的值。
我们也可以用成员变量只读控制符attr_reader来达到同样的效果。
class Person
attr_reader :name, :gender, :age
end
类似地,我们可以定义方法去改变成员变量的值。
class Person
def name=(name)
@name=name
end
def gender=(gender)
@gender=gender
end
def age=(age)
@age=age
end
end
people = Person.new('Tom', 'male', 15)
people.name = "Henry"
people.gender = "male"
people.age = 25
也可以用成员变量写控制符attr_writer来达到同样的效果。
class Person
attr_writer :name, :gender, :age
end
我们也可以使用attr_accessor来说明成员变量既可以读,也可以写。
class Person
attr_accessor :name, :gender, :age
end
也可以使用attr控制符来控制变量是否可读写。attr 只能带一个符号参数,第二个参数是一个 bool 参数,用于指示是否为符号参数产生写方法。它的默认值是 false,只产生读方法,不产生写方法。
class Person
attr :name, true #读写
attr :gender, true #读写
attr :age, true #读写
attr :id, false #只读
end
注意attr_reader,attr_writer,attr_accessor和attr不是语言的关键字,而是Module模块的方法。
class Test
attr_accessor :value
end
puts Test.instance_methods - Test.superclass.public_methods
执行结果为:
value
value=
上面代码中,我们使用Test.instance_methods得到Test类所有的实例方法,使用Test.superclass.public_methods得到Test父类所有的实例方法,然后相减就得到Test类不包含父类的所有的实例方法。
由于instance_methods方法返回值为一个Array,所以我们作差值运算,Array的具体操作后面章节会讲到。
也可以重定义方法,重定义一个方法时,新的定义会覆盖原有的定义。
下面的例子重定义类中的方法meth1,
class Test
def meth1
puts "This is meth1"
end
end
a = Test.new
a.meth1
class Test
def meth1
puts "This is new meth1"
end
end
a. meth1
执行结果为:
This is meth1
This is new meth1
重定义同一个类时,意味着对原有定义进行补充,不会覆盖原来的定义。而重定义方法时,则会覆盖原有定义。
我们可以使用self标识本身,self和Java中的this有些类似,代表当前对象。
class Person
def initialize(name, gender, age)
@name = name
@gender = gender
@age = age
end
def <=>(other)
self.age <=> other.age
end
end
<=> 方法通常意思为比较,返回值为-1,0或1分别表示小于,等于和大于。
§3.3 继承
Ruby继承的语法很简单,使用 < 即可。
class Student < Person
def initialize(name, gender, age, school)
@name = name
@gender = gender
@age = age
@school = school
end
end
Ruby语言只支持单继承,每一个类都只能有一个直接父类。这样避免了多继承的复杂度。但同时,Ruby提供了mixin的机制可以用来实现多继承。
可以使用super关键字调用对象父类的方法,当super省略参数时,将使用当前方法的参数来进行调用。
class Base
def meth(info)
puts "This is Base #{info}"
end
end
class Derived < Base
def meth(info)
puts "This is derived #{info}"
super
end
end
obj1 = Derived.new
obj1.meth("test")
执行结果为:
This is derived test
This is Base test
如果传入的参数被修改再调用super的话,那么将会使用使用修改后的值。
class Base
def meth(info)
puts "This is Base #{info}"
end
end
class Derived < Base
def meth(info)
puts "This is derived #{info}"
info = "over"
super
end
end
obj1 = Derived.new
obj1.meth("test")
执行结果为:
This is derived test
This is Base over
§3.4 特殊方法与特殊类
特殊方法是指某实例所特有的方法。一个对象有哪些行为由对向所属的类决定,但是有时候,一些特殊的对象有何其他对象不一样的行为,在多数程序设计语言中,例如C++和Java,我们必须定义一个新类,但在Ruby中,我们可以定义只从属于某个特定对象的方法,这种方法我们成为特殊方法(Singleton Method)。
class SingletonTest
def info
puts "This is This is SingletonTest method"
end
end
obj1 = SingletonTest.new
obj2 = SingletonTest.new
def obj2.info
puts "This is obj2"
end
obj1.info
obj2.info
执行结果为:
This is This is SingletonTest method
This is obj2
有时候,我们需要给一个对象定义一系列的特殊方法,如果按照前面的方法,那么只能一个一个定义:
def obj2.singleton_method1
end
def obj2.singleton_method2
end
def obj2.singleton_method3
end
……
def obj2.singleton_methodn
end
这样做非常繁复麻烦,而且无法给出一个统一的概念模型,因此Ruby提供了另外一种方法,
class << obj
……
end
obj是一个具体的对象实例,class << 代表它的特殊类。
class SingletonTest
def meth1
puts "This is meth1"
end
def meth2
puts "This is meth2"
end
end
obj1 = SingletonTest.new
obj2 = SingletonTest.new
class << obj2
def meth1
puts "This is obj2's meth1"
end
def meth2
puts "This is obj2's meth2"
end
end
obj1.meth1
obj1.meth2
obj2.meth1
obj2.meth2
执行结果为:
This is meth1
This is meth2
This is obj2's meth1
This is obj2's meth2
§3.5 类变量与类方法
类变量被一个类的所有实例对象共享,也可以被类方法访问到。类变量名以‘@@’,开始,例如‘@@number’。和全局变量,实例变量不同,类变量在使用前必须初始化:
class Person
@@number = 0 #使用前必须有初值
def initialize(name, gender, age)
@name = name
@gender = gender
@age = age
@@number += 1
end
end
类变量是私有的,在类外无法直接访问,你只能通过实例方法和类方法去访问它。
同样,类方法是属于一个类的方法,定义类方法时需要在方法前加上类名:
class Person
@@number = 0
def initialize(name, gender, age)
@name = name
@gender = gender
@age = age
@@number += 1
end
def Person.getNumber #类方法
return @@number
end
end
除了Person.getNumber这种方式定义类方法外,还可以使用其它方式定义类方法,在后续章节可以陆续见到。
§3.4 存取控制
当你设计一个类时,你需要决定哪些属性和方法可以在类外被访问到,哪些属性和方法在类外被隐藏。如果一个类有过多的属性和方法在类外可以被访问到,那么势必破坏这个类的封装性。幸运的是在Ruby中,只能通过方法去改变一个类的属性,这样我们只需要考虑方法的存取控制。
方法的存取控制有三种:
l 公有方法(Public Method)
n 方法在任何地方都可以被调用,这是方法的默认存取控制。除了initialize和initialize_cpoy方法,他们永远是私有方法。
l 保护方法(Protected Method)
n 方法只能被定义这个方法的类自己的对象和这个类的子类的对象所访问。
l 私有方法(private Method)
n 方法只能被定义这个方法的类的对象自己访问,即使是这个类的其他对象也不能访问。
Ruby中的保护方法和私有方法与一般面向对象程序设计语言的概念有所区别,保护方法的意思是方法只能方法只能被定义这个方法的类自己的对象和子类的对象访问,私有方法只能被对象自己访问。
class Test
def method1 #默认为公有方法
…
end
protected #保护方法
def method2
…
end
private #私有方法
def method3
end
public
def test_protected(arg) #arg是Test类的对象
arg.method2 #正确,可以访问同类其他对象的保护方法
end
def test_private(arg) #arg是Test类的对象
arg.method3 #错误,不能访问同类其他对象的私有方法
end
end
obj1 = Test.new
obj2 = Test.new
obj1.test_protected(obj2)
obj1.test_private(obj2)
可以看到,和C++/Java相比,Ruby提供了更好的封装性。
也可以使用以下更简单的形式:
class Test
def method1
...
end
def method2
...
end
def method3
...
end
def methdo4
...
end
public :method1
protected :method2
private :method3, :method4
end
Ruby和C++/Java的一个显著不同是存取控制是程序运行时决定的而不是静态绑定的。所以只有在访问一个受限制的方法时才会产生运行时错误。
§3.6 元类
在Ruby中一切都是对象。类和实例对象都是对象。这句话听起来有点拗口,让我们来看一个例子:
class Person
def initialize(name, gender, age)
@name = name
@gender = gender
@age = age
end
end
a = Person.new('Tom', 'male', 15)
puts a.object_id => 22429840
puts Person.object_id => 22429960
没错,类也是对象,这是Ruby和C++/Java的一个显著不同,在C++/Java中,类仅仅是一个数据抽象,并没有类也是对象这样的概念。而在Ruby中存在着元类的概念,类也是对象,所有类都是元类的实例对象。和C++/Java相比,Ruby的面向对象程度更高。
可以看到,类对象和实例对象一样有自己的ojbect_id,你可以象调用一个实例对象的方法一样去用它去调用类方法。所有类对象的类是Class类,Oject类是所有类的基类。
irb(main):003:0> Object.class
=> Class
irb(main):004:0> Object.superclass
=> nil
这样,我们可以从另一个角度去理解类变量与类方法,类变量就是一个类对象的实例变量,类方法就是指一个类对象类的特殊方法。
类方法具体可分为两种:第一种是在所有的类的父类Class中定义的,且被所有的类所共享的方法;第二种是各个类所特有的特殊方法。
类方法中的self指的是类本身,这点需要牢记,这样我们可以使用多种方式定义类方法。
class Test
#定义类方法方式1
def Test.meth1
# ...
end
#定义类方法方式2
def self.meth2
# ...
end
#定义类方法方式3
class << Test
def meth3
# ...
end
end
#定义类方法方式4
class << self
def meth4
# ...
end
end
end
§3.7 Ruby的动态性
可以重新定义同一个方法,
class RedefTest
def meth
puts "This is meth"
end
end
obj1 = RedefTest.new
obj1.meth
class RedefTest
def meth
puts "This is new meth"
end
end
obj1.meth
执行结果为:
This is meth
This is new meth
可以使用undef_method取消一个方法的定义,
class UndefTest
def meth
puts "This is meth"
end
end
obj1 = UndefTest.new
obj1.meth
class UndefTest
undef_method(:meth)
end
obj1.meth
执行结果为:
This is meth
test.rb:14: undefined method `meth' for #<UndefTest:0x2ac8240> (NoMethodError)
§3.8 变量
变量名长度只受内存大小的限制。可以通过区分Ruby变量名的首字符来区分它是局部变量、实例变量、类变量、全局变量还是常量。通常情况下,变量名的第二位字符以后是数字、字母或下划线,但有的内部变量名比较特殊,如“$?”。
§3.8.1 局部变量
局部变量以小写字母或下划线开始。
num = 1
foo
局部变量的作用域起始于声明处,结束于该声明所在的块、方法定义、类/模块定义的结尾。
2.times {
p defined?(num)
num = 10
p num
}
输出为:
nil
10
nil
10
即使声明部分未被解释器执行仍有效,因为已经经过解释器的处理。
v = 1 if false
p defined?(v)
p v
输出为:
"local-variable"
nil
但若块已经变成过程对象的话,则局部变量将一直持续到该过程对象终结为止。若多个过程对象引用同一个作用域的话,局部变量将被这些对象所共享。
(to-do例子)
§3.8.2 实例变量
以@开始的变量是实例变量,实例变量属于特定的对象。
class Person
def initialize(name, gender, age)
@name = name
@gender = gender
@age = age
end
end
上面的例子中,@name, @gender,@age都是实例变量。可以在类或子类的方法中引用实例变量,但不能直接被实例对象引用,只能通过调用类方法。若引用尚未被初始化的实例变量的话,其值为nil。
§3.8.3 类变量
以@@开始的变量是类变量。类变量在类的定义中定义,可以在类的特殊方法、实例方法等处对类变量进行赋值和引用。类变量被类,类的子类和他们的实例对象共享。
class Person
@@number = 0 #使用前必须有初值
def initialize(name, gender, age)
@name = name
@gender = gender
@age = age
@@number += 1
end
end
类变量是私有的,在类外无法直接访问,你只能通过实例方法和类方法去访问它。可以把类变量看作一种被类、子类以及它们的实例所共享的全局变量。
模块中定义的类变量(模块变量)被所有包含该模块的类所共享。
module TestModule
@@foo = 10
end
class Klass
include Foo
p @@foo += 1 # => 11
end
class Base
include Foo
p @@foo += 2 # => 12
end
§3.8.4 全局变量
以$开始的变量是全局变量,全局变量可以在程序的任何地方加以引用。全局变量无需变量声明。引用尚未初始化的全局变量时,其值为nil。
Ruby运行时环境预定义了一系列的全局变量,有关预定义的全局变量的信息,请参见附表。
§3.8.5 常量
常量以大写字母开始,常数的定义和初始化由赋值过程完成。
PI = 3.14
E = 2.71
若对已定义的常数进行赋值的话,会出现警告信息。若引用未定义的常数会引发NameError异常。
PI = 3.14
obj1 = 2 * PI * 10
PI = 3.1415 #warning: already initialized constant PI
obj2 = Foo #uninitialized constant Foo (NameError)
常量可以定义在类和模块中,不能定义在方法中。
class Meth
PI = 3.14 #OK
end
def circle_area(arg)
PI = 3.14 #ERROR
PI * arg * arg
end
若想在外部访问类或模块中的常数时,要使用“::”操作符。
class Meth
PI = 3.14
end
def circle_area(arg)
Math::PI * arg * arg
end
在类定义表达式生成类对象的同时,还会将类对象赋值给一个与该类同名的常数,引用类名也就是引用该常数。
class Test
end
p Test.class #Class
p Test #test
若想访问Object类中的常数(顶层的常数)时,也需要也使用"::"操作符,但操作符左边为空。
§3.8 与定义有关的操作
§3.8.1 alias
Alias关键字给方法或全局变量添加别名。可以给方法名指定一个标识符或Symbol作为别名。给方法添加别名时,别名方法将和此刻的原始方法绑定,此后即使重新定义了原始方法,别名方法仍然保持着重定义前的老方法的特性。若改变了某方法的内容后,又想使用修改前的方法时,别名会很有用。也可以使用Module#alias_method给方法添加别名。
# 定义meth方法
def meth
puts "This is meth"
end
#设定别名
alias :orig_meth :meth
#重定义foo
def meth
puts "This is new meth"
end
p meth
执行结果为:
This is new meth
nil
给全局变量设定别名意味两个名称指向同一个全局变量。当你向一个赋值时,另一个也会被改变。
$abc = 1
alias $xyz $abc
$xyz = 2
p [$abc, $xyz] # => [2, 2]
但是不能给正则表达式中的变量$1,$2等添加别名,另外,有些全局变量对于解释器来说是举足轻重的,若重新定义它们的话,有时会影响解释器的正常工作。
§3.8.2 undef
undef用来取消一个方法的定义,也可以使用Module#undef_method方法取消方法的定义。undef会取消方法名和方法定义之间的关系,即使超类中有同名方法,调用时也会引发异常。
class Base
def meth
puts "This is Base#meth"
end
end
class Derived < Base
def meth
puts "This is Derived#meth"
end
end
class Test1 < Derived
def meth
puts "This is Test1#meth"
end
undef_method(:meth)
end
obj1 = Test1.new
obj1.meth
执行结果为:
Tes1.rb:22: undefined method `meth' for #<Test1:0x2ac7c88> (NoMethodError)
而Module#remove_method方法只负责取消当前类中方法名和方法定义之间的关系,父类的同名方法仍可调用,这点差别非常重要。
class Base
def meth
puts "This is Base#meth"
end
end
class Derived < Base
def meth
puts "This is Derived#meth"
end
end
class Test2 < Derived
def meth
puts "This is Test2#meth"
end
remove_method(:meth)
end
obj2 = Test2.new
obj2.meth
执行结果为:
This is Derived#meth
用alias添加别名或用undef取消定义时,会修改类的接口,而不受父类的限制。继承和Mix-in的功能都是在类中添加方法,而undef则可以取消方法。但是,如果取消了类所必需的方法(被其他方法所调用的方法)的话,其后果不堪设想。
§3.8.3 defined?
Defined?用来判断表达式是否定义。若表达式尚未定义,则返回nil,若已经定义,则返回一个字符串描述该表达式的种类。
defined? Val #=> nil
defined? true #=> “true”
defined? $* #=> "global-variable"
defined? Array #=> "constant"
defined? Math::PI #=> "constant"
defined? num = 0 #=> "assignment"
defined? 100 #=> "expression"
defined? 100.times #=> "method"
虽然defined?看起来像一个方法,实际上是Ruby语法中的操作符,因此不会对参数进行计算。因此下面的表达式并不会输出“abc”。
defined? print("abc\n")
如果是方法未定义,或方法使用undef或Module#remove_method取消了原有定义,defined?都将返回nil。
注意如果一个方法以大写字母开头,使用defined? 判断时需要在方法名后添加"()"时,否则方法名会被当做常数处理。
def Foo(arg)
end
p defined? Foo # => nil
p defined? Foo() # => "method"
Foo = 1
p defined? Foo # => "constant"
还可以使用下列特殊用法:
l 判断yield是否可用
defined? yield
若yield调用可用,则返回真,具体返回值为字符串"yield"。它的作用同block_given?一样,可以判断能否以带块方式来调用某方法。
class Base
def foo
puts defined? yield
end
end
a = Base.new
a.foo
a.foo {}
执行结果为:
nil
yield
l 判断super是否可用
defined? super
若super可被调用,则返回真,具体返回值为字符串"super"。
class Base
def foo
end
end
class Derived < Base
def foo
puts defined? super
end
def fun
puts defined? super
end
end
obj = Derived.new
obj.foo
obj.fun
执行结果为:
super
nil
l 返回没有赋值但已经定义的局部变量.
defined? a = 1 #=> assignment
p a # => nil
l 在正则表达式中使用
/(.)/ =~ "foo"
p defined? $& # => "$&"
p defined? $1 # => "$1"
p defined? $2 # => nil