掌握 Swift 的字符串細節

掌握 Swift 的字符串細節

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.charactersString.CharacterView。能夠獲取字形的值,視覺上呈現爲單一的符號,是最經常使用的視圖。
  • myStr.unicodeScalarsString.UnicodeScalarView。能夠獲取 21 整數表示的 Unicode 碼位。
  • myStr.utf16String.UTF16View。用於獲取 UTF16 編碼的代碼單元。
  • myStr.utf8String.UTF8View。可以獲取 UTF8 編碼的代碼單元。

Swift 中的 CharacterView, UnicodeScalarView, UTF16View 和 UTF8View

在大多數時候開發者都在處理簡單的字符串字符,而不是深刻到編碼或者碼位這樣的細節中。後端

CharacterView 能很好地完成大多數任務:迭代字符串、字符計數、驗證是否包含字符串、經過索引訪問和比較操做等。api

讓咱們看看如何用 Swift 來完成這些任務。

1. Character 和 CharterView 的結構

String.CharacterView 的結構是一個字符內容的視圖,它是 Character 的集合。

要從字符串訪問視圖,使用字符的 characters 屬性:

Try in Swift sandbox

let message = "Hello, world"
let characters = message.characters
print(type(of: characters))// => "CharacterView"複製代碼

message.characters 返回了 CharacterView 結構.

字符視圖是 Character 結構的集合。例如,咱們能夠這樣來訪問字符視圖裏的第一個字符:

Try in Swift sandbox

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+0063LATIN SMALL LETTER C 加上 組合標記 U + 0327 COMBINING CEDILLA 組成複合字形:c + ◌̧ = ç

咱們看看在第二個選項中 Swift 是如何處理它的:

Try in Swift sandbox

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 將會出發錯誤:

Try in Swift sandbox

let singleGrapheme: Character = "c\u{0327}\u{0301}" // Works
print(singleGrapheme) // => "ḉ"

let multipleGraphemes: Character = "ab" // Error!複製代碼

即便 singleGrapheme 由 3 個 Unicode 標量組成,它建立了一個字形
multipleGraphemes 則是從兩個 Unicode 標量建立一個 Character,這將在單個 Character 結構中建立兩個分離的字母 ab,這不是被容許的操做。

2. 遍歷字符串中的字符

CharacterView 集合遵循了 Sequence 協議。這將容許在 for-in 循環中遍歷字符視圖:

Try in Swift sandbox

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(_:) 方法來迭代字符,指定一個閉包做爲第一個參數:

Try in Swift sandbox

let weather = "rain"
for char in weather.characters {
  print(char)
}
// => "r"
// => "a"
// => "i"
// => "n"複製代碼

使用 forEach(_:) 的方式與 for-in 類似,惟一的不一樣是你不能使用 continue 或者 break 語句。

要在循環中訪問當前字符串的索引能夠經過 CharacterView 提供的 enumerated() 方法。這個方法將會返回一個元組序列 (index, character)

Try in Swift sandbox

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. 統計字符

只須要訪問 CharacterViewcount 屬性就能夠得到字符串中字符的個數:

Try in Swift sandbox

let weather ="sunny"print(weather.characters.count)// => 5複製代碼

weather.characters.count 是字符串中字符的個數。

視圖中的每個字符都擁有一個字形。當相鄰字符(好比 組合標記 )被添加到字符串時,你可能發現 count 屬性沒有沒有變大。

這是由於相鄰字符並無在字符串中建立一個新的字形,而是附加到了已經存在的 基本 Unicode 字形 中。讓咱們看一個例子:

Try in Swift sandbox

var drink = "cafe"
print(drink.characters.count) // => 4
drink += "\u{0301}"
print(drink)                  // => "café"
print(drink.characters.count) // => 4複製代碼

一開始 drink 含有四個字符。

當組合標記 U+0301COMBINING ACUTE ACCENT 被添加到字符串中,它改變了上一個基本字符 e 並建立了新的字形 é。這時屬性 count 並無變大,由於字形數量仍然相同。

4. 按索引訪問字符

由於 Swift 直到它實際評估字符視圖中的字形以前都不知道字符串中的字符個數,因此沒法經過下標的方式訪問字符串索引。

你能夠經過特殊的類型 String.Index 訪問字符。

若是你須要訪問字符串中的第一個或者最後一個字符,字符視圖結構提供了 firstlast 屬性:

Try in Swift sandbox

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複製代碼

注意 firstlast 屬性將會返回可選類型 Character?

在空字符串 empty 這些屬性將會是 nil

String indexes in Swift

要獲取特定位置的字符,你必須使用 String.Index 類型(其實是 String.CharacterView.Index的別名)。字符提供了一個接受 String.Index 下標訪問字符的方法,以及預約義的索引 myString.startIndexmyString.endIndex

讓咱們使用字符串索引來訪問第一個和最後一個字符:

Try in Swift sandbox

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 參數:

Try in Swift sandbox

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 會觸發錯誤。

Try in Swift sandbox

let color ="green"
let oops = color.index(color.startIndex, offsetBy:100) // Error!複製代碼

爲了防止這種狀況,能夠指定一個 limitedBy 參數來限制最大偏移量:index(theIndex, offsetBy: theOffset, limitedBy: theLimit)。這個函數將會返回一個可選類型,當索引超出範圍時將會返回 nil

Try in Swift sandbox

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) 方法:

Try in Swift sandbox

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(_:) 方法。咱們來看一個例子:

Try in Swift sandbox

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

當你想搜索字符串時,直接查詢字符視圖是就能夠了。好比:

Try in Swift sandbox

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(值類型)。不管你是將它做爲參數進行函數調用仍是將它分配給一個變量或者常量——每次複製都將會建立一個全新的拷貝

全部的可變方法都是在空間內將字符串改變。

本節涵蓋了對字符串的常見操做。

附加字符串到另外一個字符串

附加字符串較爲簡便的方法是直接使用 += 操做符。你能夠直接將整個字符串附加到原始字符串:

Try in Swift sandbox

var bird ="pigeon"
bird +=" sparrow"
print(bird) // => "pigeon sparrow"複製代碼

字符串結構提供了一個可變方法 append()。該方法接受字符串、字符甚至字符序列,並將其附加到原始字符串。例如

Try in Swift sandbox

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() 方法能夠截取字符串:

  • 從特定索引到字符串的末尾
  • 從開頭到特定索引
  • 或者基於一個索引區間

讓咱們來看看它是如何工做的

Try in Swift sandbox

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()。此方法能夠在特定索引處插入一個字符或者一個字符序列。

新的字符將被插入到指定索引的元素以前。

來看一個例子:

Try in Swift sandbox

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:) 能夠刪除指定索引處的字符:

Try in Swift sandbox

var weather = "sunny day"
if let index = weather.characters.index(of: " ") {
  weather.remove(at: index)
  print(weather) // => "sunnyday"
}複製代碼

你也可使用 removeSubrange(_:) 來從字符串中移除一個索引區間內的所有字符:

Try in Swift sandbox

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:) 方法接受一個索引區間並能夠將區間內的字符串替換爲特定字符串。這是字符串的一個可變方法。

一個簡單的例子:

Try in Swift sandbox

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"
}複製代碼

另外一些關於字符串的可變操做

上面描述的許多字符串操做都是直接應用於字符串中的字符視圖。

若是你以爲直接對字符序列進行操做更加方便的話,那也是個不錯的選擇。

好比你能夠刪除特定索引出的字符,或者直接刪除第一個或者最後一個字符:

Try in Swift sandbox

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() 方法來翻轉字符視圖:

Try in Swift sandbox

var fruit ="peach"
var reversed =String(fruit.characters.reversed())
print(reversed)// => "hcaep"複製代碼

你能夠很簡單得過濾字符串:

Try in Swift sandbox

let fruit = "or*an*ge"
let filtered = fruit.characters.filter { char in
  return char != "*"
}
print(String(filtered)) // => "orange"複製代碼

Map 能夠接受一個閉包來對字符串進行變換:

Try in Swift sandbox

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 來對字符串來進行一些累加操做:

Try in Swift sandbox

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

掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索