对字符串的学习
猜词的谜题
# save this as guess.rb
words = ['foobar', 'baz', 'quux']
secret = words[rand(3)]
print "guess? "
while guess = STDIN.gets
guess.chop!
if guess == secret
print "You win!/n"
break
else
print "Sorry, you lose./n"
end
print "guess? "
end
print "The word was ", secret, "./n"
正则表达式
[] | 范围描述符 (比如,[a - z] 表示在a 到 z 范围内的一个字母) |
/w | 字母或数字;相当于 [0-9A-Za-z] |
/W | 非字母,数字 |
/s | [ /t/n/r/f]空字符;相当于 [ /t/n/r/f] |
/S | 非空字符 |
/d | [0-9]数字;相当于 [0-9] |
/D | 非数字字符 |
/b | 退格符 (0x08) (仅在范围描述符内部时) |
/b | 字边界(word boundary) (在范围描述符外部时) |
/B | 非字边界 |
* | 前面元素出现0或多次 |
+ | 前面元素出现1或多次 |
{m,n} | 前面元素最少出现m次,最多出现n次 |
? | 前面元素最多出现1次;相当于 {0,1} |
| | 与前面或后面的表达式匹配 |
() | 群( grouping) |
实验正则表达式的小程序,把它存为regx.rb,然后在命令行里输入'ruby regx.rb'运行.
# Requires an ANSI terminal!
|
如果有不止一种方法能匹配模式会怎样呢?
str> foozboozer
|
之所以foozbooz被匹配而不只是fooz,是因为一个正则表达符尽可能匹配最长的子串.
下面是一个将冒号分隔的数字时间段从字符串中隔离出来的模式匹配.
面我们来检查在字符串这章中出现的猜谜程序.由于这个要长一点,我们为每一行打上行数.
01 words = ['foobar', 'baz', 'quux']
|
这个程序里,我们使用了一个新的控制结构 while.只要某个指定的条件保持为真,while和它对应的end之间的代码会重复执行.
行2的rand(3)返回一个介于0-2之间的随机数.这个随机数用于提取数组 words 中的一个成员.
在行5我们通过STDIN.gets方法从标准输入读取一行.如果读行遇到时 EOF (文件结束), gets会返回nil.因此,与while相连的代码会一直执行直到它遇到^D(或DOS下的^Z),表示输入的结束.
行6的guess.chop!去掉 guess 的最后一个字符;那一定是个换行符.
行15,我们打印出要猜的词.我们写的代码是上向 print 语句传递三个参数(这三个参数一个接一个地打印),但也可以用一个参数等效地打印: 将secret写为 #{secret}以表明将它是一个要取值的变量,而非一个要打印的一般文字:
print "the word is #{secret}./n"
|
Regular expressions
最后我们来看看正则表达式一节的那个程序.
01 st = "/033[7m"
|
在行4,while的条件被硬设为 true,因此这好像构成了一个无限循环.但我们在行8和行13放置了break语句以跳出循环.这两个break语句也是 if 修饰辞(if modifier)的一个例子.一个"if修饰辞"当且仅当指明条件满足时执行它左边的语句.
再说说 chop! (出现在行9和行14).在Ruby里,我们亦可将"!"和"?"附于某些方法名字后面.惊叹号(!,有时大声地念作"bang!")暗示某些东西可能具破坏性(destructive),也就是指,某些东西可以改变它所触及的东西. chop!直接作用于一个字符串,但不带!的chop只会产生一个拷贝.下面有这一区别的演示.
ruby> s1 = "forth"
"forth"
ruby> s1.chop! # This changes s1.
"fort"
ruby> s2 = s1.chop # This puts a changed copy in s2,
"for"
ruby> s1 # ... without disturbing s1.
"fort"
|
以后你还会遇见以问号(?,有时大声地念做 "huh?")结束的方法名;这指"断言"(prediacte)方法,只会返回true或false.
行15应给予注意.首先,注意gsub!也是一个破坏函数.它通过替换所有符合 re 模式字符来修改 str(sub指替换,首字母 g 指全局, 比如,替换所有的匹配而不只是第一个匹配).到此为止,还好;但我们用什么来替代文本中的匹配部分呢? 在行1和行2里的 st 和 en 被分别定义为表示反转文本颜色(color-inverted)和恢复正常文本颜色的ANSI码. 在行15,它们被#{}括起以确保他们被前面定义的那样解释(这样我们才没看见变量名被打印出来).在这中间是 "//&".这是个小把戏.因为替换字符串是由双引号引起的,一对反斜杠会被解释为一个单一的反斜杠;所以 gsub!实际上得到的是"/&",就一段特殊代码正好就是表示"任何与模式于第一处匹配的字符".因此新的字符串在被打印出来的时候,很像原来的那个,只不过匹配的部分以反视(inverse video)的方式高亮度显示出来.
迭代器
Ruby的String类型有很多有用的迭代器:
ruby> "abc".each_byte{|c| printf "<%c>", c}; print "/n"
<a><b><c>
nil
|
each_byte 是个用于字符串中每个字符的迭代器.每个字符串由局部变量c代替.这可以翻译为类似C的代码...
ruby> s="abc";i=0
0
ruby> while i<s.length
| printf "<%c>", s[i]; i+=1
| end; print "/n"
<a><b><c>
nil
|
...然而, each_byte 迭代器在概念上要简单些,而且即使以后 String 类突然有所改变也应该可以照样工作.使用迭代器的一个好处便是在此类改变中仍然可以保持健壮;一般的,它的确是好代码的一个特点.(好,请有点儿耐心,我们将会马上谈到什么是类)
String的另一个迭代器是 each_line.
ruby> "a/nb/nc/n".each_line{|l| print l}
a
b
c
nil
|
采用迭代器,这将很轻松的取代C的大多数编程效果(找换行符,生成子串等等)
前面出现的for语句通过each迭代器实现迭代功能. String的each和each_line的工作原理差不多,让我们用for重写上面的例子:
ruby> for l in "a/nb/nc/n"
| print l
| end
a
b
c
nil
|
我们可以用retry流程控制语句连接迭代循环,它会从头执行当前循环的迭代.
ruby> c=0
0
ruby> for i in 0..4
| print i
| if i == 2 and c == 0
| c = 1
| print "/n"
| retry
| end
| end; print "/n"
012
01234
nil
|
yield有时会在一个迭代器的定义中出现. yield将流程控制移至传递给迭代器的代码域(这将会在过程对象那一节介绍更多的细节).下面的例子定义了一个repeat迭代器,会依参数的设置执行多次代码域.
ruby> def repeat(num)
| while num > 0
| yield
| num -= 1
| end
| end
nil
ruby> repeat(3) { print "foo/n" }
foo
foo
foo
nil
|
利用retry,我们可以定义一个有while相同作用的迭代器,虽然在实际应用中它太慢了.
ruby> def WHILE(cond)
| return if not cond
| yield
| retry
| end
nil
ruby> i=0; WHILE(i<3) { print i; i+=1 }
012 nil
|
搞懂什么是迭代器了吗?有一些限制,但你可以写自己的迭代器;实际上,当你定义一个新的数据类型时,为它定义一个合适的迭代器经常也很方便.这样看来,上面的例子并不是很好用.在我们理解了类以后,我们可以讨论讨论更具实际意义的迭代器.
访问控制
想想当我们在"最高层"而不是在一个类的定义里定义一个方法时会发生什么.我们可以把这样的方法设想为在像C那样的传统语言中的函数的类似物.
ruby> def square(n)
| n * n
| end
nil
ruby> square(5)
25
|
我们的新方法看起来不属于任何类,但实际上Ruby将其分给Object类,也就是所有其它类的父类.因此,所有对象现在都可以使用这一方法.这本应是正确的,但有个小陷阱:它是所有类的私有(private)方法.我们将在下面讨论这是什么意思,但一个结果是它只能以函数的风格调用,像这样:
ruby> class Foo
| def fourth_power_of(x)
| square(x) * square(x)
| end
| end
nil
ruby> Foo.new.fourth_power_of 10
10000
|
我们不允许向一个对象明确地运用这一方法:
ruby> "fish".square(5)
ERR: (eval):1: private method `square' called for "fish":String
|
这一聪明的做法在 ruby 使得提供可以像在传统语言中那样运用函数的同时保持了 ruby 的纯OO性质(函数仍是对象方法,但接受者隐式的为self).
在OO编程里,有一个不成文的习惯,我们在前面的章节里有所暗示的,即有关规格(specification)和实现(implementation)的区别,或者说对象被要求完成什么任务和实际上它是怎样完成的.对象的内部工作应该对用户保持隐蔽;他们应该只关心输入什么和输出什么,并相信对象知道它在内部是在做什么.如此,某些外面世界看不见但却在内部使用(并可以由程序员在任何需要的时候改进,而不用改变用户看到的类的对象)的方法将很有用.在下面这个普通的例子里,你可以把engine看作类的内部隐式方法.
ruby> class Test
| def times_two(a)
| print a," times two is ",engine(a),"/n"
| end
| def engine(b)
| b*2
| end
| private:engine # this hides engine from users
| end
Test
ruby> test = Test.new
#<Test:0x4017181c>
ruby> test.engine(6)
ERR: (eval):1: private method `engine' called for #<Test:0x4017181c>
ruby> test.times_two(6)
6 times two is 12.
nil
|
开始时,我们本希望test.engine(6)返回12,但随后当我们扮演一个Test对象用户时我们了解到engine是不可访问的(inaccessible).只有Test的其它方法,像 times_two可以使用 engine.我们被要求保持程序的外部接口,也就是 times_two这些方法.管理此类的程序员可以自由的更改engine(这里,可能把b*2改为b+b,假设这样可以提高性能)而不影响用户与 Test 对象打交道.这个例子当然过于简单;访问控制的优点只有当我们开始写更复杂和有趣的类时方能显现出来.
Ruby的模块非常类似类,除了:
- 模块不可以有实体
- 模块不可以有子类
- 模块由module...end定义.
这里列出了一些以$打头并跟单个字符的特殊变量.比如,$$包含了Ruby解释器的进程id,它是只读的.这里是主要的系统变量以及它们的含义(细节可在Ruby的参考手册中查到):
$! | 最近一次的错误信息 |
$@ | 错误产生的位置 |
$_ | gets最近读的字符串 |
$. | 解释器最近读的行数(line number) |
$& | 最近一次与正则表达式匹配的字符串 |
$~ | 作为子表达式组的最近一次匹配 |
$ n | 最近匹配的第n个子表达式(和$~[n]一样) |
$= | 是否区别大小写的标志 |
$/ | 输入记录分隔符 |
$/ | 输出记录分隔符 |
$0 | Ruby脚本的文件名 |
$* | 命令行参数 |
$$ | 解释器进程ID |
$? | 最近一次执行的子进程退出状态 |
上面的 $_ 和 $~ 都有作用范围.它们的名字暗示其为全局的,但它们一般都是这样用的,关于它们的命名有历史上的原因
全局变量由$开头.它们可以在程序的任何位置访问到.在初始化前,全局变量有一个特殊的值 nil.
ruby> $foo
nil
ruby> $foo = 5
5
ruby> $foo
5 |
应谨慎使用全局变量.由于在任何地方都可以被写因此他们相当危险.滥用全局变量会导致很难隔离臭虫;同时也视为程序的设计未经严格考虑.当你发现必须要使用全局变量时,记得给它一个不会在其它地方一不小心就用到的描述性名字(像上面那样叫$foo可能不是一个好想法).
全局变量的好处是其可以被跟踪;你可以做一个当变量值改变时被调用的过程.
ruby> trace_var :$x, proc{print "$x is now ", $x, "/n"}
nil
ruby> $x = 5
$x is now 5
5 |
当一个全局变量(改变时)作为一个过程的激发器,我们也管它叫活动变量(active variable).比如说,它可用于保持GUI显示的更新.
.