Day6 读书笔记&心得体会

2017-06-01  本文已影响14人  柳辉

一、读书笔记
3.4 剩余部分
当对象需要访问同类的其他对象的内部状态时,使用保护访问(protected access)方式。例如,我们希望单个的Account对象能够比较它们的原始余额,而对其余所有对象隐藏这些余额(可能因为我们要以一种不同的形式表现它们)。

class Account
  attr_reader :cleared_balance
  protected :cleared_balance
  def greater_balance_than(other)
    return @cleared_balance > other.cleared_balance
  end
end

因为属性balance是protected,只有Account的对象才可以访问它。

3.5 变量

现在我们已经辗转创建了所有这些对象,让我们确保没有丢掉它们,变量用来保存这些对象的印迹;每个变量保存一个对象的引用。
让我们通过下面的代码来验证。

person = "Tim"
person.object_id # 70176096289900
person.class # String
person # "Tim"

第一行代码,Ruby使用值“Tim”创建了一个String对象,这个对象的一个引用(reference)被保存在局部变量person,接下来的快速检查展示了这个变量具备字符串的特性,它具有对象的ID、类和值。

那么,变量是一个对象吗?在Ruby,答案是“不”,变量只是对象的引用,对象漂浮在某处一个很大的池中(大多数时候是堆,即heap中),并由变量指向它们。

让我们看一下稍复杂的例子:

person1 = "Tim"
person2 = person1
person1[0] = "J"

person1 -> "Jim"
person2 -> "Jim"

发生了什么?

原来的person1是"Tim",然后我们更改了person1的第一个字母。结果person1和person2都从"Tim"变成了"Jim"。

这都归结于变量保存的是对象引用,而非对象本身这一事实。将person1赋值给person2并不会创建任何新的对象;它只是将person1的对象引用赋值给person2,因此person1和person2都指向同一对象。

复制别名(alias)对象,潜在地给了你引用同一对象的多个变量。但这不会在你的代码中导致问题?它会的,但是并不向你想象的频繁(例如Java中的对象,也以相同的方式运作)。例如,在插图3.1的例子中,你可以通过使用String的dup方法来避免创建别名,它会创建一个新的、具有相同内容的String对象。

person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
person1 -> "Jim"
person2 -> "Tim"

你可以通过冻结一个对象来阻止其他人对其进行改动,试图更改一个被冻结的对象,Ruby将引发(raise)一个TypeError异常。

person1 = "Tim"
person2 = person1
person1.freeze -> prevent modifications to the object
person2[0] = "J"

第四章 容器、Blocks和迭代器

只有一首歌曲的点唱机是不可能受欢迎的,所以我们应该开始考虑建立一个歌曲目录和待播放歌曲列表。
它们都是容器(containers),所谓容器是指含有一个或多个对象引用的对象。

目录和播放列表需要一组相似的方法:添加一首歌曲,删除一首歌曲,返回歌曲列表等等。播放列表可能还需要执行额外的任务,例如偶尔插播广告或者记录累计的播放时间,不过我们在后面才考虑这些问题,现在看来,开发一个通用的SongList类,然后将其特化(specialize)为目录和播放列表类,似乎是个好主意。

4.1 容器(containers)

开始实现之前,我们需要决定如歌在SongList对象中存储歌曲列表。目前有3个明显的选择:(1)使用Ruby的Array(数组);(2)使用Ruby的Hash(散列表);(3)自定义列表结构。

4.1.1 数组

数组类含有一组对象引用,每个对象引用占数组中的一个位置,并由一个非负的整数索引来标识。

可以通过使用字面量(literal),或显式地创建Array对象,来创建数组,字面量数组(literal array)只不过是处于方括号中的一组对象。

b = Array.new
b.lenght -> 0
b.class -> array
b[0] = "second"

数组由[]操作符来进行索引,和Ruby的大多数操作符一样,它实际上是一个方法(Array类的一个实例方法),因此可以被子类重载,如上面例子所示,数组的下标从0开始,使用非负整数访问数组,将会返回出于该整数位置上的对象,如果此位置上没有对象,则返回nil,使用负整数访问数组,则从数组末端开始计数。

a = [1,2,3]
a[-1] -> 3
a[-2] -> 2
a[-99] -> nil

你也可以使用一对数字[start, count]来访问数组,这将返回一个包含从start开始的count个对象引用的新数组。

a = [1, 2, 3]
a[0..2]  -> [1,2,3]

最后,你还可以使用range来对数组进行索引,其开始和结束位置被两个或者3个点分隔开,两个点的形式包含结束位置,而3个点的形式不包含。

a = [1, 2, 3]
a[1] = 'bag' -> [1, "bag", 2, 3]
a[5] = 66 -> [1, "bag", 2, 3, nil, 66]

数组还有大量的其他有用的方法,使用这些方法,你可以用数组来实现栈(stack)、收集(set)、队列(queue)、双向队列(dequeue)和先进先出队列(fifo)。

4.1.2 散列表
Hashed(也称关联数组、图或词典)和数组的相似之处在于他们都是被索引的对象引用集合,不过数组只能用整数来进行索引,而hash可以用任何类型的对象来进行索引,比如字符串、正则表达式等等。当你将一个值存入hash是,其实需要提供两个对象,一个索引(key),另一个值。随后,你可以通过键去索引hash以获得其对应的值,hash中的值可以是任意类型的对象。

下面的例子使用了hash字母符表示法:处于花括号之间的key =>value配对的列表。

h = { 'dog' => 'canie', 'cat' => 'feline', 'donkey' => 'asinine' }
h.length ->3
h['dog'] -> "canine"
h['cow'] = "bob dy"

和数组相比,hashes有一个突出的优点:可以用任何对象做索引,然而它也有一个突出的去缺点:它的元素是无序的,因此很难使用hash来实现栈和队列。

你会发现hash是ruby最常用的数据结构之一。

4.1.3 实现一个SongList容器

在简单介绍了数组和hash后,该来实现点唱机的SongList了,让我们来设计一组SongList类所需要的基本方法,之后我们逐步扩充,但现在这些已经足够了。

添加给定的歌曲列表中。

delete_first() -> song

删除列表的第一首歌曲,并返回该歌曲。

delete_last -> song

删除列表的最后一首歌曲,并返回该歌曲。

[index] -> song

返回指定名字的歌曲

这个列表为如何实现SongList给出了提示。既能在尾部添加歌曲,又能在头部删除歌曲,这提示我们使用双向队列(即有两个头部的队列)可以使用Array来实现它。

同样,数组也支持返回列表中整数位置歌曲。

然而我们也需要使用歌曲名来检索歌曲,这可以通过以歌曲为键、歌曲为值的hash来实现。我们可以用hash吗?也许可以,但是我们会发现,这样会有问题。

首先,hash是无序,所以我们可能需要一个辅助数组来跟踪歌曲列表。第二,hash不支持多个键对应一个相同的值,这对实现播放列表不利,因为播放列表可能需要播放同一首歌多次,因此目前我们先使用数组来实现,并在需要的时候搜索歌名,如果这成为瓶颈,我们可以添加一些基于hash的搜索功能。

SongList类的实现以一个基本的initialize方法开始,该方法会创建容纳歌曲的数组,并将该数组的引用存储到实例变量@songs中。

Class SongList
  def initialize
    @songs = Array.new
  end
end

SongList#append方法将给定的歌曲添加到@songs数组的尾部,它会返回self,即当前SongList对象的引用。这是一本很有用的惯用法,可以让我们把对append的对个调用连接在一起。举个栗子:

class SongList
 def append(song)
   @songs.push(song)
 end
end

接着我们来添加delete_first和delete_last方法,它们分别用Array#shift和Array#pop来实现。

class SongList
  def delete_first
    @songs.shift
  end
  def delete_last
    @songs.pop
  end
end

到目前为止,都还不错哦,下一个要实现的方法是[],用它通过下标来访问元素。这种简单的代理形式的方法在Ruby代码中经常可见:如果你的代码含有大量一两行的方法,不用担心——那是你设计正确的迹象。

class SongList
  def [](index)
    @songs[index]
  end
end

现在做个简单的测试,我们将使用Ruby标准发行版自带的一个称谓TestUnit的测试框架来做这个工作。但是不会在这里详细介绍它。assert_equal方法检查它的两个参数是否相等,如果不等则立即报错。同样,assert_nil方法在它的参数不为nil时也会报错,我们将会使用这些来保证从列表中删除适当的歌曲。

测试需要必要的初始化,以告诉RUby使用TestUnit测试框架,并告诉该框架我们在写一些测试代码。然后创建一个SongList对象和4首歌曲对象,并添加歌曲到列表中(趁机炫耀一下,我们利用了“append”返回SongList对象)这一特点来连接方法调用,

接着,我们测试[]方法,验证它是否返回了指定下标处的正确的歌曲(或者nil)。最后,我们从列表的首位删除歌曲,并验证返回正确的歌曲。

require 'test/unit'

class TestSongList < Test::Unit::TestCase
  def test_delete
    list = SongList.new
    s1 = Song.new("title1", "artist1", 1)
    s2 = Song.new("title2", "artist2", 2)
    s3 = Song.new("title3", "artist3", 3)
    s4 = Song.new("title4", "artist4", 4)

    list.append(s1).append(s2).append(s3).append(s4)

    assert_equal(s1, list[0])
    assert_equal(s3, list[2])
    assert_nil(list[9])

    assert_equal(s1, list.delete_first)
    assert_equal(s2, list.delete_first)
    assert_equal(s4, list.delete_last)
    assert_equal(s3, list.delete_last)
    assert_nil(list, delete_last)
  end
end

是时候添加搜索功能了,这要求遍历列表中的所有歌曲,并检查每首歌的名字,为了实现这项功能,我们先说一下:## 迭代器

4.2 Blocks 和 迭代器

实现SongList的下一个问题是实现with_title方法,该方法接受一个字符串参数,并返回以此为歌名的歌曲,有个直接实现的方法:因为我们有歌曲数组,所以可以遍历该数组的所有元素,并查找出匹配的元素。

class SongList
  def with_title(title)
    for i in 0...@songs.length
      return @songs[i] if title == @songs[i].name
    end
    return nil
  end
end

这个方法确实可行,而且也相当常见:用for循环遍历数组,但是有没有更自然的方式呢?

的确有,在某种程度上,for循环和数组耦合过于紧密:需要知道数组的长度,然后依次获得其元素的值,直到找到一个匹配为止,为什么不只是请求数组对它的每一个元素执行一个测试呢?这正是数组的find方法要做的事情。

class SongList
  def with_title(title)
    @songs.find{|song| title == song.name}
  end
end

find方法是一种迭代器,它反复调用block中的代码。迭代器和block是Ruby最有趣的特性之一,所以我们花一点时间来了解它们。

4.2.1 实现迭代器(implementing lterators)

Ruby的迭代器只不过是可以调用block的方法而已,咋看之下,Ruby的代码区块和C、Java、C#、Perl的代码区块很相似,实际上这有点蒙蔽人——Ruby的block不是传统的意义上的、将语句组织在一起的一种方式。

首先,block在代码中只和方法调用一起出现;block和方法调用的最后一个参数处于同一行,并紧跟在其后(或者参数列表的右括号的后面)。其次,在遇到block的时候,并不立刻执行其中的代码。Ruby会记住block出现时的上下文(局部变量、当前对象等)然后执行方法调用。

在方法内部,block可以像方法一样被yield语句调用,每执行一次yield,就会调用block中的代码,当block执行结束时,控制返回到紧随yield之后的那条语句。我们来看个简单的例子。

def three_times
  yield
  yield
  yield
end
three_times { puts "Hello" }

block(花括号内的代码)和对方法three_times的调用联合在一起。该方法内部,连续3次调用了yield。每次调用时,都会执行block中的代码,并且打印出一条欢迎信息。更有趣的是,你可以传递参数给block,并获得返回值,例如,我们可以写个简单的函数返回低于某个值的所有Fibonacci数列项。

def fib_up_to(max)
  i1, i2 = 1, 1 # 并行赋值(i1 = 1 and i2 = 1)
  while i1 <= max
    yield i1
    i1, i2 = i2, i1+i2
  end
end
fib_up_to(1000) { |f| print f, " " }

输出结果:

1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

注意:程序员迷们会很乐意看到yield关键字,它模仿了Liskov的CLU语言中的yield函数,CLU是一个超过20年之久的语言,其包含的特性还未被完全开发出来。
基本Fibonnacci数列是以两个1开头的整数序列,其中的每一项都是它的前两项的和,它常被用在排序算法和自然现象分析中。

在这个例子中,yield语句带有一个参数,参数值将被传送给相关的block。在block定义中,参数列表位于两个竖线(管道符)之间,在这个例子中,变量f收到yield的参数的值,所以block能够输出数列中的下一个项(这个例子也展示了并行赋值的用法)。尽管通常block只有一个参数,但这不是必然的,block可以有任意数量的参数。

如果传递给block的参数是已存在的局部变量,那么这些变量即为block参数,它们的值可能会因为block的执行而改变,同样的规则适用于block内的变量:如果它们第一次出现在block内,那么它们就是block的局部变量。相反,如果它们先出现在block外,那么block就与外部环境共享这些变量。

在下面这个(认为设计)的例子中,我们看到了从外部环境中继承了变量a和b,而c是block的局部变量(defined?方法在其参数没有定义时返回nil)。

a = [1, 2]
b = 'cat'
a.each { |b| c = b * a[1] }
a -> [1, 2]

block 也可以返回值给方法,block内执行的最后一条表达式的值被作为yield的值返回给方法,这也是Array类的find方法的工作方式。它的实现类似于下面的代码。

class Array
  def find
    for i in 0...size
      value = self[i]
      return value if yield(value)
    end
    return nil
  end
end

输出结果:

[1, 3, 5, 7, 9].find { |v| v*v > 30 }

上面的代码把数组的元素依次传递给关联的block。如果block返回真,那么方法返回相应的元素,如果没有元素匹配,方法返回nil,这个例子展示了这种迭代器的方式的优点,数组类处理它擅长的事情,例如访问数组元素,而让应用程序代码集中精力处理特殊需求(本例的特殊需求是找到满足某些算术标准的数组项)。

一些迭代器是Ruby的许多收集(collection)类型所共有的。我们已经看了find方法,另外两个是each和collect。each可能是最简单的迭代器,它所做的就是连续访问收集的所有元素。

[ 1, 3, 5, 7, 9 ].each { |i| puts i }

输出结果:

1
3
5
7
9

echo迭代器在Ruby中有独特的作用,另一个常用的迭代器是collect,它从收集中获得各个元素并传递给block。block返回的结果被用来生成一个新的数组,例如:

["H", "A", "L"].collect { |x| x.succ } -> ["I", "B", "M"]

迭代器并不仅局限于访问数组和hash中的已有数据,从Fibonacci的例子中,我们已经看到迭代器可以返回得到的值。这个功能被RUby的输入/输出类所使用,这些类实现了一个迭代器接口以返回得到的值。这个功能被Ruby的输入/输出类所使用,这些类实现了一个迭代器接口以返回I/OL流中的连续相继的行(或字节)。下面的例子使用了do...end来定义block。这种方式定义block和使用花括号定义block的唯一区别是优先级:do...end的绑定低于{...}。

f = File.open("testfile")
f.each do |line|
  puts line
end
f.close

输出结果:

This is line one
This is line two
This is line three
And so on...

让我们再看一个有用的迭代器。inject(名字有点难理解)方法(定义在Enumerable模块中)让你可以遍历收集的所有成员以累积出一个值。例如,使用下面的代码你可以将数组中的所有元素加起来,并获得它们的累加和。

[1, 3, 5, 7].inject(0) { |sum, element| sum+element } 
[1, 3, 5, 7].inject(1) { |product, element | product*element }

inject是这样工作的:block第一次被执行时,sum被置为inject的参数,而element被置为收集的第一个元素。接下来的每次执行block时,sum被置为上次block被调用时的返回值。inject没有参数,那么它使用收集的第一个元素作为初始值,并从第二个元素开始迭代。这意味着我们可以把前面的例子写成:

  [1, 3, 5, 7].inject { |sum, element| sum+element }
  [1, 3, 5, 7].inject { |product, element| product*element }

内迭代器和外迭代器

Ruby实现迭代器的方式与其他如C++何Java等语言实现迭代器的方式,值得我们做一下比较。在Ruby中,迭代器集成于收集内部——它只不过是一个方法,和其他方法不同的是,每当产生新的值得时候调用yield。使用迭代器的不过是和该方法相关联的一个代码block而已。

在其他语言中,收集本身没有迭代器,他们生成外部辅助对象(例如Java中基于Interator接口的对象)来传送迭代器状态。从这点看来(当然还可从很多方面来看),Ruby是一种透明的语言。你在写程序的时候,Ruby语言能使你集中精力在你的工作上,而不是语言本身上。

我们值得花点时间看看为什么Ruby的内部迭代器并不总是最好的解决方案。当你需要把迭代器本身作为一个对象时(例如,将迭代器传递给一个方法,而该方法需要访问由迭代器返回一个值),它的表现欠佳了。另外,使用RUby内建的迭代器模式也难以实现并行迭代两个收集。幸运的是,Ruby提供了Generator库,该库为解决这些问题实现了外部迭代器。

4.2.2 事物 Blocks
尽管block通常和迭代器合用,但它还有其他用处。我们来看其中几个用法。
block可以用来定义必须运行在事务控制环境下的代码。比如,你经常需要打开一个文件,对其内容做些处理,然后确保在处理结束后关闭文件。尽管可以用传统的方式实现,但也存在“应该由文件负责自身的关闭”这样的观点。我们可以用block来实现这种需求。如下是一个简单且忽略了错误处理的例子:

class File
  def File.open_and_process(*args)
    f = File.open(*args)
    yield f
    f.close()
  end
end

File.open_and_process("testfile", "r") do |file|
  while line = file.gets
    puts line
  end
end

输出结果:
This is line one
This is line two
This line three
And so on...

open_and_process是一个类方法,它可以独立于任何file对象来被使用。我们希望它接受与传统的File.open一样的参数,但并不关心这些参数到底是什么?所以,我们用args表示参数,这意味着“把传递给这个方法的实际参数收集到名字为args的数组中。”然后我们调用File.open,并以args作为参数。它将把数组参数扩展成独立的参数,最终的结果是,open_and_process透明地将它所接收的任意参数都传递给了File.open。

一旦文件被打开,open_and_process将调用yield,并传递打开的文件对象给block。当block返回时,文件即被关闭,通过这种方式,关闭打开文件的责任从文件使用者身上转移到了文件本身。

让文件管理它自己的生命周期的技术如此重要,以至于Ruby的File类直接支持了这项技术,如果File.open有个关联的block,那么该block将被调用,且参数是该文件对象。当block执行结束时,文件会被关闭,这非常有趣,因为它意味着File.open有两种不同的行为:当和block一起调用时,它会执行该block并关闭文件;

当单独调用时,它会返回文件对象,使得这种行为成为可能的是,Kernel.block_given?方法,当某方法和block关联在一起调用时,Kernel.block_given?将返回真。使用该方法,可以用下面的代码(同样也忽略了错误处理)实现类似于标准的File.open方法。

class File
  def File.my_open(*args)
    result = file = File.new(*args)

    if block_given?
      result = yield file
      file.close
    end

    return result
  end
end

还有一点不足:前面的例子在使用block来控制资源时,我们还没有解决错误处理问题,如果想完整实现这些方法,那么即使处理文件的代码由于某种原因异常中断,我们也需要确保文件被关闭。后面谈到的异常处理可以解决这个问题。

4.2.3 Blocks可以作为闭包

让我们再回到点唱机上(还记得点唱机的例子吧)。在某些时候,我们需要处理用户界面——用户用来选择歌曲和控制点唱机的按钮。我们需要将行为关联到这些按钮上:当按“开始”按钮时,开始播放音乐。事实证明,Ruby语言的block是实现这种需求的合适方式,假设点唱机的硬件制造商实现了一个Ruby扩展,该扩展提供了一个基本的按钮类。

start_button = Button.new("Start")
pause_button = Button.new("Pause")

当用户按其中一个按钮时会发生什么呢?硬件开发人员做了埋伏,使得按钮按下时,调用Button类的回调函数button_pressed。向按钮类中添加功能的一个显而易见的方式是穿件Button类的子类,并让每个子类实现自己的button_pressed方法。

class StartButton < Button
  def initialize
    super("Start")
  end
  def button_pressed
    # do start actions
  end
end

start_button = StartButton.new

这样做有两个问题,首先,这会导致大量的子类,如果Button类的接口发生变化,维护代价将会提高,其次,按下按钮引发的动作所处层次不当:它们不是按钮的功能,而是使用按钮的点唱机的功能。使用Block可以解决这些问题。

songlist = SongList.new
class JukeboxButton < Button
  def initialize(label, &action)
    super(label)
    @action = action
  end

  def button_pressed
    @action.call(self)
  end
end

start_button = JukeboxButton.new("Start") { songlist.start }
pause_button = JukeboxButton.new("pause") { songlist.pause }

上面代码的关键之处在于JukeboxButton#initialize的第二个参数,如果定义方法时在最后一个参数前加一个&(例如&action),那么当调用该方法时,Ruby会寻找一个block。block将会转化成Proc类的一个对象,并赋值给了实例变量@action。这样当回调函数button_pressed被调用时,我们可以Proc#call方法去调用相应的Block。

但是,当我们创建Proc对象时,到底获得了什么?有趣的是,我们得到的不仅仅是一堆代码。和block(以及Proc对象)关联在一起的还有定义block时的上下文,即self的值、作用域内的方法、变量和常量。Ruby的神奇之处是,即使block被定义时的环境早已消失了,block仍然可以使用其原始作用域中的信息。在其他语言中,这种特性称之为闭包。

让我们来看一个故意设计的例子,该例使用了lambda方法,该方法将一个block转换成了Proc对象。

def n_times(thing)
  return lambda { |n| thing * n }
end

p1 = n_times(23)
p1.call(3)
p1.call(4)
p2 = n_times("Hello ")
p2.call(3)

n_times方法返回引用了其参数thing的Proc对象。尽管block被调用时,这个参数已经出了其作用域,但是block仍然可以访问它。

4.3 处处皆是容器

容器、block和迭代器是Ruby的核心概念,用Ruby写的代码越多,你就会发现自己对传统循环结构使用的越少。你会更多的写支持迭代自身内容的类,而且你会发现这些代码精简并易于维护。

第5章 标准类型

到目前为止,我们已经对点唱机有了好玩的实现,但同时有所取舍,前面我们降到了数组、散列、proc。但还没有真正谈到Ruby中的其他一些基本类型:数字(number)、字符串、区间(range)和正则表达式。

5.1 数字

Ruby支持整数和浮点数。整数可以是任何长度(其最大值取决于系统可用内存的大小)。一定范围内的整数(通常是-230到230-1或-262到262-1)在内部以二进制形似存储,它们是Fixnum类的对象。这个范围之外的整数存储在Bignum类的对象中(目前实现为一个可变长度的短整型集合)。这个处理是透明的,Ruby会自动管理它们之间的来回转换。

num = 81
6.times do
  puts "#{num.class}: #{num}"
  num *=num
end

输出结果:

Fixnum:81
Fixnum:6561

在书写整数时,你可以使用一个可选的前导符号,可选的进制指示符(0表示八进制, 0d表示十进制[默认]),0x表示十六进制或者0b表示二进制),后面跟一串符合适当进制的数字。下划线在数字串中被忽略(一些人在更大的数值中使用它们来代替逗号)。

控制字符的整数值可以使用?\C-x和cx(x的control版本,是x&0x9f)生成,元字符(x | 0x80^2)可以使用?\M-x生成。元字符和控制字符的组合可以使用?\M-\C-x生成。可以使用?\序列得到反斜线字符的整数值。
?a => 97 # ASCII character
?\n => 10 # code for a newline (0x0a)
?\C-a => 1 # control a = ?A & 0x9f = 0x01
?\M-a => 225 #meta sets bit 7
?\m-\C-a =>129
?\C-? => 127

与原声体系结构的double数据类型相对应,带有小数点和/或幂的数字字面量被转换成浮点对象。你必须在小数点之前和之后都给出数字(如果把1.0e3写成1.e3,Ruby会试图调用Fixnum类的e3方法)

二、心得体会
今天完成了什么?

今天的收获?

其他收获

上一篇下一篇

猜你喜欢

热点阅读