一、读书笔记
回顾昨天的收获:
什么是block、proc?
block和proc是两种不同的东西, block有形无体,proc可以将block实体化, 可以把&p看做一种运算,其中&触发p的to_proc方法,然后&会将to_proc方法返回的proc对象转换成block 。https://ruby-china.org/topics/10414
我的理解就是block是以do end 或者 {}开始结束、不能单独存在、只能被方法调用、可以传递一个或者多个参数的代码块。
第5章
5.1 数组类型
在书写整数时,你可以使用一个可选的前导符号,可选的进制指示符(0表示八进制,0d表示十进制[默认]),0x表示十六进制或者0b表示二进制,后面跟一串符合是适当进制的数字,下划线在数字串中被忽略(一些人在更大的数值中使用它们来替代逗号)。
123456
0d123456
123_456
与原生体系结构的double数据类型相对应,带有小数点和/或幂的数字字面量被转换成浮点对象,你必须在小数点之前和之后都给出数字。
所有数字都是对象,并且可以对各种形式的消息做出响应,因此,与C++不一样,Ruby使用num.abs而不是abs去得到数字的绝对值。
整数也支持集中有用的迭代器,我们已经看到了一种:前面一页的代码示例中的6.times。别的迭代器还有upto和downto,它们在两个整数之间分别向上和向下迭代,另外Numberic类提供了更通用的step方法,它更像传统的for循环。
3.times { print "X " }
1.upto(5) { |i| print i, " " }
99.downto(95) { |i| print i, " " }
50.step(80, 5) { |i| print i, " " }
最后,给Perl用户提出一个警告。那些只包含数字的字符串,当在表达式中使用时,不会被自动转换成数字。所以从文件中读取数字时常常会出现问题。比如,也许我们想得到文件中每行中两个数字的和:
3 4
5 6
7 8
下面的代码不会工作。
some_file.each do |line|
v1, v2 = line.split # split line on spaces
print v1 +v2, " "
end
这里的问题是因为输入是作为字符串而不是作为数字被读取的,加号操作符把两个字符串连接起来,这正是我们在输出中所看到的,可以使用Integer方法把字符串转换成整数来解决这个问题。
some_file.each do |line|
v1, v2 = line.split # split line on spaces
print integer(v1) +integer(v2), " "
end
5.2 字符串
Ruby字符串是8比特字节的序列,通常它们包含可打印字符,但这不是一个必要条件,字符串也可以包含二进制数据。字符串是string类的对象。
字符串字面量是处于分解符之间的字符序列,常常使用它来创建字符串,可以在字符串字面量中放置各种转义序列,要不然的话,很难在程序源文件中表示二进制数据。
程序被编译时,它们会被相应的二进制值替换,字符串分解符的类型决定了要被替换的程度,在单引号字符串中,两个连续的反斜线会被一个反斜线替换,而后跟有一个单引号的反斜线变成一个单引号。
'escape using "\\" '
输出
escape using \"\\\"
双引号字符串支持更多的转义序列,最常用的恐怕是\n,回车换行符。
可以使用#{ expr }序列把任何Ruby代码的值放进字符串中。如果代码只是全局变量、类变量或者实例变量的话,花括号可以忽略。
"Seconds/day: #{ 24*60 }"
"#{' Ho!' *3}Merry Christmas!"
"This is line #$."
要插入替换的代码可以是一条或者多条语句,而不仅仅是一个表达式。
puts "now is # {def the(a)
'the ' + a
end
the('time')} for all good coders..."
输出结果:
now is the time for all good coders...
另外还有3种方式去构建字符串变量:%q,%Q和here documents
%q和%Q分别开始界定单引号和双引号的字符串(可以把%q看成薄的引号,把%Q看成厚的引号)
%q和%Q后面的字符是分解符,如果它是开始(opening)的方括号"[",花括号"{",括号"(" 或小于号"<",字符串被一直读取直到发现相匹配的结束符号。
否则,字符串会被一直读取,直到出现下一个相同的分界符。分界符可以是任何一个非字母数字的单字节字符。
最后,可以使用here document构建字符串。
string = <<END_OF_STRING
The body of the string
is the input lines up to
one ending with the same
text that followed the '<<'
END_OF_STRING
here document由源文件中的那些行但没有包含在<<字符后面指明终结字符串的行组成。一般情况下,终结符(terminator)必须在第一列出现。当然,如果把一个减号放在<<字符后面,就可以缩进编排终结符。
print <<-STRING1, <<-STRING2
Concat
STRING1
enate
STRING2
终结结果:
Concat
enate
注意:在这些例子中,Ruby没有从这些字符换中去掉这些前导空格。
5.2.1 操作字符
字符串可能是Ruby中最大的内建类,它有75个以上的标准方法,在这里我们不会介绍所有的方法,程序库参考有一个完整的方法列表,反之,我们看看那些常用的字符串惯用的技法,在日常的编码中很可能会用上它们。
回到点唱机,尽管被设计成与互联网相连,它也在本地硬盘上保存一些流行歌曲的备份,用这种方式,如果一只松鼠咬断了网络连接,我们仍然可以娱乐客户。
由于历史原因,歌曲列表在纯文本文件中按行保存,每行有歌曲文件的名称、歌曲的持续时间、作者和标题。它们都放在以竖线分割开的各个字段中,一个典型的文件可能是:
看看这些数据,在根据它们创建Song对象之前,很明显我们会使用String类的一些方法去抽取和清理这些字段,至少我们需要做:
- 把每行分割成各个字段
- 把播放时间从mm:ss转换成秒
- 删除歌曲演唱者名字中的多余空格。
首先把每行分割成各个字段,String#split可以很好地完成这件事情,在这个例子中,我们将/\s|\s/正则表达式传递给split方法,无论split在哪里找到了竖线,竖线都可能会被多个空格围绕,正则表达式会把这行分割成各个字元(token)。因为从文件读取的行尾含有一个回车换行符,在应用split之前,我们要使用String#chomp去除它。
File.open("songdata") do |song_file|
songs = SongList.new
song_file.each do |line|
file. length, name, title = line.chomp.split(/\s*\|s*\/)
end
puts songs[1]
end
输出结果:
Song:Wonderful World--Louis Armstrong
不幸的是,无论是谁创建了这个文件,他在输入演唱会名字的时候,有时会包含多余空格,这在我们高科技的超级环绕的Day-Glo平板显示器上会显得很难看。
所以,进一步往前走,最好是删除这些多余空格。
最简单的方式是String#squeeze,它修剪(trim)重复字符,我们会使用这个方法的squeeze!形式在字符串上进行修改。
File.open("songdata") do |song_file|
songs = SongList.new
song_file.each do |line|
file, length, name, title = line.chomp.split(/\s|\s/)
name.squeeze!(" ")
songs.append(Song.new(title, name, length))
end
puts songs[1]
end
输出结果:
Songs:wonderful WorldLouis Armstrong (2.58)
最终有了分秒格式的时间:文件说2:58,我们需要秒数,它是178。可以再次使用split把冒号周围的时间字段分割出来。
mins, secs = length.split(/:/)
相反,我们会使用其他相关的方法,String#scan类似于split,因为它根据模式(pattern)把字符串分成几块,但是与split不一样,使用scan可以指定希望这些块去匹配的模式,在这个例子中,我们想为分和秒都匹配一个或者多个数字。这种匹配一个或者多个数字的模式是/\d+/。
File.open("songdata") do |song_file|
songs = SongList.new
song_file.each do |line|
file, length, name, title = line.chomp.split(/\s*\|\s*/)
name.squeeze!(" ")
mins, secs = length.scan(/\d+/)
songs.append(Song.new(title, name, mins.to_i*60+secs.to_i))
end
puts songs[1]
end
输出结果:
Song: Wonderful World-Louis Armstrong (178)
点唱机有关键字搜索的能力,从歌曲标题或歌曲演唱者名字中给定一个词,它会列出所有匹配的记录,键入fats,它也许会搜索出Fats Domino、Fats Navarro和Fats Waller的歌曲,我们会创建索引类来实现它。送入一个对象和一些字符串,它会用出现在这些字符串中的每个词(有两个或者多个字符)来索引这个对象,这回展现String类的更多方法。
class WordIndex
def initialize
@index = {}
end
def add_to_index(obj, *phrases)
phrases.each do |phrase|
phrase.scan(/\w[-\w']+/) do |word| # extract word
word.downcase!
@index[word] = [] if @index[word].nil?
@index[word].push(obj)
end
end
def lookup(word)
@index[word.downcase]
end
end
String#scan方法从字符串中抽取出匹配正则表达式的元素,在这个例子中,\w[-\w']+\模式匹配可以在词中出现任意的字符,其后跟着在方括号内指定一个或多个字符(连字符,另一个组词字符,或单引号)。为了让搜索与大小写无关,在查找的时候,我们把抽取出来的词以及用作键的词变成小写。注意第一个downcase!方法名称结尾处的感叹号,就像与原先用到的squeeze!方法一样,这个标识用来表示方法会在适当的位置修改接收者,在这个例子中,它把字符串变成小写。
我们扩展了SongList类,这样添加歌曲时会索引歌曲,我们还添加了方法,其根据给定的词去查询歌曲。
class SongList
def initialize
@songs = Array.new
@index = WordIndex.new
end
def append(song)
@songs.push(song)
@index.add_to_index(song, song.name, song.artist)
self
end
def lookup(word)
@index.lookup(word)
end
根据我们测试了所有的方法。
songs = SongList.new
song_file.each do |line|
file, length, name, title = line.chomp.split(/\s|s/)
name.squeeze!(" ")
mins, secs = length.scan(/\d+/)
songs.append(Song.new(title, name, mins.to_i*60+secs.to_i))
end
puts songs.lookup("Fats")
puts songs.lookup("ain't")
puts songs.lookup("RED")
puts songs.lookup("WoRLD")
输出结果:
在前面的这段代码,lookup方法返回了匹配的数组,当把数组传递给puts时,它只是简单地依次输出每个元素,这些元素由回车换行符分隔开。
我们完全可以再用50页篇幅来描述String类中的所有方法,不过还是让我们继续前进,看看更简单的数组类型:区间(range)
5.3 区间 (Ranges)
区间无处不在,如果Ruby帮助我们对现实世界建模,看起来它自然会支持这些区间。实际上,Ruby做的更好:它使用区间实现3种不同的特性:序列(sequence)、条件(conditionals)和间隔(intervals)。
5.3.1 区间作为序列(Ranges as Sequences)
区间的第一个可能最自然的用法是:表达序列。序列由起点、终点以及序列中产生连续值得方法。在Ruby中,使用“..”和“...”区间操作符来创建序列。两个点的形式是创建闭合的区间(包括右端的值),而3个点的形式是创建半闭半开的区间(不包括右端的值)。
1..10
'a'..'z'
my_array = [1, 2, 3]
0...my_array.length
与Perl早期版本不一样,在Ruby中,区间没有在内部用列表(list)表示:1..100000序列被存储为Range对象,它包含对两个Fixnum对象的引用,如果需要,可以使用to_a方法把区间转换成列表。
(1..10).to_a
('bar'..'bat').to_a
区间实现了许多方法可以让我们迭代它们,并且以多种方式测试它们的内容。
digits = 0..9
digits.include?(5)
digits.min
digits.max
digits.reject {|i| i < 5}
digits.each { |digit| dial(digit) }
到现在为止,我们已显示了数字和字符串的区间,当然,就像对一个面向对象语言期望的那样,Ruby可以根据你所定义的对象来创建区间。唯一的限制是这些对象必须返回在序列中的下一个对象作为对succ的响应,而且这些对象必须是可以使用<=>来比较的。有时<=>也被称为太空船(spaceship)操作符,它比较两个值,并根据第一个值是否小于、等于或大于第二值。
下面一个简单的类表示了几行#符号。我们可以将它用作点唱机基于文本界面的声量控制。
class VU
include Comparable
attr :volume
def initialize
@volume = volume
end
def inspect
#' * @volume
end
def <=>(other)
self.volume <=> other.volume
end
def succ
raise(IndexError, "Volume too big") if @volume >= 9
VU.new(@volume.succ)
end
end
因为VU类实现了succ和<=>方法,因此它可以作为区间。
medium_volume = VU.new(4)..VU.new(7)
medium_volume.to_a
medium_volume.include?(VU.new(3))
区间作为条件
除了表达序列之外,区间也可以当做条件表达式来使用,在这里它们表现得就像某种双相开关——当区间第一部分的条件为true时,它们就打开,当区间第二部分的条件为true时,它们就关闭。例如,下面的代码段,打印从标准输入得到的行的集合,每组的第一行包含了start这个词,最后一行包含end这个词。
while line = gets
puts line if line =~ /start/ ..line =~/end/
end
在幕后区间跟踪了每种测试状态。早起的Ruby版本中,裸区间(bare range)可以在if , while和类似的语句中作为条件来使用,比如,可能会把先前的代码写成。
while gets
print if /start/../end/
end
这种写法不再支持,不幸的是,它不会引发任何错误,且每次测试都会成功。
5.3.3 区间作为间隔
区间这个多面手的最后一种用法用间隔测试:看看一些值是否会落入区间表达的间隔内,使用===即case equality操作符可以做到这一点。
(1..10) === 5 -> true
(1..10) === 15 -> false
(1..10) === 3.14159 ->true
5.4 正则表达式
当文件中创建歌曲列表时,我们使用了正则表达式去匹配输入文件的字段分解符,我们声称line.split(/\s|s/)表达式匹配一条可能被空格围绕的竖线。让我们更详细的探索正则表达式,看看这个声称为什么是准确的。
正则表达式被用来根据模式对字符串匹配,Ruby提供了内建的支持,使得模式匹配和替换变得更方便和更简明,本节会介绍正则表达式的所有主要特性。
正则表达式是Regexp类型的对象,可以通过显式地调用构造函数或使用字面量形式/pattern/和%r{pattern}来创建它们。
a = Regexp.new('^\s*[a-z]')
b = /^\s*[a-z]/
c = %r{^\s*[a-z]}
一旦有了正则表达式对象,可以使用Regexp@match(string)或匹配操作符=(肯定匹配)和!(否定匹配)对字符串进行匹配。匹配操作符对String和Regexp对象均有定义。匹配操作符至少有一个操作数必须为正则表达式。(早期版本的Ruby当中,这两个操作数可能都是字符串,且第二个操作数会暗中被转换成正则表达式。)
name = "Fats Waller"
name =~ /a/
name =~ /z/
/a/ =~name
匹配操作符返回匹配发生的字符位置,它们也有副作用,会设置一些Ruby变量。$&得到与模式匹配的那部分字符串,$·得到匹配之前的那部分字符串,而$'得到匹配之后的那部分字符串。可以使用这些变量来编写show_regexp方法,以说明具体的模式在何处发生匹配。
def show_regexp(a ,re)
if a =~ re
"#{$`} <<#{$&}>>#{$'}"
else
"no match"
end
end
show_regexp('very interesting', /t/) -> very in<<eresting
show_regexp('Fats Waller', /a/) -> F<<a>>ts Waller
show_regexp('Fats Waller', /ll/) -> Fats Wa<<ll>>er
这个匹配也设置了线程局部变量(thread-local variables), $~与$1直到$9。 $~变量是MatchData对象,它持有你想知道的有关匹配的所有信息。$1等持有匹配各个部分的值,我们在后面会谈到这些。对那些看到这种类似Perl变量名就心怀畏惧的人来讲,别急。
5.4.1 模式(patterns)
每个正则表达式包含一种模式,用来对字符串进行正则表达式的匹配。
在模式内,除了., |, (, ), [, ], {, }, +, , ^, $, *和?字符之外,所有字符均匹配它们本身。
show_regexp('kangaroo', /angar/) -> yes <<|>> no
show_regexp('!@%&-=+', /%&/) -> !@<<%&>>-=+
在这些特殊的字符之前放置一个反斜线便可以匹配它们的字面量,这解释了我们曾经用到的分割歌曲行的/\s|s/模式,|意味着“匹配竖线”。没有反斜线,|字符指的是替换(后面会讲到它)。
show_regexp('yes | no', /\|/)
show_regexp('yes(no)', /\(no\)/)
show_regexp('Are you sure?', /e\?/)
反斜线后面跟着一个分母数字的字符被用来引入一个特殊的匹配构造。后面我们会介绍到,另外,正则表达式可能包含#{...}表达式替换。
锚点
默认情况下,正则表达式 会试图发现模式在字符串中出现的第一个匹配。对“mississippi”字符串匹配/iss/,它会找出从位置1开始的‘iss’子字符串。但是如果想强迫模式只匹配字符串的开始或结束部分,怎么办?
和$模式分别匹配行首和行尾。它们常常被用来锚定(anchor)模式匹配:例如只要option出现在行的开始处,/option/就会匹配这个词,\A序列匹配字符串的开始,而\z和\Z匹配字符串的结尾。(实际上,除非字符串以\n结束,\z才会匹配字符串的结尾,在这个例子中,它会在\n之前匹配)。
show_regexp("this is\nthe time", /^the/) -> this is\n <<the>> time
show_regexp("this is\nthe time", /is$/) -> this <<is>>\nthe time
show_regexp("this is\nthe time", /\Athis/) -> <<this>> is\nthe time
show_regexp("this is\nthe time", /\Athe/) -> no match
同样的,\b和\B模式分别匹配词的边界和非词(nonword)的边界,组词字符可以是字母、数字和下划线。
show_regexp("this is\nthe time", /\bis/) -> this <<is>>\nthe time
show_regexp("this is\nthe time", /\Bis/) -> th <<is>> is\nthe time
字符类
字符类是处于方括号之间字符的集合 :[characters]匹配方括号之间的任何单个字符,[aeiou]会匹配元音,[,.:;!?]匹配标点符号等等。特殊正则表达式字符的意义——.|()[{+^$*?——在方括号里面是关闭的。不过,正常的字符串替换仍然发生,因此(例如)\b表示回退空格,而\n表示回车换行符。此外,可以使用在下页表中显示的缩写形式,例如\s匹配任何空格,而不仅仅是字面量空格,这个表中第二部分的POSIX字符类,对应于ctype(3)中的那些同名宏。
show_regexp("Price $12.", /[aeiou]/) -> Pr <<i>>ce $12.
show_regexp("Price $12.", /[\s]/) -> Price << >>$12.
show_regexp("Price $12.", /[[:digit:]]/)
show_regexp("Price $12.", /[[:space:]]/) -> Price << >>$12.
show_regexp("Price $12.", /[[:punct:]aeiou]/) -> Pr <<i>>ce $12.
在方括号内,c1-c2序列表达c1和c2之间的所有字符,并包含c2.
a = 'see [Design Patterns-page 123]'
show_regexp(a, /[A-F]/) -> see [ <<D>>esign Patterns-page 123]
show_regexp(a, /[A-Fa-f]/) -> s <<e>>e [Design Patterns-page 123]
show_regexp(a, /[0-9]/) -> see [Design Patterns-page <<1>>23]
show_regexp(a, /[0-9][0-9]/) -> see [Design Patterns-page <<12>>3]
如果想在字符类包含字面量字符]和-,它必须出现在开始处,把直接放在开始的方括号后面会对字符类求反:[a-z]匹配任何非小写字母的字符。
a = 'see [Design Patterns-page 123]'
show_regexp(a, /[ ] ]/) ->see [Design Patterns-page 123 <<]>>
show_regexp(a, /[-]/) -> see [Design Patterns <<->>page 123]
show_regexp(a, /[^a-z]/) -> see << >>[Design Patterns-page 123]
show_regexp(a, /[^a-z\s]/) -> see <<[>>Design Patterns-page 123]
一些字符串被使用的如此频繁以至于Ruby为它们提供了缩写形式,这些缩写形式在接下来的表5.1中列出——它们可以用在方括号内和模式体内。
a = 'see [Design Patterns-page 123]'
show_regexp(a, /[ ] ]/) ->see [Design Patterns-page 123 <<]>>
show_regexp(a, /[-]/) -> see [Design Patterns <<->>page 123]
一些字符类被使用得如此频繁以至于Ruby为它们提供了缩写形式,这些缩写形式在接下来的表中列出——它们可以用在方括号内和模式体内。
show_regexp('It costs $12.', /\s/) -> It << >>costs $12.
show_regexp('It costs $12.', /\d/) -> It costs $ <<1>>2.
最后,出现在方括号外面的点号(.)表示除回车换行符之外的任何字符(尽管在多行模式下它也匹配回车换行符)。
a = 'It costs $12.'
show_regexp(a, /c.s/) -> It <<cos>>ts $12.
show_regexp(a, /./) -> <<I>>t costs $12.
show_regexp(a, /\./) -> It costs $12 <<.>>
字符类的缩写
\d [0-9] 数字字符
\D [^0-9] 除数字之外的任何字符
\s [\t\r\n\f] 空格字符
\S [\t\r\n\f] 除空格之外的任何字符
重复
当指定/\s|s/模式来分割歌曲列表时,也就是说我们想匹配被任意数目的空格围绕着的竖线,现在我们知道\s序列匹配单个空格,因此看起来星号()指的是“任意数目”。实际上,星号是多个修饰符中的一个,它允许对模式进行多次匹配。
如果r代表模式内先前的正则表达式,那么
r 匹配零个或者多个r的出现
r+ 匹配一个或者多个r的出现
r? 匹配零个或一个r的出现
r{m, n} 匹配至少 "m"次和最多“n”次r的出现
r{m,} 匹配至少“m”次r的出现
r{m} 只匹配“m”次r的出现
重复构造有高的优先级——它们只绑定到模式内先前的正则表达式。/ab+/匹配一个a后面跟着一个或多个b,而不是ab序列。
必须小心使用构造——/a/模式会匹配任何字符串;每个字符串都会有零个或者多个a。
这种模式被称作贪心(greedy)模式,因为默认情况下它们会匹配尽可能多的字符串。你可以通过添加问号后缀让它们匹配至少最少的字符串来改变这个默认的行为。
a = 'The moon is made of cheese'
show_regexp(a, /\w+/) -> <<The>> moon is made of cheese
show_regexp(a, /\s.*\s/) -> The << moon is made of >>cheese
show_regexp(a, /\./) -> It costs $12 <<.>>
替换
竖线是特别字符,因此行分隔符模式必须使用反斜线去转义它,为转义的竖线(|)要么是匹配在它之前的正则表达式,要么是匹配在它之后的正则表达式。
a = "red ball blue sky"
show_regexp(a, /d|e/) -> r <<e>>d ball blue sky
show_regexp(a, /al|lu/) -> red b <<al>>l blue sky
show_regexp(a, /red ball | angry sky/) -> <<red ball >>blue sky
对于粗心大意的人来说,这里有个陷阱,竖线的优先级非常低,最后这个例子匹配red ball或angry sky,但不会匹配red ball sky或red angry sky。为了匹配red ball sky或red angry sky,需要使用编组(grouping)来重载默认的优先级。
编组
你可以使用括号在正则表达式中编组(group)词目,组内的所有东西被当做单个正则表达式对待。
show_regexp('banana', /an*/) -> b <<an>>ana
show_regexp('banana', /(an)*/) -> <<>>banana
show_regexp('banana', /(an)+/) -> b <<anan>>a
a = 'red ball blue sky'
show_regexp(a, /blue|red/) -> <<red>> ball blue sky
show_regexp(a, /blue|red \w+/) -> <<red ball>> blue sky
show_regexp(a, /red|blue \w+/) -> <<red ball>> blue sky
二、心得体会
反思:
- 没有按时完成一天看《Programming Ruby》50页的计划,又高估自己了,明天改改自己吧!
- 下午7点时候忍不住偷看自己以前的文章
今天完成了什么?
- 主要看了《PR》20页
- 浏览代码 微信用户、聊天记录、二维码、模板、事件
今天的收获:
- 正则表达式的一些细节 替换、竖线、重复、编组...
- Ruby标准类型:数字、字符串、区间
- block是什么