《R和Ruby数据分析之旅》—第1章 1.1节Ruby-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

《R和Ruby数据分析之旅》—第1章 1.1节Ruby

简介: Ruby和R。帽子和鞭子作为野外考察的工具,对考古学教授来说不同寻常,同样,在探索周围世界方面,Ruby和R也并不寻常。它们能够让事情变得更有意思。

本节书摘来自异步社区《R和Ruby数据分析之旅》一书中的第1章,第1.1节Ruby,作者【新加坡】Sau Sheong Chang,更多章节内容可以访问云栖社区“异步社区”公众号查看。

第1章 握住探险之鞭——认识Ruby

R和Ruby数据分析之旅
《夺宝奇兵》一直是我最喜欢的系列电影。在我年少时,哈里森·福特就是我心中的英雄。我一直很喜欢印第安纳·琼斯抽鞭子的样子。其实正是在《夺宝奇兵》里,我第一次知道鞭子是什么东西。

《夺宝奇兵》最早的两部——《法柜奇兵》和《魔域奇兵》中,印第安纳正值壮年,坚定刚毅,脾气暴躁。在我看过这两部之后,心里就对他标志性的帽子和鞭子产生了疑惑——为什么一定要戴那样一顶毡帽,为什么居然要拿一条鞭子?

最后,所有的答案都在第三部《圣战奇兵》中揭晓了。这部电影告诉了我们印第安纳的身世,解释了帽子和鞭子的事情,还告诉了我们他为什么要做所有这一切。那可真是惊喜的一刻——虽说在大背景下其实显得并不那么重要。

那么,这跟一本编程书有何干系呢?正像印第安纳离不开帽子和鞭子一样,我们这本书也将主要使用两种工具——Ruby和R。帽子和鞭子作为野外考察的工具,对考古学教授来说不同寻常,同样,在探索周围世界方面,Ruby和R也并不寻常。它们能够让事情变得更有意思。

1.1 Ruby
R和Ruby数据分析之旅
这两种工具都各需一章专门介绍。我们首先来介绍Ruby,下一章再介绍R。显然,我肯定无法在书里仅用一章就把Ruby编程语言完完整整地解释一遍。所以,我打算提供足够的信息供你开开胃,希望能够吸引你去阅读更深入探讨Ruby的好书。

1.1.1 为什么用Ruby
你首先可能就会问(除非你已经对Ruby充满热情,已经知道为什么了,要是这样的话,你接下来点点头就可以了),为什么在这本书里我要选择Ruby作为一种主要工具。我有很多很好的理由。不过具体到我们这本书的目标上,我想集中说以下两点。

第一,Ruby是一款非常适合人类的编程语言。Ruby的创造者松本行弘经常说,他试图使Ruby很自然,而非很简单,让它就像能够反映我们的生活一样。用Ruby编程很像是在和你的好朋友——计算机交谈。Ruby的设计本意就是让编程更有意思,让人们集中精力在编程中埋头苦干。例如,要在屏幕上显示“I love Ruby”(我爱Ruby)10遍,只需要告诉计算机:

10.times do
  puts "I love Ruby"
end

如果你对C以及它的同族语言(比如Java)很熟悉,可能已经知道,要检查某个变量a_statement是否为真,你需要做这样的事情(注意,在C中需要使用整数1来代替true,因为C中没有布尔类型):

a_statement = true;
if (a_statement == true) {
  do_something();
}

而你在Ruby中当然也能做同样的事情,你可以这样做:

do_something if a_statement

这样的代码非常容易阅读,因此也就很容易维护。尽管Ruby有时也会很深奥,但通常情况下,它都是一种使他人易于阅读和理解的编程语言。你也可以想象到,对于本书,这将是一种非常有用的特征。

第二,Ruby是一种动态的语言。对于作为读者的你来说,这一点也就意味着,你可以从本书中复制代码,扔到文件(或者是你后面要看到的交互式Ruby shell)里,然后直接运行。不需要对makefile进行乱七八糟的设置,不需要得到库的正确路径,也不需要在运行示例之前先编译编译器。剪切,粘贴,运行——这就是需要做的一切。

这就是我在这本书里使用Ruby的两个主要原因。如果你很想了解为什么许多其他编程语言的程序员也开始转向Ruby,你可以访问Ruby的网站(http://www.ruby-lang.org),或者在网上搜索,你将会看到很多人对Ruby交口称赞。

1.1.2 安装Ruby
在使用Ruby之前,我们当然要首先把它安装到机器上。这是个很简单的准备活动。怎样安装主要有三种选择,选择哪种取决于你的热情有多么高涨。

1.从源代码安装
如果你雄心勃勃,可以尝试编译Ruby。这意味着你的平台上需要有能够编译Ruby的工具。所以,除非你确实想要严肃对待Ruby,否则我还是建议你安装已编译的:利用第三方工具,或利用你的平台上管理软件包的工具。

要通过源代码编译Ruby的话,可以访问http://www.ruby-lang.org/en/downloads下载源代码,然后用你平台上的编译器进行编译。在这个网站上,你还可以获取更多的信息。

2.利用第三方工具安装
或者,你也可以选择那些很流行的第三方工具。我推荐你使用最为流行的工具,在OS X或Linux上是Ruby版本管理器(Ruby Version Manager),在Windows上是RubyInstaller。

Ruby版本管理器
Ruby版本管理器(RVM)可能是非Windows平台上最为流行的第三方工具了。使用RVM有一个鲜明的优点,就是能够安装各种版本的Ruby,并且能够很容易地在各个版本之间切换。安装RVM虽然不是非常困难,但也并非一蹴而就。至少在今天来说,安装RVM要这样进行。

首先,你必须安装Git和curl。然后,在命令行中输入以下命令:

$ curl -L get.rvm.io | bash -s stable

然后,用下述命令重新载入shell(或者是类似的其他命令,这取决于你的shell):

$ source ~/.profile

这时你就可以运行rvm了。接下来要做的是,检查是否已经具备了安装Ruby的所有条件:

$ rvm requirements

一旦准备齐全,敲入rvm命令来安装你想要的Ruby版本。我们的示例中将安装Ruby 1.9.3:

$ rvm install 1.9.3

这之后,检查你想要的Ruby版本是否已经正确安装:

$ rvm list

你可以看到一系列(或者至少应该有一个)安装好的RVM Ruby。如果这是你第一次安装,那应该还没有默认的Ruby,所以你需要按下列命令设置一个:

$ rvm alias create default ruby_version

将上面的ruby_version替换成你刚刚安装上的版本(例如ruby 1.9.3p125)。大功告成啦!如果你在安装过程中受阻,可以查看RVM网站http://rvm.io/上的安装指导。

RubyInstaller
如果你使用的是Windows,那么就无法安装RVM。这种情况下,你可以创建一个虚拟机,安装上你最喜欢的GNU/Linux版本,然后再进行;或者只需使用很简单的RubyInstaller。访问http://rubyinstaller.org/downloads,下载正确的版本,然后安装。RubyInstaller包含了很多基于C的自然扩展,这是一个额外的好处。这是一款图形化的安装工具,所以很容易就可以快速完成配置。

3.使用平台上的软件包管理工具安装Ruby
如果以上方法都不适合你,你可以选择使用自己系统里的软件包管理工具,对于Debian系统(也包括Ubuntu),你可以使用下面的命令:

$ sudo apt-get install ruby1.9.1
这个命令能够安装Ruby 1.9.2。的确有点不可思议。

对于Mac机器而言,Ruby已经包含在OS X之中了,但通常是个较老的版本(Lion包含了Ruby 1.8.7,更早的系统包含的则是更老的Ruby版本)。在OS X下有一款很流行的软件包管理工具,名为Homebrew,它能够帮助你替换成最新版的Ruby。你可能猜到了,需要首先安装Homebrew。在命令行下输入以下命令:

$ /usr/bin/ruby -e "$(curl -fsSL https: //raw. github. com/gist/ 323731)"

然后用这个简单的命令安装Ruby:

$ brew install ruby

Homebrew实际上正是一组Ruby脚本的集合。

1.1.3 运行Ruby
一旦你按照前面介绍的方法装好了Ruby,就是该开始使用它的时候了!Ruby与C、C++、Java等需要编译的语言不同,在运行Ruby之前,你无须一个中间步骤来生成可执行文件。

运行Ruby代码有若干种方法,但入门的最简单方法大概就是使用安装过程中已内置的交互式Ruby工具。irb是一种Ruby REPL(读入-求值-打印循环)应用,它提供了交互式的编程环境,使你能够键入Ruby命令并实时地求出结果:

$ irb
ruby-1.9.3-p125 :001 > puts "hello world!"
hello world!
  => nil
ruby-1.9.3-p125 :002 >

请注意,一旦你输入了一条Ruby语句(在例子中,我们将字符串“hello world!”放到标准输出中),语句会立即被求值,使得“hello world!”被显示到屏幕上。在这之后,irb告诉你这条语句的值是空值(nil),因为Ruby中的puts语句返回一个空值。如果输入了这样的一条语句:

$ irb
ruby-1.9.3-p125 :001 > 1 + 1
  => 2
ruby-1.9.3-p125 :002 >

则会返回2,这正是它求值的结果。irb是一种能够快速上手的工具,而且,任何时候你如果不确定结果是什么,都可以向它寻求帮助。

另外一种运行Ruby的常见方式则是将代码保存为文件,之后通过Ruby解释器来运行文件。例如,你可以将“hello world!”保存到名为hello_world.rb的文件中。之后,你可以在命令行里输入这样的命令:

$ ruby hello_world.rb
hello world!

本书中的大部分示例都是通过这种方式来运行的。

1.1.4 引用外部库
在写一些简单的Ruby程序时,可能仅用Ruby自带的库就足够了;但更多的时候,需要一些外部库让开发更轻松。随Ruby预装的库有以下两个。

核心。
这是Ruby语言默认的类和模块的集合,包含字符串(String)、数组(Array)等。
标准库。这些库可以在Ruby源代码包的/lib目录下找到。它们随Ruby发布,但在运行时并未被默认包含。这些库包括Base64、Open URI,以及一些与网络有关的包(HTTP、IMAP、SMTP等)。
想要使用标准库或其他Ruby核心以外的库,需要在程序中用require语句来请求它们,例如:

require 'base64'

除标准库外,你将经常用到其他的外部库,或是由Ruby社区开发的,或者干脆是你自己开发的。最常用的发布Ruby库的手段是使用RubyGems——Ruby的包管理器。作为Ruby的一部分,它随标准库一起发布。因此,你可以在安装Ruby后立即使用它。

就像apt-get和yum管理Linux发行版中的软件包一样,RubyGems允许你轻松地安装或删除Ruby的库或应用程序。通过RubyGems发布的库或应用程序,需要被打包成称作gem的东西,其中包含一系列待安装的文件,以及对包进行描述的元数据文件。

gem可以在本地发布(通过扩展名为_._gem的文件),也可以通过gem服务器远程发布。过去有一些公用的gem服务器为gem提供托管服务,包括RubyForge、GitHub和GemCutter。但最近它们不同程度地为RubyGems所取代。在RubyGems的术语中,gem服务器又被称做源(source)。你可以部署自己私人的gem服务器,在其上发布预先打包好的、供内部使用的私有gem。

想在安装好的RubyGems中添加源,可以使用这条命令:

$ gem sources –add http://your.gemserver.org

若想安装本地gem,则可在控制台使用如下命令:

$ gem install some.gem –local

可以省略–local选项,但这样会多花一些时间,因为命令执行后会先搜索远程的源,使用“本地”(local)选项会告知RubyGems省略这一过程。

若想从远程的源中安装gem,通常可以这样做:

$ gem install some_gem

也可以通过下面的命令来安装某个gem的特定版本:

$ gem install some_gem –version 1.23

要列出本地已安装的所有gem,可以这样做:

$ gem list –local

1.1.5 Ruby基础
安装结束后,我们就可以开始使用Ruby了!

1.字符串
处理字符串(string)是程序中通常要做的基本工作之一。任何名副其实的编程语言都会提供一些处理字符串的手段,Ruby也不例外。实际上,Ruby在这方面功能堪称强大。

Ruby中所谓的字符串,就是由字符组成的序列。有若干种方法来定义字符串。最常见的方法可能是用单引号(')或双引号(")把它们括起来。如果用双引号,可以在其中使用转义序列,或用#``{代码}表达式来嵌入可执行的Ruby代码,实际包含在字符串中的是代码的执行结果,而并非代码本身。在被单引号括起来的字符串中,则不能这样做:

"There are #{24 * 60 * 60} seconds in a day"
=> "There are 86400 seconds in a day"

'This is also a string'
=>'This is also a string'

字符串还可以通过%q和%Q来定义。%q与单引号作用相似,%Q则与双引号作用相似,唯一的区别在于,在%q和%Q之后可以采用任意成对的界定符来括起字符串,例如:

%q/This is a string/
=> "This is a string"

%q{This is another string}
=> "This is another string"

%Q!#{'Ho! ' * 3} Merry Christmas \!!
=> "Ho! Ho! Ho!  Merry Christmas!"

最后,你还可以使用here-document来定义字符串。here-document是一种在shell命令行(如sh、csh、ksh、bash等)下或一些脚本语言(如Perl、PHP、Python,当然,也包括Ruby)中界定字符串的方法。here-document会将输入文本中的换行符和其他空白字符(包括行首的缩进)原样保留:

string = <<END_OF_STRING
    The quick brown fox jumps
    over the lazy dog.
END_OF_STRING
=> "  The quick brown fox jumps\n  over the lazy dog.\n"

注意,here-document中的界定符是第一行中<<以后的字符串,在上例中,乃是END_OF_STRING。

我虽然无法在本节中列出Ruby提供的所有字符串操作功能,但是可以给大家略举以下几例。

a = "hello "
b = "world"

a + b
=> "hello world"        # 字符串拼接(将a和b连在一起创建一个新字符串)

a << b
=> "hello world"        # 增加到字符串末尾(会修改a的值)

a * 3
=> "hello hello hello"   # 你可以使用“乘法”将一个字符串重复若干遍

c="This is a string"    # 以某个界定符为界限切分字符串,默认的界定符是空格
c.split
=> ["This", "is", "a", "string"]

2.数组和散列
与字符串同等重要,甚至可能更重要的,是操纵数据结构的能力。最重要的也是会在本书中(同样也在任何的Ruby编程工作中)经常出现的两种数据结构是数组和散列。

数组是带下标的数据容器,可存储一系列的对象。可以用方括号([])或Array类来建立数组。数组用以0开头的整数来索引,用[]操作符实现。

a = [1, 2, 'this', 'is', 3.45]
a[0]  # 1
a[1]  # 2
a[2]  # "this"

还有一些其他方法来索引数组,包括区间:

a[1..3]  # [2, 'this', 'is']

为数组的某个元素赋值时,我们同样使用方括号:

a[4] = 'an'
a    #[1,2,'this','is','an']

数组元素可以是任意的对象,包括其他数组:

a[5] = ["another", 'array']
a    # [1, 2, 'this', 'is', 'an', ['another', 'array']]

如果你对操纵数据结构比较熟悉,你可能会好奇我为什么在这一节里仅提及数组和散列。其他那些常见的数据结构(如栈、队列、集合等)哪儿去了呢?实际上,这些都可以用数组来实现:

stack = []
stack.push 1
stack.push 2
stack.push 'hello'
stack  # [1, 2, 'hello']

stack.pop  # 'hello'
stack    # [1, 2]

还有许多其他方法可以被用在数组上,你可以在Ruby网站的参考文档中找到它们,或者更进一步,打开irb来玩一玩它们。一种常见的迭代遍历数组的手段是使用each方法:

a = ['This', 'is', 'an', 'array']

a.each do |item|
   puts item
end

上述程序会把数组的每个元素在标准输出(即控制台)中输出一遍。代码中的循环由do开始,以end结束,它对数组的4个元素各执行一次。这里,我们选择名叫item的变量来代表循环中的数组元素,并用竖线将变量名括起来。有时候,为了简洁,我们用一对大括号{}来替换do…end。上述代码会产生如下结果:

This
is
an
array

注意,数组元素是按照定义时的顺序输出的。

数组有众多的方法,你还应该了解的是,Array是Enumerable(可枚举)模块的一个继承,该模块已经实现了那些方法。我们很快会讲到Enumerable模块。

散列可以被视为字典或映射,是一种可以索引一组对象的数据结构。与数组的主要不同在于,数组的下标是一个整数,而散列的下标可以是任意对象。散列可用大括号{}或Hash类来定义,并用方括号来索引。

h = { 'a' => 'this', 'b' => 'is',  'c'=>'hash'}

h['a']     # "this"
h['b']     # "is"
h['c']     # "hash"

为散列的一个元素赋值同样是用方括号:

h['some'] = 'value'
h    # {'a' => 'this', 'b' => 'is', 'c' => 'hash', 'some' => 'value'}

给散列的键(即索引下标)赋值的散列火箭(hash rocket)1风格语法在Ruby 1.9版本中被做了改进。原有的语法仍然可用,但新的语法更简单、更清爽。下面的两行代码做的是同一件事情:

h = { canon: 'camera', nikon: 'camera', iphone: 'phone'}
# 等同于
h = {:canon => 'camera', :nikon=>'camera',:iphone => 'phone' }
有许多办法可以对散列表进行迭代,最常用的方法是:

h = { canon: 'camera', nikon: 'camera', iphone: 'phone'}

h.each do |key, value|
   puts "#{key} is a #{value}"
end

就像之前我们用竖线将表示数组元素的item变量名括起来一样,这里我们用竖线将两个变量名括了起来。第一个代表每一个散列元素的键,第二个则代表该元素的值。上述代码将产生如下结果:

canon is a camera
nikon is a camera
iphone is a phone

数组和散列都是从Enumerable(可枚举)模块继承而来,换句话说,它们都是Enumerable的子类。Enumerable是一个提供了具有一系列能力——包括一些遍历、查找和排序方法——的集合类型的模块。Enumerable提供的一个非常有用的方法是map方法,该方法针对集合的每一个元素,执行语句块中提供的操作,并且返回由每个操作结果组成的新数组。下例中map方法的输入是一个数字区间(从1到4),输出的是每一个输入数据的平方。

(1..4).map do |i|
  i*i
end   #[1, 4, 9, 16]
max_by和min_by方法也是很有用的。你可能能猜到,这两个方法可以返回一个数组的最大或最小值:

a = ["cat", "horse", "monkey"]
a.min_by {|i| i.length}    # "cat"
a.max_by {|i| i.length}    # "monkey"

3.符号
Ruby包含了符号(symbol)的概念,它们是一些名称常量。符号由冒号开头,跟着是一个名字。例如,:north和:counter就是符号。符号在你需要某种类型的标识符的情形下十分有用。这时如果使用字符串,会造成浪费,因为每生成一个字符串,都要建立一个新的对象。而符号一旦定义,每次用到总会引用起初定义的唯一的那个对象。

4.条件和循环
如果你做过某种编程工作,那么对Ruby中的条件和循环就应该不陌生。Ruby具有直接或间接的C语言血统,因此它的条件语法跟C的语法十分类似。

if和unless
if表达式和其他语言中的非常像:

if pet.is_a? Dog then
   wag: tail
elsif pet.is_a? Cat then
   meow
else
   do_nothing
end

在每条语句都独占一行的情况下,then关键字是可选的。if语句的否定和相反的形式是unless语句:

unless visitor.friend?
  bark :loudly
else
  wag :tail
end

有时,当没有else语句时,if和unless可以被用作条件修饰符,来表示语句会在条件被满足的情况下执行。

wag(:tail) if pet.is_a? Dog

bark(:loudly) unless visitor.friend?

前述代码中,wag(摇动)方法在pet(宠物)对象属于Dog(狗)类时才被执行。bark(叫)方法在访客(visitor)是一个朋友(friend)时才不被执行。

最后,像C语言一样,Ruby可识别三元条件表达式:

visitor.friend? ? wag(:tail) : bark(:loudly)
这等价于:

if visitor.friend? then
   wag(:tail)
else
   bark(:loudly)
end

case表达式
在Ruby中,有两种使用case表达式的方法。一种是像一系列的if和elsif语句那样:

case
when visitor.friend?
   wag :tail
when visitor.postman?
   chase
when visitor.carries :big_juicy_bone
   jump_on visitor
else
   bark :loudly
end

第二种是更常见的,由case语句选定一个目标,每个when子句分别跟目标做一次比较:

case visitor.name
   when "Harry" then greet("Hello and welcome!")
   when "Sally" then greet("Welcome my dear!")
   when "Joseph" then greet("They are not here yet")
   else do_not_open_door
end

循环
Ruby中两种主要的循环机制是while和它的否定形式until。while循环执行在条件满足时执行循环体,这意味着循环体可能被执行0次或以上。until则相反,反复执行循环体直到条件为真为止。

while visitor.hungry?
   offer food
end
# 等同于
until visitor.full?
   offer food
end

可以看出,两种形式做了完全相同的事情。那为什么要同时提供这两种形式呢?要记得Ruby是具有表现力的,并且常常努力让程序更为智能化。尽管两种形式是相同的,但有时用其中的某一种会比另一种更自然。

像if和unless一样,while和until也可以被用作语句修饰符:

offer(food) while visitor.hungry?
# 等同于
offer(food) until visitor.full?

1.1.6 一切皆对象
关于Ruby,你可能经常耳闻的一个说法就是:在Ruby中,一切都是对象。这听起来有点极端,并且在技术的观点上不完全正确。显然,关键字,如if-else条件语法,并不是对象。然而你操作的所有东西确实都是对象。就连类也是对象,方法也如此。而且,所有求值的结果也都是对象。下面我们来看看这种机制的运作情况。

1.类和对象
创建对象的经典方法是通过某个类进行实例化:

class Dog
   attr :breed, :color, :name

   def initialize(name, color, breed)
     @name, @color, @breed = name, color, breed
   end

   def bark(volume=:softly)
     make_a_ruckus(volume)
   end
end

如果你已经用其他语言做过某种形式的面向对象编程,这对你而言应该比较熟悉。如果你没有做过,那么就可能有一点迷惑,但很容易解释清楚。上面的代码定义了一个类,某种程度上好比一个模板,你基于它来生成实例或对象。在本例中,我定义了一个Dog(狗)类,它拥有breed(品种)和color(颜色)等属性,并且每一个类的实例拥有一个特定的名字。attr关键字是一个方法调用,帮助建立3个实例变量(breed、color和name)以及一些访问这些变量的标准方法。Ruby中的实例变量均以@开头。

以def开头的几行定义了一些方法。方法是属于对象并且在对象上调用的函数。上例中有两个方法,即initialize(初始化)和bark(叫)。

initialize是一个便捷方法。每当Ruby建立一个新对象时,总是会寻找并调用名为initialize的方法。在我们的initialize方法中,通过参数传递的值为每一个实例变量赋值。

bark方法的功能很简单——制造一次“喧闹”。它的定义显示了如何为参数设定默认值(上例中为softly),默认值在调用时未给该变量传值的情况下起作用。

那么我们如何通过Dog类建立一个对象呢?

my_dog = Dog.new('Rover', :brown, 'Cocker Spaniel')
my_dog是一个变量,包含了由Dog类实例化的一个对象,括号里的几个值则经由initialize方法给出了对象的名字、颜色和品种。

2.方法
前已提及,可以用def关键字来定义方法,def后跟方法名。方法定义可以有任意数量的参数,也可以没有参数。如果你的方法不需要参数,你可以在定义时省略括号:

def growl
  make_a_ruckus{:very_softly}
end

通过前面的Dog类你可能已经注意到,你可以为方法的参数设定默认值时:

def bark(volume=:softly)
  make_a_ruckus(volume)
end

在上面的代码中,volume参数默认值是:softly这一符号量。当你为参数设定了默认值时,你可以在调用方法时包含或省略参数值:

my_dog.bark      # 此时bark方法自动使用默认值:softly
my_dog.bark(:loudly)

对于有多个参数的方法,通常的做法是将有默认值的参数放在没有默认值的参数之后。如果没有默认值的参数在后,设置默认值就变得没有意义了,因为每次调用时,所有参数都必须赋值。

方法总要返回一个值,该值可以是一个数组,这样就可以实现多个值的合并返回。返回值可以用return关键字来指定。如果到方法结束都没有遇到return,返回的将是方法中最后一个被求出的值。

3.类方法和变量
目前为止我们讨论了类的实例。前面的一个例子体现了从Dog类到my_dog对象的实例化。其中的变量和方法实际上都是属于my_dog对象的,并且也只能被my_dog对象所调用。举例来说,基于前面Dog类的定义,你不能这样做:

Dog.bark
从逻辑上讲,因为Dog类是创造各种狗对象的模板,调用Dog类的bark方法,意味着让所有的狗一起叫!然而,在某些情况下(如果你以前做过面向对象编程,你应该明白我在指什么),我们需要使用属于类而不是对象的方法甚至是变量。如何实现这个需求呢?

之前我提到,连类本身也是对象。我们下面要做的无非就是将一个类当做对象对待。要定义一个类方法,在方法名前面加上self即可:

class Dog
  attr :breed, :color, :name

  def self.total_count
    # 返回系统中狗的总数
  end
 
  # 其他方法
end

self是一个代表当前对象的关键字(与C++或Java类似)。当我们定义一个类的时候,当前对象就是这个正在定义的类。我们用self定义类中方法的时候,意味着将这个方法添加给类本身,而非类的某个实例。此时,我们是在为一个“类对象”,也即Class类的一个实例,添加方法。当我们需要定义作用在类本身上的方法时,将会经常用到这一手段。

定义类变量是一件很直接的事情,在变量名前面加上@@即可:

class Dog
   @@count = 0
   attr :breed, :color, :name

   def self.total_count
      @@count
   end

   def initialize
      @@count += 1
      # 其他初始化工作
   end
  

   #其他方法
end

注意,@@count这个类变量在类定义中被初始化为0,这项工作只被执行一次。通常,在initialize方法中初始化类变量是错误的,因为initialize方法在每个新的对象被建立时都要被执行一次。这意味着,每建立一个新对象,类变量的值都要被重置一次!

4.继承
继承是面向对象编程的基石之一。Ruby中的继承十分简便。要把一个类声明为另一个类的子类,只要在类定义中这样做:

class Spaniel < Dog
  # 其他定义工作
end

这样就创建了名叫Spaniel的子类,它继承了Dog类的一切,包括方法和成员变量。有一个问题:如果Spaniel类是Dog类的一个子类,那么Dog类又是谁的子类呢?你可以通过在Dog类上调用superclass方法来找到答案。别忘了Dog类本身也是一个对象,所以可以在其上直接调用某个方法:

Spaniel.superclass       #Dog
Dog.superclass            #Object
Object.superclass         #BasicObject
BasicObject.superclass   #nil

可以看到,Dog类是Object类的一个子类(Object类本身也是一个对象,你是不是感觉有点混乱?),而Object类又是BasicObject类的一个子类。BasicObject则是继承关系的终点,乌龟下面并非总有别的乌龟!2

我们已定义了Spaniel类,当调用它的bark方法时会发生什么呢?因为bark方法在Spaniel类中并未定义,系统将会上溯到它的超类,也就是Dog类,调用其中的同名方法。当然,如果Ruby在Dog类中也找不到bark方法,则会沿继承层次继续上溯,直到BasicObject类,如果仍未找到,则抛出一个NoMethodError(找不到方法)异常。

你不能用一个子类同时继承多个超类。有些编程语言支持多重继承,但Ruby仅支持单继承。然而,Ruby提供了模拟多重继承的机制——mixin机制,使用模块来实现。

模块是一种将一些方法、类和常量组合在一个名字空间里,避免命名冲突的手段。如果在Ruby的类中包含了模块,那么就意味着让mixin机制成为可能。因为当类中包含多个模块时,我们就可以模仿多重继承的效果。

让我们继续Dog类的例子,为Dog类定义一个名叫Canine(犬科动物)的超类:

class Canine
  # 一些定义
end

class Wolf < Canine
  # 一些定义
end

class Dog < Canine
  # 一些定义
end

我们知道,狗也是宠物,那么如果我们想将一些方法和变量组合成一个Pet(宠物)类,我们怎样能让Dog类继承这些方法和变量呢?在Ruby中,我们不能直接这样做,因为它只支持单继承,但我们可以将Pet变成一个模块:

module Pet
  def scratch_stomach
    # 这是一只好宠物!
  end
end

class Dog < Canine
  include Pet
  # 其他一些定义
end

用这种手段,Dog类就可以在不违背单继承机制的情况下,同时继承Pet和Canine的方法了。

关于mixin机制还有另外一个例子,记不记得在1.1.5节的“数组和散列”部分,我们说过Array类和Hash类都包含了Enumerable模块。

5.“像鸭子一样编程”
Ruby和Python、PHP、Smalltalk等语言,都是为人熟知的动态类型语言,而C和Java等则是静态类型语言。本质上,如果一个语言需要程序员在代码中指定数据类型,并且编译时会进行类型检查,在类型不匹配时报错,那么它就是静态类型语言。与此相反,动态类型语言不需要在代码中指定数据类型,并将类型检查留到运行时进行。

例如,在Java中,你需要先声明变量再给它赋值。

int count = 100;
然而,在Ruby中,你只需要:

count = 100
此时,你应当自觉地正确使用这个变量,这意味着,当你为变量赋了整数值时,你应当在代码中把它作为整数来用。当你使用count变量时,Ruby明白它是一个整数,并且你也应当把它当整数使用。如果你不这样用,Ruby会自动将它转换成你试图去把它视为的任何类型。这个机制被称作鸭子类型。

鸭子类型背后的思想来自所谓的鸭子测试:如果一个东西走路像鸭子,叫声也像鸭子,那么它就是一只鸭子。这意味着,对象的类型并不由对象所属的类决定,而是由该对象可以做什么来决定。

举一个简单的例子,我们定义名叫op的方法:

def op(a,b)
  a << b
end

这个方法接受两个参数,并返回单个值。方法中既未指定参数类型,也未指定返回值类型。这样做是否会带来潜在的bug呢?我们来看看如何使用这个方法。如果x和y都是字符串,返回值也将是一个字符串,此时没有问题:

x = 'hello'
y= 'world'

op(x,y)
=> 'hello world'

当x是个数组、y是个字符串时,方法将y追加到x末尾,从而返回一个新的数组:

x = ['hello']
y= 'world'

op(x,y)
=> [ "hello", "world"]

当x和y都是整数时,方法会完成一个左移位操作,将二进制的1向左移动两位,从而得到4(二进制的100):

x = 1
y = 2

op(x,y)
=> 4

这意味着什么呢?鸭子类型机制是瑕瑜互见的。最明显的缺点是使方法缺乏一致性:在传入不同的值时,方法的结果会大相径庭,这一点在程序实际运行之前都不会被检查。

一个主要的好处则是带来了更简单的代码。如果你明确知道你在做什么,它将带来更易读和易维护的代码。

最后我们要说,鸭子类型不仅仅是Ruby中一种固定的编程机制,它更像是一种哲学。如果你想确保op方法只能在参数是字符串时才可用,你可以这样做:

def op(a, b)
  throw "Input parameters to op must be string"
    unless a.is_a? String and b.is_a? String
  a << b
end

如果a或b并非字符串,程序将抛出一个异常。

1赋值符号=>形似火箭——译者注
2出自史蒂芬·霍金的《时间简史》(Bantam出版社):
一个著名的科学家(有人说是伯特兰·罗素),曾经作过一次关于天文学的公开演讲。他描述了地球如何围绕太阳公转,而太阳又是如何围绕由众多星体组成的银河系的中心公转。在演讲结束的时候,一位坐在房间后方的老太太站起来说:“你讲的都是胡说八道,我们的世界是一个驮在巨龟背上的平板”。科学家莞尔一笑,回答说:“那么那只乌龟又站在什么上面呢?”“你很聪明,年轻人,很聪明。”老太太说,“但乌龟下面还有乌龟!”

本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章
最新文章
相关文章