本节书摘来自异步社区《Ruby程序员修炼之道》一书中的第1章,第1.1节进入Ruby的世界,作者【美】David A. Black(戴维 A. 布莱克),更多章节内容可以访问云栖社区“异步社区”公众号查看。
第1章 进入Ruby的世界
Ruby程序员修炼之道(第2版)
本章主要内容
- Ruby语法的生存工具箱①
- Ruby基础编程指引:程序编写、保存、运行和错误检查
- Ruby安装指南
- Ruby的扩展机制
Ruby中易用的命令行工具,包括交互式Ruby解释器(irb)
本书的内容是Ruby基础,而本章是基础中的基石。本章的目标是让读者在开始学习Ruby之前掌握足够的知识和技巧。
接下来读者将看到Ruby的基本语法和技术,以及Ruby的运行机制:如何写一个程序,怎样使用Ruby运行程序,以及如何把一个程序分写到多个文件中。此外,读者还将学习到一些开关(switch)②的用法,即它们如何改变Ruby解释器(名为ruby的程序,用于执行用Ruby语言写的程序文件)的作用,也会学习使用一些重要的辅助工具,让读者的Ruby程序员生涯更加轻松和高效。
本章将Ruby领域的观点分为以下3个基本层次。
语言核心:设计原则、语法、语义。
Ruby支持的扩展(extension)和类库(library),以及用户自己添加到扩展中的工具。
Ruby自带的命令行工具,用于运行解释器和其他一些重要工具。
这3个部分在一个系统中环环相扣,因此在本书中它们的内容会相互贯穿,不过在本章中会尽量分开讨论。尽管如此,这3个部分的内容都会贯穿在书的每一章中。
Ruby,ruby,还是RUBY?!
Ruby是一门编程语言。我们会谈论“学习Ruby”,还会问一些问题,比如“你知道Ruby吗?”ruby是指一个计算机程序,特指Ruby的解释器,它可以读取并运行程序。可以看到这个命名方式在一些文章中出现,如“你用ruby运行我的文件,但什么也没发生。”,或者“ruby可执行文件的完全路径是什么?”最后是RUBY,准确来说,没有这样的写法。Ruby不是一个缩略语词汇,所有字母都大写的拼写方式向来都是错的。人们在对待Perl语言的时候也出现过同样的错误,或许是因为他们看到了如BASIC和COBOL这样的拼写方式,但Ruby不同。一般用“Ruby”来表示编程语言,用“ruby”来表示Ruby的解释器。
第1章不是只为服务于其他章节而存在的,它也有自己存在的价值:学习真正的Ruby技术和这门语言设计中的要点。其目标是为了引导和启发读者,但即使如此,学习过程还是会深入Ruby语言的一些关键层面。
1.1 Ruby语言基础知识
本节的目标是让读者开始接触Ruby。这里采用广度优先的方法:大致围绕语法学习、代码编写、程序运行这样一个循环过程展开。
说到这里,读者需要在自己的电脑上安装好Ruby。书中的例子都使用Ruby 2.1.0③编写。还需要准备一个文本编辑器(任何偏好的编辑器都可以,也可以用纯文本编辑器,但文字处理软件不行)和一个存放Ruby程序文件的目录(亦称文件夹)。可以命名目录为rubycode或者rubysamples,无论什么名字都可以,只要能够区别于其他工作区并方便地找到练习程序文件即可。
交互式Ruby控制台程序(irb)是最好的朋友
随Ruby发布的irb实用工具,是一个Ruby命令行工具,irb使用的广泛程度高于Ruby解释器本身。启动irb之后,键入Ruby代码,它将执行代码并输出结果值。
在命令行中键入irb,然后输入在文中所见的示例代码。例如:
100 + 32
=> 132
打开irb会话意味着可以随时测试任意数量的Ruby代码段。大多数Ruby程序员都发现irb是不可缺少的,而且本章中的一些示例也是使用irb运行的。
读者将看到下面irb的示例使用了命令行选项,它的输出结果更易于阅读。
irb --simple-prompt
运行irb时加入此选项或者不加,就可以看到--simple-prompt选项的效果。正如所见,这个简单的提示符选项将使屏幕更简洁明了。默认的(非简单的)提示符显示了更多的信息,如在交互会话时的行号统计。但从众多的示例可以看出,简单的提示符已足够使用。
因为irb是一个用于运行Ruby语言的命令行工具,所以直到1.4.2节才会详细讨论它。如有需要,可以转到那一节去看一下,这是最直接的方式。
完成Ruby的安装,并创建好工作目录的,就继续学习Ruby吧,这样我们就能分享Ruby学习和探索中的见地。掌握Ruby的语法将会是一个良好的开端。
1.1.1 Ruby语法生存包
下面3个表总结了Ruby的一些特性,这对于理解本章的例子和开始体验Ruby语言大有益处。不必记住它们,只要看一下并在稍后用到的时候回查即可。
表1-1包含了Ruby的一些基本运算。表1-2中介绍了获取基础键盘输入、将输出发送到屏幕以及基本的条件语句。表1-3简要描述了Ruby的特殊对象和注释的语法。
以上摘要表中已经包含了许多Ruby的基础和语法。读者需要能够辨认出Ruby标识符(identifier)的几种不同写法,尤其是对Ruby中的对象和方法调用有一个感性认识。我们稍后将会谈论这些内容。
1.1.2 多种多样的Ruby标识符
Ruby的标识符类型很少,一眼就能辨认和区分它们。标识符体系如下。
变量(variable):
局部变量(local variable);
实例变量(instance variable);
类变量(class variable);
全局变量(global variable)。
常量(constant);
关键字(keyword);
方法名(method name)。
这是一个很小的体系,很容易掌握,这一节将讨论它们。记住本节的目标是学习辨认不同的标识符。本书后面将学习如何使用和使用它们的时机。这只是标识符知识的第一课。
1.变量
局部变量以小写字母或者下划线开头,包含字母、下划线或数字。x、string、abc、start_value和firstName都是有效的局部变量命名方式。然而,值得注意的是,在组合多个单词以命名局部变量时,Ruby的约定是使用下划线作为命名规范,而不使用驼峰命名法,如使用first_name而不使用firstName。
实例变量为独立的对象存储信息,它通常以一个单独的符号(@)开头,后面的字符使用与局部变量相同的命名规则,如@age和@last_name。尽管局部变量不能以大写字母开头,但是实例变量可以在@符号之后的第一个位置使用大写字母(但不能使用数字)。但是通常来说,@符号之后还是使用小写字母。
类变量在每一个类层级上存储信息(同样,现在也不用担心它的语义)。它与实例变量使用相同的命名规则,只有一点不同,它以两个@符号(@@)开头,如@@running_total。
全局变量可以通过它的美元引导符号($)辨认出来,如$population。跟在$符号之后的语句不使用局部变量的命名规则。有一些全局变量名为$:、$1和$/,还有$stdin和$LOAD_PATH。但只要以$符号开头,它就是一个全局变量。这些非字母的标识符是预定义的,因此不必担心其中的标点符号是否合法。
表1-4总结了Ruby变量的命令规范。
2.常量
常量使用大写字母开头。A、String、FirstName、STDIN都是有效的常量命名。在Ruby命名规范中,如遇到命名多词组合的常量时,可以使用驼峰命名法(FirstName)也可以使用下划线分隔且所有字母大写(FIRST_NAME)的方式。
3.关键字
Ruby拥有很多的关键字,它们是预定义的保留词,与特定编程任务和上下文关联。关键字包括def(用于方法定义)、class(用于类定义)、if(条件执行)和_FILE_(当前被执行文件的名称)。关键字大约有40个,通常是简短的、单一词汇(与下划线组合多单词方式相反)的标识符。
4.方法名
Ruby中的方法命名遵从与局部变量相同的规则和约定(除了以?、!或=结尾的符号,其重要作用稍后会讲述)。这是一种设计理念:方法并不因其自身作为方法而被人关注,而是简单地作为提供值的表达式融入到程序的结构中。在一些上下文中,很难一眼就区分出一个表达式是一个局部变量还是一个方法名,这一切源自设计,是有意为之。
讲完方法,现在已经有了一张Ruby标识符的“路线图”,让我们谈一下编程语言的语义,尤其是对象及其方法的重要作用。
1.1.3 方法调用、消息和Ruby对象
Ruby把所有的数据结构和值都看做对象,从整数和字符串这样简单的标量(原子的)值,到数组(array)这样的复杂的数据结构一概如此。每个对象都能响应一组特定的消息,对象能够接收的每个消息直接对应一个方法——有名称的、可以被有执行能力的对象触发的可执行例程。
对象也可以用字面量构造器表示,如字符串用双引号,或者已绑定值的变量。消息通过特殊的点运算符(.)送达:点右边的消息被发送到点左边的对象上。(另外,有许多特殊的给对象发送消息的方式,但是点是最常用和基础的方式。)参见表1-1中的示例:
x = "100".to_i
点(.)意味着消息to_i被发送给字符串对象"100"。字符串对象"100"作为消息的接收者被调用。也可以说是方法to_i被字符串对象"100"调用。方法调用的结果是生成整数100,然后作为赋值运算的右表达式赋值给变量x。
为什么使用两种术语?
何苦为该说“发送to_i消息”还是说“调用to_i方法”而烦恼?对于同一操作为什么有两种不同描述?因为它们不完全相同。大多数时候,发送消息给接收的对象,对象就会执行对应的方法。但有些时候是没有对应方法的,对于点右边的任意标识符,并不能确保接收者拥有的方法与发送的消息相匹配。
这听起来有些混乱,其实不然。因为对象可以拦截未知的消息并使它们拥有具体含义。例如,Ruby on Rails Web开发框架大量使用了如下的技术:发送未知消息到对象并拦截那些消息,然后能够在使用当前数据库表的列名作为动态条件的情况下顺畅运行。
方法可以带有参数,这个参数同时也是对象。(虽然有些用于创建和操作对象的语法结构本身不是对象,但在Ruby中几乎所有一切都是对象。)下面是一个带有参数的方法调用:
x = "100".to_i(9)
字符串对象100调用方法to_i并传递参数9,生成一个九进制的100所得到的十进制整数,因此x现在等于十进制的81。
这个例子也同时展示了使用圆括号包含参数的方式。这些圆括号通常情况下是可选的,但是在大多数复杂的情况下,为了避免语法上的歧义,圆括号的使用是必要的。大部分程序员都尽可能在方法调用时使用圆括号,这样做是为了避免歧义。
完整的Ruby程序是由对象以及发送给对象的消息所组成。作为一个Ruby程序员,大多数时间要么是定义对象所能完成的任务(定义方法),要么是请求对象完成这些任务(给对象发送消息)。
接下来在书中将会深入地讲述这些内容。再说明一下,这一段简短的概述是引领读者进入Ruby世界的其中一步。当看到点出现在某些令人费解的位置时,应该把它理解为一个发送给对象(左边)的消息(右边)。同时,也该记住一些裸词(bareword)风格的方法调用方式,例如puts在如下示例中的调用方式:
puts "Hello."
这里尽管缺少消息发送所需要的点以及该消息的显式接收者,却依然发送了消息puts并传递了参数"Hello."给一个对象:默认对象self。在程序运行期间,虽然作为self的对象会通过特定规则发生改变,但self总是被预定义好的。在第5章中将会对self进行详细阐述。现在,只要知道像puts这样的裸词的方法调用方式即可。
对象的概念在Ruby中是最为重要的,与此紧密相关并扮演重要角色的概念是类(class)。
用类解释对象的由来
类定义了一组行为和功能,每一个对象是一个具体类的实例。Ruby提供了大量的内置类,它们代表了重要的功能数据类型(如String、Array、Fixnum)。每次创建一个字符串对象的同时,就创建了一个String类的实例。
用户可以编写自己的类,甚至可以修改已经存在的Ruby类。如果不喜欢字符串或者数组的行为,可以修改它。这看起来虽然不好,但是在Ruby中是允许这样做的。(第13章中将描述修改内置类的利弊。)
尽管每一个Ruby对象都是类的一个实例,但是类的概念却不如对象的概念那么重要。那是因为对象可以发生改变,它可以获得在类中没有定义过的方法和行为。类负责将对象变为实际的存在,这就是耳熟能详的实例化(instantiation)过程。而对象在实例化之后,就进入了自己的生命周期。
对象有能力包含一个在类中没有定义的行为,这是设计Ruby作为一门编程语言时最为核心的原则之一。正如读者所猜测,书中将在不同的上下文中不断地回到这一点来讨论。对目前而言,只要意识到尽管每个对象对应一个类,但对象的行为不由对象的类唯一决定即可。
了解了Ruby的知识(疑惑之时回看一下这些内容)后,让我们尝试着运行一个程序吧。
1.1.4 编写和保存一个简单程序
在本节中,将会在之前创建的Ruby示例程序目录中创建一个程序文件。第一个程序是一个摄氏—华氏度转换工具。
**注意
当然,在真实场景中,温度的转换使用的是浮点数。而这里在输入输出时使用整数,主要是为了专注于程序的结构和程序的执行。
这个例子将被反复提到,并根据本书的需要进行添加和修改。它遵循如下的迭代过程。**
整理程序的输出结果。
从用户的键盘输入中接收输入数据。
从文件中读取数值。
将程序结果写入一个文件。
第1个版本很简单,仅关注文件创建和程序运行的过程,而不用深入程序逻辑的细节。
创建第一个程序文件
使用一个纯文本编辑器,输入代码清单1-1所示的代码到一个文本文件并保存为c2f.rb到示例目录中。
注意:根据用户的操作系统的不同,Ruby程序文件有可能仅用文件名或一个短名称就可以独立运行,并不需要使用文件扩展名。尽管如此,请记住,.rb的文件扩展名在一些情况下是强制的,这主要是涉及拥有多个文件(后面会详述)的程序和有文件间相互查找机制的程序。在本书中,所有Ruby程序文件名都以.rb结尾,这是为了确保示例程序可以在尽可能多的平台上运行,同时尽可能少地对系统进行管理干预。
现在,已经有了一个完整的(虽然很小)Ruby程序,可以运行它了。
1.1.5 给Ruby提供程序
运行一个Ruby程序需要给Ruby解释器传递程序的源码文件(或者多个文件),这个Ruby解释器名为ruby,后面会依次解释。在提供一个程序给ruby而不是请求Ruby运行程序之后,它会检查程序代码的语法错误。
1.检查语法错误
如果在转换公式中使用31替换32,就会发生一个程序性错误。Ruby还是会适时地运行程序并给出有缺陷的结果。但是假如程序的第2行中不小心遗漏了右圆括号,那就是语法错误,Ruby将不能运行这个程序。
$ ruby broken_c2f.rb
broken_c2f.rb:5: syntax error, unexpected end-of-input, expecting ')'
(错误出现在第5行,即程序的最后一行,因为Ruby一直在耐心等候右圆括号出现,结果却没有。)
Ruby 解释器提供一种便捷的方式来检查语法错误而不必运行程序。它会读取文件并指出语法是否有错。为了检查源文件的语法错误,可以这样做:
$ ruby -cw c2f.rb
命令行中的-cw标志(flag)是两个标志的简写,它们分别是:-c和-w。标志-c意味着检查语法错误。标志-w可以激活高级别的警告:如果程序都合乎Ruby语法,Ruby就不会发出警告,除非有比语法更值得商榷的理由。
假如输入的文件正确,将在屏幕上会看到如下信息:
Syntax OK
2.运行程序
为了运行程序,再次提供源文件给解释器,但是这一次不用加入-c和-w标志。
$ ruby c2f.rb
如果一切顺利,将可以看到计算的结果输出如下:
The result is
212
.
计算的结果正确,但是计算的结果超过了3行,这看起来不够好。
3.温度转换器的第二次迭代
问题要追溯到puts命令和print命令的区别。假如字符串没有以一个已经存在的换行符结束,puts命令会在它打印的字符串尾部插入新换行符。相反,print打印字符串之后就停止了,它不会自动跳转到下一行。
为了修正这个问题,把前两行的puts命令改为print:
print "The result is "
print fahrenheit
puts "."
(注意,is后面的空格,它是为了确保在is和数值之间有一个空格。)现在输出的是:
The result is 212.
puts是“put(就是print)string”的缩写。尽管put没有直观的表示会调用换行符,但是puts会这样做:如同print,打印用户的数据,之后自动地转到新一行。假如让puts打印已经以换行符结束的一行,它不会再次添加换行符。
假如读者已经在其他编程语言中使用过打印的工具,而这些工具没有自动添加换行符(如Perl语言的print函数),那么可以自己编写类似Ruby中的实现,一个能打印值并添加换行符的工具:
print fahrenheit, "n"
尽管如此,大可不必这样去做,puts已经实现了。习惯使用puts吧,并在这个过程中遵循使用其他的Ruby习语和约定。
**警告
在一些操作系统平台中(尤其是在Windows中),程序运行的结尾会打印输出额外的换行符。这意味着实际上print已经代替了puts,而puts很难被系统检测到。意识到这两者的区别,并在最常用的场景中使用其中一个,可以充分确信得到所期望的结果。**
看一下屏幕的输出,接下来将扩展一点I/O领域的知识,包括键盘的输入和文件的操作。
1.1.6 键盘和文件I/O
Ruby提供了很多在程序执行过程中读取数据的技术,包括从键盘读取和从磁盘文件读取。它们有许多用途:不仅仅在编写每个应用程序的过程中会用到,在编写维护、转换、管理或者操纵用户的工作环境的代码时也几乎一定会用到。本章中包括了一些输入处理的技术,更多关于I/O操作的技术详见第12章。
1.键盘输入
反复执行100摄氏度等于212华氏度的程序,其价值是有限的,更有价值的程序则是可以自由指定华氏温度并获得对应的摄氏温度。
修改程序完成如上的功能需要几个步骤,分别需要使用表1-1和表1-2中提及的方法gets(获取键盘输入的一行数据)和to_i(转换为整型),读者应该已经熟悉它们其中的一个。由于这不仅仅是修订版本而是一个新程序,所以把代码清单1-2中的代码版本放到名为c2fi.rb的新文件中(i意味着交互)。
代码清单1-2 交互式温度转换器(c2fi.rb)
print "Hello. Please enter a Celsius value: "
celsius = gets
fahrenheit = (celsius.to_i * 9 / 5) + 32
print "The Fahrenheit equivalent is "
print fahrenheit
puts "."
下面为新程序的一组运行结果:
$ ruby c2fi.rb
Hello. Please enter a Celsius value: 100
The Fahrenheit equivalent is 212.
$ ruby c2fi.rb
Hello. Please enter a Celsius value: 23
The Fahrenheit equivalent is 73.
简化代码
将代码清单1-2中代码的输入、计算、输出操作进行简化。简化重写后如下:
print "Hello. Please enter a Celsius value: "
print "The Fahrenheit equivalent is ", gets.to_i * 9 / 5 + 32, ".\n"
虽然这个版本确实减少了变量使用,但是会要求阅读代码的人接受这组有些密集(但是简短)的表达式。任何既定的程序通常都需要在长代码(也许更清晰)和短代码(也许有点晦涩)之间进行抉择。而有时候,短代码则更为清晰,这就是Ruby的编码风格。
如果不深究更多细节,现在这个版本已经是一个通用的摄氏度转华氏度的解决方案。接下来,让我们学习文件的读取。
2.读取文件
从Ruby程序中读取文件并不困难,至少在大多数情况下,比键盘输入还要容易。温度转换器的下一个版本将从一个文件中读取一个数组,然后从摄氏度转换为华氏度。
首先,创建一个文件并命名为temp.dat(温度数据),同时包含一个数字:
100
现在,创建第三个程序,命名为c2fin.rb(in意为文件输入),如代码清单1-3所示。
代码清单1-3 使用文件输入的温度转换器(c2fin.rb)
puts "Reading Celsius temperature value from data file..."
num = File.read("temp.dat")
celsius = num.to_i
fahrenheit = (celsius * 9 / 5) + 32
puts "The number is " + num
print "Result: "
puts fahrenheit
这一次,示例运行的输出结果如下:
$ ruby c2fin.rb
Reading Celsius temperature value from data file...
The number is 100
Result: 212
自然地,如果在文件中改变数字,结果将会不同。那么,怎样将计算的结果写入文件中呢?
3.写入文件
从最简单的操作来说,文件写入要比文件读取复杂一些。正如代码清单1-4所示,执行写入文件的操作时,主要的额外步骤是要指定文件的模式,在这个例子中是w(意为写入)。保存代码清单1-4所示的这个版本,命名为c2fout.rb并运行它。
代码清单1-4 使用文件输入的温度转换器(c2fout.rb)
print "Hello. Please enter a Celsius value: "
celsius = gets.to_i
fahrenheit = (celsius * 9 / 5) + 32
puts "Saving result to output file 'temp.out'"
fh = File.new("temp.out", "w")
fh.puts fahrenheit
fh.close
调用方法 fh.puts Fahrenheit的作用,是将 Fahrenheit的值输出到由fh对象进行写入处理的文件中。如果检查文件temp.out,无论输入什么数字,都可以看到转换好的华氏温度值。
作为练习,可以尝试着把前面的例子进行合并,让它从一个文件读取数字,转换为华氏度,之后把结果写入不同的文件中。在适时引入一些Ruby语法的同时,下一节会检验Ruby的安装,然后很快还会依次看到Ruby如何管理扩展和库。