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

上一章:變數與型別 目錄 下一章:陣列與串列