在线版的内容可能落后于电子书,如果想及时获得更新,请购买电子书

第 4 章 Rails 背后的 Ruby

有了第 3 章的例子做铺垫,本章要介绍一些对 Rails 来说很重要的 Ruby 知识。Ruby 语言的知识点很多,不过对 Rails 开发者而言需要掌握的很少。我们采用的方式有别于常规的 Ruby 学习过程。本章的目标是,不管你有没有 Ruby 编程经验,都得让你掌握编写 Rails 应用所需的 Ruby 知识。这一章的内容很多,第一次阅读不能完全掌握也没关系。后续的章节会经常提到本章的内容。

4.1 导言

从前一章得知,即使完全不懂 Ruby 语言,我们也可以创建 Rails 应用的骨架,以及编写测试。我们依赖于书中提供的测试代码,得到错误信息,然后让测试组件通过。但是我们不能总是这样,所以这一章暂时不讲网站开发,而要正视我们的短肋——Ruby 语言。

3.2 节一样,我们将在单独的主题分支中修改:

$ git checkout -b rails-flavored-ruby

4.5 节,我们再合并到 master 分支。

4.1.1 内置的辅助方法

前一章末尾我们修改了几乎是静态内容的页面,让它们使用 Rails 布局,把视图中的重复去掉了。我们使用的布局如代码清单 4.1 所示(和代码清单 3.35 一样)。

代码清单 4.1:演示应用的网站布局
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                                              'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

我们把注意力集中在这一行:

<%= stylesheet_link_tag 'application', media: 'all',
                                       'data-turbolinks-track': 'reload' %>

这行代码使用 Rails 内置的 stylesheet_link_tag 方法(详细信息参见 Rails API 文档),在所有媒介类型中引入 application.css。对有经验的 Rails 开发者来说,这行代码看起来很简单,但是其中至少有四个 Ruby 知识点可能会让你困惑:内置的 Rails 方法,调用方法时不用括号,符号(Symbol)和散列(Hash)。这几点本章都会介绍。

4.1.2 自定义辅助方法

Rails 除了提供很多内置的方法供我们在视图中使用之外,还允许我们自己定义。这种方法叫辅助方法(helper)。为了说明如何自己定义辅助方法,我们来看看代码清单 4.1 中标题那一行:

<%= yield(:title) %> | Ruby on Rails Tutorial Sample App

这行代码要求每个视图都要使用 provide 方法定义标题,例如:

<% provide(:title, "Home") %>
<h1>Sample App</h1>
<p>
  This is the home page for the
  <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
  sample application.
</p>

那么,如果我们不提供标题会怎样呢?标题一般都包含一个公共部分,为了更具体些,会再加上变动的部分。我们在布局中用了个小技巧,基本上已经实现了这样的标题。如果在视图中不调用 provide 方法,也就是不提供变动的部分,那么得到的标题会变成:

 | Ruby on Rails Tutorial Sample App

也就是说,标题中有公共部分,但前面还显示了竖线。

为了解决这个问题,我们要自定义一个辅助方法,名为 full_title。如果视图中没有定义页面的标题,full_title 返回标题的公共部分,即“Ruby on Rails Tutorial Sample App”;如果定义了,则在变动部分后面加上一个竖线,如代码清单 4.2 所示。[1]

代码清单 4.2:定义 full_title 辅助方法
app/helpers/application_helper.rb
module ApplicationHelper

  # 根据所在的页面返回完整的标题
  def full_title(page_title = '')
    base_title = "Ruby on Rails Tutorial Sample App"
    if page_title.empty?
      base_title
    else
      page_title + " | " + base_title
    end
  end
end

现在,这个辅助方法定义好了,我们可以用它来简化布局。把下面这行:

<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>

改成:

<title><%= full_title(yield(:title)) %></title>

代码清单 4.3 所示。

代码清单 4.3:使用 full_title 辅助方法的网站布局 GREEN
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                                              'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

为了让这个辅助方法起作用,我们要在首页的视图中把不必要的单词“Home”删掉,只保留标题的公共部分。首先,我们要修改测试代码,如代码清单 4.4 所示,确认标题中没有字符串 "Home"

代码清单 4.4:修改首页的标题测试 RED
test/controllers/static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end
end

接着,运行测试组件,确认有一个测试失败:

代码清单 4.5RED
$ rails test
3 tests, 6 assertions, 1 failures, 0 errors, 0 skips

为了让测试通过,我们要把首页视图中的 provide 那行删除,如代码清单 4.6 所示。

代码清单 4.6:没定义页面标题的首页视图 GREEN
app/views/static_pages/home.html.erb
<h1>Sample App</h1>
<p>
  This is the home page for the
  <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
  sample application.
</p>

现在测试应该能通过了:

代码清单 4.7GREEN
$ rails test

(注意,之前运行 rails test 时都显示了通过和失败测试的数量,为了行文简洁,从这以后都会省略这些信息。)

4.1.1 节引入应用的样式表那行代码一样,代码清单 4.2 的内容对有经验的 Rails 开发者来说也很简单,但其中有很多重要的 Ruby 知识:模块、方法定义、可选的方法参数、注释、局部变量赋值、布尔值、流程控制、字符串拼接和返回值。本章会一一介绍这些知识。

4.2 字符串和方法

我们学习 Ruby 主要使用的工具是 Rails 控制台,它是用来与 Rails 应用交互的命令行工具,在 2.3.3 节介绍过。控制台基于 Ruby 的交互程序(irb)开发,因此能使用 Ruby 语言的全部功能。(4.4.4 节会介绍,控制台还可以访问 Rails 环境。)

如果使用云端 IDE,我建议添加几个 irb 配置参数。使用简单的 nano 文本编辑器打开家目录中的 .irbrc 文件:

$ nano ~/.irbrc

然后写入代码清单 4.8 中的内容。[2]这段代码的作用是简化 irb 提示符,以及禁用一些烦人的自动缩进行为。

代码清单 4.8:添加几个 irb 配置
~/.irbrc
IRB.conf[:PROMPT_MODE] = :SIMPLE
IRB.conf[:AUTO_INDENT_MODE] = false

编辑好之后,按 Ctrl-X 键退出 nano,然后输入 y 确认保存 ~/.irbrc 文件。

现在,执行下述命令启动控制台:

$ rails console
Loading development environment
>>

默认情况下,控制台在开发环境中启动,这是 Rails 定义的三个独立环境之一(另外两个是测试环境和生产环境)。这三个环境的区别对本章不重要,但是对后文就重要了,我们将在 7.1.1 节详细介绍。

控制台是学习的好工具,请尽情探索它的用法。别担心,你(几乎)不会破坏任何东西。如果在控制台中遇到问题,可以按 Ctrl-C 键结束当前执行的操作,或者按 Ctrl-D 键直接退出。与常规的 shell 终端一样,我们可以使用上箭头获取前一个命令,这能节省不少时间。

在阅读本章后续内容的过程中,你会发现查阅 Ruby API 很有帮助。API 中有很多信息(或许太多了),例如,如果想进一步了解 Ruby 字符串,可以查看 String 类的文档。

4.2.1 注释

Ruby 中的注释以井号 #(也叫“散列符号”,或者更诗意一点,叫“散列字元”)开头,一直到行尾结束。Ruby 会忽略注释,但是注释对人类读者(往往也包括代码的编写者)很有用。在下面的代码中

# 根据所在的页面返回完整的标题
def full_title(page_title = '')
  .
  .
  .
end

第一行就是注释,说明其后方法的作用。

在控制台中一般不用写注释,不过为了说明代码的作用,我会按照下面的形式加上注释,例如:

$ rails console
>> 17 + 42   # 整数加法运算
=> 59

阅读的过程中,在控制台中输入或者复制粘贴命令时,如果愿意你可以不加注释,反正控制台会忽略注释。

4.2.2 字符串

对 Web 应用来说,字符串或许是最重要的数据结构,因为网页的内容就是从服务器发送给浏览器的字符串。我们先在控制台中体验一下字符串:

$ rails console
>> ""         # 空字符串
=> ""
>> "foo"      # 非空字符串
=> "foo"

这些是字符串字面量,使用双引号(")创建。控制台回显的是每一行的计算结果。这里,字符串字面量的结果就是字符串本身。

我们还可以使用 + 号拼接字符串:

>> "foo" + "bar"    # 字符串拼接
=> "foobar"

"foo""bar" 拼接得到的结果是字符串 "foobar"[3]

另一种创建字符串的方式是使用特殊的句法 #{} 进行插值操作:[4]

>> first_name = "Michael"    # 变量赋值
=> "Michael"
>> "#{first_name} Hartl"     # 字符串插值
=> "Michael Hartl"

我们先把 "Michael" 赋值给变量 first_name,然后将其插入字符串 "#{first_name} Hartl" 中。我们也可以把两个字符串都赋值给变量:

>> first_name = "Michael"
=> "Michael"
>> last_name = "Hartl"
=> "Hartl"
>> first_name + " " + last_name    # 字符串拼接,中间加了空格
=> "Michael Hartl"
>> "#{first_name} #{last_name}"    # 等效的插值
=> "Michael Hartl"

注意,最后两个表达式的作用相同,不过我倾向于使用插值的方式。在两个字符串中间加入一个空格(" ")显得很别扭。

打印字符串

打印字符串最常用的 Ruby 方法是 puts(读作“put ess”,意思是“打印字符串”):

>> puts "foo"     # 打印字符串
foo
=> nil

puts 方法还有一个副作用:puts "foo" 先把字符串打印到屏幕上,然后返回空值字面量——nil 在 Ruby 中是个特殊值,表示“什么都没有”。(为了行文简洁,后续内容会省略 => nil。)

从前面的例子可以看出,puts 方法会自动在输出的字符串后面换行。功能类似的 print 方法则不会:

>> print "foo"    # 打印字符串,不换行
foo=> nil

可以看出,输出的 foo 后面直接跟着提示符。

换行通常使用“\n”符号表示。我们可以在字符串中加上换行符,让 printputs 的效果一样:

>> print "foo\n"  # 作用与 `puts "foo"` 一样
foo
=> nil

单引号字符串

目前介绍的例子都使用双引号创建字符串,不过 Ruby 也支持用单引号创建字符串。大多数情况下这两种字符串的效果是一样的:

>> 'foo'          # 单引号创建的字符串
=> "foo"
>> 'foo' + 'bar'
=> "foobar"

不过,二者之间有个重要的区别:Ruby 不会对单引号字符串进行插值操作:

>> '#{foo} bar'     # 单引号字符串不能进行插值操作
=> "\#{foo} bar"

注意,控制台返回的是双引号字符串,因此要使用反斜线转义特殊字符,例如 #{

如果双引号字符串可以做单引号能做的所有事,而且还能进行插值,那么单引号字符串存在的意义是什么呢?单引号字符串的用处在于它们真的就是字面值,只包含你输入的字符。例如,反斜线在很多系统中都很特殊,例如在换行符 \n 中。如果有一个变量需要包含一个反斜线,使用单引号就很简单:

>> '\n'       # 反斜线和 n 字面值
=> "\\n"

与前面的 # 符号一样,Ruby 要使用一个额外的反斜线来转义反斜线——在双引号字符串中,要表达一个反斜线就要使用两个反斜线。对简单的例子来说,这省不了多少事,但是如果有很多需要转义的字符就显得出它的作用了:

>> 'Newlines (\n) and tabs (\t) both use the backslash character \.'
=> "Newlines (\\n) and tabs (\\t) both use the backslash character \\."

最后,有一点要注意,单双引号基本上可以互换使用,Rails 源码中经常混用,没有章法可循,对此我们只能默默接受——“欢迎进入 Ruby 世界!不久你就会习惯的。”

练习
  1. 把你当前所在的省份和城市分别赋值给 provincecity 变量。

  2. 使用字符串插值打印一个字符串(使用 puts 方法),在省份和城市之间加上逗号,例如“广东省,广州市”。

  3. 把前一题中的逗号换成制表符。

  4. 如果把前一题中的双引号换成单引号,结果如何?

4.2.3 对象和消息传送

在 Ruby 中,一切皆对象,包括字符串和 nil 都是。我们会在 4.4.2 节介绍对象在技术层面上的意义,不过一般很难通过阅读一本书就理解对象,你要多看一些例子才能建立对对象的感性认识。

对象的作用说起来很简单:响应消息。例如,字符串对象可以响应 length 消息,返回字符串中包含的字符数量:

>> "foobar".length        # 把 length 消息传给字符串
=> 6

一般来说,传给对象的消息是“方法”,即在这个对象上定义的函数。[5]字符串还可以响应 empty? 方法:

>> "foobar".empty?
=> false
>> "".empty?
=> true

注意,empty? 方法末尾有个问号,这是 Ruby 的约定,说明方法返回的是布尔值,即 truefalse。布尔值在流程控制中特别有用:

>> s = "foobar"
>> if s.empty?
>>   "The string is empty"
>> else
>>   "The string is nonempty"
>> end
=> "The string is nonempty"

如果分支很多,可以使用 elsifelse + if):

>> if s.nil?
>>   "The variable is nil"
>> elsif s.empty?
>>   "The string is empty"
>> elsif s.include?("foo")
>>   "The string includes 'foo'"
>> end
=> "The string includes 'foo'"

布尔值还可以使用 &&(与)、||(或)和 !(非)运算符结合在一起使用:

>> x = "foo"
=> "foo"
>> y = ""
=> ""
>> puts "Both strings are empty" if x.empty? && y.empty?
=> nil
>> puts "One of the strings is empty" if x.empty? || y.empty?
"One of the strings is empty"
=> nil
>> puts "x is not empty" if !x.empty?
"x is not empty"
=> nil

在 Ruby 中一切都是对象,因此 nil 也是对象,所以它也可以响应方法。举个例子,to_s 方法基本上可以把任何对象转换成字符串:

>> nil.to_s
=> ""

结果显然是个空字符串,我们可以通过下面的方法串联(chain)验证这一点:

>> nil.empty?
NoMethodError: undefined method `empty?' for nil:NilClass
>> nil.to_s.empty?      # 消息串联
=> true

我们看到,nil 对象本身无法响应 empty? 方法,但是 nil.to_s 可以。

有一个特殊的方法可以测试对象是否为空,你或许能猜到是哪个方法:

>> "foo".nil?
=> false
>> "".nil?
=> false
>> nil.nil?
=> true

下面的代码

puts "x is not empty" if !x.empty?

演示了 if 关键字的另一种用法:编写一个当且只当 if 后面的表达式为真值时才执行的语句。还有个对应的 unless 关键字也可以这么用:

>> string = "foobar"
>> puts "The string '#{string}' is nonempty." unless string.empty?
The string 'foobar' is nonempty.
=> nil

我们需要注意一下 nil 对象的特殊性,除了 false 本身之外,所有 Ruby 对象中它是唯一一个布尔值为“假”的。我们可以使用 !!(读作“bang bang”)对对象做两次取反操作,把对象转换成布尔值:

>> !!nil
=> false

除此之外,其他所有 Ruby 对象都是“真”值,数字 0 也是:

>> !!0
=> true
练习
  1. 字符串“racecar”有多长?

  2. 使用 reverse 方法确认前一题中的字符串经过反转之后内容仍然一样。

  3. 把字符串“racecar”赋值给变量 s。使用比较运算符 == 确认 ss.reverse 相等。

  4. 代码清单 4.9的运行结果是什么?如果把 s 变量的值改成“onomatopoeia”呢?提示:使用上箭头获取前一个命令,然后编辑。

代码清单 4.9:简单的回文测试
>> puts "It's a palindrome!" if s == s.reverse

4.2.4 定义方法

在控制台中,可以像定义 home 动作(代码清单 3.8)和 full_title 辅助方法(代码清单 4.2)一样定义方法。(在控制台中定义方法有点麻烦,我们通常在文件中定义,这里只是为了演示。)例如,我们要定义一个名为 string_message 的方法,它有一个参数,返回值取决于参数是否为空:

>> def string_message(str = '')
>>   if str.empty?
>>     "It's an empty string!"
>>   else
>>     "The string is nonempty."
>>   end
>> end
=> :string_message
>> puts string_message("foobar")
The string is nonempty.
>> puts string_message("")
It's an empty string!
>> puts string_message
It's an empty string!

如最后一个命令所示,我们可以完全不指定参数(此时可以省略括号)。因为 def string_message(str = '') 中提供了参数的默认值,即空字符串。所以,str 参数是可选的,如果不指定,就使用默认值。

注意,Ruby 方法不用显式指定返回值,方法的返回值是最后一个语句的计算结果。上面这个函数的返回值是两个字符串中的一个,具体是哪一个取决于 str 参数是否为空。在 Ruby 方法中也可以显式指定返回值,下面这个方法和前面的等价:

>> def string_message(str = '')
>>   return "It's an empty string!" if str.empty?
>>   return "The string is nonempty."
>> end

(细心的读者可能会发现,其实没必要使用第二个 return,这一行是方法的最后一个表达式,不管有没有 return,字符串 "The string is nonempty." 都会作为返回值返回。不过两处都加上 return 看起来更好。)

还有一点很重要,方法并不关心参数的名字是什么。在前面定义的第一个方法中,可以把 str 换成任意有效的变量名,例如 the_function_argument,但是方法的作用不变:

>> def string_message(the_function_argument = '')
>>   if the_function_argument.empty?
>>     "It's an empty string!"
>>   else
>>     "The string is nonempty."
>>   end
>> end
=> nil
>> puts string_message("")
It's an empty string!
>> puts string_message("foobar")
The string is nonempty.
练习
  1. 代码清单 4.10 中的 FILL_IN 换成正确的比较表达式,定义一个测试回文的方法。提示:使用代码清单 4.9 中的比较表达式。

  2. 使用前一题定义的方法测试“racecar”和“onomatopoeia”,确认第一个词是回文,而第二个词不是。

  3. palindrome_tester("racecar") 上调用 nil? 方法,确认它的返回值是 nil(即在那个方法上调用 nil? 方法的结果是 true)。这是因为 palindrome_tester 方法是把结果打印出来的,而没有返回。

代码清单 4.10:测试回文的方法
>> def palindrome_tester(s)
>>   if FILL_IN
>>     puts "It's a palindrome!"
>>   else
>>     puts "It's not a palindrome."
>>   end
>> end

4.2.5 回顾标题的辅助方法

下面我们来理解一下代码清单 4.2 中的 full_title 辅助方法,[6]在其中加上注解之后如代码清单 4.11 所示:

代码清单 4.11:注解 full_title 方法
app/helpers/application_helper.rb
module ApplicationHelper

  # 根据所在的页面返回完整的标题                          # 在文档中显示的注释
  def full_title(page_title = '')                     # 定义方法,参数可选
    base_title = "Ruby on Rails Tutorial Sample App"  # 变量赋值
    if page_title.empty?                              # 布尔测试
      base_title                                      # 隐式返回
    else
      page_title + " | " + base_title                 # 字符串拼接
    end
  end
end

我们把方法定义、变量赋值、布尔测试、流程控制和字符串拼接[7]利用起来,定义了一个可以在网站布局中使用的辅助方法。这里还有一个知识点——module ApplicationHelper:模块为我们提供了一种把相关方法组织在一起的方式,我们可以使用 include 把模块插入其他类中。编写普通的 Ruby 程序时,你要自己定义模块,然后再显式将其引入类中,但是辅助方法所在的模块会由 Rails 为我们引入,结果是,full_title 方法自动在所有视图中可用。

4.3 其他数据类型

虽然 Web 应用最终都是处理字符串,但也需要其他的数据类型来生成字符串。本节介绍一些对开发 Rails 应用很重要的其他 Ruby 数据类型。

4.3.1 数组和值域

数组是一组具有特定顺序的元素。前面还没用过数组,不过理解数组对理解散列有很大帮助(4.3.3 节),也有助于理解 Rails 中的数据模型(例如 2.3.3 节用到的 has_many 关联,13.1.3 节会做详细介绍)。

目前,我们已经花了很多时间理解字符串,从字符串过渡到数组可以从 split 方法开始:

>>  "foo bar     baz".split     # 把字符串拆分成有三个元素的数组
=> ["foo", "bar", "baz"]

上述操作得到的结果是一个有三个字符串的数组。默认情况下,split 在空格处把字符串拆分成数组,不过也可以在几乎任何地方拆分:

>> "fooxbarxbaz".split('x')
=> ["foo", "bar", "baz"]

和大多数编程语言的习惯一样,Ruby 数组的索引也从零开始,因此数组中第一个元素的索引是 0,第二个元素的索引是 1,依此类推:

>> a = [42, 8, 17]
=> [42, 8, 17]
>> a[0]               # Ruby 使用方括号获取数组元素
=> 42
>> a[1]
=> 8
>> a[2]
=> 17
>> a[-1]              # 索引还可以是负数
=> 17

我们看到,Ruby 使用方括号获取数组中的元素。除了方括号之外,Ruby 还为一些经常需要获取的元素提供了别名方法:[8]

>> a                  # 只是为了看一下 a 的值是什么
=> [42, 8, 17]
>> a.first
=> 42
>> a.second
=> 8
>> a.last
=> 17
>> a.last == a[-1]    # 用 == 运算符对比
=> true

最后一行用到了相等比较运算符 ==,Ruby 和其他语言一样还提供了 !=(不等)等其他运算符:

>> x = a.length       # 和字符串一样,数组也可以响应 length 方法
=> 3
>> x == 3
=> true
>> x == 1
=> false
>> x != 1
=> true
>> x >= 1
=> true
>> x < 1
=> false

除了 length(上述代码的第一行)之外,数组还可以响应一系列其他方法:

>> a
=> [42, 8, 17]
>> a.empty?
=> false
>> a.include?(42)
=> true
>> a.sort
=> [8, 17, 42]
>> a.reverse
=> [17, 8, 42]
>> a.shuffle
=> [17, 42, 8]
>> a
=> [42, 8, 17]

注意,上面的方法都没有修改 a 的值。如果想修改数组的值,要使用相应的“炸弹”(bang)方法(之所以这么叫是因为,这里的感叹号经常都读作“bang”):

>> a
=> [42, 8, 17]
>> a.sort!
=> [8, 17, 42]
>> a
=> [8, 17, 42]

还可以使用 push 方法向数组中添加元素,或者使用等价的 << 运算符:

>> a.push(6)                  # 把 6 加到数组末尾
=> [42, 8, 17, 6]
>> a << 7                     # 把 7 加到数组末尾
=> [42, 8, 17, 6, 7]
>> a << "foo" << "bar"        # 串联操作
=> [42, 8, 17, 6, 7, "foo", "bar"]

最后一个命令说明,可以把添加操作串在一起使用;也说明,与其他语言不同,在 Ruby 中数组可以包含不同类型的数据(本例中包含整数和字符串)。

前面用 split 把字符串拆分成数组,我们还可以使用 join 方法进行相反的操作:

>> a
=> [42, 8, 17, 6, 7, "foo", "bar"]
>> a.join                       # 没有连接符
=> "4281767foobar"
>> a.join(', ')                 # 连接符是一个逗号和空格
=> "42, 8, 17, 6, 7, foo, bar"

与数组有点类似的是值域(range),使用 to_a 方法把它转换成数组或许更好理解:

>> 0..9
=> 0..9
>> 0..9.to_a              # 错了,to_a 在 9 上调用了
NoMethodError: undefined method `to_a' for 9:Fixnum
>> (0..9).to_a            # 调用 to_a 时要用括号包住值域
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

虽然 0..9 是有效的值域,不过上面第二个表达式告诉我们,调用方法时要加上括号。

值域经常用于获取数组中的一组元素:

>> a = %w[foo bar baz quux]         # %w 创建一个元素为字符串的数组
=> ["foo", "bar", "baz", "quux"]
>> a[0..2]
=> ["foo", "bar", "baz"]

有个特别有用的技巧:值域的结束值使用 -1 时,不用知道数组的长度就能从起始值开始一直获取到最后一个元素:

>> a = (0..9).to_a
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>> a[2..(a.length-1)]               # 显式使用数组的长度
=> [2, 3, 4, 5, 6, 7, 8, 9]
>> a[2..-1]                         # 小技巧,索引使用 -1
=> [2, 3, 4, 5, 6, 7, 8, 9]

值域也可以使用字符定义:

>> ('a'..'e').to_a
=> ["a", "b", "c", "d", "e"]
练习
  1. 在逗号和空格处分拆字符串“A man, a plan, a canal, Panama”,把结果赋值给变量 a

  2. 不指定连接符,把 a 连接起来,然后把结果赋值给变量 s

  3. 在空白处分拆 s,然后再连接起来。使用代码清单 4.10 中的方法确认得到的结果不是回文。使用 downcase 方法,确认 s.downcase 是回文。

  4. 创建字母 az 的值域,第 7 个元素是什么? 把值域反过来呢?提示:两次都要把值域转换成数组。

4.3.2

数组和值域可以响应的方法中有很多都可以跟着一个块(block),这是 Ruby 最强大也是最难理解的功能:

>> (1..5).each { |i| puts 2 * i }
2
4
6
8
10
=> 1..5

这段代码在值域 (1..5) 上调用 each 方法,然后又把 { |i| puts 2 * i } 这个块传给 each 方法。|i| 两边的竖线在 Ruby 中用来定义块变量。只有方法本身才知道如何处理后面跟着的块。这里,值域的 each 方法会处理后面的块,块中有一个局部变量 ieach 会把值域中的各个值传进块中,然后执行其中的代码。

花括号是表示块的一种方式,除此之外还有另一种方式:

>> (1..5).each do |i|
?>   puts 2 * i
>> end
2
4
6
8
10
=> 1..5

块中的内容可以多于一行,而且经常多于一行。本书遵照一个常用的约定,当块只有一行简单的代码时使用花括号形式;当块是一行很长的代码,或者有多行时使用 do..end 形式:

>> (1..5).each do |number|
?>   puts 2 * number
>>   puts '-'
>> end
2
-
4
-
6
-
8
-
10
-
=> 1..5

上面的代码用 number 代替了 i,我想告诉你的是,变量名可以使用任何值。

除非你已经有了一些编程知识,否则对块的理解是没有捷径的。你要做的是多看,看多了就会习惯这种用法。[9]幸好人类擅长从实例中归纳出一般性。下面举几个例子,其中几个用到了 map 方法:

>> 3.times { puts "Betelgeuse!" }   # 3.times 后跟的块没有变量
"Betelgeuse!"
"Betelgeuse!"
"Betelgeuse!"
=> 3
>> (1..5).map { |i| i**2 }          # ** 表示幂运算
=> [1, 4, 9, 16, 25]
>> %w[a b c]                        # 再说一下,%w 用于创建元素为字符串的数组
=> ["a", "b", "c"]
>> %w[a b c].map { |char| char.upcase }
=> ["A", "B", "C"]
>> %w[A B C].map { |char| char.downcase }
=> ["a", "b", "c"]

可以看出,map 方法返回的是在数组或值域中每个元素上执行块中代码后得到的结果。在最后两个命令中,map 后面的块在块变量上调用一个方法,这种操作经常使用简写形式:

>> %w[A B C].map { |char| char.downcase }
=> ["a", "b", "c"]
>> %w[A B C].map(&:downcase)
=> ["a", "b", "c"]

(简写形式看起来有点儿奇怪,其中用到了符号,4.3.3 节会介绍。)这种写法比较有趣,一开始是由 Rails 扩展实现的,但人们太喜欢了,现在已经集成到 Ruby 核心代码中。

最后再看一个使用块的例子。我们看一下代码清单 4.4 中的一个测试用例:

test "should get home" do
  get static_pages_home_url
  assert_response :success
  assert_select "title", "Ruby on Rails Tutorial Sample App"
end

现在不需要理解细节(其实我也不懂),从 do 关键字可以看出,测试的主体其实就是个块。test 方法的参数是一个字符串(测试的描述)和一个块,运行测试组件时会执行块中的内容。

现在我们来分析一下我在 1.5.4 节生成随机二级域名时使用的那行 Ruby 代码:[10]

('a'..'z').to_a.shuffle[0..7].join

我们一步步分解:

>> ('a'..'z').to_a                     # 由全部英文字母组成的数组
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
"p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
>> ('a'..'z').to_a.shuffle             # 打乱数组
=> ["c", "g", "l", "k", "h", "z", "s", "i", "n", "d", "y", "u", "t", "j", "q",
"b", "r", "o", "f", "e", "w", "v", "m", "a", "x", "p"]
>> ('a'..'z').to_a.shuffle[0..7]       # 取出前 8 个元素
=> ["f", "w", "i", "a", "h", "p", "c", "x"]
>> ('a'..'z').to_a.shuffle[0..7].join  # 把取出的元素合并成字符串
=> "mznpybuj"
练习
  1. 创建值域 0..16,把前 17 个元素的平方打印出来。

  2. 定义一个名为 yeller 的方法,它的参数是一个由字符组成的数组,返回值是一个字符串,由数组中字符的大写形式组成。确认 yeller([’o’, ’l’, ’d’]) 的返回值是 OLD。提示:要用到 mapupcasejoin 方法。

  3. 定义一个名为 random_subdomain 的方法,返回八个随机字母组成的字符串。

  4. 代码清单 4.12 中的问号换成正确的方法,结合 splitshufflejoin 方法,把指定字符串中的字符打乱。

代码清单 4.12:字符串打乱函数的骨架
>> def string_shuffle(s)
>>   s.?('').?.?
>> end
>> string_shuffle("foobar")
=> "oobfra"

4.3.3 散列和符号

散列(Hash)本质上就是数组,只不过它的索引不局限于只能使用数字。(实际上在一些语言中,特别是 Perl,因为这个原因而把散列叫做“关联数组”。)散列的索引(或者叫“键”)几乎可以使用任何对象。例如,可以使用字符串做键:

>> user = {}                          # {} 是一个空散列
=> {}
>> user["first_name"] = "Michael"     # 键为 "first_name",值为 "Michael"
=> "Michael"
>> user["last_name"] = "Hartl"        # 键为 "last_name",值为 "Hartl"
=> "Hartl"
>> user["first_name"]                 # 获取元素的方式与数组类似
=> "Michael"
>> user                               # 散列的字面量形式
=> {"last_name"=>"Hartl", "first_name"=>"Michael"}

散列通过一对花括号中包含一些键值对的形式表示,如果只有一对花括号而没有键值对({})就是一个空散列。注意,散列中的花括号和块中的花括号不是一个概念。(是的,这可能会让你困惑。)散列虽然与数组类似,但二者却有一个很重要的区别:散列中的元素没有特定的顺序。[11]如果看重顺序,就要使用数组。

通过方括号的形式每次定义一个元素的方式不太敏捷,使用 => 分隔的键值对这种字面量形式定义散列要简洁得多:

>> user = { "first_name" => "Michael", "last_name" => "Hartl" }
=> {"last_name"=>"Hartl", "first_name"=>"Michael"}

在上面的代码中我用到了一个 Ruby 句法约定,在左花括号后面和右花括号前面加入了一个空格,不过控制台会忽略这些空格。(不要问我为什么这些空格是约定俗成的,或许是某个 Ruby 编程大牛喜欢这种形式,然后约定就产生了。)

目前为止散列的键都使用字符串,在 Rails 中用符号(Symbol)做键很常见。符号看起来有点儿像字符串,只不过没有包含在一对引号中,而是在前面加一个冒号。例如,:name 就是一个符号。你可以把符号看成没有约束的字符串:[12]

>> "name".split('')
=> ["n", "a", "m", "e"]
>> :name.split('')
NoMethodError: undefined method `split' for :name:Symbol
>> "foobar".reverse
=> "raboof"
>> :foobar.reverse
NoMethodError: undefined method `reverse' for :foobar:Symbol

符号是 Ruby 特有的数据类型,在其他语言中很少见。初看起来感觉很奇怪,不过 Rails 经常用到,所以你很快就会习惯。符号和字符串不同,并不是所有字符都能在符号中使用:

>> :foo-bar
NameError: undefined local variable or method `bar' for main:Object
>> :2foo
SyntaxError

只要以字母开头,其后都使用单词中常用的字符就没事。

用符号做键时,可以按照如下的方式定义 user 散列:

>> user = { :name => "Michael Hartl", :email => "michael@example.com" }
=> {:name=>"Michael Hartl", :email=>"michael@example.com"}
>> user[:name]              # 获取 :name 键对应的值
=> "Michael Hartl"
>> user[:password]          # 获取未定义的键对应的值
=> nil

从上面的例子可以看出,散列中没有定义的键对应的值是 nil

因为符号做键的情况太普遍了,Ruby 1.9 干脆为这种用法定义了一种新句法:

>> h1 = { :name => "Michael Hartl", :email => "michael@example.com" }
=> {:name=>"Michael Hartl", :email=>"michael@example.com"}
>> h2 = { name: "Michael Hartl", email: "michael@example.com" }
=> {:name=>"Michael Hartl", :email=>"michael@example.com"}
>> h1 == h2
=> true

第二种句法把“符号 ⇒”变成了“键的名字:”形式:

{ name: "Michael Hartl", email: "michael@example.com" }

这种形式更好地沿袭了其他语言(例如 JavaScript)中散列的表示方式,在 Rails 社区中也越来越受欢迎。这两种方式现在都在使用,所以你要能识别它们。可是,新句法有点让人困惑,因为 :name 本身是一种数据类型(符号),但 name: 却没有意义。不过在散列字面量中,:name =>name: 作用一样。因此,{ :name => "Michael Hartl" }{ name: "Michael Hartl" } 是等效的。如果要表示符号,只能使用 :name(冒号在前面)。

散列中元素的值可以是任何对象,甚至是另一个散列,如代码清单 4.13 所示。

代码清单 4.13:嵌套散列
>> params = {}        # 定义一个名为 params(parameters 的简称)的散列
=> {}
>> params[:user] = { name: "Michael Hartl", email: "mhartl@example.com" }
=> {:name=>"Michael Hartl", :email=>"mhartl@example.com"}
>> params
=> {:user=>{:name=>"Michael Hartl", :email=>"mhartl@example.com"}}
>>  params[:user][:email]
=> "mhartl@example.com"

Rails 大量使用这种散列中有散列的形式(或称为“嵌套散列”),我们从 7.3 节起会接触到。

与数组和值域一样,散列也能响应 each 方法。例如,下面是一个名为 flash 的散列,它的键是两个判断条件,:success:danger

>> flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", :danger=>"It failed."}
>> flash.each do |key, value|
?>   puts "Key #{key.inspect} has value #{value.inspect}"
>> end
Key :success has value "It worked!"
Key :danger has value "It failed."

注意,数组的 each 方法后面的块只有一个变量,而散列的 each 方法后面的块接受两个变量,分别表示键和对应的值。所以散列的 each 方法每次遍历都会以一个键值对为单位进行。

这段代码用到了很有用的 inspect 方法,它的作用是返回被调用对象的字符串字面量表示形式:

>> puts (1..5).to_a            # 把数组以字符串的形式打印出来
1
2
3
4
5
>> puts (1..5).to_a.inspect    # 输出数组的字面量形式
[1, 2, 3, 4, 5]
>> puts :name, :name.inspect
name
:name
>> puts "It worked!", "It worked!".inspect
It worked!
"It worked!"

顺便说一下,因为使用 inspect 打印对象的方式经常使用,为此还有一个专门的快捷方式,p 方法:[13]

>> p :name             # 等价于 'puts :name.inspect'
:name
练习
  1. 定义一个散列,把键设为 'one''two''three',对应的值分别是 'uno''dos''tres'。迭代这个散列,把各个键值对以 "'#key' in Spanish is '#value'" 的形式打印出来。

  2. 创建三个散列,分别命名为 person1person2person3,把名和姓赋值给 :first:last 键。然后创建一个名为 params 的散列,让 params[:father] 对应 person1params[:mother] 对应 person2params[:child] 对应 person3。验证一下 params[:father][:first] 的值是否正确。

  3. 定义一个散列,使用符号做键,分别表示名字、电子邮件地址和密码摘要,把键对应的值分别设为你的名字、电子邮件地址和一个由 16 个随机小写字母组成的字符串。

  4. 找一个在线 Ruby API,查阅散列的 merge 方法。下述表达式的值是什么?

  { "a" => 100, "b" => 200 }.merge({ "b" => 300 })

4.3.4 重温引入 CSS 的代码

现在我们要重新认识一下代码清单 4.1 中在布局中引入层叠样式表的代码:

<%= stylesheet_link_tag 'application', media: 'all',
                                       'data-turbolinks-track': 'reload' %>

我们现在基本上可以理解这行代码了。4.1 节简单提到过,Rails 定义了一个特殊的函数用于引入样式表,下面的代码

stylesheet_link_tag 'application', media: 'all',
                                   'data-turbolinks-track': 'reload'

就是对这个函数的调用。不过还有几个奇怪的地方。第一,括号哪去了?在 Ruby 中,括号是可以省略的,所以下面两种写法是等价的:

# 调用函数时可以省略括号
stylesheet_link_tag('application', media: 'all',
                                   'data-turbolinks-track': 'reload')
stylesheet_link_tag 'application', media: 'all',
                                   'data-turbolinks-track': 'reload'

第二,media 部分显然是一个散列,但是怎么没用花括号?调用函数时,如果散列是最后一个参数,可以省略花括号。所以下面两种写法是等价的:

# 如果最后一个参数是散列,可以省略花括号
stylesheet_link_tag 'application', { media: 'all',
                                     'data-turbolinks-track': 'reload' }
stylesheet_link_tag 'application', media: 'all',
                                   'data-turbolinks-track': 'reload'

最后,为什么下述代码写成两行还能正确解析?

stylesheet_link_tag 'application', media: 'all',
                                   'data-turbolinks-track': 'reload'

因为在这种情况下,Ruby 不关心有没有换行。[14]我之所以把代码写成两行,是要保证每行代码不超过 80 个字符。[15]

所以,下面这段代码

stylesheet_link_tag 'application', media: 'all',
                                   'data-turbolinks-track': 'reload'

调用了 stylesheet_link_tag 函数,并且传入两个参数:一个是字符串,指明样式表的路径;另一个是散列,包含两个元素,第一个指明媒介类型,第二个启用 Rails 4.0 增加的 Turbolink 功能。因为使用的是 <%= %>,函数的执行结果会通过 ERb 插入模板中。如果在浏览器中查看网页的源码,会看到引入样式表所用的 HTML,如代码清单 4.14 所示。(你可能会在 CSS 的文件名后看到额外的字符,例如 ?body=1。这是 Rails 加入的,用以确保修改 CSS 后浏览器会重新加载。)

代码清单 4.14:引入 CSS 的代码生成的 HTML
<link data-turbolinks-track="true" href="/assets/application.css" media="all"
rel="stylesheet" />

4.4 Ruby 类

我们之前说过,Ruby 中的一切都是对象。本节我们要自己定义一些对象。Ruby 和其他面向对象的语言一样,使用类来组织方法,然后实例化类,创建对象。如果你刚接触面向对象编程(Object-Oriented Programming,简称 OOP),这些听起来都似天书一般,那我们来看一些实例吧。

4.4.1 构造方法

我们看过很多使用类初始化对象的例子,不过还没自己动手做过。例如,我们使用双引号初始化一个字符串,双引号就是字符串的字面构造方法:

>> s = "foobar"       # 使用双引号字面构造方法
=> "foobar"
>> s.class
=> String

我们看到,字符串可以响应 class 方法,返回值是字符串所属的类。

除了使用字面构造方法之外,我们还可以使用等价的具名构造方法(named constructor),即在类名上调用 new 方法:[16]

>> 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)。

练习
  1. 从 1 到 10 的值域,它的字面构造方法是什么?

  2. 使用 Range 类和 new 方法怎么编写构造方法?提示:这里要为 new 方法提供两个参数。

  3. 使用 == 运算符确认前两题使用字面构造方法和具名构造方法创建的值域相等。

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.1String 类的继承关系

要想更深入地理解类,最好的方法是自己动手编写一个。我们来定义一个名为 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.15 所示。(你要退出控制台,然后在控制台中输入这些代码,这样才能把之前定义的 Word 类清除掉。)

代码清单 4.15:在控制台中定义 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 所示。

word inheritance ruby 1 9
图 4.2代码清单 4.15 中定义的 Word 类(非内置类)的继承关系

注意,在代码清单 4.15 中检查单词和单词的反转是否相同时,要在 Word 类中访问单词。这在 Ruby 中使用 self 关键字[17]引用:在 Word 类中,self 代表的是对象本身。所以我们可以使用

self == self.reverse

检查单词是否为回文。其实,在类中调用方法或访问属性时可以不用 self.(赋值例外),因此也可以写成

self == reverse
练习
  1. 值域的类继承关系是怎样的?散列和符号呢?

  2. 代码清单 4.15 中的 self.reverse 换成 reverse,确认 palindrome? 方法依然可用。

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 中的。9.1 节会再介绍一些 Rails 扩展 Ruby 类的例子。

练习
  1. 验证“racecar”是回文,而“onomatopoeia”不是。印度南部方言“Malayalam”是回文吗?提示:先变成小写。

  2. 代码清单 4.16 为模板,为 String 类添加 shuffle 方法。提示:参照代码清单 4.12

  3. 删掉 self.,确认代码清单 4.16 依然可用。

代码清单 4.16:添加到 String 类中的 shuffle 方法的模板
>> class String
>>   def shuffle
>>     self.?('').?.?
>>   end
>> end
>> "foobar".shuffle
=> "borafo"

4.4.4 控制器类

讨论类和继承时你可能觉得似曾相识,不错,我们之前见过,在 StaticPages 控制器中(代码清单 3.20):

class StaticPagesController < ApplicationController

  def home
  end

  def help
  end

  def about
  end
end

你现在应该可以理解,至少有点能理解这些代码的意思了:StaticPagesController 是一个类,继承自 ApplicationController,其中有三个方法,分别是 homehelpabout。因为 Rails 控制台会加载本地的 Rails 环境,所以我们可以在控制台中创建控制器,查看它的继承关系:[18]

>> 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.3StaticPagesController 类的继承关系
练习
  1. 第 2 章创建的玩具应用中运行 Rails 控制台,确认可以使用 User.new 创建用户对象。

  2. 找出那个用户对象的类继承关系。

4.4.5 User

我们将自己定义一个类,以此结束对 Ruby 的介绍。这个类名为 User,目的是实现 第 6 章用到的 User 模型。

到目前为止,我们都在控制台中定义类,这样很快捷,但也有点不爽。现在我们要在应用的根目录中创建一个名为 example_user.rb 的文件,然后写入代码清单 4.17 中的内容。

代码清单 4.17:定义 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 accessor),也就是定义读值方法(getter)和设值方法(setter),用于读取和设定 @name@email 实例变量(2.2.2 节3.4.2 节练习简介过)。在 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 = "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 节介绍过,如果最后一个参数是散列,可以省略花括号。我们可以把一个预先定义好的散列传给 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 章开始,我们会使用散列初始化对象,这种技术叫做批量赋值(mass assignment),在 Rails 中很常见。

练习
  1. User 类中定义一个名为 full_name 的方法,返回用户的名字和姓,中间以空格分开。

  2. 添加一个名为 alphabetical_name 的方法,返回用户的姓和名字,中间以逗号分开。

  3. 确认 full_name.splitalphabetical_name.split(', ').reverse 得到的结果一样。

4.5 小结

至此,对 Ruby 语言的介绍结束了。第 5 章会好好利用这些知识来开发演示应用。

我们不会使用 4.4.5 节创建的 example_user.rb 文件,所以我建议把它删除:

$ rm example_user.rb

然后把其他的改动提交到源码仓库中,合并到 master 分支之后,再推送到 Bitbucket,然后部署到 Heroku 中:

$ git commit -am "Add a full_title helper"
$ git checkout master
$ git merge rails-flavored-ruby

为了确保无误,最好在推送或部署之前运行测试组件:

$ rails test

确认无误后,推送到 Bitbucket 中:

$ git push

最后,部署到 Heroku 中:

$ git push heroku

4.5.1 本章所学

  • Ruby 提供了很多处理字符串的方法;

  • 在 Ruby 中一切皆对象;

  • 在 Ruby 中定义方法使用 def 关键字;

  • 在 Ruby 中定义类使用 class 关键字;

  • Ruby 内建支持的数据类型有数组、值域和散列;

  • Ruby 块是一种灵活的语言接口,可以遍历可枚举的数据类型;

  • 符号是一种标记,与字符串类似,但没有额外的束缚;

  • Ruby 支持对象继承;

  • 可以打开并修改 Ruby 内置的类;

  • 单词“deified”是回文;

  1. 如果辅助方法是针对某个特定控制器的,应该把它放进该控制器对应的辅助文件中。例如,为 StaticPages 控制器创建的辅助方法一般放在 app/helper/static_pages_helper.rb 中。在这个例子中,我们想在所有页面中都使用 full_title 方法,所以要放在一个特殊的辅助文件中,即 app/helper/application_helper.rb
  2. nano 编辑器对新手来说更简单,不过我都使用 Vim 做这种简单的编辑。如果想简单学习 Vim,请阅读《Learn Enough Text Editor to Be Dangerous》。
  3. 关于“foo”和“bar”,以及不太相关的“foobar”和“FUBAR”的起源,请查看 Jargon File 中介绍“foo”的文章
  4. 熟悉 Perl 或 PHP 的编程人员,可以把这个功能与自动插值美元符号开头的变量相对应,例如 "foo $bar"
  5. 抱歉,本章在“函数”和“方法”两个称呼之间随意变换。在 Ruby 中这二者是同一个概念:所有方法都是函数,所有函数也都是方法,因为一切皆对象。
  6. 其实还有一个地方我们不理解,那就是 Rails 是怎么把这些联系在一起的:把 URL 映射到动作上,让 full_title 辅助方法可以在视图中使用,等等。这是个很有意思的话题,我建议你以后好好了解一下。不过使用 Rails 并不需要完全了解 Rails 的运作机制。
  7. 这里你可能想使用字符串插值,其实本书前几版使用的都是插值,但 provide 方法会把字符串转换成 SafeBuffer 对象,而不是普通的字符串。插入视图模板的 HTML 会过度转义,把“Help’s on the way”转换成“Help&#39;s on the way”。(感谢读者 Jeremy Fleischman 指出这个小问题。)
  8. 这段代码中使用的 second 方法不是 Ruby 定义的,而是 Rails 添加的。在这里可以使用这个方法是因为,Rails 控制台会自动加载 Rails 对 Ruby 的扩展。
  9. 块是闭包(closure),知道这一点对资深编程人员可能会有些帮助。闭包是一种匿名函数,其中附带了一些数据。
  10. 第 1 章说过,使用 ('a'..'z').to_a.sample(8).join 也能得到相同的结果,而且更简洁。
  11. 在 Ruby 1.9 及以后的版本中,其实会按照元素输入时的顺序保存散列,不过依赖特定的顺序显然是不明智的。
  12. 没有约束的好处是,符号很容易进行比较,字符串要按照字母一个一个比较,而符号只需比较一次。这就使得符号成为散列键的最佳选择。
  13. 其实二者之间有些细微差别,p 返回打印的对象,而 puts 始终返回 nil。感谢读者 Katarzyna Siwek 指出这一点。
  14. 换行符在一行的结尾处,作用是开始新的一行。在代码中,换行符用 \n 表示。
  15. 数列数会让你发疯的,所以很多文本编辑器都提供了一个视觉标识。例如,如果再看一下图 1.8 的话,你可能会发现右边有一条细线,它可以帮助你把一行代码控制在 80 个字符以内。云端 IDE 默认会显示这条竖线。如果使用 TextMate,可以在如下菜单中找到这个功能:View > Wrap Column > 78。在 Sublime Text 中则是:View > Ruler > 78,或:View > Ruler > 80。
  16. 返回值可能由于 Ruby 版本的不同而有所不同。这个例子假设你使用的是 Ruby 1.9.3 或以上版本。
  17. 关于 Ruby 类和 self 关键字,请阅读 RailsTips 中的《Class and Instance Variables in Ruby》一文。
  18. 你没必要知道继承关系中每个类的作用。我也不知道它们都是干什么的,而我从 2005 年起就开始使用 Ruby on Rails 了。这可能意味着以下两个问题中的一个:第一,我是个废柴;第二,不需要知道所有内部知识也能成为熟练的 Rails 开发者。我们当然都希望是第二点。