- 原文地址:Mastering Swift: essential details about strings
- 原文作者:Dmitri Pavlutin
- 譯文出自:掘金翻譯計劃
- 譯者:Tuccuay
- 校對者:oOatuo , lsvih
掌握 Swift 的字串細節
String 型別在任何程式語言中都是一個重要的組成部分。而使用者從 iOS 應用的螢幕上能讀取到最有效的資訊也來自文字。
為了觸及更多的使用者,iOS 應用必須國際化以支援大量現代語言。Unicode 標準解決了這個問題,不過這也給我們使用 string 型別帶來了額外的挑戰性。
從一方面來說,程式語言在處理字串時應該在 Unicode 複雜性和效能之間取得平衡。而另一方面,它需要為開發者提供一個舒適的結構來處理字串。
而在我看來,Swift 在這兩方面都做的不錯。
幸運的是 Swift 的 string 型別並不是像 JavaScript 或者 Java 那樣簡單的 UTF-16 序列。
對一個 UTF-16 碼單元序列執行 Unicode 感知的字串操作是很痛苦的:你可能會打破代理對或組合字元序列。
Swift 對此有著更好的實現方式。字串本身不再是集合,而是能夠根據不同情況為內容提供不同的 view。其中一個特殊的 view: String.CharacterView
則是完全支援 Unicode 的。
對於 let myStr = "Hello, world"
來說,你可以訪問到下面這些 view:
myStr.characters
即String.CharacterView
。可以獲取字形的值,視覺上呈現為單一的符號,是最常用的檢視。myStr.unicodeScalars
即String.UnicodeScalarView
。可以獲取 21 整數表示的 Unicode 碼位。myStr.utf16
即String.UTF16View
。用於獲取 UTF16 編碼的程式碼單元。myStr.utf8
即String.UTF8View
。能夠獲取 UTF8 編碼的程式碼單元。
在大多數時候開發者都在處理簡單的字串字元,而不是深入到編碼或者碼位這樣的細節中。
CharacterView
能很好地完成大多數任務:迭代字串、字元計數、驗證是否包含字串、通過索引訪問和比較操作等。
讓我們看看如何用 Swift 來完成這些任務。
1. Character 和 CharterView 的結構
String.CharacterView
的結構是一個字元內容的檢視,它是 Character
的集合。
要從字串訪問檢視,使用字元的 characters
屬性:
let message = "Hello, world"
let characters = message.characters
print(type(of: characters))// => "CharacterView"複製程式碼
message.characters
返回了 CharacterView
結構.
字元檢視是 Character
結構的集合。例如,我們可以這樣來訪問字元檢視裡的第一個字元:
let message = "Hello, world"
let firstCharacter = message.characters.first!
print(firstCharacter) // => "H"
print(type(of: firstCharacter)) // => "Character"
let capitalHCharacter: Character = "H"
print(capitalHCharacter == firstCharacter) // => true複製程式碼
message.characters.first
返回了一個可選型別,內容是它的第一個字元 "H"
.
這個字元例項代表了單個符號 H
。
在 Unicode 標準中,H
代表 Latin Capital letter H (拉丁文大寫字母 H),碼位是 U+0048
。
讓我們掠過 ASCII 看看 Swift 如何處理更復雜的符號。這些字元被渲染成單個視覺符號,但實際上是由兩個或更多個 Unicode 標量 組成。嚴格來說這些字元被稱為 字形簇
重點: CharacterView
是字串的字形簇集合。
讓我們看看 ç
的字形。他可以有兩種表現形式:
- 使用
U+00E7
LATIN SMALL LETTER C WITH CEDILLA (拉丁文小寫變音字母 C):被渲染為ç
- 或者使用組合字元序列:
U+0063
LATIN SMALL LETTER C 加上 組合標記U + 0327
COMBINING CEDILLA 組成複合字形:c
+◌̧
=ç
我們看看在第二個選項中 Swift 是如何處理它的:
let message = "c\u{0327}a va bien" // => "ça va bien"
let firstCharacter = message.characters.first!
print(firstCharacter) // => "ç"
let combiningCharacter: Character = "c\u{0327}"
print(combiningCharacter == firstCharacter) // => true複製程式碼
firstCharacter
包含了一個字形 ç
,它是由兩個 Unicode 標量 U+0063
and U+0327
組合渲染出來的。
Character
結構接受多個 Unicode 標量來建立一個單一的字形。如果你嘗試在單個 Character
中新增更多的字形,Swift 將會出發錯誤:
let singleGrapheme: Character = "c\u{0327}\u{0301}" // Works
print(singleGrapheme) // => "ḉ"
let multipleGraphemes: Character = "ab" // Error!複製程式碼
即使 singleGrapheme
由 3 個 Unicode 標量組成,它建立了一個字形 ḉ
。
而 multipleGraphemes
則是從兩個 Unicode 標量建立一個 Character
,這將在單個 Character
結構中建立兩個分離的字母 a
和 b
,這不是被允許的操作。
2. 遍歷字串中的字元
CharacterView
集合遵循了 Sequence
協議。這將允許在 for-in
迴圈中遍歷字元檢視:
let weather ="rain"for char in weather.characters {print(char)}// => "r" // => "a" // => "i" // => "n"複製程式碼
我們可以在 for-in
迴圈中訪問到 weather.characters
中的每個字元。char
變數將會在迭代中依次分配給 weather
中的 "r"
, "a"
, "i"
和 "n"
字元。
當然你也可以用 forEach(_:)
方法來迭代字元,指定一個閉包作為第一個引數:
let weather = "rain"
for char in weather.characters {
print(char)
}
// => "r"
// => "a"
// => "i"
// => "n"複製程式碼
使用 forEach(_:)
的方式與 for-in
相似,唯一的不同是你不能使用 continue
或者 break
語句。
要在迴圈中訪問當前字串的索引可以通過 CharacterView
提供的 enumerated()
方法。這個方法將會返回一個元組序列 (index, character)
:
let weather = "rain"
for (index, char) in weather.characters.enumerated() {
print("index: \(index), char: \(char)")
}
// => "index: 0, char: r"
// => "index: 1, char: a"
// => "index: 2, char: i"
// => "index: 3, char: n"複製程式碼
enumerated()
方法在每次迭代時返回元組 (index, char)
。index
變數即為迴圈中當前字元的索引,而 char
變數則是迴圈中當前的字元。
3. 統計字元
只需要訪問 CharacterView
的 count
屬性就可以獲得字串中字元的個數:
let weather ="sunny"print(weather.characters.count)// => 5複製程式碼
weather.characters.count
是字串中字元的個數。
檢視中的每一個字元都擁有一個字形。當相鄰字元(比如 組合標記 )被新增到字串時,你可能發現 count
屬性沒有沒有變大。
這是因為相鄰字元並沒有在字串中建立一個新的字形,而是附加到了已經存在的 基本 Unicode 字形 中。讓我們看一個例子:
var drink = "cafe"
print(drink.characters.count) // => 4
drink += "\u{0301}"
print(drink) // => "café"
print(drink.characters.count) // => 4複製程式碼
一開始 drink
含有四個字元。
當組合標記 U+0301
COMBINING ACUTE ACCENT 被新增到字串中,它改變了上一個基本字元 e
並建立了新的字形 é
。這時屬性 count
並沒有變大,因為字形數量仍然相同。
4. 按索引訪問字元
因為 Swift 直到它實際評估字元檢視中的字形之前都不知道字串中的字元個數,所以無法通過下標的方式訪問字串索引。
你可以通過特殊的型別 String.Index
訪問字元。
如果你需要訪問字串中的第一個或者最後一個字元,字元檢視結構提供了 first
和 last
屬性:
let season = "summer"
print(season.characters.first!) // => "s"
print(season.characters.last!) // => "r"
let empty = ""
print(empty.characters.first == nil) // => true
print(empty.characters.last == nil) // => true複製程式碼
注意 first
和 last
屬性將會返回可選型別 Character?
。
在空字串 empty
這些屬性將會是 nil
。
要獲取特定位置的字元,你必須使用 String.Index
型別(實際上是 String.CharacterView.Index
的別名)。字元提供了一個接受 String.Index
下標訪問字元的方法,以及預定義的索引 myString.startIndex
和 myString.endIndex
。
讓我們使用字串索引來訪問第一個和最後一個字元:
let color = "green"
let startIndex = color.startIndex
let beforeEndIndex = color.index(before: color.endIndex)
print(color[startIndex]) // => "g"
print(color[beforeEndIndex]) // => "n"複製程式碼
color.startIndex
是第一個字元的索引,所以 color[startIndex]
表示為 g
。color.endIndex
表示結束位置,或者簡單的說是比最後一個有效下標引數大的位置。要訪問最後一個字元,你必須計算它的前一個索引:color.index(before: color.endIndex)
要通過偏移訪問字元的位置, 在 index(theIndex, offsetBy: theOffset)
方法中使用 offsetBy
引數:
let color = "green"
let secondCharIndex = color.index(color.startIndex, offsetBy: 1)
let thirdCharIndex = color.index(color.startIndex, offsetBy: 2)
print(color[secondCharIndex]) // => "r"
print(color[thirdCharIndex]) // => "e"複製程式碼
指定 offsetBy
引數,你將可以放特定偏移量位置的字元。
當然,offsetBy
引數是的步進是字串的字形。即偏移量適用於 ChacterView
中的 Chacter
例項。
如果索引超出範圍,Swift 會觸發錯誤。
let color ="green"
let oops = color.index(color.startIndex, offsetBy:100) // Error!複製程式碼
為了防止這種情況,可以指定一個 limitedBy
引數來限制最大偏移量:index(theIndex, offsetBy: theOffset, limitedBy: theLimit)
。這個函式將會返回一個可選型別,當索引超出範圍時將會返回 nil
:
let color = "green"
let oops = color.index(color.startIndex, offsetBy: 100,
limitedBy: color.endIndex)
if let charIndex = oops {
print("Correct index")
} else {
print("Incorrect index")
}
// => "Incorrect index"複製程式碼
oops
是一個可選型別 String.Index?
。展開可選型別可以驗證索引是否超出了字串的範圍。
5. 檢查子串是否存在
驗證子串是否存在的最簡單方法是呼叫 contains(_ other: String)
方法:
import Foundation
let animal = "white rabbit"
print(animal.contains("rabbit")) // => true
print(animal.contains("cat")) // => false複製程式碼
animal.contains("rabbit")
將返回 true
因為 animal
包含了 "rabbit"
字串。
那麼當子字串不存在的時候 animal.contains("cat")
的值將為 false
。
要驗證字串是否具有特定的字首或字尾,可以使用 hasPrefix(_:)
和 hasSuffix(_:)
方法。我們來看一個例子:
importFoundationlet
animal = "white rabbit"
print(animal.hasPrefix("white")) // => true
print(animal.hasSuffix("rabbit")) // => true複製程式碼
"white rabbit"
以 "white"
開頭並以 "rabbit"
結尾。所以我們呼叫 animal.hasPrefix("white")
和 animal.hasSuffix("rabbit")
方法都將返回 true
。
當你想搜尋字串時,直接查詢字元檢視是就可以了。比如:
let animal = "white rabbit"
let aChar: Character = "a"
let bChar: Character = "b"
print(animal.characters.contains(aChar)) // => true
print(animal.characters.contains {
$0 == aChar || $0 == bChar
}) // => true複製程式碼
contains(_:)
將驗證字元檢視是否包含指定檢視。
而第二個函式 contains(where predicate: (Character) -> Bool)
則是接受一個閉包並執行驗證。
6. 字串操作
字串在 Swift 中是 value type(值型別)。無論你是將它作為引數進行函式呼叫還是將它分配給一個變數或者常量——每次複製都將會建立一個全新的拷貝。
所有的可變方法都是在空間內將字串改變。
本節涵蓋了對字串的常見操作。
附加字串到另一個字串
附加字串較為簡便的方法是直接使用 +=
操作符。你可以直接將整個字串附加到原始字串:
var bird ="pigeon"
bird +=" sparrow"
print(bird) // => "pigeon sparrow"複製程式碼
字串結構提供了一個可變方法 append()
。該方法接受字串、字元甚至字元序列,並將其附加到原始字串。例如
var bird = "pigeon"
let sChar: Character = "s"
bird.append(sChar)
print(bird) // => "pigeons"
bird.append(" and sparrows")
print(bird) // => "pigeons and sparrows"
bird.append(contentsOf: " fly".characters)
print(bird) // => "pigeons and sparrows fly"複製程式碼
從字串中擷取字串
使用 substring()
方法可以擷取字串:
- 從特定索引到字串的末尾
- 從開頭到特定索引
- 或者基於一個索引區間
讓我們來看看它是如何工作的
let plant = "red flower"
let strIndex = plant.index(plant.startIndex, offsetBy: 4)
print(plant.substring(from: strIndex)) // => "flower"
print(plant.substring(to: strIndex)) // => "red "
if let index = plant.characters.index(of: "f") {
let flowerRange = index..<plant.endIndex
print(plant.substring(with: flowerRange)) // => "flower"
}複製程式碼
字串下標接受一個區間或者封閉區間作為字元索引。這有助於根據範圍擷取子串:
Try in Swift sandbox (target=undefined)
let plant ="green tree"let excludeFirstRange =
plant.index(plant.startIndex, offsetBy:1)..<plant.endIndex
print(plant[excludeFirstRange]) // => "reen tree"
let lastTwoRange = plant.index(plant.endIndex, offsetBy:-2)..<plant.endIndex
print(plant[lastTwoRange]) // => "ee"複製程式碼
插入字串
字串型別提供了可變方法 insert()
。此方法可以在特定索引處插入一個字元或者一個字元序列。
新的字元將被插入到指定索引的元素之前。
來看一個例子:
var plant = "green tree"
plant.insert("s", at: plant.endIndex)
print(plant) // => "green trees"
plant.insert(contentsOf: "nice ".characters, at: plant.startIndex)
print(plant) // => "nice green trees"複製程式碼
移除字元
可變方法 remove(at:)
可以刪除指定索引處的字元:
var weather = "sunny day"
if let index = weather.characters.index(of: " ") {
weather.remove(at: index)
print(weather) // => "sunnyday"
}複製程式碼
你也可以使用 removeSubrange(_:)
來從字串中移除一個索引區間內的全部字元:
var weather = "sunny day"
let index = weather.index(weather.startIndex, offsetBy: 6)
let range = index..<weather.endIndex
weather.removeSubrange(range)
print(weather) // => "sunny"複製程式碼
替換字串
replaceSubrange(_:with:)
方法接受一個索引區間並可以將區間內的字串替換為特定字串。這是字串的一個可變方法。
一個簡單的例子:
var weather = "sunny day"
if let index = weather.characters.index(of: " ") {
let range = weather.startIndex..<index
weather.replaceSubrange(range, with: "rainy")
print(weather) // => "rainy day"
}複製程式碼
另一些關於字串的可變操作
上面描述的許多字串操作都是直接應用於字串中的字元檢視。
如果你覺得直接對字元序列進行操作更加方便的話,那也是個不錯的選擇。
比如你可以刪除特定索引出的字元,或者直接刪除第一個或者最後一個字元:
var fruit = "apple"
fruit.characters.remove(at: fruit.startIndex)
print(fruit) // => "pple"
fruit.characters.removeFirst()
print(fruit) // => "ple"
fruit.characters.removeLast()
print(fruit) // => "pl"複製程式碼
使用字元檢視中的 reversed()
方法來翻轉字元檢視:
var fruit ="peach"
var reversed =String(fruit.characters.reversed())
print(reversed)// => "hcaep"複製程式碼
你可以很簡單得過濾字串:
let fruit = "or*an*ge"
let filtered = fruit.characters.filter { char in
return char != "*"
}
print(String(filtered)) // => "orange"複製程式碼
Map 可以接受一個閉包來對字串進行變換:
let fruit = "or*an*ge"
let mapped = fruit.characters.map { char -> Character in
if char == "*" {
return "+"
}
return char
}
print(String(mapped)) // => "or+an+ge"複製程式碼
或者使用 reduce 來對字串來進行一些累加操作:
let fruit = "or*an*ge"
let numberOfStars = fruit.characters.reduce(0) { countStars, char in
if (char == "*") {
return countStarts + 1
}
return countStars
}
print(numberOfStars) // => 2複製程式碼
7. 說在最後
首先要說,大家對於字串內容持有的不同觀點看起來似乎過於複雜。
而在我看來這是一個很好的實現。字串可以從不同的角度來看待:作為字形集合、UTF-8 / UTF-16 碼位或者簡單的 Unicode 標量。
根據你的任務來選擇合適的檢視。在大多數情況下,CharacterView
都很合適。
因為字元檢視中可能包含來自一個或多個 Unicode 標量組成的字形。因此字串並不能像陣列那樣直接被整數索引。不過可以用特殊的 String.Index
來索引字串。
雖然特殊的索引型別導致在訪問單個字串或者操作字串時增加了一些難度。我接受這個成本,因為在字串上進行真正的 Unicode 感知操作真的很棒!
對於字元操作你有沒有找到更舒適的方法?寫下評論我們一起來討論一些吧!
P.S. 不知道你有沒有興趣閱讀我的另一篇文章:detailed overview of array and dictionary literals in Swift
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃。