💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 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://box.kancloud.cn/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 对象 => #<Word:0x22d0b20> >> 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 &lt; 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://box.kancloud.cn/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 => #<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](#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://box.kancloud.cn/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 => #<User:0x224ceec @email=nil, @name=nil> >> 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 <user@example.com>" ``` 这段代码中的点号 `.`,在 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:0x225167c @email="mhartl@example.com", @name="Michael Hartl"> >> user.formatted_email => "Michael Hartl <mhartl@example.com>" ``` 从[第 7 章](chapter7.html#sign-up)开始,我们会使用哈希初始化对象,这种技术叫做“批量赋值”(mass assignment),在 Rails 中很常见。