Rational Edge: Ruby:编程语言中的精华

进一步了解 Ruby,一种快速普及的动态语言。探究它的迷人特性,并了解程序设计人员用它在做什么。 本文来自于 The Rational Edge

Gary Pollice, 实践教授, Worcester Polytechnic Institute

Author photoGary Pollice 是麻省 Worcester 市 Worcester Polytechnic Institute 的一名实践教授。他教授软件工程、设计、测试以及其它计算机科学的课程,同时也指导学生项目。在进入学术界之前,他从事了 35 年多的软件开发,开发过各种软件,包括商业应用到编译器和工具等等。他在行业内的最后一份工作是在 IBM Rational 软件,他是有名的“RUP 倔老头”,同时也是最早的 Rational Suite 团队成员之一。 他是《小型团队软件开发:以 RUP 为中心的方法》(Software Development for Small Teams: A RUP-Centric Approach)一书的主要作者,该书由 Addison-Wesley 于 2004 年出版。他拥有数学专业文学学士学位,以及计算机科学理学硕士学位。



2007 年 9 月 17 日

rubies上个月我谈到了一些动态语言和它们的一些能力。我非常浅显地讨论了两种较流行的语言,Perl 和 Python,中的一些共有特性和差别。这个月我将更深入探讨最近一些年一直流行的语言,Ruby。我将分析 Ruby 提供的一些有趣的特性,以及人们用它创建的程序类型。

为什么是 Ruby?

虽然 Ruby 出现了十多年了,为什么最近如此流行?一年中,它在 Tiobe 索引中,比任何其他主要语言出现得都多。1 我认为有许多导致兴趣高涨的原因。首先,那些真正热爱在 Smalltalk 中编程的人将 Ruby 作为动态语言的选择。与整个程序设计人群相比,这是相当少的人数,但他们狂热地致力于该语言,并且几乎所有我见过的 Smalltalk 专家都是内行的面向对象程序设计人员。许多 Smalltalk 迷在 Agile 运动中很积极,并且促进了 Smalltalk 及其 Agile 的认证。当然在许多会议上能听到他们的呼声。

广泛的 IDE 选择

我是一个工具师,并且喜欢使用软件工具来构建软件。有相当多用于 Ruby 的开发环境。本专栏的忠实读者不会对我用得相当多的 Eclipse 的 Ruby 插件感到惊讶。2 但这不是我处理 Ruby 时使用的唯一的 IDE。我已经发觉 FreeRIDE(FreeRIDE Project Wiki)非常好,特别是在我的 Linux 系统上。绑定到 Windows MSI installer 上的 SciTE 编辑器在 Windows 上工作得很好,而 Komodo 在所有平台上都工作得很好(Komodo IDE)。3 这只是我曾使用的环境中的几种。如果您在 Web 上搜索 Ruby IDE,那么您将找到更多,所以应该有一种适合您的需要。

乐趣因素

使用 Ruby 的另一个原因是它真的很有趣。Yukihiro “Matz” Matsumoto,Ruby 的创造者,给出的创建该语言的其中一个目标是令程序设计简单有趣。一旦您开始使用 Ruby,您似乎重新获得了您在开始程序设计职业时所拥有的一些欢乐和惊奇(许多年以前有时候我一想它就发抖)。

对于我来说,Ruby 不仅“像”脚本语言,这是我大部分使用 Perl 和 Python 的目的,尽管我已经用 Python 进行过许多程序设计。虽然 JavaScript 和 PHP 已经广泛用于 Web 应用程序的构建,但是他们仍旧像脚本语言。Ruby 给我的感觉多于使用一个工艺精巧的工具。

让我们来更好一点地解释这个“乐趣因素”。一些语言非常强大,但它们就像推土机。它们在擅长的方面做得非常好,但当您努力想进行不擅长的方面时,并且想要性能和简练时,推土机不是您要使用的东西。许多语言提供大量特性,但它们似乎不能很好地配合。这些“洗碗槽”语言试图为每个人做所有事。它们为做同一件事提供许多方法。我们在 Ruby 上发现这些。Ruby 也有一些这样的特性,但不像一些语言那么多。由于我发现这令它们很容易学,所以我倾向于喜欢在我的语言中的一点最低原则。

Least Surprise 原则

另一个使我觉得使用 Ruby 很快乐的事情是 Least Surprise 原则。一旦您了解了基础,事情就似乎按照您认为它们应该的方式进行。当然,有时候您不得不在参数周围加上圆括号,或者表达式没有按照您计划的方式求值,但该原则不仅如此。让我们来看看几个实例。对于这些简单的实例,我将介绍如何将它们写成交互的 Ruby shell 程序,称为 irb。4

使用 Ruby

Java 程序设计人员知道 Java 有可以让您看到类和对象“之下”的 Reflection API。让我们假定您有某个 Java 对象,并且您想要知道响应(方法)的是什么消息,并且按照升序打印出它们的名称。以下 Java 代码(图 1)完成这些功能:

import java.lang.reflect.*;
import java.util.*;

public class MethodViewer
{
    public static void main(String[] args)
    {
        LinkedList<String> l = new LinkedList<String>();
        Method[] methods = "hello".getClass().getMethods();
        for (Method m : methods) {
            l.add(m.getName());
        }
        String[] names = new String[l.size()];
        names = l.toArray(names);
        Arrays.sort(names);
        for (String m : names) {
            System.out.println(m);
        }
    }
}

图 1:打印出排好序的 Java 对象的方法的列表

也许有一些更好的编码技巧来将代码缩短一点,但我喜欢让我的代码保持相当的可读性。

现在,让我们来看看我用 Ruby 如何做:

puts "hello".methods.sort

就这些!让我们把它拆开来看。puts 只是“打印字符串(print string)”命令。它打印出本行的字符串。表达式的下一个部分比较有趣。由‘.’分割的三个部分组成了 puts 的参数。这似乎对大多数曾经用面向对象的语言或方法语言编程的读者来说是熟悉的。该表达式是方法的组合。首先,对字符串对象“hello”调用 methods 方法。该方法返回可以用“hello”字符串调用的一组方法的名称。然后调用 sort 方法根据它们默认的排序对方法名排序。您可能会怀疑这里调用的是什么类的 sort 方法。可能是 Array 类。Ruby,像其他面向对象语言一样,支持多态性。我怎样能知道调用的是 Array 类的 sort 方法呢?我写另一个表达式来确认:

"hello".methods.class

class 方法的结果是一个 Class 类型的对象,但由于我在 irb 工具中工作,所以打印出了该表达式结果,并且我得到了它的字符串表示。也就是说,调用了 Class 对象的 to_s 方法。像 Java 的 toString 方法一样,如果该对象的类定义了 to_s 方法,那么就使用它,否则,它上溯到继承树,直到找到一个并使用它。 Object 类有一个默认的 to_s 方法,因此总会有某个结果。

对于我来说,这一切似乎讲得通,我认为它是可读的。不存在优秀的 Perl 程序设计人员常规使用的那类特殊变量。5 但这些相当简单的实例,仅仅让您初步了解了 Ruby 完成您想要它做的事情的方式。让我们来看看一个稍微大些的实例,仅仅向您展示您如何能够应用一些您已经知道的程序设计原则,以及在短时间内写出有用的程序。

生成测试代码

所有的动态语言都擅长生成数据。绝大多数时间,您会传递某类文本项,并且写出许多不同的记录。如果您是从上个月开始看的,那么您可能记得,我附带了一个介绍我交给三个以前的学生的任务(写一个评分程序)的 PDF。这里是输入的描述:6

第一个记录包含形成期末成绩的不同类型的任务的数量,n,举例来说,小测验、测试、考试,等等。下 n 个记录包含每种任务类型的名称、一个字符的缩写,以及该类型的任务对课程成绩贡献的分数(您可以将其看作期末成绩的百分比)。该数字总是整数。
余下的记录包含实际的成绩。每个记录的格式为:
T SID G P
T = 任务的类型(来自上面的一个字母的缩写), SID = 学生 ID, G = 成绩(该任务赢得的总分数),而 P = 该任务值的总分数。每个字段由一个(或多个)空格分隔。

该输入的示例看起来像:

4
Quiz Q 25
Homework H 25
Exam E 25
Project P 25
H 125 12 15
H 137 5 15
H 104 15 15
Q 147 9 10
...

测试代码

为了确保程序有效,我需要一些输入,并且我想要一个相当大型的数据集。我决定写一个 Ruby 程序来执行此任务。下面介绍了我的思想,我如何实现代码,以及代码的样子。

首先,即使 Ruby 允许您书写脚本,但我还是试图将尽量多的内容放入类中。如果您定义方法,但不把它们放入类中,那么它们会自动加入 Object 类。(记住,Ruby 中的所有内容都是对象)。现在,对于我的实例,我想要类,但不希望不得不创建类的对象。我仅仅想要把属于一起的方法放在一起。因此,我所有的方法都是类方法,就像 Java 中的静态方法一样。由于我是自顶向下进行,所以我开始在 GenerateGradeData 类中写 makeGradeData 方法。7 最初的代码看起来如下(图 2):8

class CreateGradeData
  def CreateGradeData.makeGradeData(nStudents, types)
    puts types.size
  end
end

图 2:成绩测试数据生成程序中的第一个方法

我不确定我会如何表示每类任务所需的参数,但是我知道我需要表示我想要多少学生,以及有多少任务类型。注意,方法定义在以下关键词之间:defend。您还会注意到,我不需要在语句之间(除了特定情况下)加入语句分隔符,像分号。将类名作为方法名的前缀来指示该方法是类的方法。

图 2 中的程序没做太多事情,但它打印出了测试数据的第一行:任务类型的数量。这里,我可以通过在我的文件中的类定义之后添加以下代码行来测试:

 CreateGradeData.makeGradeData(5, [1, 2, 3])

我所输入的学生的数量是多少,甚至我放入的对象的类型是什么都没关系,因为在程序中用不到。而我唯一用到 types 数组的地方是取它的大小,这样我可以添加任意类型的有大小的对象。在这种情况下,它是三个对象的数组。当我运行该脚本时,它打印出数字‘3’。我们正在取得进展。9

为了书写下 n 行测试数据,我需要知道每个任务类型的名称、其缩写,和对于此类型的总分。因此,我知道,每个 types 数组元素都必须有这三项。我决定将每个元素本身做成数组。现在我通过添加一行来变更 makeGradeData 方法(图 3):

class CreateGradeData
  def CreateGradeData.makeGradeData(nStudents, types)
    puts types.size
    types.each {|i| puts i[0] + " " + i[1] + " " + i[2].to_s}
  end
end

图 3:输出任务类型。

附加的行展现出了 Ruby 中我最喜欢的特性之一,代码块。方法可以有相关联的代码块。在此情况下,each 方法都有一个代码块,并且被与之相关的括号括起来。10 在执行中的某个时刻,each 方法执行 yield 指令,在 Array 类的 each 方法的情况下,给该指令一个正巧成为列表中下一个元素的参数。该参数绑定到块中的一个参数上,由 |i| 指示。每次执行 each 方法时,就会执行代码块,并且打印出包含任务类型,i,前三个元素的字符串。记住,类型是数组,所以我可以用下标。数组第三个元素是该任务类型的分数,所以我必须调用 to_s 方法,将该整数转换为字符串。

现在,我变更调用 makeGradeData 的方式,来做这些:

t = [
  ["Quizzes", "Q", 10],
  ["Exams", "E", 25],
  ["Homework", "H", 25],
  ["Project", "P", 30],
  ["Participation", "C", 10]
];
CreateGradeData.makeGradeData(25, t)

我的输出现在如下:

5
Quizzes Q 10
Exams E 25
Homework H 25
Project P 30
Participation C 10

接下来,创建我需要的学生 ID 也许是个好主意。我只要创建某组 nStudents 数字。这应该是相当随机的,尽管我也可以传入一组数字,但是我想要让程序做这件事。因此,我将写一个方法,makeStudents,我可以用我需要的学生数作为参数调用它,并且它将返回一组学生 ID。此方法的代码如图 4。

def CreateGradeData.makeStudents(nStudents)
    result = []
    (0..(nStudents-1)).each do |i|
      x = 10000 + rand(90000)
      while result.include?(x) do
        x = 10000 + rand(90000)
      end
      result[i] = x
    end
    return result
  end

图 4:创建随机的一组学生 ID

这是另一个类方法。我做的第一件事情是创建保存学生 ID 的数组。现在我需要生成适当数量的学生 ID。我决定学生 ID 应该为五位数子,从 10000 到 99999。Ruby 正好给出了我需要的。首先,创建了一个 Range 对象:(0..(nStudents-1))。这是一个表示从 0 到值 nStudents-1 的整数范围的对象。由于在上面的实例输入中我已经使用了 25 个学生,所以范围是 0..24。并且,它是我可以对其发消息的对象。在这种情况下,我发送 each 消息,以及 do 和 end 之间的代码块,并且将范围中下一个数绑定到 i 参数上。我需要 i 从 0 开始,因为我想要将它用作学生 ID 的结果数组的索引。我获得 0 和 89999 之间的一个随机数,并且加上 10000,以获得所需范围内的数字。接下来,我确保该数字不在 ID 集合中,当我确信不在时,我将其加入数组中。

注意用于检查结果数组中是否包含该数字的方法的名称。方法名末尾的问号意思是该方法在测试某些东西,并且将返回 True 或 False。这是个惯例,不是需求。

剩下要做的唯一事情是生成个别成绩。我对 makeGradeData 做了最后的变更,如图 5 所示。对于每个任务类型,我用任务类型和学生 ID 数组作为参数调用 makeGrades 方法。

def CreateGradeData.makeGradeData(nStudents, types)
    puts types.size
    types.each {|i| puts i[0] + " " + i[1] + " " + i[2].to_s}
    studentIds = CreateGradeData.makeStudents(nStudents)
    types.each {|i| CreateGradeData.makeGrades(i, studentIds)}
  end

图 5:最后的 makeGradeData 方法

剩下要处理的仅有的一些问题是如何确定为每个任务生成的成绩的范围,以及每种类型的任务要生成多少。我决定再次在 types 参数中使用嵌套数组。types 数组的每个元素的结构是:

  • 任务类型的名称
  • 它的缩写
  • 关于期末成绩的分数
  • 该类型任务的数组,其中每个元素是一个两个元素的数组,该数组包含为任务生成的最高和最低分值

现在,完整的输入及对 makeGradeData 的调用如下:

t = [
  ["Quizzes", "Q", 10, 
	[[0, 10], [0, 10], [0, 10], [0, 10], [0, 10], [0, 10]]],
  ["Exams", "E", 25, [[50, 100],[40, 100]]],
  ["Homework", "H", 25, 
	[[5, 25], [10, 50], [15, 50], [8, 20], [0, 15], [5, 20]]],
  ["Project", "P", 30, [[40, 75], [5, 25]]],
  ["Participation", "C", 10, [[0, 10]]]
];
CreateGradeData.makeGradeData(25, t)

我创建了两个用来完成测试数据生成器的额外方法。这些方法如图 6 所示。

def CreateGradeData.makeGrades(type, sIds)
    a = type[1]     # abbreviation 
    g = type[3]     # array of generating parameters
    g.each do |i|
      sIds.each{|s| CreateGradeData.generateGrade(s, a, i)}
    end
  end

  def CreateGradeData.generateGrade(sId, a, g)
    grade = g[0] + rand(g[1]-g[0]+1)
    puts a + " " + sId.to_s + " " + grade.to_s + " " + g[1].to_s
  end

图 6:程序中余下的方法

makeGrades 方法中,我使用了嵌套在一个代码块中的另一个代码块。这是嵌套的迭代。对于每个任务(在 g 中)代码通过学生 ID 进行循环,并且生成适当范围内的成绩。

该程序总共包含三十行代码(一些仅仅是 end 语句)。依我看来,它还是十分易读的。您可以看看此程序,以及注释。

结束语

本文仅仅浅谈了一些 Ruby 的特性。许多程序设计人员告诉我,自从它们开始用 Ruby 进行程序设计以来,是怎样再次真正地喜欢上程序设计的。我认为仅仅为此,我们就该好好“谢谢” Matz。但关于 Ruby 远比我在这里向您介绍的要多。我希望您花些时间看看下面的一些参考资料。

我还计划谈论用于开发 Ruby 的 Web 应用程序的 Rails 框架,但不得不等到下次了。

参考资源

Programming Ruby,2ed.,David Thomas 和 Andrew Hunt 著,Pragmatic Bookshelf,2004 年出版,ISBN 0974514055。

Ruby Cookbook,Lucas Carlson 和 Leonard Richardson 著,O'Reilly Media,Inc. 2006 年,ISBN 0596523696。

Programming Ruby,上面书籍的在线版本,但您会想要纸制的书籍。这是您用计算机时很好的参考资料。http://www.ruby-doc.org/docs/ProgrammingRuby/

Ruby-doc.org,关于 Ruby 的在线资源。http://www.ruby-doc.org/

Try Ruby!,Ruby 的交互在线教程。http://tryruby.hobix.com/

Learning Ruby,Daniel Carrera 写的一篇很好的扩展教程。http://www.math.umd.edu/~dcarrera/ruby/0.3/

注释

1参见 http://www.tiobe.com

2http://rubyeclipse.sourceforge.net/ 可以找到有关 Ruby Development Tools(RDT)插件的信息。还有一篇 developerWorks 上的 Neal Ford 写的关于 RDT 的文章。

3 Komodo 致力于多种动态语言,如果您打算用不同的语言进行许多工作的话,就值得一看。(使用 Eclipse 插件 Ruby Development Tools)。还有针对 Python(http://pydev.sourceforge.net/)和 Perl(http://e-p-i-c.sourceforge.net/)的 Ruby 插件。

4 irb 程序,或类似的交互 shell,与大多数 Ruby 实现捆绑在一起,并且一些提到的 IDE 中内嵌了 irb。

5 Ruby 有许多特殊变量,虽然它为那些想要特殊变量的人提供了这些内容,但是还是存在其他更易读的完成同样工作的方法。我见过的大多数 Ruby 代码避开了太多的特殊变量的使用。

6 这取自上个月专栏中附带的归档中的 SPEC 文件:http://public.dhe.ibm.com/software/dw/rationaledge/jul07/pollice_june07.zip

7 Ruby 有一些严格的命名规范。类名必须以大写字母开头,方法以小写字母开头,实例变量的前缀是‘@’,而类变量的前缀是‘@@’。起初这些规则给为带来了些小的烦扰,但每种语言都有一些您必须遵照的限制。我很快就克服了。

8 我在代码中确实做了一些注释,但本次讨论中将不考虑它们。

9 Ruby 中有一个类似于 Java 的 JUnit 的单元测试框架。我常常使用它,但对于此实例,我仅仅想要确保在不担心回归的情况下,它达到了我预期的目标。

10 如果代码块需要一行以上,您就把它放在关键字 defend 之间来代替在括号之间。

参考资料

学习

讨论

  • 参与论坛讨论
  • 现在开办了一个特别为 Rational Edge 的文章创办的 新论坛,现在您就可以分享您对本文或本期杂志或以前杂志中的其他文章的想法。阅读世界各地您的同行们所说的内容,生成您自己的讨论,或者加入正在进行的讨论。单击 这里 开始。
  • 全球 Rational 用户组社区

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Rational, Open source
ArticleID=255737
ArticleTitle=Rational Edge: Ruby:编程语言中的精华
publish-date=09172007