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 的父类是 ObjectObject 的父类是 BasicObject,但是 BasicObject 就没有父类了。这样的关系对每个 Ruby 对象都适用:只要在类的继承关系上往上多走几层,就会发现 Ruby 中的每个类最终都继承自 BasicObject,而它本身没有父类。这就是“Ruby 中一切皆对象”技术层面上的意义。

string inheritance ruby 1 9图 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 &lt; 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 所示。

word inheritance ruby 1 9图 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,其中有三个方法,分别是 homehelpabout。因为 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 分开学习和理解。

static pages controller inheritance图 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] 会返回 nilattributes[: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 中很常见。