Swift编程四(字符串和字符)
案例代码下载
字符串和字符
字符串是一系列字符,如"hello, world"或"albatross"。Swift字符串由String类型表示。可以通过各种方式访问一个String的内容,包括作为Character值的集合。
Swift String和Character类型提供了一种快速,Unicode-compliant 的方式来处理代码中的文本。字符串创建和操作的语法是轻量级和可读的,字符串语法与C类似。字符串连接就像将两个字符串与+运算符组合一样简单,字符串可变性通过在常量或变量之间进行选择来管理,就像Swift中的任何其他值一样。您还可以使用字符串将常量,变量,文字和表达式插入到更长的字符串中,这一过程称为字符串插值。这样可以轻松地为显示,存储和打印创建自定义字符串值。
尽管语法简单,但Swift的String类型是一种快速,现代的字符串实现。每个字符串都由与编码无关的Unicode字符组成,并支持在各种Unicode表示中访问这些字符。
注意: Swift的String类型与Foundation的NSString类是桥接的。Foundation还扩展String为公开定义的方法NSString。这意味着,如果您导入Foundation,则可以用NSString在不进行强制转换的情况下访问String的这些方法。
有关String与Foundation和Cocoa一起使用的更多信息,请参阅字符串和NSString之间的桥接。
字符串文字
您可以用String在代码中包含预定义值作为字符串文字。字符串文字是由双引号(")包围的字符序列。
使用字符串文字作为常量或变量的初始值:
let someString = "Some string literal value"
需要注意的是Swift推测someString的类型为String的,因为它有一个字符串值初始化。
多行字符串文字
如果需要跨越多行的字符串,请使用多行字符串文字 - 由三个双引号括起来的字符序列:
let quotation = """
The White Rabbit put on his spectacles. "Where shall I begin,
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""
多行字符串文字包括其开始和结束引号之间的所有行。该字符串从开始引号(""")后面的第一行开始,到结束引号之前的行结束,这意味着下面的字符串都不会以换行符开头或结尾:
let singleLineString = "These are the same."
let multilineString = """
These are the same.
"""
当源代码在多行字符串文字中包含换行符时,该换行符也会出现在字符串的值中。如果您想使用换行符来使源代码更容易阅读,但是您不希望换行符成为字符串值的一部分,请在这些行的末尾写一个反斜杠(\):
let softWrappedQuotation = """
The White Rabbit put on his spectacles. "Where shall I begin, \
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on \
till you come to the end; then stop."
"""
要创建以换行符开头或结尾的多行字符串文字,请将空行写为第一行或最后一行。例如:
let lineBreaks = """
This string starts with a line break.
It also ends with a line break.
"""
可以缩进多行字符串以匹配周围的代码。在右引号标记(""")之前的空格告诉Swift在所有其他行之前要忽略那些空格。但是,如果您在行的开头写入空格以及结束引号之前的空格,则包含该空格。
image
在上面的示例中,即整个多行字符串文字是缩进的,字符串中的第一行和最后一行也不以任何空格开头。中间行比结束引号标记有更多的空格,所以它从额外的四个空格缩进开始。
字符串文字中的特殊字符
字符串文字可以包含以下特殊字符:
- 转义的特殊字符\0(空字符),\(反斜杠),\t(水平制表符),\n(换行符),\r(回车符),"(双引号)和'(单引号)
- 任意的Unicode标量值,写为\u{n},其中n是一个1-8位的十六进制数(Unicode统一在下文的Unicode讨论)
下面的代码显示了这些特殊字符的四个示例。wiseWords常量包含两个转义双引号。dollarSign,blackHeart和sparklingHeart常量包含Unicode标量格式展示:
let wiseWords = "\"Imagination is more important than knowledge\" - Einstein"
let dollarSign = "\u{24}"
let blackHeart = "\u{2665}"
let sparklingHeart = "\u{1F496}"
因为多行字符串文字使用三个双引号而不是一个,所以您可以在多行字符串文字中包含双引号(")而不转义它。要将文本"""包含在多行字符串中,请至少转义其中一个引号。例如:
let threeDoubleQuotationMarks = """
Escaping the first quotation mark \"""
Escaping all three quotation marks \"\"\"
"""
初始化空字符串
要创建空String值作为构建更长字符串的起点,请将空字符串文字指定给变量,或使用String初始化语法初始化新实例:
let anotherEmptyString = String()
String通过检查其布尔isEmpty属性来确定值是否为空:
if emptyString.isEmpty {
print("Nothing to see here")
}
字符串可变性
您可以通过将String分配给特定变量(在这种情况下可以修改)或常量(在这种情况下不能修改)来指定是否可以修改(或可变):
var variableString = "Horse"
variableString += " and carriage"
let constantString = "Highlander"
//constantString += " and another Highlander"
注意: 这种方法不同于Objective-C和Cocoa中的可变字符串,您可以在两个类(NSString和NSMutableString)之间进行选择,以指示字符串是否可变。
字符串是值类型
Swift的String类型是值类型。如果创建新String值,则在将String值传递给函数或方法时,或者将其赋值给常量或变量时,将复制该值。在每种情况下,String都会创建现有值的新副本,并传递或分配新副本,而不是原始版本。结构和枚举是值类型中描述了值类型。
Swift的默认复制String行为可确保当函数或方法为您传递一个String值时,很明显您拥有该确切的String值,无论它来自何处。除非您自己修改,否则您可以确信传递的字符串不会被修改。
在幕后,Swift的编译器优化了字符串的使用,因此只有在绝对必要的情况下才会进行实际的复制。这意味着在使用字符串作为值类型时,您总能获得出色的性能。
使用字符
您可以通过使用for-in循环遍历字符串来访问String的各个Character值:
for character in "Dog!🐶" {
print(character)
}
for-in循环在for-in循环中描述。
然而,您可以通过提供Character类型从单字符字符串文字创建独立Character常量或变量:
let exclamationMark: Character = "!"
String可以通过用Character值数组作为初始化参数:
let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"]
let catString = String(catCharacters)
print(catString)
连接字符串和字符
String值可以与加法运算符(+)添加(或连接)在一起来创建新String值:
var instruction = "look over"
instruction += string2
可以使用String类型的方法append()将Character值附加到String变量:
let exclamationMark: Character = "!"
welcome.append(exclamationMark)
注意: 不能将一个String或Character附加到现有Character变量,因为Character值必须仅包含单个字符。
如果使用多行字符串文字来构建较长的字符串,则希望字符串中的每一行都以换行符结束,包括最后一行。例如:
let badStart = """
one
two
"""
let end = """
three
"""
print(badStart + end)
let goodStart = """
one
two
"""
print(goodStart + end)
在上面的代码中,连接badStart与end产生两行字符串,这是不期望的结果。因为badStart最后一行不以换行符结束,所以该行与end第一行结合。相比之下,goodStart两行都以换行符结束,所以当它与end结合时有三行,正如预期的那样。
字符串插值
字符串插值是String一种通过在字符串文字中包含常量,变量,文字和表达式的值混合构造新值的方法。可以在单行和多行字符串文字中使用字符串插值。插入到字符串文字中的每个项目都包含在一对括号中,前缀为反斜杠(\):
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier)*2.5)"
在上面的示例中,multiplier的值插入到字符串文字中(multiplier)。此占位符将替换multiplier为评估字符串插值以创建实际字符串时的实际值。
multiplier的值也是字符串中较大表达式的一部分。此表达式计算。Double(multiplier) * 2.5的值,并将result(7.5)插入到字符串中。在这种情况下,当它包含在字符串文字中时表达式被写为(Double(multiplier) * 2.5)
注意: 在插值字符串内的括号内写的表达式不能包含未转义的反斜杠(\),回车符或换行符。但是,它们可以包含其他字符串文字。
Unicode
Unicode是用于在不同书写系统中编码,表示和处理文本的国际标准。它使您能够以标准化形式表示来自任何语言的几乎任何字符,并从外部源(如文本文件或网页)读取和写入这些字符。Swift String和Character类型完全符合Unicode,如本节所述。
Unicode标量值
在幕后,Swift的原生String类型是根据Unicode标量值构建的。Unicode标量值是表示字符或修饰符的唯一21位数字,例如U+0061表示LATIN SMALL LETTER A"a"或U+1F425表示FRONT-FACING BABY CHICK"🐥"。
请注意,并非所有21位Unicode标量值都分配给字符 - 某些标量保留用于将来分配或用于UTF-16编码。已分配给一个字符标量值通常还具有一个名字,如在上面实例的LATIN SMALL LETTER A和FRONT-FACING BABY CHICK。
扩展字形集合
Swift Character类型的每个实例都代表一个扩展字形集合。扩展字形集合是一个或多个Unicode标量的序列(当组合时)产生单个人类可读字符。
这是一个例子。该字母é可以表示为单个Unicode标量é(LATIN SMALL LETTER E WITH ACUTE,或U+00E9)。但是,同一个字母也可以表示为一对标量 - 标准字母e(LATIN SMALL LETTER E,或U+0065),后跟COMBINING ACUTE ACCENT标量(U+0301)。COMBINING ACUTE ACCENT标量图形应用到它前面的标量,当由一个支持Unicode的文本的渲染系统渲染时把一个标量e到é。
在这两种情况下,字母é都表示为单个Swift中表示扩展字形集合的Character值。在第一种情况下,集群包含单个标量; 在第二种情况下,它是两个标量的集合:
let eAcute: Character = "\u{E9}"
let combinedEAcute: Character = "\u{65}\u{301}"
扩展字形集合是将许多复杂的脚本字符表示为单个Character值的灵活方式。例如,韩语字母表中的韩语音节可以表示为预先组合或分解的序列。这两个表示在Swift中都符合Character的单个值:
let precomposed: Character = "\u{D55C}"
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}"
扩展字形集合使可以封闭标记标量(例如COMBINING ENCLOSING CIRCLE,或U+20DD)以将其他Unicode标量作为单个Character值的一部分包含在内:
let enclosedEAcute: Character = "\u{E9}\u{20DD}"
地区标志符号的Unicode标量可以成对组合以形成单个Character值,例如REGIONAL INDICATOR SYMBOL LETTER U(U+1F1FA)和REGIONAL INDICATOR SYMBOL LETTER S(U+1F1F8)的这种组合:
let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}"
计算字符
要检索字符串中Character值的个数,请使用字符串的count属性:
let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
print("unusualMenagerie has \(unusualMenagerie.count) characters")
请注意,Swift将扩展字形集合用于Character值意味着字符串连接和修改可能并不总是会影响字符串的字符数。
例如,如果使用四个字符的单词初始化一个新字符串cafe,然后在字符串的末尾附加一个COMBINING ACUTE ACCENT(U+0301),则结果字符串的字符数仍为4第四个字符,第四个字符是é而不是e:
var word = "cafe"
print("the number of characters in \(word) is \(word.count)")
word += "\u{301}"
print("the number of characters in \(word) is \(word.count)")
注意: 扩展字形集合可以由多个Unicode标量组成。这意味着不同的字符和相同字符的不同表示可能需要不同的内存量来存储。因此,Swift中在一个字符串里的字符不会用相同数量的内存表示。因此,如果不遍历字符串以确定其扩展字形集合边界,则无法计算字符串中的字符数。如果使用特别长的字符串值,请注意该count属性必须遍历整个字符串中的Unicode标量,以确定该字符串的字符。 count属性返回的字符数不总是与包含相同字符的NSString的length属性相同。NSString的长度基于字符串的UTF-16表示中的16位代码单元的数量,而不是字符串中Unicode扩展字形集合的数量。
访问和修改字符串
可以通过其方法和属性或使用下标语法来访问和修改字符串。
字符串索引
每个String值都有一个关联的索引类型,String.Index它对应Character于字符串中每个值的位置。
如上所述,不同的字符可能需要不同的内存量来存储,因此为了确定哪个Character位于特定位置,您必须从开头或结尾遍历String的每个Unicode标量。因此,Swift字符串不能用整数值索引。
使用startIndex属性访问String第一个位置的Character。endIndex属性是String中最后一个字符后的位置。因此,endIndex属性不是字符串下标的有效参数。如果 String是空的,startIndex和endIndex是相等的。
可以使用index(before:)和index(after:)方法访问给定索引之前和之后的索引String。要访问偏离给定索引的索引,可以使用该index(_:offsetBy:)方法而不是多次调用其中一种方法。
可以使用下标语法来访问String特定Character索引:
let greeting = "Guten Tag!"
greeting[greeting.startIndex]
greeting[greeting.index(before: greeting.endIndex)]
greeting[greeting.index(after: greeting.startIndex)]
let index = greeting.index(greeting.startIndex, offsetBy: 7)
greeting[index]
greeting[2]
尝试访问字符串范围之外的索引或字符串范围的之外索引的Character将触发运行时错误。
greeting[greeting.endIndex]
greeting.index(after: greeting.endIndex)
使用indices属性可以访问字符串中单个字符的所有索引:
for index in greeting.indices {
print("\(greeting[index]) ", terminator: "")
}
注意: 可以对任何符合Collection的协议的类型使用startIndex与endIndex属性和index(before:),index(after:)以及index(_:offsetBy:)方法。这包括String,以及集合类型,如Array,Dictionary和Set。
插入和删除
要将单个字符插入到字符串的指定索引处中,请使用insert(_:at:)方法;在指定索引处插入另一个字符串的内容,请使用该insert(contentsOf:at:)方法:
var welcome = "hello"
welcom.insert("!", at: welcom.endIndex)
welcome.insert(contentsOf: " there", at: welcom.index(before: welcom.endIndex))
要从指定索引处的字符串中删除单个字符,请使用该remove(at:)方法;删除指定范围内的子字符串,请使用以下removeSubrange(_:)方法:
welcome.remove(at: welcom.index(before: welcom.endIndex))
let range = welcom.index(welcom.endIndex, offsetBy: -6) ..< welcom.endIndex
welcome.removeSubrange(range)
子字符串
从字符串中获取子字符串时 - 例如,使用下标或类似方法prefix(_:) - 结果是一个Substring的实例,而不是另一个字符串。Swift中的子字符串与字符串具有大多数相同的方法,这意味着您可以像处理字符串一样使用子字符串。但是,与字符串不同的是在对字符串执行操作时使用的子字符串仅有很短时间。当您准备好将结果存储更长时间时,将子字符串转换为String实例。例如:
let greeting = "hello, world!"
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex
let beginning = greeting[..<index]
let newString = String(beginning)
与字符串一样,每个子字符串都有一个内存区域,用于存储构成子字符串的字符。字符串和子字符串之间的区别在于,作为性能优化,子字符串可以重用用于存储原始字符串的部分内存,或者用于存储另一个子字符串的内存的一部分。(字符串具有类似的优化,但如果两个字符串共享内存,则它们是相等的。)此性能优化意味着在修改字符串或子字符串之前,不必支付复制内存的性能成本。如上所述,子串不适合长期存储 - 因为它们重用原始字符串的存储,只要使用任何子字符串,整个原始字符串就必须保存在内存中。
在上面的示例中,greeting是一个字符串,这意味着它有一个内存区域,其中存储了构成字符串的字符。因为beginning是greeting的子串,所以它重用了使用greeting的内存。相反,newString是一个字符串 - 当它从子字符串创建时,它有自己的存储空间。下图显示了这些关系:
image
注意: String和Substring二者符合StringProtocol协议,这意味着它们的常用的字符串操作函数接受StringProtocol的值。可以使用String或Substring值调用此类函数。
比较字符串
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")
}
如果两个String值(或两个Character值)的扩展字形集合在规范上等效,则它们被认为是相等的。如果扩展字形集合具有相同的语言含义和外观,则它们在规范上是等效的,即使它们是由幕后的不同Unicode标量组成的。
例如,LATIN SMALL LETTER E WITH ACUTE(U+00E9)在规范上等效于LATIN SMALL LETTER E(U+0065)后跟COMBINING ACUTE ACCENT(U+0301)。这两个扩展的字形集合都是表示字符é的有效方式,因此它们被认为是等价的:
let eAcuteQuestion = "Voulez-vous un caf\u{E9}?"
let combinedEAcuteQuestion = "Voulez-vous un caf\u{65}\u{301}?"
if eAcuteQuestion == combinedEAcuteQuestion {
print("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.")
}
注意: Swift中的字符串和字符比较不是locale-sensitive的。
前缀和后缀相等
要检查字符串是否具有特定字符串前缀或后缀,请调用字符串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"
]
可以使用数组中的hasPrefix(_:)方法来计算romeoAndJuliet中播放的第1幕中的场景数量:
let act1SceneCount = romeoAndJuliet.filter { (item) -> Bool in
return item.hasPrefix("Act 1 ")
}.count
print("There are \(act1SceneCount) scenes in Act 1")
使用该hasSuffix(_:)方法计算在Capulet’s mansion和Friar Lawrence’s cell发生的场景数量:
var mansionCount = 0
var cellCount = 0
for item in romeoAndJuliet {
if item.hasSuffix("Capulet’s mansion") {
mansionCount += 1
} else if item.hasSuffix("Friar Lawrence’s cell") {
cellCount += 1
}
}
print("\(mansionCount) mansion scenes; \(cellCount) cell scenes")
注意: hasPrefix(:)和hasSuffix(:)在每一个串的方法执行遍历扩展字形集合之间的字符逐字符规范等价比较,如在字符串和字符相等中所述的。
字符串的Unicode表示
将Unicode字符串写入文本文件或其他存储时,该字符串中的Unicode标量将以多种Unicode定义的编码形式之一进行编码。每个表单都以小块(称为代码单元)对字符串进行编码。这些包括UTF-8编码格式(将字符串编码为8位代码单元),UTF-16编码格式(将字符串编码为16位代码单元)和UTF-32编码格式(将字符串编码为32位代码单元)。
Swift提供了几种不同的方法来访问字符串的Unicode表示。您可以使用for- in语句迭代字符串,以Character的Unicode扩展字形集合的形式访问其各个值。使用Characters中描述了此过程。
或者,访问String其他三个符合Unicode的表示之一中的值:
- UTF-8代码单元的集合(使用字符串的utf8属性访问)
- UTF-16代码单元的集合(使用字符串的utf16属性访问)
- 一组21位Unicode标量值,相当于字符串的UTF-32编码形式(使用字符串的unicodeScalars属性访问)
下面各实例中展示出了下面的字符串的不同表示,它是由所述字符的向上的不同表示D,o,g,‼(DOUBLE EXCLAMATION MARK或Unicode标U+203C),以及🐶字符(DOG FACE或Unicode标U+1F436):
let dogString = "Dog‼🐶"
UTF-8表示法
可以通过遍历String的utf8属性来访问UTF-8表示。此属性的类型String.UTF8View是无符号8位(UInt8)值的集合,对于字符串的UTF-8表示形式,每个字节对应一个值:
image
let dogString = "Dog‼🐶"
for codeUnit in dogString.utf8 {
print("\(codeUnit) ", terminator: "")
}
在上面的例子中,前三个十进制codeUnit值(68,111,103)所表示的字符D,o和g,其UTF-8表示相同的ASCII表示。接下来的三个十进制codeUnit值(226,128,188)是一个三字节UTF-8表示的DOUBLE EXCLAMATION MARK字符。最后四个codeUnit值(240,159,144,182)是一个四字节UTF-8表示的DOG FACE字符。
UTF-16表示法
可以通过遍历String的utf16属性来访问UTF-16表示。此属性的类型String.UTF16View是无符号16位(UInt16)值的集合,对于字符串的UTF-16表示形式,每个16位代码单元对应一个值:
image
for codeUnit in dogString.utf16 {
print("\(codeUnit) ", terminator: "")
}
再次,前三个codeUnit值(68,111,103)所表示的字符D,o和g,其UTF-16代码单元具有如在字符串的UTF-8表示相同的值(因为这些Unicode标量表示ASCII字符)。
第四codeUnit值(8252)是十六进制值的十进位等值203C,它代表了Unicode标U+203C的DOUBLE EXCLAMATION MARK字符。此字符可以表示为UTF-16中的单个代码单元。
第五和第六个codeUnit值(55357和56374)是字符DOG FACE的UTF-16代理对表示。这些值的高代理值U+D83D(十进制值55357)和一个低代理值U+DC36(十进制值56374)。
Unicode标量表示
可以通过遍历String的unicodeScalars属性来访问值的Unicode标量表示。此属性的类型是类型UnicodeScalarView值的集合UnicodeScalar。
每个UnicodeScalar都有一个value属性,返回21位的标量值,表示在一个UInt32值内:
image
for scalar in dogString.unicodeScalars {
print("\(scalar.value) ", terminator: "")
}
UnicodeScalar属性的前三个值(68,111,103)依次表示字符D,o和g。
第四codeUnit值(8252)再次是十六进制值的十进位等值203C,它代表了Unicode标U+203C的。DOUBLE EXCLAMATION MARK字符
UnicodeScalar属性的属性值,128054是十六进制值的十进位等值1F436,它代表了Unicode标U+1F436的DOG FACE字符。
作为查询其value属性的替代方法,每个UnicodeScalar值也可用于构造新String值,例如使用字符串插值:
for scalar in dogString.unicodeScalars {
print("(scalar) ")
}