4.4 Ruby 类
最后更新于:2022-04-01 22:28:35
# 4.4 Ruby 类
我们之前说过,Ruby 中的一切都是对象。本节我们要自己定义一些对象。Ruby 和其他面向对象的语言一样,使用类来组织方法,然后实例化类,创建对象。如果你刚接触“面向对象编程”(Object-Oriented Programming,简称 OOP),这些听起来都似天书一般,那我们来看一些实例吧。
## 4.4.1 构造方法
我们看过很多使用类初始化对象的例子,不过还没自己动手做过。例如,我们使用双引号初始化一个字符串,双引号是字符串的字面构造方法:
```
>> s = "foobar" # 使用双引号字面构造方法
=> "foobar"
>> s.class
=> String
```
我们看到,字符串可以响应 `class` 方法,返回值是字符串所属的类。
除了使用字面构造方法之外,我们还可以使用等价的“具名构造方法”(named constructor),即在类名上调用 `new` 方法:[[15](#fn-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](#fig-string-inheritance-ruby-1-9) 所示。可以看到,`String` 的父类是 `Object`,`Object` 的父类是 `BasicObject`,但是 `BasicObject` 就没有父类了。这样的关系对每个 Ruby 对象都适用:只要在类的继承关系上往上多走几层,就会发现 Ruby 中的每个类最终都继承自 `BasicObject`,而它本身没有父类。这就是“Ruby 中一切皆对象”技术层面上的意义。
![string inheritance ruby 1 9](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bcff57ec5.png)图 4.1:`String` 类的继承关系
要想更深入地理解类,最好的方法是自己动手编写一个类。我们来定义一个名为 `Word` 的类,其中有一个名为 `palindrome?` 方法,如果单词顺读和反读都一样就返回 `true`:
```
>> class Word
>> def palindrome?(string)
>> string == string.reverse
>> end
>> end
=> :palindrome?
```
我们可以按照下面的方式使用这个类:
```
>> w = Word.new # 创建一个 Word 对象
=> #
>> w.palindrome?("foobar")
=> false
>> w.palindrome?("level")
=> true
```
如果你觉得这个例子有点大题小做,很好,我的目的达到了。定义一个新类,可是只创建一个接受一个字符串作为参数的方法,这么做很古怪。既然单词是字符串,让 `Word` 继承 `String` 不就行了,如[代码清单 4.12](#listing-word-class) 所示。(你要退出控制台,然后再在控制台中输入这写代码,这样才能把之前的 `Word` 定义清除掉。)
##### 代码清单 4.12:在控制台中定义 `Word` 类
```
>> class Word < String # Word 继承自 String
>> # 如果字符串和反转后相等就返回 true
>> def palindrome?
>> self == self.reverse # self 代表这个字符串本身
>> end
>> end
=> nil
```
其中,`Word < String` 在 Ruby 中表示继承([3.2 节](chapter3.html#static-pages)简介过),这样除了定义 `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](#fig-word-inheritance-ruby-1-9) 所示。
![word inheritance ruby 1 9](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bcff689e6.png)图 4.2:[代码清单 4.12](#listing-word-class) 中定义的 `Word` 类(非内置类)的继承关系
注意,在[代码清单 4.12](#listing-word-class) 中检查单词和单词的反转是否相同时,要在 `Word` 类中引用这个单词。在 Ruby 中使用 `self` 关键字[[16](#fn-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)的,像用户名之类的就不应该是空格或[空白](http://en.wikipedia.org/wiki/Whitespace_(computer_science)),所以 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 节](chapter8.html#remember-me)会再介绍一些 Rails 扩展 Ruby 类的例子。)
## 4.4.4 控制器类
讨论类和继承时你可能觉得似曾相识,不错,我们之前见过,在静态页面控制器中([代码清单 3.18](chapter3.html#listing-adding-the-about-page)):
```
class StaticPagesController < ApplicationController
def home
end
def help
end
def about
end
end
```
你现在可以理解,至少有点能理解,这些代码的意思了:`StaticPagesController` 是一个类,继承自 `ApplicationController`,其中有三个方法,分别是 `home`、`help` 和 `about`。因为 Rails 控制台会加载本地的 Rails 环境,所以我们可以在控制台中创建一个控制器,查看一下它的继承关系:[[17](#fn-17)]
```
>> controller = StaticPagesController.new
=> #
>> 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](#fig-static-pages-controller-inheritance) 所示。
我们还可以在控制台中调用控制器的动作,动作其实就是方法:
```
>> controller.home
=> nil
```
`home` 动作的返回值为 `nil`,因为它是空的。
注意,动作没有返回值,或至少没返回真正需要的值。如我们在[第 3 章](chapter3.html#mostly-static-pages)看到的,`home` 动作的目的是渲染网页,而不是返回一个值。但是,我记得没在任何地方调用过 `StaticPagesController.new`,到底怎么回事呢?
原因在于,Rails 是用 Ruby 编写的,但 Rails 不是 Ruby。有些 Rails 类就像普通的 Ruby 类一样,不过也有些则得益于 Rails 的强大功能。Rails 是单独的一门学问,应该和 Ruby 分开学习和理解。
![static pages controller inheritance](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bcff7cc52.png)图 4.3:静态页面控制器的类继承关系
## 4.4.5 用户类
我们要自己定义一个类,结束对 Ruby 的介绍。这个类名为 `User`,目的是实现 [第 6 章](chapter6.html#modeling-users)用到的用户模型。
到目前为止,我们都在控制台中定义类,这样很快捷,但也有点不爽。现在我们要在应用的根目录中创建一个名为 `example_user.rb` 的文件,然后写入[代码清单 4.13](#listing-example-user) 中的内容。
##### 代码清单 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 节](chapter2.html#mvc-in-action)和 [3.6 节](chapter3.html#mostly-static-pages-exercises)简介过)。在 Rails 中,实例变量的意义在于,它们自动在视图中可用。而通常实例变量的作用是在 Ruby 类中不同的方法之间传递值。(稍后会更详细地介绍这一点。)实例变量总是以 `@` 符号开头,如果未定义,值为 `nil`。
第一个方法,`initialize`,在 Ruby 中有特殊的意义:执行 `User.new` 时会调用这个方法。这个 `initialize` 方法接受一个参数,`attributes`:
```
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
```
`attributes` 参数的默认值是一个空哈希,所以我们可以定义一个没有名字或没有电子邮件地址的用户。(回想一下 [4.3.3 节](#hashes-and-symbols)的内容,如果键不存在就返回 `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
=> #
>> example.name # 返回 nil,因为 attributes[:name] 是 nil
=> nil
>> example.name = "Example User" # 赋值一个非 nil 的名字
=> "Example User"
>> example.email = "user@example.com" # 赋值一个非 nil 的电子邮件地址
=> "user@example.com"
>> example.formatted_email
=> "Example User "
```
这段代码中的点号 `.`,在 Unix 中指“当前目录”,`'./example_user'` 告诉 Ruby 在当前目录中寻找这个文件。接下来的代码创建了一个空用户,然后通过直接赋值给相应的属性来提供他的名字和电子邮件地址(因为有 `attr_accessor` 所以才能赋值)。我们输入 `example.name = "Example User"` 时,Ruby 会把 `@name` 变量的值设为 `"Example User"`(`email` 属性类似),然后就可以在 `formatted_email` 中使用。
[4.3.4 节](#css-revisited)介绍过,如果最后一个参数是哈希,可以省略花括号。我们可以把一个预先定义好的哈希传给 `initialize` 方法,再创建一个用户:
```
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #
>> user.formatted_email
=> "Michael Hartl "
```
从[第 7 章](chapter7.html#sign-up)开始,我们会使用哈希初始化对象,这种技术叫做“批量赋值”(mass assignment),在 Rails 中很常见。
';