Swift-字符串和字符

一个字符串 是一系列字符的集合,例如hello, worldalbatross。Swift的字符串是String类型。可以通过不同访问字符串的内容,例如作为Character值的集合。
Swift的StringCharacter类型提供了一个快速的,兼容Unicode的方式处理文本内容。字符串创建和操作的语法是轻量级的,易读的,同C语言的语法类似。连接两个字符串只需要通过+运算符便可完成,像Swift的其它类型一样,字符串的可变性取决于它是一个常量还是一个变量。可以将常量,变量,字面量和表达式插入到一个更长的字符串中,这个过程称为字符串插值。这使得创建一个自定义的字符串来进行展示,存储和打印变得简单。
尽管语法简洁,但是Swift的String类型却是一个快速,现代的字符串实现。每一个字符都是由独立编码的Unicode字符组成,并且提供了以不同的Unicode表现形式访问这些Unicode字符的方式。

注意
Swift的String类型与Foundation框架的NSString类进行了桥接。Foundation框架同样扩展了String来暴露NSString的方法。这意味着如果你引入了Foundation框架,可以在String上直接调用NSString的方法而无需转换。
更多关于结合Foundation和Cocoa使用String的内容,详见Using Swift with Cocoa and Objective-C (Swift 3.0.1)Working with Cocoa Data Types

目录

  • 字符串字面量
  • 初始化一个空字符串
  • 字符串可变性
  • 字符串是值类型
  • 使用字符
  • 连接字符串和字符
  • 字符串插值
  • Unicode
  • 字符统计
  • 字符串访问和修改
  • 字符串比较
  • 字符串的Unicode表示

字符串字面量

可以在代码中包括预定义的String值作为字符串字面量。一个字符串字面量是被引号("")包围的一段固定序列的文本字符。
使用字符串字面量作为一个常量或者变量的初始值:

let someString = "Some string literal value"

注意,Swift将someString推断为String类型,因为它被初始化为一个字符串字面量。

注意
更多关于在字符串字面量使用特殊字符的信息,详见Special Characters in String Literals

初始化一个空字符串

想创建一个空的String值变量作为创建一个更长的字符串的起点,可以赋值为一个空的字符串字面量,或者使用初始化语法初始化一个新的String实例:

var emptyString = ""               // empty string literal
var anotherEmptyString = String()  // initializer syntax
// these two strings are both empty, and are equivalent to each other

可以通过检查字符串的isEmpty布尔属性来判断一个String的值是否为空。

if emptyString.isEmpty {
    print("Nothing to see here")
}
// Prints "Nothing to see here"

字符串可变性

一个指定的字符串是否可变取决于它被赋值为一个常量还是变量:

var variableString = "Horse"
variableString += " and carriage"
// variableString is now "Horse and carriage"
 
let constantString = "Highlander"
constantString += " and another Highlander"
// this reports a compile-time error - a constant string cannot be modified

注意
在OC与Cocoa中,通过选择使用NSString或者NSMutableString来决定一个字符串是否可变,这种处理方式和Swift是不同的。

字符串是值类型

Swift的String类型是一个值类型。创建一个新的String值,当它被传递到一个函数或者方法,或者被赋值给一个常量或则变量时,它的值是被复制的。在每种情况下,都创建了一个已知值的副本,然后这个副本被传递或者赋值,而不是原始的值。值类型会在结构体和枚举是值类型介绍。
String这种默认的值拷贝行为确保了当一个函数或者方法传入了一个String值时,你清晰的知道你拥有这个确切的String值,而无需考虑它的来源。你可以很自信的认为你传入的String值不会被修改,除非你自己修改了它。
实际上,Swift的编译器会最优化字符串的使用,所以实际的拷贝只会发生在绝对必要时。这意味着当把一个字符串作为值类型处理时总能获取到最优的性能。

使用字符

可以通过在for-in循环中遍历一个字符串的characters属性来获取它的每一个Character值。

for character in "Dog!🐶".characters {
    print(character)
}
// D
// o
// g
// !
// 🐶

for-in循环在for-in循环介绍
同样的,通过为一个单字符的字符串字面量标注一个Character型的类型注释来创建一个单独的Character常量或者变量:

let exclamationMark: Character = "!"

可以通过将一个由Character值组成的数组当成参数传入String的初始化方法中来构造一个String值:

let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"]
let catString = String(catCharacters)
print(catString)
// Prints "Cat!🐱"

连接字符串和字符

String值可以通过加号运算符(+)相加来创建一个新的String值:

let string1 = "hello"
let string2 = " there"
var welcome = string1 + string2
// welcome now equals "hello there"

同样可以使用加等于运算符(+=)拼接一个String值:

var instruction = "look over"
instruction += string2
// instruction now equals "look over there"

可以通过使用String类型的append()方法拼接一个Character值:

let exclamationMark: Character = "!"
welcome.append(exclamationMark)
// welcome now equals "hello there!"

注意
不能向一个Character变量拼接String或者Character,因为一个Character值只能包含一个字符。

字符串插值

字符串插值 是一种通过将常量,变量,字面量和表达式的值混合包含进一个字符串字面量来构建一个新的String值的方式。每一个插入到字符串字面量的元素都被包装进前缀为一个反斜杠的一对圆括号中:

let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"

上例中,multiplier的值以\(multiplier)的形式被插入到字符串字面量中。当使用字符串插值创建一个实际的字符时,此占位符会被替换为multiplier实际的值。
multiplier的值稍后也会作为这个字符串中一个更大的表达式的一部分。这个表达式计算Double(multiplier) * 2.5的值并将结果(7.5)插入这个字符串。此例中,这个表达式被写成\(Double(multiplier) * 2.5)包含进这个字符串字面量中。

注意
一个插值字符串中,圆括号内的表达式不能包含非转义的反斜杠(\\),回车或者换行。但是可以包含其它字符串字面量。

Unicode

Unicode 是一个在不同文字系统下用于文本的编码,表示和处理的国际标准。它以一种标准的形式允许你几乎可以描述任何语言的任何字符,并且可以对文本文件或者网页这样的外部数据源进行读写操作。Swift的StringCharacter类型是完全兼容Unicode的。

Unicode标量

实际上,Swift的String类型是基于Unicode标量 值建立的。一个Unicode标量是一个字符或者标识符唯一的21位数字,例如U+0061代表小写的拉丁字母aU+1F425代表小鸡表情🐥

注意
一个Unicode标量是范围U+0000U+D7FF或者U+E000U+10FFFF的任何码位。Unicode标量不包含范围为U+D800U+DFFF的Unicode代理项码位。

注意并不是所有的21位Unicode码位都分配给了字符--一些标量是留作未来分配的。分配给字符的标量通常都有一个名字,例如例子中的小写拉丁字母a和小鸡符号🐥

字符串字面量的特殊字符

字符串字面量可以包含以下特殊字符

  • 转义特殊字符\0(空字符),\\\\(反斜线),\t(制表符),\n(换行符),\r(回车符),\"(双引号),\'(单引号)
  • 一个任意的写成\u{n}的Unicode标量,n是一个1-8位的16进制数,它等于一个合法的Unicode码位。

以下四个例子的代码展示了这些特殊字符。wiseWords常量包含两个转义的双引号字符,dollarSignblackHeartsparklingHeart常量展示了Unicode标量格式:

let wiseWords = "\"Imagination is more important than knowledge\" - Einstein"
// "Imagination is more important than knowledge" - Einstein
let dollarSign = "\u{24}"        // $,  Unicode scalar U+0024
let blackHeart = "\u{2665}"      // ♥,  Unicode scalar U+2665
let sparklingHeart = "\u{1F496}" // 💖, Unicode scalar U+1F496

可扩展的字符簇

每一个Swift的Character类型实例都代表了一个单独的可扩展的字符簇。一个可扩展的字符簇是一个由一个或多个Unicode标量组成的序列,这些标量提供了人类可以读懂的字符。
举个例子,字母é可以被表示为单独的Unicode标量é(二声的小写拉丁字母e)。但是它同样也可以被表示为一对标量--一个标准的字母e(小写的拉丁字母e或者U+0065)和一个二声标量(U+0301)。这个二声标量形象的应用在了它前面的标量,当一个可识别Unicode的文本渲染系统渲染它的时候,将e转化为了é
下面的两个例子中,字母é被表示为一个单独的SwiftCharacter值,这个值代表了一个可扩展的字符簇。第一个例子中,这个簇包含了一个单独的标量;第二个例子中,包含了两个标量:

let eAcute: Character = "\u{E9}"                         // é
let combinedEAcute: Character = "\u{65}\u{301}"          // e followed by ́
// eAcute is é, combinedEAcute is é

可扩展的字符簇可以灵活地将许多复杂的脚本字符表现为一个单独的Character值。例如,韩语字母表的韩语音节既可以表示为组合的也可以表示为分解的序列。这些表现方式在Swift中都代表了一个单独的Character值:

let precomposed: Character = "\u{D55C}"                  // 한
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}"   // ᄒ, ᅡ, ᆫ
// precomposed is 한, decomposed is 한

可扩展的字符簇可以使用包围标记(例如COMBINING ENCLOSING CIRCLE或者U+20DD)将其它Unicode标量包围起来作为一个单独的Character值的一部分:

let enclosedEAcute: Character = "\u{E9}\u{20DD}"
// enclosedEAcute is é⃝

代表区域指示符号的Unicode标量可以组合成一对来表示一个单独的Character值,例如REGIONAL INDICATOR SYMBOL LETTER U(U+1F1FA)和REGIONAL INDICATOR SYMBOL LETTER S(U+1F1F8)的组合:

let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}"
// regionalIndicatorForUS is 🇺🇸

字符统计

如果想要获取一个字符串中Character值的数量,使用字符串的Character属性的count属性:

let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
print("unusualMenagerie has \(unusualMenagerie.characters.count) characters")
// Prints "unusualMenagerie has 40 characters"

注意在Swift中,使用可扩展的字符簇作为Character值意味着字符串的连接或者修改并不一定会影响字符串的字符数量。
例如,如果使用四个字符长度的单词cafe初始化一个新的字符串,然后在尾部拼接一个COMBINING ACUTE ACCENT (U+0301),结果仍然是含有4个字符,第四个字符是é而不是e

var word = "cafe"
print("the number of characters in \(word) is \(word.characters.count)")
// Prints "the number of characters in cafe is 4"
 
word += "\u{301}"    // COMBINING ACUTE ACCENT, U+0301
 
print("the number of characters in \(word) is \(word.characters.count)")
// Prints "the number of characters in café is 4"

注意
可扩展的字符簇可以由多个Unicode标量构成。这意味着不同的字符以及相同字符的不同表示方式可以请求不同的存储空间。因此Swift的字符在一个字符串中并不一定占据相同的存储空间。所以在没有遍历整个字符串确定它的可扩展的字符簇边界时是不能计算字符的数量的。如果处理一个特别长的字符值,需要注意Character属性必须遍历整个字符串的所有Unicode标量才能确定字符的数量。
通过Character属性返回的字符数量并不总是和包含相同字符的通过NSStringlength属性返回的长度相同。NSString的长度是基于字符串在UTF-16表示方式下16位码元的数量而不是Unicode可扩展字符簇的数量。

访问和修改字符串

可以通过方法,属性或者下标语法访问和修改一个字符串。

字符串索引

每一个String值都有一个关联的索引类型 (String.Index),它与字符的位置一一对应。
前面提到,不同的字符可以请求不同的存储空间,所以为了确定一个指定位置的Character,必须从String的头部或者尾部遍历每个Unicode标量。因此,Swift的字符串不能被整型值索引。
使用startIndex属性访问一个String的首个CharacterendIndex属性是String中的最后一个值之后的位置。因此endIndex属性属性不是一个字符串下标的合法值。如果一个String为空,startIndexendIndex相同。
使用Stringindex(before:)index(after:)方法访问一个给定索引的前一个或后一个索引。使用index(_:offsetBy:)访问一个距离给定索引较远的索引,而不是多次调用另外两个方法。
使用下标语法访问一个指定的String索引的Character

let greeting = "Guten Tag!"
greeting[greeting.startIndex]
// G
greeting[greeting.index(before: greeting.endIndex)]
// !
greeting[greeting.index(after: greeting.startIndex)]
// u
let index = greeting.index(greeting.startIndex, offsetBy: 7)
greeting[index]
// a

试图访问一个字符串范围之外的索引或者索引对应的Character会触发运行时错误。

greeting[greeting.endIndex] // Error
greeting.index(after: greeting.endIndex) // Error

使用Character属性的indices属性访问字符串中字符的所有索引。

for index in greeting.characters.indices {
    print("\(greeting[index]) ", terminator: "")
}
// Prints "G u t e n   T a g ! "

注意
可以在任何遵循Collection协议的类型上使用startIndexendIndex属性,index(before:)index(after:)index(_:offsetBy:)方法。这些类型包括在这介绍的String,也包括集合类型如ArrayDictionarySet

插入和删除

使用insert(_:at:)方法在字符串的指定位置插入一个单独的字符,使用insert(contentsOf:at:)方法在字符串的指定位置插入另一个字符串的内容。

var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)
// welcome now equals "hello!"
 
welcome.insert(contentsOf: " there".characters, at: welcome.index(before: welcome.endIndex))
// welcome now equals "hello there!"

使用remove(at:)方法在字符串的指定位置删除一个单独的字符,使用removeSubrange(_:)方法在字符串的指定范围删除一个子串:

welcome.remove(at: welcome.index(before: welcome.endIndex))
// welcome now equals "hello there"
 
let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex
welcome.removeSubrange(range)
// welcome now equals "hello"

注意
可以在任何遵循RangeReplaceableCollection协议的类型上使用insert(_:at:)insert(contentsOf:at:)remove(at:)removeSubrange(_:)方法。这些类型包括在这介绍的String,也包括集合类型如ArrayDictionarySet

比较字符串

Swift提供了三种方式比较文本值:字符串和字符相等,前缀相等,后缀相等。

字符串和字符相等

使用等于运算符(==)和不等于运算符(!=)检查字符串和字符的等同性:

let quotation = "We're a lot alike, you and I."
let sameQuotation = "We're a lot alike, you and I."
if quotation == sameQuotation {
    print("These two strings are considered equal")
}
// Prints "These two strings are considered equal"

如果两个String值或者两个Character值的可扩展的字符簇是相等的那么它们两个就认为是相等的。如果可扩展的字符簇有同样的语义和外观就认为它们是相等的,即使它们实际上是由不同的Unicode标量构成。
例如,LATIN SMALL LETTER E WITH ACUTE(U+00E9)等于LATIN SMALL LETTER E(U+0065)加上COMBINING ACUTE ACCENT(U+0301)。这些可扩展的字符簇都是合法的方式代表字符é,所以它们被认为是相等的。

// "Voulez-vous un café?" using LATIN SMALL LETTER E WITH ACUTE
let eAcuteQuestion = "Voulez-vous un caf\u{E9}?"
 
// "Voulez-vous un café?" using LATIN SMALL LETTER E and COMBINING ACUTE ACCENT
let combinedEAcuteQuestion = "Voulez-vous un caf\u{65}\u{301}?"
 
if eAcuteQuestion == combinedEAcuteQuestion {
    print("These two strings are considered equal")
}
// Prints "These two strings are considered equal"

相反的,英语中使用的LATIN CAPITAL LETTER A(U+0041或者A)与俄语中的CYRILLIC CAPITAL LETTER A(U+0410或者А)是不相等的。这两个字符只是视觉上相似,但没有相同的语义:

let latinCapitalLetterA: Character = "\u{41}"
 
let cyrillicCapitalLetterA: Character = "\u{0410}"
 
if latinCapitalLetterA != cyrillicCapitalLetterA {
    print("These two characters are not equivalent.")
}
// Prints "These two characters are not equivalent."

注意
Swift的字符串和字符比较并不区分地域。

前缀和后缀相等

使用字符串的hasPrefix(_:)hasSuffix(_:)方法检测一个字符串是有有一个指定的字符前缀或者后缀,这两个方法都使用一个单独的String类型参数并返回一个布尔值。
下面的例子使用一个字符串数组表示莎士比亚的戏剧<<罗密欧与朱丽叶>>前两场的场地:

let romeoAndJuliet = [
    "Act 1 Scene 1: Verona, A public place",
    "Act 1 Scene 2: Capulet's mansion",
    "Act 1 Scene 3: A room in Capulet's mansion",
    "Act 1 Scene 4: A street outside Capulet's mansion",
    "Act 1 Scene 5: The Great Hall in Capulet's mansion",
    "Act 2 Scene 1: Outside Capulet's mansion",
    "Act 2 Scene 2: Capulet's orchard",
    "Act 2 Scene 3: Outside Friar Lawrence's cell",
    "Act 2 Scene 4: A street in Verona",
    "Act 2 Scene 5: Capulet's mansion",
    "Act 2 Scene 6: Friar Lawrence's cell"
]

可以对romeoAndJuliet数组使用hasPrefix(_:)方法计算这部剧第一场的场景数量:

var act1SceneCount = 0
for scene in romeoAndJuliet {
    if scene.hasPrefix("Act 1 ") {
        act1SceneCount += 1
    }
}
print("There are \(act1SceneCount) scenes in Act 1")
// Prints "There are 5 scenes in Act 1"

同样的,使用hasSuffix(_:)方法计算发生在Capulet’s mansion或Friar Lawrence’s cell附近的场景的数量:

var mansionCount = 0
var cellCount = 0
for scene in romeoAndJuliet {
    if scene.hasSuffix("Capulet's mansion") {
        mansionCount += 1
    } else if scene.hasSuffix("Friar Lawrence's cell") {
        cellCount += 1
    }
}
print("\(mansionCount) mansion scenes; \(cellCount) cell scenes")
// Prints "6 mansion scenes; 2 cell scenes"

注意
hasPrefix(_:)hasSuffix(_:)方法在每个字符串的可扩展字符簇之间执行逐字符的等同性比较,详见字符和字符串

字符串的Unicode表示

当向一个文本文件或者一些其它储存器写入一个Unicode字符串时,字符串的Unicode标量会使用Unicode定义的几种编码格式编码。每种格式都会将字符串编码进小的块,也就是码元。这些包括UTF-8编码格式(将字符串编码成8位的码元),UTF-16编码格式(将字符串编码成16位的码元),UTF-32编码格式(将字符串编码成32位的码元)。
Swift提供了几种不同的方式访问字符串的Unicode表现形式。可以使用for-in声明遍历字符串来以可扩展的字符簇的形式访问它的每个Character值。详见使用字符串
同样可以以另外三种Unicode兼容的表现形式访问一个String值:

  • UTF-8码源的集合(通过utf8属性访问)
  • UTF-16码源的集合(通过utf16属性访问)
  • 21位Unicode标量值的集合,等于字符串的UTF-32编码格式(通过unicodeScalars属性访问)

下面的字符串由字符Dog(DOUBLE EXCLAMATION MARK,或者Unicode标量U+203C)和🐶字符(DOG FACE或者Unicode标量U+1F436)构成,下面的例子展示了这个字符串的不同表现方式:

let dogString = "Dog‼🐶"

UTF-8表现方式

可以通过遍历字符串的utf8属性来访问一个字符串的UTF-8形式。这个属性是String.UTF8View类型的,这个类型是一个无符号8位值(UInt8)的集合,它的每个字节都是UTF-8的表现方式:

for codeUnit in dogString.utf8 {
    print("\(codeUnit) ", terminator: "")
}
print("")
// Prints "68 111 103 226 128 188 240 159 144 182 "

上例中,前3个十进制codeUnit值(68, 111, 103)代表了字符Dog,这些字符的UTF-8形式和它们的ASCII形式是一样的。接下来的3个十进制codeUnit值(226, 128, 188)是字符DOUBLE EXCLAMATION MARK的三位UTF-8形式。最后的的4个十进制codeUnit值(240, 159, 144, 182)是字符DOG FACE的四位UTF-8形式。

UTF-16表现方式

可以通过遍历字符串的utf16属性来访问一个字符串的UTF-16形式。这个属性是String.UTF16View类型的,这个类型是一个无符号16位值(UInt16)的集合,它的每个字节都是UTF-16的表现方式:

for codeUnit in dogString.utf16 {
    print("\(codeUnit) ", terminator: "")
}
print("")
// Prints "68 111 103 8252 55357 56374 "

同样的,前3个codeUnit值(68, 111, 103)代表了字符Dog,这些字符的UTF-16码元和它们的UTF-8形式有同样的值(因为这些Unicode标量代表ASCII字符)。
第四个codeUnit值(8252)是一个等同于16进制值203C的十进制值,它代表了字符DOUBLE EXCLAMATION MARK的Unicode标量U+203C。这个字符可以被表示成一个单独的UTF-16码元。
第五和第六个codeUnit值 (5535756374) 是字符DOG FACE的 UTF-16 表示。这些值为U+D83D(十进制值为55357)的高代理值,和U+DC36(十进制值为56374)的低代理值。

Unicode标量表示

可以通过遍历字符串的unicodeScalars属性来访问字符串的Unicode标量形式。这个属性是UnicodeScalarView类型的,这个类型是一个UnicodeScalar类型值的集合。
每个unicodeScalars有一个value属性返回这个标量的21位值,用UInt32表示:

for scalar in dogString.unicodeScalars {
    print("\(scalar.value) ", terminator: "")
}
print("")
// Prints "68 111 103 8252 128054 "

前三个UnicodeScalar值(68111103)的value属性仍然代表字符Dog
第四个codeUnit值(8252)仍然是一个等于十六进制203C的十进制值。它代表了DOUBLE EXCLAMATION MARK字符的Unicode标量U+203C
第五个UnicodeScalar值的value属性,128054,是一个十六进制1F436的十进制表示。其等同于DOG FACE的Unicode标量U+1F436
作为查询它们的value属性的一种替代方法,每个UnicodeScalar值也可以用来构建一个新的String值,例如字符串插值:

for scalar in dogString.unicodeScalars {
    print("\(scalar) ")
}
// D
// o
// g
// ‼
// 🐶

上一篇:Swift-基础运算符
下一篇:Swift-集合类型

推荐阅读更多精彩内容