4.4 Ruby 类
我们之前说过,Ruby 中的一切都是对象。本节我们要自己定义一些对象。Ruby 和其他面向对象的语言一样,使用类来组织方法,然后实例化类,创建对象。如果你刚接触“面向对象编程”(Object-Oriented Programming,简称 OOP),这些听起来都似天书一般,那我们来看一些实例吧。
4.4.1 构造方法
我们看过很多使用类初始化对象的例子,不过还没自己动手做过。例如,我们使用双引号初始化一个字符串,双引号是字符串的字面构造方法:
>> s = "foobar" # 使用双引号字面构造方法
=> "foobar"
>> s.class
=> String
我们看到,字符串可以响应 class
方法,返回值是字符串所属的类。
除了使用字面构造方法之外,我们还可以使用等价的“具名构造方法”(named constructor),即在类名上调用 new
方法:[15]
>> s = String.new("foobar") # 字符串的具名构造方法
=> "foobar"
>> s.class
=> String
>> s == "foobar"
=> true
这段代码中使用的具名构造方法和字面构造方法是等价的,只是更能表现我们的意图。
数组和字符串类似:
>> a = Array.new([1, 3, 2])
=> [1, 3, 2]
不过哈希就有点不同了。数组的构造方法 Array.new
可接受一个可选的参数指明数组的初始值,Hash.new
可接受一个参数指明元素的默认值,就是当键不存在时返回的值:
>> h = Hash.new
=> {}
>> h[:foo] # 试图获取不存在的键 :foo 对应的值
=> nil
>> h = Hash.new(0) # 让不存在的键返回 0 而不是 nil
=> {}
>> h[:foo]
=> 0
在类上调用的方法,如本例的 new
,叫“类方法”(class method)。在类上调用 new
方法,得到的结果是这个类的一个对象,也叫做这个类的“实例”(instance)。在实例上调用的方法,例如 length
,叫“实例方法”(instance method)。
4.4.2 类的继承
学习类时,理清类的继承关系会很有用,我们可以使用 superclass
方法:
>> s = String.new("foobar")
=> "foobar"
>> s.class # 查找 s 所属的类
=> String
>> s.class.superclass # 查找 String 的父类
=> Object
>> s.class.superclass.superclass # Ruby 1.9 使用 BasicObject 作为基类
=> BasicObject
>> s.class.superclass.superclass.superclass
=> nil
这个继承关系如图 4.1 所示。可以看到,String
的父类是 Object
,Object
的父类是 BasicObject
,但是 BasicObject
就没有父类了。这样的关系对每个 Ruby 对象都适用:只要在类的继承关系上往上多走几层,就会发现 Ruby 中的每个类最终都继承自 BasicObject
,而它本身没有父类。这就是“Ruby 中一切皆对象”技术层面上的意义。
图 4.1:String
类的继承关系
要想更深入地理解类,最好的方法是自己动手编写一个类。我们来定义一个名为 Word
的类,其中有一个名为 palindrome?
方法,如果单词顺读和反读都一样就返回 true
:
>> class Word
>> def palindrome?(string)
>> string == string.reverse
>> end
>> end
=> :palindrome?
我们可以按照下面的方式使用这个类:
>> w = Word.new # 创建一个 Word 对象
=> #<Word:0x22d0b20>
>> w.palindrome?("foobar")
=> false
>> w.palindrome?("level")
=> true
如果你觉得这个例子有点大题小做,很好,我的目的达到了。定义一个新类,可是只创建一个接受一个字符串作为参数的方法,这么做很古怪。既然单词是字符串,让 Word
继承 String
不就行了,如代码清单 4.12 所示。(你要退出控制台,然后再在控制台中输入这写代码,这样才能把之前的 Word
定义清除掉。)
代码清单 4.12:在控制台中定义 Word
类
>> class Word < String # Word 继承自 String
>> # 如果字符串和反转后相等就返回 true
>> def palindrome?
>> self == self.reverse # self 代表这个字符串本身
>> end
>> end
=> nil
其中,Word < String
在 Ruby 中表示继承(3.2 节简介过),这样除了定义 palindrome?
方法之外,Word
还拥有所有字符串拥有的方法:
>> s = Word.new("level") # 创建一个 Word 实例,初始值为 "level"
=> "level"
>> s.palindrome? # Word 实例可以响应 palindrome? 方法
=> true
>> s.length # Word 实例还继承了普通字符串的所有方法
=> 5
Word
继承自 String
,我们可以在控制台中查看类的继承关系:
>> s.class
=> Word
>> s.class.superclass
=> String
>> s.class.superclass.superclass
=> Object
这个继承关系如图 4.2 所示。
图 4.2:代码清单 4.12 中定义的 Word
类(非内置类)的继承关系
注意,在代码清单 4.12 中检查单词和单词的反转是否相同时,要在 Word
类中引用这个单词。在 Ruby 中使用 self
关键字[16]引用:在 Word
类中,self
代表的就是对象本身。所以我们可以使用
self == self.reverse
来检查单词是否为“回文”。其实,在类中调用方法或访问属性时可以不用 self.
(赋值时不行),所以也可以写成 self == reverse
。
4.4.3 修改内置的类
虽然继承是个很强大的功能,不过在判断回文这个例子中,如果能把 palindrome?
加入 String
类就更好了,这样(除了其他方法外)我们可以在字符串字面量上调用 palindrome?
方法。现在我们还不能直接调用:
>> "level".palindrome?
NoMethodError: undefined method `palindrome?' for "level":String
有点令人惊讶的是,Ruby 允许你这么做,Ruby 中的类可以被打开进行修改,允许像我们这样的普通人添加一些方法:
>> class String
>> # 如果字符串和反转后相等就返回 true
>> def palindrome?
>> self == self.reverse
>> end
>> end
=> nil
>> "deified".palindrome?
=> true
(我不知道哪一个更牛:Ruby 允许向内置的类中添加方法,或 "deified"
是个回文。)
修改内置的类是个很强大的功能,不过功能强大意味着责任也大,如果没有很好的理由,向内置的类中添加方法是不好的习惯。Rails 自然有很好的理由。例如,在 Web 应用中我们经常要避免变量的值是空白(blank)的,像用户名之类的就不应该是空格或空白),所以 Rails 为 Ruby 添加了一个 blank?
方法。Rails 控制台会自动加载 Rails 添加的功能,下面看几个例子(在 irb
中不可以):
>> "".blank?
=> true
>> " ".empty?
=> false
>> " ".blank?
=> true
>> nil.blank?
=> true
可以看出,一个包含空格的字符串不是空的(empty),却是空白的(blank)。还要注意,nil
也是空白的。因为 nil
不是字符串,所以上面的代码说明了 Rails 其实是把 blank?
添加到 String
的基类 Object
中的。8.4 节会再介绍一些 Rails 扩展 Ruby 类的例子。)
4.4.4 控制器类
讨论类和继承时你可能觉得似曾相识,不错,我们之前见过,在静态页面控制器中(代码清单 3.18):
class StaticPagesController < ApplicationController
def home
end
def help
end
def about
end
end
你现在可以理解,至少有点能理解,这些代码的意思了:StaticPagesController
是一个类,继承自 ApplicationController
,其中有三个方法,分别是 home
、help
和 about
。因为 Rails 控制台会加载本地的 Rails 环境,所以我们可以在控制台中创建一个控制器,查看一下它的继承关系:[17]
>> controller = StaticPagesController.new
=> #<StaticPagesController:0x22855d0>
>> controller.class
=> StaticPagesController
>> controller.class.superclass
=> ApplicationController
>> controller.class.superclass.superclass
=> ActionController::Base
>> controller.class.superclass.superclass.superclass
=> ActionController::Metal
>> controller.class.superclass.superclass.superclass.superclass
=> AbstractController::Base
>> controller.class.superclass.superclass.superclass.superclass.superclass
=> Object
这个继承关系如图 4.3 所示。
我们还可以在控制台中调用控制器的动作,动作其实就是方法:
>> controller.home
=> nil
home
动作的返回值为 nil
,因为它是空的。
注意,动作没有返回值,或至少没返回真正需要的值。如我们在第 3 章看到的,home
动作的目的是渲染网页,而不是返回一个值。但是,我记得没在任何地方调用过 StaticPagesController.new
,到底怎么回事呢?
原因在于,Rails 是用 Ruby 编写的,但 Rails 不是 Ruby。有些 Rails 类就像普通的 Ruby 类一样,不过也有些则得益于 Rails 的强大功能。Rails 是单独的一门学问,应该和 Ruby 分开学习和理解。
图 4.3:静态页面控制器的类继承关系
4.4.5 用户类
我们要自己定义一个类,结束对 Ruby 的介绍。这个类名为 User
,目的是实现 第 6 章用到的用户模型。
到目前为止,我们都在控制台中定义类,这样很快捷,但也有点不爽。现在我们要在应用的根目录中创建一个名为 example_user.rb
的文件,然后写入代码清单 4.13 中的内容。
代码清单 4.13:定义 User
类
example_user.rb
class User
attr_accessor :name, :email
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
def formatted_email
"#{@name} <#{@email}>"
end
end
这段代码有很多地方要说明,我们一步步来。先看下面这行:
attr_accessor :name, :email
这行代码为用户的名字和电子邮件地址创建“属性访问器”(attribute accessors),也就是定义了“获取方法”(getter)和“设定方法”(setter),用来取回和赋值 @name
和 @email
实例变量(2.2.2 节和 3.6 节简介过)。在 Rails 中,实例变量的意义在于,它们自动在视图中可用。而通常实例变量的作用是在 Ruby 类中不同的方法之间传递值。(稍后会更详细地介绍这一点。)实例变量总是以 @
符号开头,如果未定义,值为 nil
。
第一个方法,initialize
,在 Ruby 中有特殊的意义:执行 User.new
时会调用这个方法。这个 initialize
方法接受一个参数,attributes
:
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
attributes
参数的默认值是一个空哈希,所以我们可以定义一个没有名字或没有电子邮件地址的用户。(回想一下 4.3.3 节的内容,如果键不存在就返回 nil
,所以如果没定义 :name
键,attributes[:name]
会返回 nil
,attributes[:email]
也是一样。)
最后,类中定义了一个名为 formatted_email
的方法,使用被赋了值的 @name
和 @email
变量进行插值,组成一个格式良好的用户电子邮件地址:
def formatted_email
"#{@name} <#{@email}>"
end
因为 @name
和 @email
都是实例变量(如 @
符号所示),所以在 formatted_email
方法中自动可用。
我们打开控制台,加载(require
)这个文件,实际使用一下这个类:
>> require './example_user' # 加载 example_user 文件中代码的方式
=> true
>> example = User.new
=> #<User:0x224ceec @email=nil, @name=nil>
>> example.name # 返回 nil,因为 attributes[:name] 是 nil
=> nil
>> example.name = "Example User" # 赋值一个非 nil 的名字
=> "Example User"
>> example.email = "[email protected]" # 赋值一个非 nil 的电子邮件地址
=> "[email protected]"
>> example.formatted_email
=> "Example User <[email protected]>"
这段代码中的点号 .
,在 Unix 中指“当前目录”,'./example_user'
告诉 Ruby 在当前目录中寻找这个文件。接下来的代码创建了一个空用户,然后通过直接赋值给相应的属性来提供他的名字和电子邮件地址(因为有 attr_accessor
所以才能赋值)。我们输入 example.name = "Example User"
时,Ruby 会把 @name
变量的值设为 "Example User"
(email
属性类似),然后就可以在 formatted_email
中使用。
4.3.4 节介绍过,如果最后一个参数是哈希,可以省略花括号。我们可以把一个预先定义好的哈希传给 initialize
方法,再创建一个用户:
>> user = User.new(name: "Michael Hartl", email: "[email protected]")
=> #<User:0x225167c @email="[email protected]", @name="Michael Hartl">
>> user.formatted_email
=> "Michael Hartl <[email protected]>"
从第 7 章开始,我们会使用哈希初始化对象,这种技术叫做“批量赋值”(mass assignment),在 Rails 中很常见。