- 原文地址:Mastering Swift: essential details about strings
- 原文做者:Dmitri Pavlutin
- 譯文出自:掘金翻譯計劃
- 譯者:Tuccuay
- 校對者:oOatuo , lsvih
String 類型在任何編程語言中都是一個重要的組成部分。而用戶從 iOS 應用的屏幕上能讀取到最有效的信息也來自文本。前端
爲了觸及更多的用戶,iOS 應用必須國際化以支持大量現代語言。Unicode 標準解決了這個問題,不過這也給咱們使用 string 類型帶來了額外的挑戰性。react
從一方面來講,編程語言在處理字符串時應該在 Unicode 複雜性和性能之間取得平衡。而另外一方面,它須要爲開發者提供一個溫馨的結構來處理字符串。android
而在我看來,Swift 在這兩方面都作的不錯。ios
幸運的是 Swift 的 string 類型並非像 JavaScript 或者 Java 那樣簡單的 UTF-16 序列。git
對一個 UTF-16 碼單元序列執行 Unicode 感知的字符串操做是很痛苦的:你可能會打破代理對或組合字符序列。github
Swift 對此有着更好的實現方式。字符串自己再也不是集合,而是可以根據不一樣狀況爲內容提供不一樣的 view。其中一個特殊的 view: String.CharacterView
則是徹底支持 Unicode 的。編程
對於 let myStr = "Hello, world"
來講,你能夠訪問到下面這些 view:swift
myStr.characters
即 String.CharacterView
。能夠獲取字形的值,視覺上呈現爲單一的符號,是最經常使用的視圖。myStr.unicodeScalars
即 String.UnicodeScalarView
。能夠獲取 21 整數表示的 Unicode 碼位。myStr.utf16
即 String.UTF16View
。用於獲取 UTF16 編碼的代碼單元。myStr.utf8
即 String.UTF8View
。可以獲取 UTF8 編碼的代碼單元。在大多數時候開發者都在處理簡單的字符串字符,而不是深刻到編碼或者碼位這樣的細節中。後端
CharacterView
能很好地完成大多數任務:迭代字符串、字符計數、驗證是否包含字符串、經過索引訪問和比較操做等。api
讓咱們看看如何用 Swift 來完成這些任務。
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
,這不是被容許的操做。
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
變量則是循環中當前的字符。
只須要訪問 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
並無變大,由於字形數量仍然相同。
由於 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?
。展開可選類型能夠驗證索引是否超出了字符串的範圍。
驗證子串是否存在的最簡單方法是調用 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)
則是接受一個閉包並執行驗證。
字符串在 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複製代碼
首先要說,你們對於字符串內容持有的不一樣觀點看起來彷佛過於複雜。
而在我看來這是一個很好的實現。字符串能夠從不一樣的角度來看待:做爲字形集合、UTF-8 / UTF-16 碼位或者簡單的 Unicode 標量。
根據你的任務來選擇合適的視圖。在大多數狀況下,CharacterView
都很合適。
由於字符視圖中可能包含來自一個或多個 Unicode 標量組成的字形。所以字符串並不能像數組那樣直接被整數索引。不過能夠用特殊的 String.Index
來索引字符串。
雖然特殊的索引類型致使在訪問單個字符串或者操做字符串時增長了一些難度。我接受這個成本,由於在字符串上進行真正的 Unicode 感知操做真的很棒!
對於字符操做你有沒有找到更溫馨的方法?寫下評論咱們一塊兒來討論一些吧!
P.S. 不知道你有沒有興趣閱讀個人另外一篇文章:detailed overview of array and dictionary literals in Swift
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。