很多你不知道的 Swift String 細節,像是 Unicode、Index 或是 Substring 大小事...
String 其實是一連串的 character 所組合而成的,可以用不同型態來表示 String,像是 "Hello, Swift!"、"Swift 字串與字元" 或是 "\u{24}" ( Unicode scalar U+0024 )。
咦?什麼是 "\u{24}" 呢?這是一個 Unicode,Swift 近幾年橫空出世,對於 Unicode 其實提供了很多快速且相容的方式去處理,轉換成人看得懂的文字。
在基本型別篇有稍微提到字串,讓我們來看一下可以如何使用。
在開始介紹 String 之前,先來談談 Swift 中如何用 Unicode 表達文字。
String 是一連串的 character 所組合而成,然而 character 只能被指派一個字元或是多個 Code Points ( Unicode 的 code point 範圍是從 0x00000 ~ 0x10FFFF ),像這種多個 Code Points 序列又稱 Grapheme Cluster。
Grapheme 是書寫系統中最小的單位,像是 a 或是 à 都是 grapheme,但是他們因為重音的差異,在使用 Code Points 表達時,後者多了一個 Code Point,像這種 Code Point 的集合,就是一個 Grapheme Cluster。
let grapheme = "\u{0061}" // a
let graphemeCluster = "\u{0061}\u{0300}" // à
你可以使用雙引號 ""
,來去宣告一個 String,然而 Swift 會自動推斷為字串型別。
let 我是字串 = "This is a string."
如果你想使用多行數,你也可以使用三個引號 """
,去宣告一個 String。
let multilineString = """
This is multiline string.
"一次多行沒問題。"
"OK 的。"
"""
但是你不能用雙引號的方式來去表達多行數的字串,你會馬上看見 Xcode 的錯誤訊息。
以多行數的方式來表達有一點要注意的是,右引號前的空格是要告訴 Swift,在引號內的其他行都會忽略相同數量的空格。
let indentedString = """
這行不會有空格
這行有空格
這行不會有空格
"""
/*
Prints:
這行不會有空格
這行有空格
這行不會有空格
*/
如果你直接在字串裡使用雙引號,在編譯的時候就會出錯,但是想要在字串中輸入雙引號要怎麼辦呢?
Swift 提供了幾個規則,允許在 String 中輸入特殊字元:
\0
:Null Character\\
:Backslash\t
:Horizontal Tab\n
:Line Feed\r
:Carriage Return\"
:Double Quotation Mark\'
:Single Quotation Mark\u{n}
:Unicode 的 scalar value,n 代表 1 至 8 位的十六進制數值let nullCharacter = "空字元: 這是\0測試"
let backslash = "反斜槓: \\"
let horizontalTab = "水平縮排: 水平\t縮排"
let lineFeed = "換行: 一天\n一蘋果"
let carriageReturn = "回車: 一天\r一頻果"
let doubleQuotationMark = "\"Yeah,可以使用雙引號了\""
let singleQuotationMark = "\'Yeah,可以使用單引號了\'"
let unicodeString1 = "\u{0001f436}"
let unicodeString2 = "\u{6211}\u{7684}\u{8eab}\u{9ad8}\u{0031}\u{0038}\u{0030}\u{516c}\u{5206}"
/*
Prints:
空字元: 這是測試
反斜槓: \
水平縮排: 水平 縮排
換行: 一天
一蘋果
回車: 一天一頻果
"Yeah,可以使用雙引號了"
'Yeah,可以使用單引號了'
?
我的身高180公分
*/
你可以直接在多行數的字串直接使用 "
和 '
這兩個特殊字元,如果要使用其他的特殊字元,也是可以,不過要在前面加上 \
。
let multipleLineString = """
可以使用 " '
"""
如果像在多行數字串再加上 """
的話,也可以在前後引號加上 #
,這樣就可以使用 """
。
let multipleLineString = #"""
可以使用 """
"""#
在開頭有提到,String 是由一連串的 character 組合而成,所以 character 在宣告初始值的時候,只接受一個字元。
let passedCharacterExample: Character = "A" // Pass (O)
let failedCharacterExample: Character = "Apple" // Failed (X)
String 使用 Collection Protocol,可以使用 for ... in
loop 來走訪 String 中每一個 character。
for character in "Hello, Swift!" {
print(character)
}
/*
Prints:
H
e
l
l
o
,
S
w
i
f
t
!
*/
要初始化一個新的 String 變數或常數,可以直接用雙引號( "
) 作為初始值,透過型別推論定義為 String,另外一種方式可以使用 String()
作為初始值,效果是一樣的。
let exampleString = ""
let initializeString = String()
從 Swift 官方 API Swift > String
找到,底下提供了一個 public 變數 isEmpty,可以用來確認字串是否是空字串。
if exampleString.isEmpty && initializeString.isEmpty {
print("空字串")
}
// Prints: 空字串
針對 String 變數,你可以透過 +
或是 +=
的方式來連接不同字串,修改變數的值,但是如果用這種方式修改常數,編譯的時候就會出錯。
當然,Character 不允許這樣的方式修改,因為 Character 只能接受一個字元。
var mutableString = "一天一頻果"
mutableString = mutableString + "荷包的錢遠離我"
var periodCharacter = "."
mutableString += periodCharacter
print(mutableString)
let constantString = "一天一頻果"
constantString += ""
// Warning: Left side of mutating operator isn't mutable: 'constantString' is a 'let' constant
甚至是使用 append()
的方法去連接 String 或是 Character
var message = "一天一蘋果,"
message.append("荷包的錢遠離我")
print(message) // 一天一蘋果,荷包的錢遠離我
字串表達還有另一種方式,允許在前後雙引號中建構新的字串,使用 \( )
的方式,在括弧內就像一般在寫 Swift 語言一樣,允許帶入變數常數,甚至是對其進行運算,最後會最後會轉換成 String。
let three = 3
let result = "3 * 3 = \(three * 3)"
print(result) // 3 * 3 = 9
如果你想把 "3 * 3 = \(three * 3)"
像這種 String Interpolation 用字串表達出來,可以在前後雙引號多加一個 #
。
let three = 3
let result = #"3 * 3 = \(three * 3)"#
print(result) // 3 * 3 = \(three * 3)
承上題,如果我想用這種方式使用 String Interpolation 可以嗎?答案是可以的。先前提到 String Interpolation 的使用方式是 \( )
,只要再多一個 #
,變成 \#( )
就可以使用了。
let three = 3
let result = #"3 * 3 = \#(three * 3)"#
print(result) // 3 * 3 = 9
在 \( )
,括弧內不允許使用像是 \
、\n
或是 \t
這種特殊字串
let three = 3
let result = #"3 * 3 = \#(three * 3 \t)"#
// Warning: Invalid component of Swift key path
一樣地可以從 Swift 官方 API Swift > String
看到,有一個 public 變數 count,可以回傳字串的 character 數量。
let message = "猜猜看我總共有幾個字啊?"
print("字數總共有: \(message.count) 個字") // 字數總共有: 12 個字
還記得開頭提到的 Grapheme Cluster,即使看起來好像用了很多字,但是轉換出來他終究還是一個字 à
。
let graphemeCluster = "\u{0061}\u{0300}" // à
print(graphemeCluster.count) // 1
奇怪,明明 Code Points 有兩個,但是為什麼 count 數量只有一個呢?承如上面所說,針對這種 Grapheme Cluster 要得到實際 character 數量前,必須先走訪完所有 Code Points,才能得知 Character 數量,也因此 Counting String 的 Time Complexity 是 ,所以如果要使用 count 去計算一個很長的 String,可能其中又夾雜了幾個特殊符號,這時候就要注意 counting 的時間會稍微久一些。
上面有提到,在一個 String 中,會有多個 characters,而這每一個 character 又對應到一個 Unicode scalar,必須走訪完整個 String 才能知道每一個 character 的位置在哪裡,所以 Swift 的 String 沒辦法直接使用整數值的索引取得 character。
Swift 也提供了幾個屬性來允許 String 找到特定的 character:
startIndex
:回傳 String 中第一個 character 的位置endIndex
:回傳 String 中最後一個 character 後的位置 (注意,不是最後一個,是最後一個後)let exampleString = "ABCDE"
exampleString[exampleString.startIndex] // A
exampleString[exampleString.endIndex] // Fatal error: String index is out of bounds
但是這樣要如何取得 String 中最後一個 character?
Swift 還提供了幾個方法可以解決這類的問題:
String.index(before:)
:找出某個 index 前一個的 indexString.index(after:)
:找出某個 index 後一個的 indexexampleString[exampleString.index(before: exampleString.endIndex)] // E
exampleString[exampleString.index(after: exampleString.startIndex)] // B
同理,如果以 endIndex 作為基準點去使用 String.index(after:)
方法,因為後面也找不到值了,就會發生 Fatal Error。
exampleString[exampleString.index(after: exampleString.endIndex)] // Fatal error: String index is out of bounds
甚至你可以直接放大招使用 index(_:offsetBy:)
方法,從特定的位置,往前或往後找尋目標 index。
exampleString[exampleString.index(exampleString.endIndex, offsetBy: -1)] // E
exampleString[exampleString.index(exampleString.startIndex, offsetBy: 3)] // D
也因為 String 使用 Collection Protocol,也可以使用 for ... in String.indices
去走訪每個 character index。
for index in exampleString.indices {
print(exampleString[index])
}
/*
Prints:
A
B
C
D
E
*/
你可以使用 String.insert(_:at:)
插入一個 character 到字串的某個特定的位置。
var message = "你好"
message.insert("。", at: message.endIndex) // 你好。
如果你想插入字串,就必須使用另外一個方法:
String.insert(contentsOf:at:)
:contentsOf 帶入你想插入的字串
var message = "你好"
message.insert(contentsOf: " 小明。", at: message.endIndex) // 你好 小明
Swift String 提供了兩個 remove 的方法,來移除特定位置的 character 或是字串:
String.remove(_:at:)
:移除某一個位置的 character。String.removeSubrange(_:)
:移除某一段的字元或字串。var message = "ABCDE"
message.remove(at: message.index(before: message.endIndex)) // message is ABCD.
message.removeSubrange(message.startIndex..<message.index(message.startIndex, offsetBy: 3)) // message is D.
你可以擷取某一段 String 作為 SubString
var message = "ABCDE"
let subString = message[message.index(message.startIndex, offsetBy: 2)..<message.endIndex] // CDE
但是仔細去看會發現 subString 的型別不是 String 而是 Substring,其實這兩個長得很像,都使用了 StringProtocol,差別就在於 Substrings 使用上會比 String 來得快而且更有效率,為什麼會這樣?
當你從一個 String 切出一個 Substrings 時,不會進行 Copy-on-write,Substrings 共享了 String 原本的 Storage,這也讓 Substring 的使用效率提高。
但是這有個小缺點就是,因為 Substring 與 String 共享同一個 Storage,即時沒有任何引用呼叫,只要 Substring 還存在著,系統就必須保留整個 String,所以有可能造成長時間佔用記憶體資源,最終導致 memory leak,所以在使用上要注意,可以轉換為一個新的 String,這樣就會回收保存舊的 String 的 memory。