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