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