Skip to content

Instantly share code, notes, and snippets.

@cicihu
Last active December 24, 2015 04:29
Show Gist options
  • Save cicihu/6744400 to your computer and use it in GitHub Desktop.
Save cicihu/6744400 to your computer and use it in GitHub Desktop.
关于 Ruby 的技术笔记

数字的字面表示法 - Numerical Literals

Ruby 允许我们使用以下前导符号来表示不同的进制基数:

前导符号 进制 范例 转换为十进制
0b 二进制 0b10_0100 #=> 42
0 八进制 0377 #=> 255
0d 十进制(缺省) (0d)12_345 #=> 12345
0x 十六进制 0xabcd #=> 43981

如上表所示,数字之间可以插入 _ 而不会影响数字的意思,这有助于清晰的分辨数字的位数。

十进制是最常用的数字进制,因此前导符号 0d 是可以省略的;其他进制也很重要,下面以八进制和二进制为例来看看如何操作文件的权限:

require 'fileutils'
include FileUtils

# 如果你对 Unix 的文件系统不陌生的话,下面的 0755 应该很眼熟吧?
chmod 0755, 'filename'

# 如果你对 rwx -> 八进制 不在行的话,也可以用二进制来做转化:
#
# U  G  O
# rwxr-xr-x
# 111101101

permission = 0b111_101_101
permission.to_s(8)  #=> 755
chmod permission, 'filename'

字符的字面表示法 - Character Literals

和 C 不同,Ruby 没有字符数据类型,但是 Ruby 有字符的字面量表示法:

?c        #=> "c"
?l        #=> "l"
?\n       #=> "\n" (newline)
?\C-a     #=> "\u0001" (control a)
?\M-\C-a  #=> "\xE1" (meta and control a)

如你所见,字符字面量不过是一个长度为 1 的字符串罢了,根据 Programming Ruby 1.9&2.0 (4th edition) 所述:

Do yourself a favor and forget this section. It's far easier to use regular octal and hex escape sequences thant to remember these ones. Use "a" rather than ?a, and use "\n" rather than ?\n.

不过字符字面量也有其用武之地,比如下例:

case $stdin.getc.downcase
when ?y then puts "Proceeding..."
when ?n then puts "Aborting."
else puts "I don't understand."

我们使用 $stdin 来获取用户的输入,接着作为条件来控制代码逻辑,这也不错。

单元测试 - Unit Test

什么是单元测试?为什么需要它?

单元测试是专注于一小段代码(故称“单元”)的测试方法,所谓“单元”,通常是指一个独立的方法甚或是一个方法中的某几行代码。单元测试有别于其他测试手段的根本之处就在于它不去考虑整个系统的全局交互。

为什么如此严格的规定?因为根本上所有的软件都是构建在许多层次之上;在某个层次上编写的代码会依赖于在其下某层次里代码的正确运行。如果被依赖的代码包含错误,那么所有更高层次的代码都会受到潜在的影响。

某人在某一时刻编写了包含错误的代码,或许两个月之后你就会调用它,结果造成了错误使得你不得不回过头来去检查他的代码里存在的问题。当你问他为什么要这样写的时候,他的回答往往是:“我记不得了,那可是两个月之前的事了!”

如果这段代码有单元测试覆盖着又会怎样?至少会发生以下三件事:

  1. 他对代码的记忆不会随着时间的流逝而变得模糊,只要看一眼测试便能够回想起当初编写这些代码的动机;

  2. 因为这些测试只和他所编写的代码相关且粒度很小,所以他可以毫不费力的去定位错误所在而无需重新审视所有的代码;

  3. 如果当初的单元测试未能体现出今天所产生的问题,那就说明某种边界条件当时没有被考虑到。然而现在只需要简单地补上相关的测试然后进行针对性的重构即可。

单元测试帮助开发者写出更好的代码,这种助益甚至体现在写下真实的代码之前,因为思考如何编写测试会引领你去创造更优雅、更解耦的代码设计。它还能帮助你即时的反馈代码当前运行的状况。即使代码完成之后,单元测试依然发挥着它的作用,比如说在代码可运行的前提下消除错误或重构,或者帮助其他人去理解你所编写的代码。

总而言之,单元测试是好东西。

Ruby 与单元测试框架(MiniTest

Ruby 语言在进入到 1.9 时代以后,核心团队决定使用 MiniTest 来取代 Test/Unit 作为内建的单元测试框架。为什么?

因为 MiniTest 更轻量、更快速、更简单!而且这种转换的代价非常之小,因为核心团队为 MiniTest 设计了一个针对 Test/Unit 的兼容层,这使得在语法上的变化几乎为零。我们来看看一个简单的例子:

require 'minitest/autorun'

class TruthTest < MiniTest::Unit::TestCase  #=> 唯一的变化就是继承的目标
  def test_truth
    assert true
  end
end

Test/Unit 风格 vs Spec 风格

测试代码的编写风格是开发者选择测试框架的一个重要理由,先来看一下以 Test/Unit 和 RSpec 为代表的风格对比:

# Test/Unit Style
assert_equal expected, actual

# Rspec Style
actual.should_equal expected

相比而言,RSpec 提供的语法风格更加接近自然语言,这正是它广受欢迎的重要原因之一。MiniTest 的另一大优点则是兼具了这两种风格的写法,因而开发者可以自由选择自己所爱的。

首先我们来看一下上述两种风格在代码实现上是如何相互兼容的:

class Object
  def should_equal(expected)
    assert_equal expected, self
  end
end

可见,should_xxx 的语法实际上就是 assert_xxx 的一层语法糖而已。MiniTest::Spec 提供了类似的转换,只不过把 should_xxx 换成 must_xxx 而已。以下两个例子都可以运行并且效果相同,不过可以从二者的对比看出 MiniTest 是如何转换这两种风格的:

require 'minitest/autorun'

describe 'Truth' do
  it 'is truthy' do
    true.must_equal true
  end

  def test_truthy
    assert true
  end
end

class TruthSpec < MiniTest::Spec
  def test_truthy
    true.must_equal true
  end
end

注意在上例中两种风格的混用,这在 MiniTest 中是完全可行的,你可以任意调换彼此的位置来“混搭”测试代码的风格,因为实际上 MiniTest::Spec 就继承自 MiniTest::Unit::TestCase

Ruby 中的并行机制

多进程

Kernel.system

Kernel.system 在一个子进程内执行给定的命令;如果命令可以找到并且正确执行就返回 true。如果命令没有找到则抛出异常。如果命令运行之后出现了错误则返回 false,你可以在全局变量 $? 取到子进程的退出码。

system 方法的一个问题在于命令的输出位置是系统的标准输出(Standard Output),不过可以使用反引号字符来捕获子进程的输出结果:

system('date')  #=> true
`date`          #=> "2013年 9月28日 星期六 12时12分12秒 CST\n"

Kernel.system 能帮我们运行子进程并得到返回状态或结果,但往往我们需要更复杂的交互。我们需要保持和子进程的会话,向其发送数据或者从子进程那里获得数据,IO.popen 为我们提供了这种机制。

IO.popen

IO.popen 以子进程的方式运行命令并在同时把子进程的标准输入/输出和 Ruby 的 IO 对象联系起来。Ruby 程序写入这个 IO 对象,子进程可以在标准输入内读取它;反之无论子进程向这个 IO 对象写入了什么,Ruby 程序也可以从中读取。

代码块与子进程

IO.popen 运行命令的时候可以传递 IO 对象给可选的代码块:

IO.popen('date') { |io| puts "Date is #{io.gets}" }

#=> Date is 2013年 9月28日 星期六 12时12分12秒 CST

IO 对象会在代码块执行完毕后自动关闭,这就和 File.open 是一样的。

Ruby and the Web

Running Ruby as Common Gateway Interface (CGI) programs

用 Ruby 写 CGI 脚本?简单至极:

#!/usr/bin/env ruby

print "HTTP/1.0 200 OK\r\n"
print "Content-type: text/html\r\n\r\n"
print "<html><body>Hello, It's#{Time.now}!</body></html>\r\n"

然而这种写法也太低层级了,你还需要自己编写请求解析,会话管理,输出换码等等等等……幸运的是 Ruby 提供了很多内置库/基准库来帮你简化这些编码工作。

使用 CGI 基准库

Ruby 基础库提供了 CGI 类,内建许多子类/模块来帮助你方便的操作表单、会话等等。

转码

处理 URL 和 HTML 时,要时刻注意特殊字符的转码问题(注意 escapeElement):

require 'cgi'

puts CGI.escape("Cici Hu loves Ruby & JavaScript!")
#=> Cici+Hu+loves+Ruby+%26+JavaScript%21

puts CGI.escapeHTML('a < 100 && a > 0')
#=> a &lt; 100 &amp;&amp; a &gt; 0

puts CGI.escapeElement('<a href="#"><img src="example.png"></a>', 'a')
#=> &lt;a href=&quot;#&quot;&gt;<img src="example.png">&lt;/a&gt;

以上三个方法都有配套的反处理方法,分别是:CGI::unescapeCGI::unescapeHTMLCGI::unescapeElement

查询参数

通过表单传递数据是很常见的,它略微复杂些,来看一个例子:

<!DOCTYPE html>
<html>
  <head>
    <title>Test Form</title>
  </head>
  <body>
    <h1>I like Ruby because:</h1>
    <form action="cgi-bin/survey.rb">
      <p>
        <input type="checkbox" name="reason" value="flexible">
        It's flexible
      </p>
      <p>
        <input type="checkbox" name="reason" value="transparent">
        It's transparent
      </p>
      <p>
        <input type="checkbox" name="reason" value="perlish">
        It's perlish
      </p>
      <p>
        <input type="checkbox" name="reason" value="fun">
        It's fun
      </p>
      <p>
        Your name: <input type="text" name="name">
      </p>
      <p>
        <input type="submit">
      </p>
    </form>
  </body>
</html>

假设我们选了表单中的第一项和第四项,并且填写了名字为 Cici Hu,提交后请求会找到 cgi-bin/survey.rb 文件,我们就在这里处理请求传递过来的参数:

require 'cgi'

cgi = CGI.new
cgi['name']       #=> "Cici Hu"
cgi['reason']     #=> "flexible"

哎?怎么 reason 只有一项呀?因为完整的请求参数需要使用 CGI#params 来访问:

require 'cgi'

cgi = CGI.new
cgi.params        #=> {"name"=>["Cici Hu"], "reason"=>["flexible", "fun"]}
cgi.params['name']       #=> ["Cici Hu"]
cgi.params['reason']     #=> ["flexible", "fun"]

cgi.has_key?('name')     #=> true
cgi.has_key?('age')      #=> false

CGI#has_key? 可以帮助确定在请求中是否包含指定的参数。

生成 HTML(CGI::HtmlExtension

CGI::HtmlExtension 模块包含了好多方法来帮助你生成 HTML 代码,例如:

require 'cgi'

cgi = CGI.new("html5")
cgi.out do
  cgi.html do
    cgi.head { cgi.title { "Test Page" } }
    cgi.body do
      cgi.form do
        cgi.hr +
        cgi.h1 { "Leave message:" } +
        cgi.textarea("get_text") +
        cgi.br +
        cgi.submit
      end
    end
  end
end

当然了,这只是举例子说明其可能性,然而在现实中我们是不可能用这么笨的办法来生成 HTML 代码的,那是模板引擎的工作——比如说 erbHaml 等等

鸭子类型(Duck Typing)

面向对象的编程模型常常会带给人一种误解,即_“一个对象的类型就是它的类”_——对象是类的实例,类是对象的类型。

其实这并不完全正确。在大多数典型的强类型语言(如:Java)中,为了良好的构造类与对象的对应关系,每一个对象的生成都要声明其构造类的名称,这种机制正是带给人上述误解的根源。

然而面向对象的编程并不一定非要限制对象的类型如何,更为重要的是对象所表现出的行为而不是它们的身份。我们把这种理念称之为_“鸭子类型”_,它在 Ruby 语言中体现的尤其淋漓尽致。

“鸭子类型”是什么意思?

如果一个对象走起来像鸭子,叫起来也像鸭子,那么解释器就认为它是一只鸭子。

这里面关键的逻辑就在于:不是因为一个对象是鸭子(类),我们才定义它如何走如何叫(属性/方法),而是因为一个对象的(属性/方法)都和鸭子(类)表现得一样,所以我们就认为它是一只鸭子。

因此,即使该对象真的不是从鸭子类里实例化出来的,但它能够做和鸭子一模一样的事情,那么我们在代码里就可以把它当做一只鸭子来看待。当然这是比较罕见的现象,因为设计良好的代码肯定在逻辑上也是禁得起推敲的,但这里的重点是:我们使用对象的目的是“做应该做的事情”,“对象究竟是什么”反而不那么重要。

这一点在写测试的时候非常易于体现,实际上我们写测试的时候就应该专注于去描述“代码应该_做_什么”,而不是“代码应该_是_什么”。

用一个较完整的示例来说明这一点吧。

class Customer
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def append_fullname_to_file(file)
    file << "#{@firstname} #{@last_name}"
  end
end

很简单的一个类,我们要关注的是 append_fullname_to_file 方法的测试。由代码可知该方法的目的是构造 customer 的全名并将其追加到一个文件的末尾,如果我们头脑里考虑的是“代码是什么?”这个问题,那我们肯定会写出如下的测试来:

require 'minitest/autorun'
require_relative 'customer'

class TestCustomer < MiniTest::Test
  def test_append_fullname_to_file
    customer = Customer.new("Cici", "Hu")
    file = File.open("tmpfile", "w") do |f|
      customer.append_fullname_to_file(f)
    end
    file = File.open("tmpfile") do |f|
      assert_equal("Cici Hu", f.gets)
    end
    ensure
      File.delete("tmpfile") if File.exist?("tmpfile")
    end
  end
end

很自然,对吧?因为我们要把全名追加到一个文件中,那自然需要一个文件咯。但测试并非真实环境,所以我们不得不建立一个临时文件来模拟写入和读取,并且在测试的最后还得将这个临时文件删除掉。说真的,这个测试写得并不怎么样!

仔细想想——忘记“代码是什么”,考虑“代码做什么”——尽管在实际使用的时候我们的确操作的是一个文件,但对于测试来说我们只需要达成以下两个目标就好了:

  1. 全名需要放入一个“容器”;
  2. 可以从“容器”中读取全名——这是为了断言需要。

我们在真实代码中所考虑的“容器”是一个文件(类型),但测试的时候真的就必须是这个文件(类型)吗?

当然不!文件里保存的无非就是字符,在上例中我们使用 File.open 来读取文件的内容做断言,实际上读取出来的正是我们写入进去的全名(字符串)。既然如此,为什么一定要用一个文件呢?直接用字符串可不可以?

require 'minitest/autorun'
require_relative 'customer'

class TestCustomer < MiniTest::Test
  def test_append_fullname_to_file
    customer = Customer.new("Cici", "Hu")
    file = ""
    customer.append_fullname_to_file(file)
    assert_equal("Cici Hu", file)
  end
end

这样简单多了,对吗?在这个测试里你就可以把文件想象为一个字符串,因为对于 append_fullname_to_file 方法而言它需要的仅仅是一个“容器”而已,只不过我们在现实中选择了用独立的文件来做这个“容器”;然而从对象的行为角度来考虑,在此例中一个字符串和一个文件都可以很好地做到这一点。

所以,换成数组其实也是一样的:

require 'minitest/autorun'
require_relative 'customer'

class TestCustomer < MiniTest::Test
  def test_append_fullname_to_file
    customer = Customer.new("Cici", "Hu")
    file = []
    customer.append_fullname_to_file(file)
    assert_equal(["Cici Hu"], file)
  end
end

实际上,考虑到会在文件中保存多个用户的全名,数组反而会更方便测试。

这就是鸭子类型:关注代码的“行为”而不是代码的“身份”,这是面向对象编程的一条重要理念。

方法

类方法定义的风格

有两种标准的方式来定义类方法,以下是两个等价的例子:

class MyClass
  def self.class_method_foo
  end

  def self.class_method_bar
  end
end

# or

class MyClass
  class << self
    def class_method_foo
    end

    def class_method_bar
    end
  end
end

从语言的角度上来看这两种方式没什么区别,它们都能很好地工作。不过考虑到日常实践,它们则各有优点:

方式一:def self.method_name; end

  1. 更简短,便于添加单个类方法

显而易见,方式一比方式二整体少了两行代码,的确更加简短——不过谈不上更简洁,因为方式二多出的两行一点都不影响可读性。

  1. 对新手更友好

方式二中的 class << self; end 会让新手觉得很诡异吧?

方式二:class << self; end

  1. 类方法与实例方法分开定义

方式一可以把类方法和实例方法混在一起定义。如果方法定义很多,你很难一眼看出哪些是类方法哪些是实例方法。方式二则不存在这个问题。

  1. 便于编辑、搜索、及重构

记得初学 Rails 的时候,经常在定义类方法的时候忘记 self,方式二则可以帮助避免犯这种错误。此外,因为所有的方法命名都不需要加 self 了,不但看起来统一,搜索(替换)也更加方便了;还有就是重构也是一样的。

  1. 可以分开定义 私有/保护 类方法

方式二可以在 class << self; end 内使用 privateprotected 来修饰方法访问的作用域;这些定义可以和针对实例方法的区分开来。

  • 当然,如果是 def self.foo; end 这种方式,也可以用 private_class_method :foo 来显式定义私有类方法。

Ruby 中的方法调用

方法通过将自己的名字传递给接收者来完成调用:

receiver.method_name

如果接收者并未指明,self 就是接收者:

puts  #=> puts 方法调用的接收者即是:self

接收者先在它自己的类里查找方法定义,如果找不到就继续向上从它的祖先类里查找。通过包含模块添加进来的实例方法被看做是在当前类的匿名父类中定义的。

如果没有找到方法,Ruby 就为接收者调用 method_missing 方法;所以我们可以在接收者的所属类中重写 method_missing 方法来自定义找不到方法后的处理逻辑。

钩子方法(Hook Methods)

什么是钩子方法?

钩子方法是由 Ruby 解释器在特定事件发生之时从其内部调用的方法。你可以在恰当的上下文中定义这些钩子方法(用特定的名称),Ruby 会在对应的事件发生时调用它们。

钩子方法排排坐

与方法相关的

method_added, method_missing, method_removed, method_undefined, singleton_method_added, singleton_method_removed, singleton_method_undefined

与类和模块相关的

append_features, const_missing, extend_object, extended, included, inherited, initialize_clone, initialize_copy, initialize_dup

与对象编组(Object marshaling)相关的

marshal_dump, marshal_load

与强制类型转换相关的

coerce, induced_from, to_{xxx}

继承钩子方法

如果一个类定义了名为 inherited 的类方法,那么 Ruby 将在该类被继承时调用此方法。

这个钩子方法常用来让父类跟踪它的子类们。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment