掌握 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前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划

相关文章
相关标签/搜索