跳转到内容

BOO大全/字串处理

维基教科书,自由的教学读本

上一章:变数与型别 目录 下一章:阵列与串列


字串处理[编辑]

Boo 的字串完全与底层的 CLI System.String 相符合。如果你已经使用过其他 .NET 语言的话,那么大部分的技巧仍然适用。

>>> s = "Hello, Dolly!"
(String) 'Hello, Dolly!'
>>> s.Length
(Int32) 13
>>> len(s) # Python style!
(Int32) 13
>>> s[0]
(Char) H
>>> s[0] = char('h')
-----^
ERROR: Property 'System.String.Chars' is read only.

这儿有个重点﹔这错误讯息说字串物件是不可改变的﹔一旦建立了,物件本身的字元无法被改变。

在 Python 裡,s[0]表示回傳一個長度只有 1 的字串,而不是字元本身。

所有字串的操作像是串接,会产生新字串。w+=a 看来会改变物件本身 (C++ 正是如此),但实际上,它是 w=w+a 的简化版本。变数 w 接收一个的字串,而旧的字串将会被舍弃。(这让你觉得困惑吗?请参考垃圾收集更有效率的字串处理)。

>>> w = "World"
(String) 'World'
>>> h = "Hello"
(String) 'Hello'
>>> h + ", " + w
(String) 'Hello, World'
>>> w = w + " + dog"
(String) 'World + dog'
>>> w
(String) 'World + dog'
>>> w += ' cat'
(String) 'World + dog cat'

Boo 用来玩弄 CLI 字串再也适合不过了,因为它认定大多的程式都需要处理文字资料,所以已经准备了许多具有威力的特性。一般来说,所有字串的比较都是区分大小写的:

>>> s == "Hello, dolly!"
(Boolean) false
>>> s.Substring(0,3)
(String) 'Hel'
>>> s.Substring(7,3)
(String) 'Dol'
>>> s.StartsWith("Hell")
(Boolean) true
>>> s.IndexOf("Dolly")
(Int32) 7
>>> s.IndexOf("dolly")
(Int32) -1
>>> s.Replace("!","")
(String) 'Hello, Dolly'
>>> s.Replace("Dolly","dolly")
(String) 'Hello, dolly!'
>>> 

怎么作不区分大小写的比较呢?String.Compare的第二个引数可以关闭区分大小写的比较。这个函数类似 C 的 strcmp﹔如果完全吻合,传回 0,如果字串不同时,则视情况传回 +1 或 -1。

>>> string.Compare("One","one",true)
(Int32) 0
>>> string.Compare("One","Two",true)
(Int32) -1

垃圾收集[编辑]

在 Boo 里的物件会被自动回收。如果一个物件悬置,而且没有其他物件或变数参考到它的话,那么它可能就被认定为垃圾,并且被垃圾收集机制回收。

更有效率的字串处理[编辑]

如果要让字串的串接更有效率,你应该改使用 StringBuilder。甚至,如果你已经知道未来的成长数量,最好在初始 StringBuilder 时,就指定其容量。

字串插值[编辑]

在 Boo 里,有好几种用来建立复杂字串的方法,各有各的好处。第一种,就是重复地使用字串串接 (也就是多载的 + 运算子)﹔第二种则是使用类别库里提供的 Format方法﹔第三种则是字串插值。第一种方法在阅读上确实不如其他两种方法!

>>> first = "Bill"
>>> last = "Gates"
>>> print "'" + first + "' = '" + last + "'"
'Bill' = 'Gates'
>>> print string.Format("'{0}' = '{1}'",first,last)
'Bill' = 'Gates'
>>> print "'${first}' = '${last}'"
'Bill' = 'Gates'

我们待会再回头讲FormatFormat可以让你控制如何更精确地显示数值或其他型别的资料。基本上它像是 C 的 printf 格式化,它将变数移到格式字串之后,而这会显得很长。

Boo 的字串插值在多行字串时,显得特别有用。

Name = "John"
Manager = "Catbert"
stuff = """
Dear ${Name},

Your application is being considered. Please be patient, and don't phone us.

Yours,
${Manager}
"""

print stuff

输出结果

Dear John,

Your application is being considered. Please be patient, and don't phone us.

Yours,
Catbert

有时候我们不想有字串插值,这种情况下,改用单引号的字串。

任何合法的 Boo 运算式都可以使用在 ${} 里面,但太长的运算式会难以阅读:

>>> "It is now ${DateTime.Now}, ${Environment.GetEnvironmentVariable('USERNAME')}"
(String) 'It is now 2/25/2006 3:39:21 PM, steve'

字串该使用单引号还是双引号?最好是选定一种并且尽可能地一致﹔毕竟,语言本身并不在意你怎么使用,但是阅读程式的人会很困扰。单引号字串可以被嵌在双引号字串里而无须作任何讨厌的 C 形式的逸出处理。

Format 方法让你可以精确地控制如何将数值转为字串。

>>> String.Format("{0:n}",20_433_344)
'20,433,344.00'
>>> String.Format("{0:C}",2.45)
'$2.45'
>>> String.Format("{0:E},{1:E},{2:E}",1.0,Math.PI,2.3)
'1.000000E+000,3.141593E+000,2.300000E+000'
>>> String.Format("Port was {0:X}",0xFF << 4)
'Port was FF0'
>>> String.Format("{0,10}{1,10}",10.99,3.99)
'     10.99      3.99'
>>> String.Format("{0,10:C}{1,10:C}",10.99,3.99)
'    $10.99     $3.99'

使用 # 可以让你有更多的掌控权。举例来说,这可以将标准的十位电话号码显示的更好。留意下面例子的ToString,它已经被多载过,所以作用与 String.Format() 相同。

>>> num = 0123456789
>>> String.Format("{0:(0##) ###-####}",num)
'(012) 345-6789'
>>> num.ToString("(0##) ###-####")
'(012) 345-6789'

这些格式方法也适用于 WriteWriteLine 方法:

>>> for x in (1.0,2,3,5,6):
... 	Console.Write("{0:E} ",x)
... 
1.000000E+000 2.000000E+000 3.000000E+000 5.000000E+000 6.000000E+000
>>> 

Python形式的字串[编辑]

Boo 可以在字串上使用slicing。这是借镜自 Python 而来的一个很好的特性﹔你可以指定范围以撷取某部分的字串。如果没有指定上限,就表示下限之后的所有字元﹔如果没有下限的话,就表示字串开头到指定上限间的所有字元﹔-1表示从后面数来第一个字元,-2则是从后面数来第二个字元,以此类推。

>>> s="Hello, World!"
'Hello, World!'
>>> s[0:1]
'H'
>>> s[1:2]
'e'
>>> s[1:]
'ello, World!'
>>> s[:-1]
'Hello, World'

这个特性也适用于阵列或串列。

切割字串[编辑]

常见的操作是将一个长的字串切割为字串阵列,传递一个或多个分隔字元给 String.Split 方法即可:

>>> s = "one two three four"
'one two three four'
>>> s.Split(char(' '),char('\t'))
('one', 'two', 'three', 'four')
>>> "jane,jimmy,alfred".Split(char(','))
('jane', 'jimmy', 'alfred')
>>> s.Split()
('one', 'two', 'three', 'four')

注意,与 C# 不同之处,你不能把 null 当作 String.Split 的引数。

String.Split的多载版本非常有用,它提供了额外的引数,可以用来指定传回字串阵列的最大值。所以可以轻易地将字串切割为第一个子字串与剩余字串:

>>> s.Split((char(' '),char('\t')),2)
('one', 'two three four')

第一个引数令人困扰﹔第一个例子里,将多个分隔字元当作引数传入,但第二个例子,却将多个分隔字元作为阵列传入。在如果只有一个分隔字元的情况时,这样写看起来很笨拙:

>>> names.Split((char(','),),2)
('jane', 'jimmy,alfred')
>>> names.Split(",".ToCharArray(),2)
('jane', 'jimmy,alfred')

这儿,我使用了两种可以将单一字元建构为阵列的方法﹔注意到第一个方法了吗?额外的逗号 ',' 可以让 Boo 认定它是一个字元阵列。

在使用 'String.Split 时,这种情况可能会让你觉得很意外:

>>> input = "20   4  2      4"
'20   4  2      4'
>>> input.Split(char(' '))
('20', '', '', '4', '', '2', '', '', '', '', '', '4')

事实上,以分隔字元来看,这是一个很恰当的结果,但在处理文字资料时,这可能不是你想要的结果。你可以用下列的代码来避免:

for w in input.Split(char(' ')):
	if len(w) > 0:
	     print w

译注:或是利用Generator方法

def RemoveEmpty( enumerator ):
  for i in enumerator:
    if len(i)>0:
      yield i
print array( RemoveEmpty( input.Split( char(' ') ) ) )

在 .NET Framework 2 里,已经针对这个需求加入了第三个引数StringSplitOptions:

>>> input.Split( (char(' '),), 10, StringSplitOptions.RemoveEmptyEntries )
('20', '4', '2', '4')

另外一种切割字串的方法是使用 正则运算式(Regular Expression)。指定一个或多个空白字元的方式是:'\s',这将会找到所有 ' '、'\t'的字元﹔而 '+'则表示 '\s' 将会出现一次或多次。(如果你不使用 '+',结果将会与前面提到的意外结果一样。)

>>> out = /\s+/.Split(input)
('20', '4', '2', '4')

不幸的是,这也有个意外的状况:在字串的最前面或最后面有空白字元时,会与你想像的不同。

>>> input = " 20 4   2 "
' 20 4   2 '
>>> out = /\s+/.Split(input)
('', '20', '4', '2', '')

一般来说,String.SplitRegex.Split来的快。

正规运算式[编辑]

有许多技术能大幅地增加你身为程序员的能力,正则运算式(Regular Expression)无疑地是其中之一,同时它也是一个跨语言的技巧﹔在 Boo、C# 甚或其他语言,都有相似的语法。字串的处理上,不外乎就是寻找、萃取与取代文字,正则运算式是最适合处理这些事情的了。然而,学习曲线有点陡峭,使用一个可互动的语言,如 Boo,会容易许多。

Boo 有正则运算式的语法可以简化 .NET 正则运算式的用法。举例来说,下列程式会打印出以字母开头的所有行:

for line in System.Console.In:
	if line =~ /^\s[a-zA-Z]+/:
		print line

不使用正则运算式语法与 =~ 运算子的话,会是这样:

import System.Text.RegularExpressions
wordPattern = Regex("""^\s[a-zA-Z]+""");
for line in System.Console.In:
	if wordPattern.Match(line) != Match.Empty:
		print line

三个双引号字串的使用是为了要让字串里可以包含反斜线 '\'﹔这与 C# 的 @"...."相同。同时,也需要先产生 Regex 实体,在只是要作比对的这件事情上,这显得很不必要,而且没有效率。与后面的例子(预先初始正规运算式、再比对)相较之下,在程式里直接使用正则运算式才是比较容易了解的用法。

另外, /.../ 里面不可以有空白字元。这是因为 Boo 需要在算术运算式与正则运算式之间做出区别。 x/2 + y/3 应该是算术运算式,不是正则运算式。Boo 提供了延伸的语法: @,可以让 /.../ 之间放置空白字元,例如:@/this dog is called \w+/。通常,最好使用 \s 来表示空白字元,因为它适用于空白字元与 tab 字元,这两个字元通常被视为一体,不需要加以区别。

无论哪一种语言,Boo 都是一个探索正则运算式的好工具。这儿我们试着在字串里找一个后面为整数的字(word):

>>> 'fred 20' =~ /\w+\s\d+/
true
>>> 'fred  20' =~ /\w+\s\d+/
false
>>> 'fred  20' =~ /\w+\s+\d+/
true
>>> '552  20' =~ /\w+\s+\d+/
true
>>> '552  20' =~ /[a-zA-Z]+\s+\d+/
false

第一个式子并不是好的运算式,因为只能抓到字(word)与整数间只有一个空白的情况。在第四个例子里,你会发现一连串的数字也被算是字(word),因为'\w'把一连串的数字也认定为字(word)。所以我们需要改用 [A-Z,a-z]:/[A-Z,a-z]+\s\d+/ 作精确地指定。

=~ 运算式非常便利,但如果需要更多资讯的话,可以使用 Regex.Match,它会回传一个 Match 物件:

>>> r = /[a-zA-Z]+\s+\d+/
>>> m = r.Match('so far, we have fred  999')
>>> m.Value
'fred  999'
>>> m.Index
16
>>> m.Length
9

正则运算式可以包含群组。在小括号( ) 里的任何字元会被认定为群组,可以用来取得字串里符合样式的子字串。Match的Group属性包含了符合样式结果的集合,看看下面的例子:

>>> r = /([a-zA-Z]+)\s+(\d+)/
>>> m = r.Match('defininitely johnny 505')
>>> gg = m.Groups
>>> gg.Count
3
>>> gg[1]
johnny
>>> gg[2]
505

上一章:变数与型别 目录 下一章:阵列与串列