在 Swift 中,枚举类型是一等(first-class)类型。它们采用了很多在传统上只被类(class)所支持的特性,例如计算属性(computed properties),用于提供枚举值的附加信息,实例方法(instance methods),用于提供和枚举值相关联的功能。枚举也可以定义构造函数(initializers)来提供一个初始值;可以在原始实现的基础上扩展它们的功能;还可以遵循协议(protocols)来提供标准的功能。
directionToHead = .south switch directionToHead { case .north: print("Lots of planets have a north") case .south: print("Watch out for penguins") case .east: print("Where the sun rises") case .west: print("Where the skies are blue") } // 打印“Watch out for penguins”
你可以这样理解这段代码:
“判断 directionToHead 的值。当它等于 .north,打印 “Lots of planets have a north”。当它等于 .south,打印 “Watch out for penguins”。”
let somePlanet =Planet.earth switch somePlanet { case .earth: print("Mostly harmless") default: print("Not a safe place for humans") } // 打印“Mostly harmless”
你可以使用一个 switch 语句来检查不同的条形码类型,和之前使用 Switch 语句来匹配枚举值的例子一样。然而,这一次,关联值可以被提取出来作为 switch 语句的一部分。你可以在 switch 的 case 分支代码中提取每个关联值作为一个常量(用 let 前缀)或者作为一个变量(用 var 前缀)来使用:
1 2 3 4 5 6 7
switch productBarcode { case .upc(let numberSystem, let manufacturer, let product, let check): print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).") case .qrCode(let productCode): print("QR code: \(productCode).") } // 打印“QR code: ABCDEFGHIJKLMNOP.”
如果一个枚举成员的所有关联值都被提取为常量,或者都被提取为变量,为了简洁,你可以只在成员名称前标注一个 let 或者 var:
let positionToFind =11 iflet somePlanet =Planet(rawValue: positionToFind) { switch somePlanet { case .earth: print("Mostly harmless") default: print("Not a safe place for humans") } } else { print("There isn't a planet at position \(positionToFind)") } // 打印“There isn't a planet at position 11”
indirectenumArithmeticExpression { case number(Int) case addition(ArithmeticExpression, ArithmeticExpression) case multiplication(ArithmeticExpression, ArithmeticExpression) }
let five =ArithmeticExpression.number(5) let four =ArithmeticExpression.number(4) let sum =ArithmeticExpression.addition(five, four) let product =ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))
let strings = numbers.map { (number) -> Stringin var number = number var output ="" repeat { output = digitNames[number %10]!+ output number /=10 } while number >0 return output } // strings 常量被推断为字符串类型数组,即 [String] // 其值为 ["OneSix", "FiveEight", "FiveOneZero"]
map(_:) 为数组中每一个元素调用了一次闭包表达式。你不需要指定闭包的输入参数 number 的类型,因为可以通过要映射的数组类型进行推断。
在该例中,局部变量 number 的值由闭包中的 number 参数获得,因此可以在闭包函数体内对其进行修改,(闭包或者函数的参数总是常量),闭包表达式指定了返回类型为 String,以表明存储映射值的新数组类型为 String。
下例中定义了一个名为 minMax(array:) 的函数,作用是在一个 Int 类型的数组中找出最小值与最大值。
1 2 3 4 5 6 7 8 9 10 11 12
funcminMax(array: [Int]) -> (min: Int, max: Int) { var currentMin = array[0] var currentMax = array[0] for value in array[1..<array.count] { if value < currentMin { currentMin = value } elseif value > currentMax { currentMax = value } } return (currentMin, currentMax) }
minMax(array:) 函数返回一个包含两个 Int 值的元组,这些值被标记为 min 和 max ,以便查询函数的返回值时可以通过名字访问它们。
funcminMax(array: [Int]) -> (min: Int, max: Int)? { if array.isEmpty { returnnil } var currentMin = array[0] var currentMax = array[0] for value in array[1..<array.count] { if value < currentMin { currentMin = value } elseif value > currentMax { currentMax = value } } return (currentMin, currentMax) }
你可以使用可选绑定来检查 minMax(array:) 函数返回的是一个存在的元组值还是 nil:
1 2 3 4
iflet bounds = minMax(array: [8, -6, 2, 109, 3, 71]) { print("min is \(bounds.min) and max is \(bounds.max)") } // 打印“min is -6 and max is 109”
funcgreet(person: String, fromhometown: String) -> String { return"Hello \(person)! Glad you could visit from \(hometown)." } print(greet(person: "Bill", from: "Cupertino")) // 打印“Hello Bill! Glad you could visit from Cupertino.”
var someInt =3 var anotherInt =107 swapTwoInts(&someInt, &anotherInt) print("someInt is now \(someInt), and anotherInt is now \(anotherInt)") // 打印“someInt is now 107, and anotherInt is now 3”
let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4] for (animalName, legCount) in numberOfLegs { print("\(animalName)s have \(legCount) legs") } // cats have 4 legs // ants have 6 legs // spiders have 8 legs
for index in1...5 { print("\(index) times 5 is \(index *5)") } // 1 times 5 is 5 // 2 times 5 is 10 // 3 times 5 is 15 // 4 times 5 is 20 // 5 times 5 is 25
上面的例子中,index 是一个每次循环遍历开始时被自动赋值的常量。这种情况下,index 在使用前不需要声明,只需要将它包含在循环的声明中,就可以对其进行隐式声明,而无需使用 let 关键字声明。
如果你不需要区间序列内每一项的值,你可以使用下划线(_)替代变量名来忽略这个值:
1 2 3 4 5 6 7 8
let base =3 let power =10 var answer =1 for_in1...power { answer *= base } print("\(base) to the power of \(power) is \(answer)") // 输出“3 to the power of 10 is 59049”
Swift 提供两种类型的条件语句:if 语句和 switch 语句。通常,当条件较为简单且可能的情况很少时,使用 if 语句。而 switch 语句更适用于条件较复杂、有更多排列组合的时候。并且 switch 在需要用到模式匹配(pattern-matching)的情况下会更有用。
If
if 语句最简单的形式就是只包含一个条件,只有该条件为 true 时,才执行相关代码:
1 2 3 4 5
var temperatureInFahrenheit =30 if temperatureInFahrenheit <=32 { print("It's very cold. Consider wearing a scarf.") } // 输出“It's very cold. Consider wearing a scarf.”
上面的例子会判断温度是否小于等于 32 华氏度(水的冰点)。如果是,则打印一条消息;否则,不打印任何消息,继续执行 if 块后面的代码。
temperatureInFahrenheit =40 if temperatureInFahrenheit <=32 { print("It's very cold. Consider wearing a scarf.") } else { print("It's not that cold. Wear a t-shirt.") } // 输出“It's not that cold. Wear a t-shirt.”
temperatureInFahrenheit =90 if temperatureInFahrenheit <=32 { print("It's very cold. Consider wearing a scarf.") } elseif temperatureInFahrenheit >=86 { print("It's really warm. Don't forget to wear sunscreen.") } else { print("It's not that cold. Wear a t-shirt.") } // 输出“It's really warm. Don't forget to wear sunscreen.”
在上面的例子中,额外的 if 语句用于判断是不是特别热。而最后的 else 语句被保留了下来,用于打印既不冷也不热时的消息。
实际上,当不需要完整判断情况的时候,最后的 else 语句是可选的:
1 2 3 4 5 6
temperatureInFahrenheit =72 if temperatureInFahrenheit <=32 { print("It's very cold. Consider wearing a scarf.") } elseif temperatureInFahrenheit >=86 { print("It's really warm. Don't forget to wear sunscreen.") }
在这个例子中,由于既不冷也不热,所以不会触发 if 或 else if 分支,也就不会打印任何消息。
Switch
switch 语句会尝试把某个值与若干个模式(pattern)进行匹配。根据第一个匹配成功的模式,switch 语句会执行对应的代码。当有可能的情况较多时,通常用 switch 语句替换 if 语句。
switch 语句最简单的形式就是把某个值与一个或若干个相同类型的值作比较:
1 2 3 4 5 6 7 8 9
switchsome value to consider { case value 1: respond to value 1 case value 2, value 3: respond to value 2 or 3 default: otherwise, do something else }
switch 语句由多个 case 构成,每个由 case 关键字开始。为了匹配某些更特定的值,Swift 提供了几种方法来进行更复杂的模式匹配,这些模式将在本节的稍后部分提到。
与 if 语句类似,每一个 case 都是代码执行的一条分支。switch 语句会决定哪一条分支应该被执行,这个流程被称作根据给定的值切换(switching)。
switch 语句必须是完备的。这就是说,每一个可能的值都必须至少有一个 case 分支与之对应。在某些不可能涵盖所有值的情况下,你可以使用默认(default)分支来涵盖其它所有没有对应的值,这个默认分支必须在 switch 语句的最后面。
下面的例子使用 switch 语句来匹配一个名为 someCharacter 的小写字符:
1 2 3 4 5 6 7 8 9 10
let someCharacter: Character="z" switch someCharacter { case"a": print("The first letter of the alphabet") case"z": print("The last letter of the alphabet") default: print("Some other character") } // 输出“The last letter of the alphabet”
在这个例子中,第一个 case 分支用于匹配第一个英文字母 a,第二个 case 分支用于匹配最后一个字母 z。因为 switch 语句必须有一个 case 分支用于覆盖所有可能的字符,而不仅仅是所有的英文字母,所以 switch 语句使用 default 分支来匹配除了 a 和 z 外的所有值,这个分支保证了 swith 语句的完备性。
不存在隐式的贯穿
与 C 和 Objective-C 中的 switch 语句不同,在 Swift 中,当匹配的 case 分支中的代码执行完毕后,程序会终止 switch 语句,而不会继续执行下一个 case 分支。这也就是说,不需要在 case 分支中显式地使用 break 语句。这使得 switch 语句更安全、更易用,也避免了漏写 break 语句导致多个语言被执行的错误。
注意
虽然在 Swift 中 break 不是必须的,但你依然可以在 case 分支中的代码执行完毕前使用 break 跳出,详情请参见 Switch 语句中的 break。
每一个 case 分支都必须包含至少一条语句。像下面这样书写代码是无效的,因为第一个 case 分支是空的:
1 2 3 4 5 6 7 8 9
let anotherCharacter: Character="a" switch anotherCharacter { case"a": // 无效,这个分支下面没有语句 case"A": print("The letter A") default: print("Not the letter A") } // 这段代码会报编译错误
不像 C 语言里的 switch 语句,在 Swift 中,switch 语句不会一起匹配 "a" 和 "A"。相反的,上面的代码会引起编译期错误:case "a": 不包含任何可执行语句——这就避免了意外地从一个 case 分支贯穿到另外一个,使得代码更安全、也更直观。
为了让单个 case 同时匹配 a 和 A,可以将这个两个值组合成一个复合匹配,并且用逗号分开:
1 2 3 4 5 6 7 8
let anotherCharacter: Character="a" switch anotherCharacter { case"a", "A": print("The letter A") default: print("Not the letter A") } // 输出“The letter A”
let somePoint = (1, 1) switch somePoint { case (0, 0): print("\(somePoint) is at the origin") case (_, 0): print("\(somePoint) is on the x-axis") case (0, _): print("\(somePoint) is on the y-axis") case (-2...2, -2...2): print("\(somePoint) is inside the box") default: print("\(somePoint) is outside of the box") } // 输出“(1, 1) is inside the box”
在上面的例子中,switch 语句会判断某个点是否是原点 (0, 0),是否在红色的 x 轴上,是否在橘黄色的 y 轴上,是否在一个以原点为中心的4x4的蓝色矩形里,或者在这个矩形外面。
不像 C 语言,Swift 允许多个 case 匹配同一个值。实际上,在这个例子中,点 (0, 0)可以匹配所有四个 case。但是,如果存在多个匹配,那么只会执行第一个被匹配到的 case 分支。考虑点 (0, 0)会首先匹配 case (0, 0),因此剩下的能够匹配的分支都会被忽视掉。
值绑定(Value Bindings)
case 分支允许将匹配的值声明为临时常量或变量,并且在 case 分支体内使用 —— 这种行为被称为值绑定(value binding),因为匹配的值在 case 分支体内,与临时的常量或变量绑定。
下面的例子将下图中的点 (x, y),使用 (Int, Int) 类型的元组表示,然后分类表示:
1 2 3 4 5 6 7 8 9 10
let anotherPoint = (2, 0) switch anotherPoint { case (let x, 0): print("on the x-axis with an x value of \(x)") case (0, let y): print("on the y-axis with a y value of \(y)") caselet (x, y): print("somewhere else at (\(x), \(y))") } // 输出“on the x-axis with an x value of 2”
在上面的例子中,switch 语句会判断某个点是否在红色的 x 轴上,是否在橘黄色的 y 轴上,或者不在坐标轴上。
这三个 case 都声明了常量 x 和 y 的占位符,用于临时获取元组 anotherPoint 的一个或两个值。第一个 case ——case (let x, 0) 将匹配一个纵坐标为 0 的点,并把这个点的横坐标赋给临时的常量 x。类似的,第二个 case ——case (0, let y) 将匹配一个横坐标为 0 的点,并把这个点的纵坐标赋给临时的常量 y。
一旦声明了这些临时的常量,它们就可以在其对应的 case 分支里使用。在这个例子中,它们用于打印给定点的类型。
请注意,这个 switch 语句不包含默认分支。这是因为最后一个 case ——case let(x, y) 声明了一个可以匹配余下所有值的元组。这使得 switch 语句已经完备了,因此不需要再书写默认分支。
Where
case 分支的模式可以使用 where 语句来判断额外的条件。
下面的例子把下图中的点 (x, y)进行了分类:
1 2 3 4 5 6 7 8 9 10
let yetAnotherPoint = (1, -1) switch yetAnotherPoint { caselet (x, y) where x == y: print("(\(x), \(y)) is on the line x == y") caselet (x, y) where x ==-y: print("(\(x), \(y)) is on the line x == -y") caselet (x, y): print("(\(x), \(y)) is just some arbitrary point") } // 输出“(1, -1) is on the line x == -y”
在上面的例子中,switch 语句会判断某个点是否在绿色的对角线 x == y 上,是否在紫色的对角线 x == -y 上,或者不在对角线上。
这三个 case 都声明了常量 x 和 y 的占位符,用于临时获取元组 yetAnotherPoint 的两个值。这两个常量被用作 where 语句的一部分,从而创建一个动态的过滤器(filter)。当且仅当 where 语句的条件为 true 时,匹配到的 case 分支才会被执行。
就像是值绑定中的例子,由于最后一个 case 分支匹配了余下所有可能的值,switch 语句就已经完备了,因此不需要再书写默认分支。
复合型 Cases
当多个条件可以使用同一种方法来处理时,可以将这几种可能放在同一个 case 后面,并且用逗号隔开。当 case 后面的任意一种模式匹配的时候,这条分支就会被匹配。并且,如果匹配列表过长,还可以分行书写:
1 2 3 4 5 6 7 8 9 10 11
let someCharacter: Character="e" switch someCharacter { case"a", "e", "i", "o", "u": print("\(someCharacter) is a vowel") case"b", "c", "d", "f", "g", "h", "j", "k", "l", "m", "n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z": print("\(someCharacter) is a consonant") default: print("\(someCharacter) is not a vowel or a consonant") } // 输出“e is a vowel”
这个 switch 语句中的第一个 case,匹配了英语中的五个小写元音字母。相似的,第二个 case 匹配了英语中所有的小写辅音字母。最终,default 分支匹配了其它所有字符。
let stillAnotherPoint = (9, 0) switch stillAnotherPoint { case (let distance, 0), (0, let distance): print("On an axis, \(distance) from the origin") default: print("Not on an axis") } // 输出“On an axis, 9 from the origin”
上面的 case 有两个模式:(let distance, 0) 匹配了在 x 轴上的值,(0, let distance) 匹配了在 y 轴上的值。两个模式都绑定了 distance,并且 distance 在两种模式下,都是整型——这意味着分支体内的代码,只要 case 匹配,都可以获取到 distance 值。
在上面的例子中,想要把 Character 所有的的可能性都枚举出来是不现实的,所以使用 default 分支来包含所有上面没有匹配到字符的情况。由于这个 default 分支不需要执行任何动作,所以它只写了一条 break 语句。一旦落入到 default 分支中后,break 语句就完成了该分支的所有代码操作,代码继续向下,开始执行 if let 语句。
贯穿(Fallthrough)
在 Swift 里,switch 语句不会从上一个 case 分支跳转到下一个 case 分支中。相反,只要第一个匹配到的 case 分支完成了它需要执行的语句,整个 switch 代码块完成了它的执行。相比之下,C 语言要求你显式地插入 break 语句到每个 case 分支的末尾来阻止自动落入到下一个 case 分支中。Swift 的这种避免默认落入到下一个分支中的特性意味着它的 switch 功能要比 C 语言的更加清晰和可预测,可以避免无意识地执行多个 case 分支从而引发的错误。
如果你确实需要 C 风格的贯穿的特性,你可以在每个需要该特性的 case 分支中使用 fallthrough 关键字。下面的例子使用 fallthrough 来创建一个数字的描述语句。
1 2 3 4 5 6 7 8 9 10 11
let integerToDescribe =5 var description ="The number \(integerToDescribe) is" switch integerToDescribe { case2, 3, 5, 7, 11, 13, 17, 19: description +=" a prime number, and also" fallthrough default: description +=" an integer." } print(description) // 输出“The number 5 is a prime number, and also an integer.”
print("The shopping list contains \(shoppingList.count) items.") // 输出“The shopping list contains 2 items.”(这个数组有2个项)
使用布尔属性 isEmpty 作为一个缩写形式去检查 count 属性是否为 0:
1 2 3 4 5 6
if shoppingList.isEmpty { print("The shopping list is empty.") } else { print("The shopping list is not empty.") } // 打印“The shopping list is not empty.”(shoppinglist 不是空的)
Swift 中的 Set 类型被写为 Set<Element>,这里的 Element 表示 Set 中允许存储的类型,和数组不同的是,集合没有等价的简化形式。
创建和构造一个空的集合
你可以通过构造器语法创建一个特定类型的空集合:
1 2 3
var letters =Set<Character>() print("letters is of type Set<Character> with \(letters.count) items.") // 打印“letters is of type Set<Character> with 0 items.”
favoriteGenres 被声明为一个变量(拥有 var 标示符)而不是一个常量(拥有 let 标示符),因为它里面的元素将会在下面的例子中被增加或者移除。
一个 Set 类型不能从数组字面量中被单独推断出来,因此 Set 类型必须显式声明。然而,由于 Swift 的类型推断功能,如果你想使用一个数组字面量构造一个 Set 并且该数组字面量中的所有元素类型相同,那么你无须写出 Set 的具体类型。favoriteGenres 的构造形式可以采用简化的方式代替:
1
var favoriteGenres: Set= ["Rock", "Classical", "Hip hop"]
print("I have \(favoriteGenres.count) favorite music genres.") // 打印“I have 3 favorite music genres.”
使用布尔属性 isEmpty 作为一个缩写形式去检查 count 属性是否为 0:
1 2 3 4 5 6
if favoriteGenres.isEmpty { print("As far as music goes, I'm not picky.") } else { print("I have particular music preferences.") } // 打印“I have particular music preferences.”
你可以通过调用 Set 的 remove(_:) 方法去删除一个元素,如果该值是该 Set 的一个元素则删除该元素并且返回被删除的元素值,否则如果该 Set 不包含该值,则返回 nil。另外,Set 中的所有元素可以通过它的 removeAll() 方法删除。
1 2 3 4 5 6
iflet removedGenre = favoriteGenres.remove("Rock") { print("\(removedGenre)? I'm over it.") } else { print("I never much cared for that.") } // 打印“Rock? I'm over it.”
使用 contains(_:) 方法去检查 Set 中是否包含一个特定的值:
1 2 3 4 5 6
if favoriteGenres.contains("Funk") { print("I get up on the good foot.") } else { print("It's too funky in here.") } // 打印“It's too funky in here.”
遍历一个集合
你可以在一个 for-in 循环中遍历一个 Set 中的所有值。
1 2 3 4 5 6
for genre in favoriteGenres { print("\(genre)") } // Classical // Jazz // Hip hop
print("The dictionary of airports contains \(airports.count) items.") // 打印“The dictionary of airports contains 2 items.”(这个字典有两个数据项)
使用布尔属性 isEmpty 作为一个缩写形式去检查 count 属性是否为 0:
1 2 3 4 5 6
if airports.isEmpty { print("The airports dictionary is empty.") } else { print("The airports dictionary is not empty.") } // 打印“The airports dictionary is not empty.”
iflet oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") { print("The old value for DUB was \(oldValue).") } // 输出“The old value for DUB was Dublin.”
iflet airportName = airports["DUB"] { print("The name of the airport is \(airportName).") } else { print("That airport is not in the airports dictionary.") } // 打印“The name of the airport is Dublin Airport.”
iflet removedValue = airports.removeValue(forKey: "DUB") { print("The removed airport's name is \(removedValue).") } else { print("The airports dictionary does not contain a value for DUB.") } // 打印“The removed airport's name is Dublin Airport.”
字符串是是一系列字符的集合,例如 "hello, world","albatross"。Swift 的字符串通过 String 类型来表示。而 String 内容的访问方式有多种,例如以 Character 值的集合。
Swift 的 String 和 Character 类型提供了一种快速且兼容 Unicode 的方式来处理代码中的文本内容。创建和操作字符串的语法与 C 语言中字符串操作相似,轻量并且易读。通过 + 符号就可以非常简单的实现两个字符串的拼接操作。与 Swift 中其他值一样,能否更改字符串的值,取决于其被定义为常量还是变量。你可以在已有字符串中插入常量、变量、字面量和表达式从而形成更长的字符串,这一过程也被成为字符串插值。尤其是在为显示、存储和打印创建自定义字符串值时,字符串插值操作尤其有用。
尽管语法简易,但 Swift 中的 String 类型的实现却很快速和现代化。每一个字符串都是由编码无关的 Unicode 字符组成,并支持访问字符的多种 Unicode 表示形式。
注意
Swift 的 String 类型与 Foundation NSString 类进行了无缝桥接。Foundation 还对 String 进行扩展使其可以访问 NSString 类型中定义的方法。这意味着调用那些 NSString 的方法,你无需进行任何类型转换。
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 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." """
为了让一个多行字符串字面量开始和结束于换行符,请将换行写在第一行和最后一行,例如:
1 2 3 4 5 6
let lineBreaks =""" This string starts with a line break. It also ends with a line break. """
一个多行字符串字面量能够缩进来匹配周围的代码。关闭引号(""")之前的空白字符串告诉 Swift 编译器其他各行多少空白字符串需要忽略。然而,如果你在某行的前面写的空白字符串超出了关闭引号(""")之前的空白字符串,则超出部分将被包含在多行字符串字面量中。
let wiseWords ="\"Imagination is more important than knowledge\" - Einstein" // "Imageination is more important than knowledge" - Enistein let dollarSign ="\u{24}"// $,Unicode 标量 U+0024 let blackHeart ="\u{2665}"// ♥,Unicode 标量 U+2665 let sparklingHeart ="\u{1F496}"// 💖,Unicode 标量 U+1F496
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 ACTUE ACCENT)的标量(U+0301),这样一对标量就表示了同样的字母 é。 这个急促重音的标量形象的将 e 转换成了 é。
在这两种情况中,字母 é 代表了一个单一的 Swift 的 Character 值,同时代表了一个可扩展的字形群。在第一种情况,这个字形群包含一个单一标量;而在第二种情况,它是包含两个标量的字形群:
1 2 3
let eAcute: Character="\u{E9}"// é let combinedEAcute: Character="\u{65}\u{301}"// e 后面加上 ́ // eAcute 是 é, combinedEAcute 是 é
可扩展的字形集是一个将许多复杂的脚本字符表示为单个字符值的灵活方式。例如,来自朝鲜语字母表的韩语音节能表示为组合或分解的有序排列。在 Swift 都会表示为同一个单一的 Character 值:
1 2 3
let precomposed: Character="\u{D55C}"// 한 let decomposed: Character="\u{1112}\u{1161}\u{11AB}"// ᄒ, ᅡ, ᆫ // precomposed 是 한, decomposed 是 한
可拓展的字符群集可以使包围记号(例如 COMBINING ENCLOSING CIRCLE 或者 U+20DD)的标量包围其他 Unicode 标量,作为一个单一的 Character 值:
1 2
let enclosedEAcute: Character="\u{E9}\u{20DD}" // enclosedEAcute 是 é⃝
地域性指示符号的 Unicode 标量可以组合成一个单一的 Character 值,例如 REGIONAL INDICATOR SYMBOL LETTER U(U+1F1FA)和 REGIONAL INDICATOR SYMBOL LETTER S(U+1F1F8):
1 2
let regionalIndicatorForUS: Character="\u{1F1FA}\u{1F1F8}" // regionalIndicatorForUS 是 🇺🇸
计算字符数量
如果想要获得一个字符串中 Character 值的数量,可以使用 count 属性:
1 2 3
let unusualMenagerie ="Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪" print("unusualMenagerie has \(unusualMenagerie.count) characters") // 打印输出“unusualMenagerie has 40 characters”
注意在 Swift 中,使用可拓展的字符群集作为 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
let greeting ="Hello, world!" let index = greeting.firstIndex(of: ",") ?? greeting.endIndex let beginning = greeting[..<index] // beginning 的值为 "Hello"
// 把结果转化为 String 以便长期存储。 let newString =String(beginning)
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") } // 打印输出“These two strings are considered equal”
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(_:) 方法来计算话剧中第一幕的场景数:
1 2 3 4 5 6 7 8
var act1SceneCount =0 for scene in romeoAndJuliet { if scene.hasPrefix("Act 1 ") { act1SceneCount +=1 } } print("There are \(act1SceneCount) scenes in Act 1") // 打印输出“There are 5 scenes in Act 1”
相似地,你可以用 hasSuffix(_:) 方法来计算发生在不同地方的场景数:
1 2 3 4 5 6 7 8 9 10 11
var mansionCount =0 var cellCount =0 for scene in romeoAndJuliet { if scene.hasSuffix("Capulet's mansion") { mansionCount +=1 } elseif scene.hasSuffix("Friar Lawrence's cell") { cellCount +=1 } } print("\(mansionCount) mansion scenes; \(cellCount) cell scenes") // 打印输出“6 mansion scenes; 2 cell scenes”
运算符是检查、改变、合并值的特殊符号或短语。例如,加号(+)将两个数相加(如 let i = 1 + 2)。更复杂的运算例子包括逻辑与运算符 &&(如 if enteredDoorCode && passedRetinaScan)。
Swift 支持大部分标准 C 语言的运算符,且为了减少常见编码错误做了部分改进。如:赋值符(=)不再有返回值,这样就消除了手误将判等运算符(==)写成赋值符导致代码错误的缺陷。算术运算符(+,-,*,/,% 等)的结果会被检测并禁止值溢出,以此来避免保存变量时由于变量大于或小于其类型所能承载的范围时导致的异常结果。当然允许你使用 Swift 的溢出运算符来实现溢出。详情参见 溢出运算符。
Swift 还提供了 C 语言没有的区间运算符,例如 a..<b 或 a...b,这方便我们表达一个区间内的数值。
本章节只描述了 Swift 中的基本运算符,高级运算符 这章会包含 Swift 中的高级运算符,及如何自定义运算符,及如何进行自定义类型的运算符重载。
let name ="world" if name =="world" { print("hello, world") } else { print("I'm sorry \(name), but I don't recognize you") } // 输出“hello, world", 因为 `name` 就是等于 "world”
Swift 还增加了可选(Optional)类型,用于处理值缺失的情况。可选表示 “那儿有一个值,并且它等于 x ” 或者 “那儿没有值” 。可选有点像在 Objective-C 中使用 nil ,但是它可以用在任何类型上,不仅仅是类。可选类型比 Objective-C 中的 nil 指针更加安全也更具表现力,它是 Swift 许多强大特性的重要组成部分。
Swift 是一门类型安全的语言,这意味着 Swift 可以让你清楚地知道值的类型。如果你的代码需要一个 String ,类型安全会阻止你不小心传入一个 Int 。同样的,如果你的代码需要一个 String,类型安全会阻止你意外传入一个可选的 String 。类型安全可以帮助你在开发阶段尽早发现并修正错误。
let (statusCode, statusMessage) = http404Error print("The status code is \(statusCode)") // 输出“The status code is 404” print("The status message is \(statusMessage)") // 输出“The status message is Not Found”
如果你只需要一部分元组值,分解的时候可以把要忽略的部分用下划线(_)标记:
1 2 3
let (justTheStatusCode, _) = http404Error print("The status code is \(justTheStatusCode)") // 输出“The status code is 404”
此外,你还可以通过下标来访问元组中的单个元素,下标从零开始:
1 2 3 4
print("The status code is \(http404Error.0)") // 输出“The status code is 404” print("The status message is \(http404Error.1)") // 输出“The status message is Not Found”
你可以在定义元组的时候给单个元素命名:
1
let http200Status = (statusCode: 200, description: "OK")
给元组中的元素命名后,你可以通过名字来获取这些元素的值:
1 2 3 4
print("The status code is \(http200Status.statusCode)") // 输出“The status code is 200” print("The status message is \(http200Status.description)") // 输出“The status message is OK”
C 和 Objective-C 中并没有可选类型这个概念。最接近的是 Objective-C 中的一个特性,一个方法要不返回一个对象要不返回 nil,nil 表示“缺少一个合法的对象”。然而,这只对对象起作用——对于结构体,基本的 C 类型或者枚举类型不起作用。对于这些类型,Objective-C 方法一般会返回一个特殊值(比如 NSNotFound)来暗示值缺失。这种方法假设方法的调用者知道并记得对特殊值进行判断。然而,Swift 的可选类型可以让你暗示任意类型的值缺失,并不需要一个特殊值。
来看一个例子。Swift 的 Int 类型有一种构造器,作用是将一个 String 值转换成一个 Int 值。然而,并不是所有的字符串都可以转换成一个整数。字符串 "123" 可以被转换成数字 123 ,但是字符串 "hello, world" 不行。
下面的例子使用这种构造器来尝试将一个 String 转换成 Int:
1 2 3
let possibleNumber ="123" let convertedNumber =Int(possibleNumber) // convertedNumber 被推测为类型 "Int?", 或者类型 "optional Int"
因为该构造器可能会失败,所以它返回一个可选类型(optional)Int,而不是一个 Int。一个可选的 Int 被写作 Int? 而不是 Int。问号暗示包含的值是可选类型,也就是说可能包含 Int 值也可能不包含值。(不能包含其他任何值比如 Bool 值或者 String 值。只能是 Int 或者什么都没有。)
nil
你可以给可选变量赋值为 nil 来表示它没有值:
1 2 3 4
var serverResponseCode: Int? =404 // serverResponseCode 包含一个可选的 Int 值 404 serverResponseCode =nil // serverResponseCode 现在不包含值
iflet actualNumber =Int(possibleNumber) { print("\'\(possibleNumber)\' has an integer value of \(actualNumber)") } else { print("\'\(possibleNumber)\' could not be converted to an integer") } // 输出“'123' has an integer value of 123”
这段代码可以被理解为:
“如果 Int(possibleNumber) 返回的可选 Int 包含一个值,创建一个叫做 actualNumber 的新常量并将可选包含的值赋给它。”
如果转换成功,actualNumber 常量可以在 if 语句的第一个分支中使用。它已经被可选类型 包含的 值初始化过,所以不需要再使用 ! 后缀来获取它的值。在这个例子中,actualNumber 只被用来输出转换结果。
你可以在可选绑定中使用常量和变量。如果你想在 if 语句的第一个分支中操作 actualNumber 的值,你可以改成 if var actualNumber,这样可选类型包含的值就会被赋给一个变量而非常量。
你可以包含多个可选绑定或多个布尔条件在一个 if 语句中,只要使用逗号分开就行。只要有任意一个可选绑定的值为 nil,或者任意一个布尔条件为 false,则整个 if 条件判断为 false,这时你就需要使用嵌套 if 条件语句来处理,如下所示:
if age >10 { print("You can ride the roller-coaster or the ferris wheel.") } elseif age >0 { print("You can ride the ferris wheel.") } else { assertionFailure("A person's age can't be less than zero.") }
对每个节点,在计算时同时考虑两项代价指标:当前节点与起始点的距离,以及当前节点与目标点的距离:
f = g + h f = g + h f=g+h其中 f f f为总代价, g g g为当前节点距离起始点的距离, h h h为当前节点距离目标点的距离。
而在计算距离时,又可以采用两种方式: 欧氏距离:
L = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 L = \sqrt{\left( x_1 - x_2 \right) ^2 + \left( y_1 - y_2 \right) ^2} L=(x1−x2)2+(y1−y2)2 曼哈顿距离:
L = ∣ x 1 − x 2 ∣ + ∣ y 1 − y 2 ∣ L = \lvert x_1 - x_2 \rvert + \lvert y_1 - y_2 \rvert L=∣x1−x2∣+∣y1−y2∣为了计算方便,本文计算 h h h时采用曼哈顿距离,这也是A*算法中的一贯做法。
为了方便计数,在计算每一个节点时,在栅格左上角写 f f f值,左下角写 g g g值,右下角写 h h h值。
从父节点到子节点可以水平、竖直、对角线位移计算 g g g值,而从子节点到目标点只能使用水平、竖直位移计算曼哈顿距离 h h h值。
对于点B:从A到B只需右移一格,因此B的 g = 10 g=10 g=10;从B到M需要先右移四格,再上移三格,因此B的 h = 40 + 30 = 70 h = 40+30=70 h=40+30=70。这样B的 f = g + h = 10 + 70 = 80 f=g+h=10+70=80 f=g+h=10+70=80。将三者都记在B格中。
对于点C:从A到C只需向右上方平移一格,因此C的 g = 14 g=14 g=14;从C到M需要先右移四格,再上移两格,因此C的 h = 40 + 20 = 60 h=40+20=60 h=40+20=60,继而C的 f = g + h = 74 f=g+h=74 f=g+h=74。
同理可以计算出D的 g = 14 , h = 80 , f = 94 g=14, h=80,f=94 g=14,h=80,f=94,以及其他几个子节点的值。
将这8个子节点进行对比,可以发现,C点的 f f f值最小,因此选取C点为下一步搜寻的父节点, A C ‾ \overline{AC} AC即为路径迈出的第一步。
如图所示,现在将C作为新的父节点。
对于J点:
从C到J只需右移一格,因此J的 g = 10 + g C = 10 + 14 = 24 g=10+g_C =10+14=24 g=10+gC=10+14=24。需要注意的是,J点的 g g g值是从C到J的 g g g加上从A到C的 g g g,即当前子节点的 g g g值是从起点到该点的所有 g g g累和。
从J到M需要先右移三格,再上移两格,因此J的 h = 30 + 20 = 50 h = 30+20=50 h=30+20=50。这样J的 f = g + h = 24 + 50 = 74 f=g+h=24+50=74 f=g+h=24+50=74。将三者都记在J格中。
对于I点:
从C到I只需向右上方平移一格,同样地,I点的 g g g值是从C到I的 g g g加上从A到C的 g g g,即当前子节点的 g g g值是从起点到该点的所有 g g g累和,因此I的 g = 14 + g C = 14 + 14 = 28 g=14+g_C =14+14=28 g=14+gC=14+14=28。
从I到M需要先右移三格,再上移一格,因此I的 h = 30 + 10 = 40 h = 30+10=40 h=30+10=40。这样I的 f = g + h = 28 + 40 = 68 f=g+h=28+40=68 f=g+h=28+40=68。将三者都记在I格中。
同样计算出其他点的值。
可以看出,I点的 f f f值最小,因此选择I点作为路径上的下一个点,此时路径变为 A C I ‾ \overline{ACI} ACI。
如此一步步进行迭代,最后找到最优轨迹。
1.1.2 由计算得出的小结论
每个节点中需要存储至少3个值: g , h , f g,h,f g,h,f。
当子节点迭代到目标点本身时, h = 0 h=0 h=0,即当前节点到目标点的距离为0.
在算法实施时, g g g的计算可以取10或14,即从父节点到子节点可以水平/竖直位移,也可以沿对角线位移;而 h h h的计算只能取10的倍数,即从当前子节点到目标点只能水平/竖直位移。前者可以采用欧氏距离,后者只能采用曼哈顿距离。
鉴于上一点,可以预想,算法的具体实施过程中,需要有两个数组保存“待计算子节点”和“已被选中的节点”。当“待计算子节点”中某个点符合 f f f最小时,就将其加入“已选中的节点”,并在“待计算子节点”中删除该点,防止后续重复计算。
当算法结束时,保存在“已选中的节点”中的所有点,按照顺序即为找出的路径。
算法结束时,最后一个点的 f f f值,即为从起点到终点所用的距离。
算法的停止条件:1)当“待计算子节点”中没有点时,即已经没有点可供寻找了;2)当当前父节点恰为目标节点时,即 h = 0 h=0 h=0。
1.2 算法逻辑结构
A*算法的逻辑结构如下:
1)初始化。导入地图信息,设置障碍物区域,设置起点start、终点target、栅格数量 m × n m \times n m×n等。
2)数据预处理。建立“待计算子节点”的数组openlist,“已选中的节点”的数组closelist,保存路径的数组closelist_path。除此之外,还需建立一个当前子节点集合children,用来保存当前父节点周围8个子节点的坐标,以及父节点本身parent;还有保存代价值 g , h , f g, h,f g,h,f的数组openlist_cost和closelist_cost。
3)对子节点们children中的每个节点child:
若该子节点不在“待计算子节点”节点openlist中,则追加进去;
若在,则计算出该child的 g g g值,该 g g g值是从起点到父节点parent的距离加上父节点到该子节点的距离。若该 g g g值小于之前openlist_cost中的 g g g最小值,那么就将openlist_cost中的最小 g g g值更新;
4)由于该代价最小点已经加入了轨迹,因此将该点加入clost_list和closelist_path,并从openlist中剔除;
5)更新openlist中的最小代价值,并以其为父节点开始新一轮搜索。
%% 进入循环 flag = 1; while flag % 找出父节点的忽略closelist的子节点 children = child_nodes_cal(parent ,m, n, obs, closelist); % 判断这些子节点是否在openlist中,若在,则更新;没在,则追加到openlist中 for i = 1 : size(children, 1) child = children(i, :); [in_flag, openlist_idx] = ismember(child, openlist, 'rows'); % in_flag表示该child_node是否在openlist中 % openlist_idx表示该child_node在openlist中的行数 g = openlist_cost(min_idx, 1) + norm(parent - child); % 原来的g加上新的g h = abs(child(1) - target(1)) + abs(child(2) - target(2)); f = g + h; if in_flag % 若在,则比较更新g, f if g < openlist_cost(openlist_idx, 1) % openlist_cost(openlist_idx,1)指的是openlist_cost中idx这一行(即child_node所在的一行)的第一个坐标,即g openlist_cost(openlist_idx, 1) = g; openlist_cost(openlist_idx, 3) = f; openlist_path{openlist_idx, 2} = [openlist_path{min_idx, 2}; child]; % openlist_path的第i行第2列为一个列向量,分别是起始节点和当前child,此处定义新的起始节点为openlist_path(min_idx,2), 而openlist_path(min_idx,2)指第min_idx行所对应的 child在openlist_path中对应的路径,相当于把新的child附加到了路径上,延长了路径 end else openlist(end+1, :) = child; openlist_cost(end+1, :) = [g, h, f]; openlist_path{end+1, 1} = child; openlist_path{end, 2} = [openlist_path{min_idx, 2}; child]; end end % 从openlist移除代价最小的节点到closelist closelist(end+1, :) = openlist(min_idx, :); closelist_cost(end+1, :) = openlist_cost(min_idx, 3); closelist_path(end+1, :) = openlist_path(min_idx, :); % 同样地,openlist中少了该节点 openlist(min_idx, :) = []; openlist_cost(min_idx, :) = []; openlist_path(min_idx, :) = []; % 重新搜索:从openlist搜索移动代价最小的节点,作为新的父节点 [~, min_idx] = min(openlist_cost(:, 3)); parent = openlist(min_idx, :); % 判断是否搜索到终点 if parent == target closelist(end+1, :) = openlist(min_idx, :); closelist_cost(end+1, 1) = openlist_cost(min_idx, 1); closelist_path(end+1, :) = openlist_path(min_idx, :); flag = 0; end
end
2.4.1
对这一部分循环来说,首先利用child_nodes_cal函数得出当前父节点parent周围的子节点们children;
对每一个子节点child,判断其是否在“待计算子节点”openlist列表中——in_flag=1表示在列表中,同时openlist_idx为该子节点在openlist中的索引号。
判断完成后,首先计算该子节点的 g , h , f g,h,f g,h,f值,注意: g g g值为父节点parent的 g g g加上该child子节点的 g g g值。
2.4.2
计算 g , h , f g,h,f g,h,f之后,再来看子节点在openlist中的情况。由于循环中查看了parent周围所有子节点的情况,所以一定会存在某个子节点的 g g g比其他子节点的 g g g都小。如果找到了这样的子节点,那么就更新该子节点在openlist_cost中对应位置的 g , h , f g,h,f g,h,f值。
另一方面,如果该child不在openlist中,那么就把该child添加到“待计算子节点”列表中,其 g , h , f g,h,f g,h,f值添加到openlist_cost代价列表中,其路径[openlist_path{min_idx, 2}; child]添加到路径openlist_path中。