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'
我们待会再回头讲Format,Format可以让你控制如何更精确地显示数值或其他型别的资料。基本上它像是 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'
这些格式方法也适用于 Write 和 WriteLine 方法:
>>> 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.Split 比 Regex.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