Farmer, keshiim 播种太阳🌞

小n快乐成长🍉


  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

  • 公益404

Swift:枚举

发表于 2017-11-10 | 分类于 学习 , Swift | | 阅读次数

枚举为一组相关值定义了一个通用类型,从而可以让你在代码中类型安全地操作这些值。

如果你熟悉 C ,那么你可能知道 C 中的枚举会给一组整数值分配相关的名称。Swift 中的枚举则更加灵活,并且不需给枚举中的每一个成员都提供值。如果一个值(所谓“原始”值)要被提供给每一个枚举成员,那么这个值可以是字符串、字符、任意的整数值,或者是浮点类型。

而且,枚举成员可以指定任意类型的值来与不同的成员值关联储存,这更像是其他语言中的 union 或variant 的效果。你可以定义一组相关成员的合集作为枚举的一部分,每一个成员都可以有不同类型的值的合集与其关联。

Swift 中的枚举是具有自己权限的一类类型。它们使用了许多一般只被类所支持的特性,例如计算属性用来提供关于枚举当前值的额外信息,并且实例方法用来提供与枚举表示的值相关的功能。枚举同样也能够定义初始化器来初始化成员值;而且能够遵循协议来提供标准功能。(🐂吧?)

枚举语法

你可以用 enum关键字来定义一个枚举,然后将其所有的定义内容放在一个大括号({})中

1
2
3
enum SomeEnumeration {
// enumeration definition goes here
}

这是一个指南针的四个主要方向的例子:

1
2
3
4
5
6
enum CompassPoint {
case north
case south
case east
case west
}

在一个枚举中定义的值(比如: north, south, east和 west)就是枚举的成员值(或成员) case关键字则明确了要定义成员值。

不像 C 和 Objective-C 那样,Swift 的枚举成员在被创建时不会分配一个默认的整数值。在上文的 CompassPoint例子中, north, south, east和 west并不代表 0, 1, 2和 3。而相反,不同的枚举成员在它们自己的权限中都是完全合格的值,并且是一个在 CompassPoint中被显式定义的类型。

多个成员值可以出现在同一行中,要用逗号隔开:

1
2
3
4
enum Planet {
//行星
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

正如 Swift 中其它的类型那样,它们的名称(例如: CompassPoint和 Planet)需要首字母大写。给枚举类型起一个单数的而不是复数的名字,从而使得它们能够顾名思义:

1
var directionToHead = CompassPoint.west

好玩的语法:当与 CompassPoint中可用的某一值一同初始化时 directionToHead的类型会被推断出来。一旦 directionToHead以 CompassPoint类型被声明,你就可以用一个点语法把它设定成不同的 CompassPoint值。

1
directionToHead = .east //东

使用 Switch 语句来匹配枚举值

用 switch语句来匹配每一个单独的枚举值:

1
2
3
4
5
6
7
8
9
10
11
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")
}
//print Where the sun rises

正如在控制流中所描述的那样,当判断一个枚举成员时, switch语句应该是全覆盖的。如果 .west的 case被省略了,那么代码将不能编译,因为这时表明它并没有覆盖 CompassPoint的所有成员。要求覆盖所有枚举成员是因为这样可以保证枚举成员不会意外的被漏掉。

否者,使用default情况来包含那些不被显示明确写出的成员:

1
2
3
4
5
6
7
8
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
// Prins "Mossly harmless"

关联值

上面的栗子展示了枚举成员是怎样在他们各自的权限中被定义(和被分类)的。你可以给 Planet.earth设定常量或变量,然后再使用这个值。总之,有时将其它类型的关联值与这些成员值一起存储是很有用的。这样你就可以将额外的自定义信息和成员值一起储存,并且允许你在代码中使用每次调用这个成员时都能使用它。

你可以定义 Swift 枚举来存储任意给定类型的关联值,如果需要的话不同枚举成员关联值的类型可以不同。

举个栗子,假设库存跟踪系统需要按两个不同类型的条形码跟踪产品,一些产品贴的是用数字 0~9 的 UPC-A 格式一维条形码。每一个条码数字都含有一个“数字系统”位,之后是五个“制造商代码”数字和五个“产品代码”数字。而最后则是一个“检测”位来验证代码已经被正确扫描:
barcode_UPC_2x
其它的产品则贴着二维码,它可以使用任何 ISO 8859-1 字符并且编码最长有 2953 个字符的字符串:
barcode_QR_2x
这样可以让库存跟踪系统很方便的以一个由 4 个整数组成的元组来储存 UPC-A 条形码,然而二维码则可以被存储为一个任意长度的字符串中。

在 Swift 中,为不同类型产品条码定义枚举大概是这种姿势:

1
2
3
4
5
enum Barcode {
//关联值
case upc(Int, Int, Int, Int)
case qrCode(String)
}

这可以读作:

“定义一个叫做 Barcode的枚举类型,它要么用 (Int, Int, Int, Int)类型的关联值获取 upc 值,要么用 String 类型的关联值获取一个 qrCode的值。”

这个定义并不提供任何实际的 Int或者 String的值——它只定义当 Barcode常量和变量与 Barcode. upc或 Barcode. qrCode相同时可以存储的关联值的类型。

新的条码就可以用任意一个类型来创建了:

1
var productBarcode = Barcode.upc(8, 85909, 51226, 3)

这个栗子创建了一个叫做 productBarcode的新变量而且给它赋值了一个 Barcode.upc的值关联了值为 (8, 85909, 51226, 3)的元组值。

同样的产品可以被分配一个不同类型的条码:

1
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

这时,最初的 Barcode.upc和它的整数值将被新的 Barcode.qrCode和它的字符串值代替。 Barcode类型的常量和变量可以存储一个 .upc或一个 .qrCode(和它们的相关值一起存储)中的任意一个,但是它们只可以在给定的时间内存储它们其中之一。

不同的条码类型可以用 switch 语句来检查。这一次,总之,相关值可以被提取为 switch 语句的一部分。你提取的每一个相关值都可以作为常量(用 let前缀) 或者变量(用 var前缀)在 switch的 case中使用:

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).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."

如果对于一个枚举成员的所有的相关值都被提取为常量,或如果都被提取为变量,为了简洁,你可以用一个单独的 var或 let在成员名称前标注:

1
2
3
4
5
6
7
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."

原始值

关联值 中条形码的栗子展示了枚举成员是如何声明它们存储不同类型的相关值的。作为相关值的另一种选择,枚举成员可以用相同类型的默认值预先填充(称为原始值)。

这里有一个和已命名的枚举成员一起存储的原始 ASCII 码的例子:

1
2
3
4
5
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}

这里,一个叫做 ASCIIControlCharacter的枚举原始值被定义为类型 Character,并且被放置在了更多的一些 ASCII 控制字符中。

注意:原始值与关联值不同。原始值是当你第一次定义枚举的时候,它们用来预先填充的值,正如上面的三个 ASCII 码。特定枚举成员的原始值是始终相同的。关联值在你基于枚举成员的其中之一创建新的常量或变量时设定,并且在你每次这么做的时候这些关联值可以是不同的。

隐式指定的原始值

当你在操作存储整数或字符串原始值枚举的时候, 你不必显式地给每一个成员都分配一个原始值 。当你没有分配时,Swift 将会自动为你分配值。

实际上,当整数值被用于作为原始值时,每成员的隐式值都比前一个大一。如果第一个成员没有值,那么它的值是 0。

下面的枚举是先前的 Planet枚举的简化,用整数原始值来代表从太阳到每一个行星的顺序:

1
2
3
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}

此🌰:Planet.mercury有一个明确的原始值 1 , Planet.venus的隐式原始值是 2,以此类推。

当字符串被用于原始值,那么每一个成员的隐式原始值则是那个成员的名称。

下面的枚举是先前 CompassPoint枚举的简化,有字符串的原始值来代表每一个方位的名字:

1
2
3
enum CompassPoint: String {
case north, south, east, west
}

CompassPoint.south有一个隐式原始值 “south” ,以此类推。

你可以用 rawValue属性来访问一个枚举成员的原始值:

1
2
3
4
5
let earthsOrder = Planet.Earth.rawValue
// earthsOrder is 3
let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection is "west"

从原始值初始化

如果你用原始值类型来定义一个枚举,那么枚举就会自动收到一个可以接受原始值类型的值的初始化器(叫做 rawValue的形式参数)然后返回一个枚举成员或者 nil 。你可以使用这个初始化器来尝试创建一个枚举的新实例。

这个例子从它的原始值 7来辨认出 Uranus :

1
2
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet is of type Planet? and equals Planet.Uranus

总之,不是所有可能的 Int值都会对应一个行星。因此原始值的初始化器总是返回可选的枚举成员。在上面的例子中, possiblePlanet的类型是 Planet? ,或者“可选项 Planet”

如果你尝试寻找有位置 11的行星,那么被原始值初始化器返回的可选项 Planet值将会是 nil:

1
2
3
4
5
6
7
8
9
10
11
12
let positionToFind = 11
if let somePlanet = Planet1(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)")
}
// Prints "There isn`t a planet at position 11"

递归枚举

枚举在对序号考虑固定数量可能性的数据建模时表现良好,比如用来做简单整数运算的运算符。这些运算符允许你组合简单的整数数学运算表达式比如5到更复杂的比如5+4.

数学表达式的一大特征就是它们可以内嵌。比如说表达式(5 + 4) * 2 在乘法右手侧有一个数但其他表达式在乘法的左手侧。因为数据被内嵌了,用来储存数据的枚举同样需要支持内嵌——这意味着枚举需要被递归。

递归枚举是拥有另一个枚举作为枚举成员关联值的枚举。当编译器操作递归枚举时必须插入间接寻址层。你可以在声明枚举成员之前使用 indirect关键字来明确它是递归的。

1
2
3
4
5
enum ArithmeticExpresstion {
case number(Int)
indirect case addition(ArithmeticExpresstion, ArithmeticExpresstion)
indirect case multiplication(ArithmeticExpresstion, ArithmeticExpresstion)
}

你同样可以在枚举之前写 indirect 来让整个枚举成员在需要时可以递归:

1
2
3
4
5
indirect enum ArithmeticExpresstion {
case number(Int)
case addition(ArithmeticExpresstion, ArithmeticExpresstion)
case multiplication(ArithmeticExpresstion, ArithmeticExpresstion)
}

这个枚举可以储存三种数学运算表达式:单一的数字,两个两个表达式的加法,以及两个表达式的乘法。 addition 和 multiplication 成员拥有同样是数学表达式的关联值——这些关联值让嵌套表达式成为可能。比如说,表达式(5 + 4) * 2 乘号右侧有一个数字左侧有其他表达式。由于数据是内嵌的,用来储存数据的枚举同样需要支持内嵌——这就是说枚举需要递归。下边的代码展示了为 (5 + 4) * 2创建的递归枚举 ArithmeticExpression :

1
2
3
4
let five = ArithmeticExpresstion.number(5)
let four = ArithmeticExpresstion.number(4)
let sum = ArithmeticExpresstion.addition(five, four)
let product = ArithmeticExpresstion.multiplication(sum, ArithmeticExpresstion.number(2))

递归函数是一种操作递归结构数据的简单方法。比如说,这里有一个判断数学表达式的函数:

1
2
3
4
5
6
7
8
9
10
11
12
func evaluate(_ expression: ArithmeticExpresstion) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}
print(evaluate(product))
// Prints "18"

这个函数通过直接返回关联值来判断普通数字。它通过衡量表达式左手侧和右手侧判断是加法还是乘法,然后对它们加或者乘。

摘自:swift 官网
所有代码在Xcode9 Swift4 环境编译没问题,代码戳这里 https://github.com/keshiim/LearnSwift4

Swift:闭包

发表于 2017-11-10 | 分类于 学习 , Swift | | 阅读次数

What is closure?

闭包是可以在你的代码中被传递和引用的功能性独立模块。Swift 中的闭包和 C 以及 Objective-C 中的 blocks 很像,还有其他语言中的匿名函数也类似。

闭包能够捕获和存储定义在其上下文中的任何常量和变量的引用,这也就是所谓的闭合并包裹那些常量和变量,因此被称为“闭包”,Swift 能够为你处理所有关于捕获的内存管理的操作。

在函数章节中有介绍的全局和内嵌函数,实际上是特殊的闭包。闭包符合如下三种形式中的一种:

  • 全局函数是一个有名字但不会捕获任何值的闭包;
  • 内嵌函数是一个有名字且能从其上层函数捕获值的闭包;
  • 闭包表达式是一个轻量级语法所写的可以捕获其上下文中常量或变量值的没有名字的闭包。

Swift 的闭包表达式拥有简洁的风格,鼓励在常见场景中实现简洁,无累赘的语法。常见的优化包括:

  • 利用上下文推断形式参数和返回值的类型;
  • 单表达式的闭包可以隐式返回;
  • 简写实际参数名;
  • 尾随闭包语法。

闭包表达式

闭包表达式是一种在简短行内就能写完闭包的语法。闭包表达式为了缩减书写长度又不失易读明晰而提供了一系列的语法优化。下边的闭包表达式栗子通过使用几次迭代展示 sorted(by:)方法的精简来展示这些优化,每一次都让相同的功能性更加简明扼要。

Sorted 方法

Swift 的标准库提供了一个叫做 sorted(by:) 的方法,会根据你提供的排序闭包将已知类型的数组的值进行排序。一旦它排序完成, sorted(by:) 方法会返回与原数组类型大小完全相同的一个新数组,该数组的元素是已排序好的。原始数组不会被sorted(by:) 方法修改。

这是将被排序的初始数组:

1
let names = ["Chris","Alex","Ewa","Barry","Daniella"]

sorted(by:) 方法接收一个接收两个与数组内容相同类型的实际参数的闭包,然后返回一个 Bool 值来说明第一个值在排序后应该出现在第二个值的前边还是后边。如果第一个值应该出现在第二个值之前,排序闭包需要返回 true ,否则返回 false
这个栗子对一个 String 类型的数组进行排序,因此排序闭包需为一个 (String, String) -> Bool 的类型函数。

提供符合其类型需求的普通函数,并将它作为 sorted(by:)方法的形式参数传入:

1
2
3
4
5
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

如果第一个字符串 s1 大于第二个字符串 s2, backwards(_:,_:)函数将返回 true,这意味着 s1 应该在排序数组中排在 s2 的前面。以此类推,实现倒叙排列。

总之,这样来写本质上只是一个单一表达函数( a > b )是非常啰嗦的。在这个栗子中,我们更倾向于使用闭包表达式在行内写一个简单的闭包。

闭包表达式语法

闭包表达式语法有如下的一般形式:

1
2
3
{ (parameters) -> (return type) in
statements
}

闭包表达式语法能够使用常量形式参数、变量形式参数和输入输出形式参数,但不能提供默认值。可变形式参数也能使用,但需要在形式参数列表的最后面使用。元组也可被用来作为形式参数和返回类型。

下面这个栗子展示一个之前backward(_:_:)函数的闭包表达版本:

1
2
3
4
5
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
print(reversedNames)
// prints ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

需要注意的是行内闭包的形式参数类型和返回类型的声明与 backwards(_:_:) 函数的声明相同。在这两个方式中,都书写成 (s1: String, s2: String) -> Bool。总之对于行内闭包表达式来说,形式参数类型和返回类型都应写在花括号内而不是花括号外面。

闭包的函数整体部分由关键字 in 导入,这个关键字表示闭包的形式参数类型和返回类型定义已经完成,并且闭包的函数体即将开始。

闭包的函数体特别短以至于能够只用一行来书写:

1
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

从语境中推断类型

因排序闭包为实际参数来传递给函数,故 Swift 能推断它的形式参数类型和返回类型。 sorted(by:) 方法期望它的第二个形式参数是一个 (String, String) -> Bool 类型的函数。这意味着(String, String)和 Bool 类型不需要被写成闭包表达式定义中的一部分,因为所有的类型都能被推断,返回箭头 (->) 和围绕在形式参数名周围的括号也能被省略:

1
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })

当把闭包作为行内闭包表达式传递给函数,形式参数类型和返回类型都可以被推断出来。所以说,当闭包被用作函数的实际参数时你都不需要用完整格式来书写行内闭包。

然而,如果你希望的话仍然可以明确类型,并且在读者阅读你的代码时如果它能避免可能存在的歧义的话还是值得的。在这个 sorted(by:) 方法的栗子中,闭包的目的很明确,即排序被替换。对读者来说可以放心的假设闭包可能会使用 String 值,因为它正帮一个字符串数组进行排序。

从单表达式闭包隐式返回

单表达式闭包能够通过从它们的声明中删掉 return 关键字来隐式返回它们单个表达式的结果,前面的栗子可以写作:

1
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

这里, sorted(by:) 方法的第二个实际参数的函数类型已经明确必须通过闭包返回一个 Bool 值。因为闭包的结构包涵返回 Bool 值的单一表达式 (s1 > s2),因此没有歧义,并且 return 关键字能够被省略。

简写的实际参数名

Swift 自动对行内闭包提供简写实际参数名,你也可以通过 $0 , $1 , $2 等名字来引用闭包的实际参数值。

如果你在闭包表达式中使用这些简写实际参数名,那么你可以在闭包的实际参数列表中忽略对其的定义,并且简写实际参数名的数字和类型将会从期望的函数类型中推断出来。 in 关键字也能被省略,因为闭包表达式完全由它的函数体组成:

1
reversedNames = names.sorted(by: { $0 > $1 } )

运算符函数

实际上还有一种更简短的方式来撰写上述闭包表达式。Swift 的 String 类型定义了关于大于号( >)的特定字符串实现,让其作为一个有两个 String 类型形式参数的函数并返回一个 Bool 类型的值。这正好与 sorted(by:) 方法的第二个形式参数需要的函数相匹配。因此,你能简单地传递一个大于号,并且 Swift 将推断你想使用大于号特殊字符串函数实现:

1
reversedNames = names.sorted(by: >)

尾随闭包

如果你需要将一个很长的闭包表达式作为函数最后一个实际参数传递给函数,使用尾随闭包将增强函数的可读性。尾随闭包是一个被书写在函数形式参数的括号外面(后面)的闭包表达式:

1
2
3
4
5
6
7
8
9
10
11
func someFunctionThatTakesAClosure(closure: () -> Void) {
//function body goes here
}
//here`s how you call this function without using a trailing closure
someFunctionThatTakesAClosure(closure: {
//closure`s body goes here
})
//here`s how you call this function with a trailing closure instead
someFunctionThatTakesAClosure {
//trailing closure`s body goes here
}

字符串排列闭包也可以作为一个尾随闭包被书写在 sorted(by:) 方法的括号外面:

1
reversedNames = names.sorted() {$0 > $1}

如果闭包表达式作为函数的唯一实际参数传入,而你又使用了尾随闭包的语法,那你就不需要在函数名后边写圆括号了:

1
reversedNames = names.sorted {$0 > $1}

当闭包很长以至于不能被写成一行时尾随闭包就显得非常有用了。举个栗子,Swift 的 Array 类型中有一个以闭包表达式为唯一的实际参数的 map(_:) 方法。数组中每一个元素调用一次该闭包,并且返回该元素所映射的值(有可能是其他类型)。具体的映射方式和返回值的类型有闭包来指定。

在给数组中每一个成员提供闭包时, map(_:)方法返回一个新的数组,数组中包涵与原数组一一对应的映射值。

使用一个带有尾随闭包的 map(_:) 方法将一个包含 Int 值的数组转换成一个包含 String 值的数组。这个数组 [16,58,510]被转换成一个新的数组 ["OneSix","FiveEight","FiveOneZero"] :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let digitNames = [0: "zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"]
let numbers = [16, 58, 510]
let strings = numbers.map { (number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
print(strings)
// prints ["OneSix", "FiveEight", "FiveOnezero"]

map(_:) 方法在数组中为每一个元素调用了一次闭包表达式。你不需要指定闭包的输入形式参数 number 的类型,因为它能从数组中将被映射的值来做推断。

捕获值

一个闭包能够从上下文捕获已被定义的常量和变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍能够在其函数体内引用和修改这些值。

在 Swift 中,一个能够捕获值的闭包最简单的模型是内嵌函数,即被书写在另一个函数的内部。一个内嵌函数能够捕获外部函数的实际参数并且能够捕获任何在外部函数的内部定义了的常量与变量。

这里有个命名为 makeIncrement 的函数栗子,其中包含了一个名叫 incrementer 一个内嵌函数。这个内嵌 incrementer() 函数能在它的上下文捕获两个值, runningTotal 和 amount 。在捕获这些值后,通过 makeIncrement 将 incrementer作为一个闭包返回,每一次调用 incrementer 时,将以 amount作为增量来增加 runningTotal :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
let incrementByTwo = makeIncrementer(forIncrement: 2)
print(incrementByTwo())
print(incrementByTwo())
print(incrementByTwo())
//prints 2
// 4
// 6

incrementer() 函数是没有任何形式参数, runningTotal 和 amount 不是来自于函数体的内部,而是通过捕获主函数的 runningTotal 和 amount 把它们内嵌在自身函数内部供使用。当调用 makeIncrementer 结束时通过引用捕获来确保不会消失,并确保了在下次再次调用 incrementer 时, runningTotal 将继续增加。

如果你建立了第二个 incrementer ,它将会有一个新的、独立的 runningTotal 变量的引用:

1
2
3
let incrementBySeven = makeIncrementer(forIncrement: 7)
print(incrementBySeven())
//prints 7

再次调用原来增量器 ( incrementByTwo ) 继续增加它自己的变量 runningTotal 的值,并且不会影响 incrementBySeven 捕获的变量 runningTotal 值(8)。

闭包是引用类型

在上面例子中, incrementBySeven 和 incrementByTwo 是常量,但是这些常量指向的闭包仍可以增加已捕获的变量 runningTotal 的值。这是因为函数和闭包都是引用类型。

无论你什么时候安赋值一个函数或者闭包给常量或者变量,你实际上都是将常量和变量设置为对函数和闭包的引用。这上面这个例子中,闭包选择 incrementByTwo 指向一个常量,而不是闭包它自身的内容。
这也意味着你赋值一个闭包到两个不同的常量或变量中,这两个常量或变量都将指向相同的闭包:

1
2
3
let alsoIncrementByTwo = incrementByTwo
print(alsoIncrementByTwo())
//prints 10

逃逸闭包

当闭包作为一个实际参数传递给一个函数的时候,我们就说这个闭包逃逸了,因为它可以在函数返回之后被调用。当你声明一个接受闭包作为形式参数的函数时,你可以在形式参数前写 @escaping 来明确闭包是允许逃逸的。

闭包可以逃逸的一种方法是被储存在定义于函数外的变量里。比如说,很多函数接收闭包实际参数来作为启动异步任务的回调。函数在启动任务后返回,但是闭包要直到任务完成——闭包需要逃逸,以便于稍后调用。举例来说:

1
2
3
4
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHanlder: @escaping () -> Void) {
completionHandlers.append(completionHanlder)
}

函数 someFunctionWithEscapingClosure(_:) 接收一个闭包作为实际参数并且添加它到声明在函数外部的数组里。如果你不标记函数的形式参数为 @escaping ,你就会遇到编译时错误。

让闭包 @escaping 意味着你必须在闭包中显式地引用 self ,比如说,下面的代码中,传给 someFunctionWithEscapingClosure(_:)的闭包是一个逃逸闭包,也就是说它需要显式地引用 self 。相反,传给 someFunctionWithNonescapingClosure(_:) 的闭包是非逃逸闭包,也就是说它可以隐式地引用 self 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func someFunctionWithEscapingClosure(completionHanlder: @escaping () -> Void) {
completionHandlers.append(completionHanlder)
}
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 11
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completionHandlers.first?()
print(instance.x)
// Prints "100"

自动闭包

自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值。这个语法的好处在于通过写普通表达式代替显式闭包而使你省略包围函数形式参数的括号。

调用一个带有自动闭包的函数是很常见的,但实现这类函数就不那么常见了。比如说, assert(condition:message:file:line:) 函数为它的 condition 和 message 形式参数接收一个自动闭包;它的 condition 形式参数只有在调试构建是才评判,而且 message 形式参数只有在 condition 是 false 时才评判。

自动闭包允许你延迟处理,因此闭包内部的代码直到你调用它的时候才会运行。对于有副作用或者占用资源的代码来说很有用,因为它可以允许你控制代码何时才进行求值。下面的代码展示了闭包如何延迟求值。

1
2
3
4
5
6
7
8
9
10
11
12
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"

尽管 customersInLine 数组的第一个元素以闭包的一部分被移除了,但任务并没有执行直到闭包被实际调用。如果闭包永远不被调用,那么闭包里边的表达式就永远不会求值。注意 customerProvider 的类型不是 String 而是 () -> String ——一个不接受实际参数并且返回一个字符串的函数。

当你传一个闭包作为实际参数到函数的时候,你会得到与延迟处理相同的行为。

1
2
3
4
5
6
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

上边的函数 serve(customer:) 接收一个明确的返回下一个客户名称的闭包。下边的另一个版本的 serve(customer:) 执行相同的任务但是不使用明确的闭包而是通过 @autoclosure 标志标记它的形式参数使用了自动闭包。现在你可以调用函数就像它接收了一个 String 实际参数而不是闭包。实际参数自动地转换为闭包,因为 customerProvider 形式参数的类型被标记为 @autoclosure 标记。

1
2
3
4
5
6
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

如果你想要自动闭包允许逃逸,就同时使用 @autoclosure 和 @escaping 标志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0)) //体现自动闭包的特性
collectCustomerProviders(customersInLine.remove(at: 0))
print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"
// customersInLine is []空了

上边的代码中,不是调用传入后作为 customerProvider 实际参数的闭包, collectCustomerProviders(_:) 函数把闭包追加到了 customerProviders 数组的末尾。数组声明在函数的生效范围之外,也就是说数组里的闭包有可能在函数返回之后执行(体现逃逸闭包特性)。结果, customerProvider 实际参数的值必须能够逃逸出函数的生效范围。

摘自:swift 官网
所有代码在Xcode9 Swift4 环境编译没问题,代码戳这里 https://github.com/keshiim/LearnSwift4

Swift:函数

发表于 2017-11-10 | 分类于 学习 , Swift | | 阅读次数

函数是一个独立的代码块,用来执行特定的任务。通过给函数一个名字来定义它的功能,并且在需要的时候,通过这个名字来“调用”函数执行它的任务。

Swift 统一的函数语法十分灵活,可以表达从简单的无形式参数的 C 风格函数到复杂的每一个形式参数都带有局部和外部形式参数名的 Objective-C 风格方法的任何内容。

  • 形式参数能提供一个默认的值来简化函数的调用
  • 也可以被当作输入输出形式参数被传递
  • 它在函数执行完成时修改传递来的变量。

Swift中函数也是一等公民,每个函数都有类型,由函数的形式参数类型和返回类型组成。可做用作其他函数的参数或返回值,同时也可以写在其他函数内部来在内嵌范围封装有用的功能。

定义和调用函数

当你定义了一个函数的时候,你可以选择定义一个或者多个命名的分类的值作为函数的输入(所谓的形式参数),并且/或者定义函数完成后将要传回作为输出的值的类型(所谓它的返回类型)。

每一个函数都有一个函数名,它描述函数执行的任务。要使用一个函数,你可以通过“调用”函数的名字并且传入一个符合函数形式参数类型的输入值(所谓实际参数)来调用这个函数。——给函数提供的实际参数的顺序必须符合函数的形式参数列表顺序。

下边栗子🌰中的函数叫做 greet(person:) ,跟它的功能一致——它接收一个人的名字作为输入然后返回对这个人的问候。要完成它,你需要定义一个输入形式参数——一个叫做 person 的 String类型值——并且返回一个 String 类型,它将会包含对这个人的问候:

1
2
3
4
5
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}
print(greet(person: "zheng"))

这些信息都被包含在了函数的定义中,它使用一个 func的关键字前缀。你可以用一个返回箭头 -> (一个连字符后面跟一个向右的尖括号)来明确函数返回的类型。
你可以通过在 person 标签后边给函数 greet(person:)传入一个用圆括号包裹的 String实际参数值来调用它,例如 greet(person: "Anna")。

为了简化这个函数的主体,我们将创建信息和返回语句组合到一行:

1
2
3
4
5
func greetAgain(person: String) -> String {
return "Hello again, " + person + "!"
}
print(greetAgain(person: "Anna"))
// Prints "Hello again, Anna!"

函数的形式参数和返回值

在 Swift 中,函数的形式参数和返回值非常灵活。你可以定义从一个简单的只有一个未命名形式参数的工具函数到那种具有形象的参数名称和不同的形式参数选项的复杂函数之间的任何函数。

无形式参数的函数

函数没有要求必须输入一个参数,这里有一个没有输入形式参数的函数,无论何时它被调用永远会返回相同的 String信息:

1
2
3
4
func sayHelloWorld() -> String {
return "Hello, world"
}
print(sayHelloWorld())

函数的定义仍然需要在名字后边加一个圆括号,即使它不接受形式参数也得这样做。当函数被调用的时候也要在函数的名字后边加一个空的圆括号。

多形式参数的函数

函数可以输入多个形式参数,可以写在函数后边的圆括号内,用逗号分隔。

这个函数以一个人的名字以及是否被问候过为输入,并返回对这个人的相应的问候:

1
2
3
4
5
6
7
8
9
10
11
12
func greetAgain(person: String) -> String {
return "Hello again, \(person)!"
}
func greet(person: String, alreadyGreeted: Bool) -> String {
if alreadyGreeted {
return greetAgain(person: person)
} else {
return greet(person: person)
}
}
print(greet(person: "Tim", alreadyGreeted: true))
// Prints "Hello again, Tim!"

通过在圆括号中传入带有 person 标签的 String实际参数值和带有 alreadyGreeted 标签的 Bool实际参数值来调用 greet(person:alreadyGreeted:)这个函数,实际参数之间用逗号分隔。注意这个函数与之前展示的函数greet(person:)是明显不同的。尽管两个函数都叫做 greet , greet(person:alreadyGreeted:) 接收两个实际参数但 greet(person:)函数只接收一个。

无返回值的函数

函数定义中没有要求必须有一个返回类型。下面是另一个版本的 greet(person:)函数,它将自己的 String值打印了出来而不是返回它:

1
2
3
4
func greet(person: String) {
print("Hello, \(person)!")
}
greet(person: "Dave")

正因为它不需要返回值,函数在定义的时候就没有包含返回箭头(->)或者返回类型。

注意:严格来讲,函数 greet(person:)还是有一个返回值的,尽管没有定义返回值。没有定义返回类型的函数实际上会返回一个特殊的类型 Void。它其实是一个空的元组,作用相当于没有元素的元组,可以写作 ()

当函数被调用时,函数的返回值可以被忽略:

1
let _ = printAndCount(string: string)

多返回值的函数

为了让函数返回多个值作为一个复合的返回值,你可以使用元组类型作为返回类型。

下面的栗子定义了一个叫做minMax(array:)的函数,它可以找到类型为 Int的数组中最大数字和最小数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func minMax(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
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
print(minMax(array: [1, 2, -4, 3, 74]))
// prints (min: -4, max: 74)

函数 minMax(array:)返回了一个包含两个 Int值的元组。这两个值被 min和 max 标记,这样当查询函数返回值的时候就可以通过名字访问了。

可选元组返回类型

如果元组在函数的返回类型中有可能“没有值”,你可以用一个可选元组返回类型来说明整个元组的可能是 nil 。书法是在可选元组类型的圆括号后边添加一个问号(?)例如 (Int, Int)? 或者 (String, Int, Bool)? 。

上面的函数minMax(array:)返回了一个包含两个 Int值的元组。总之,函数不会对传入的数组进行安全性检查。如果 array的实际参数包含了一个空的数组,上面定义的函数 minMax(array:)在尝试调用 array[0]的时候就会触发一个运行时错误。

为了安全的处理这种“空数组”的情景,就需要把minMax(array:)函数的返回类型写做可选元组,当数组是空的时候返回一个 nil值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func minMax2(array: [Int]) -> (min: Int, max: Int)? {
guard !array.isEmpty else {
return nil
}
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
print(minMax2(array: [1, -3, 1, 2, 67]) ?? "") //prints (min: -3, max: 67)
print(minMax2(array: []) ?? "") //prints ""

函数实际参数标签和形式参数名

每一个函数的形式参数都包含实际参数标签和形式参数名。实际参数标签用在调用函数的时候;在调用函数的时候每一个实际参数前边都要写实际参数标签。形式参数名用在函数的实现当中。默认情况下,形式参数使用它们的形式参数名作为实际参数标签。

1
2
3
4
5
func someFunction(firstParameterName: Int, secondParameterName: Int) {
// In the function body, firstParameterName and secondParameterName
// refer to the argument values for the first and second parameters.
}
someFunction(firstParameterName: 1, secondParameterName: 2)

左右的形式参数必须有唯一的名字。尽管有可能多个形式参数拥有相同的实际参数标签,唯一的实际参数标签有助于让你的代码更加易读。

指定实际参数标签

在提供形式参数名之前写实际参数标签,用空格分隔:

1
2
3
4
5
func someFunction(argumentLabel parameterName: Int) {
// In the function body, parameterName refers to the argument value
// for that parameter.
}
someFunction(argumentLabel: xx)

省略实际参数标签

如果对于函数的形式参数不想使用实际参数标签的话,可以利用下划线( _ )来为这个形式参数代替显式的实际参数标签。

1
2
3
4
5
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
// In the function body, firstParameterName and secondParameterName
// refer to the argument values for the first and second parameters.
}
someFunction(1, secondParameterName: 2)

默认形式参数值

你可以通过在形式参数类型后给形式参数赋一个值来给函数的任意形式参数定义一个默认值。如果定义了默认值,你就可以在调用函数时候省略这个形式参数。

1
2
3
4
5
6
func someFunction(parameterWithDefault: Int = 12) {
// In the function body, if no arguments are passed to the function
// call, the value of parameterWithDefault is 12.
}
someFunction(parameterWithDefault: 6) // parameterWithDefault is 6
someFunction() // parameterWithDefault is 12

把不带有默认值的形式参数放到函数的形式参数列表中带有默认值的形式参数前边,不带有默认值的形式参数通常对函数有着重要的意义——把它们写在前边可以便于让人们看出来无论是否省略带默认值的形式参数,调用的都是同一个函数。

可变形式参数

一个可变形式参数可以接受零或者多个特定类型的值。当调用函数的时候你可以利用可变形式参数来声明形式参数可以被传入值的数量是可变的。可以通过在形式参数的类型名称后边插入三个点符号( …)来书写可变形式参数。传入到可变参数中的值在函数的主体中被当作是对应类型的数组。

举个栗子,一个可变参数的名字是 numbers类型是 Double...在函数的主体中它会被当作名字是 numbers 类型是 [Double]的常量数组。

1
2
3
4
5
6
7
8
9
10
11
func arithmeticMean(numbers: Double...) -> Double {
var total: Double = 0
for number in numbers {
total += number
}
return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// returns 3.0, which is the arithmetic mean of these five numbers
arithmeticMean(3, 8.25, 18.75)
// returns 10.0, which is the arithmetic mean of these three numbers

注意:一个函数最多只能有一个可变形式参数。

输入输出形式参数

你想函数能够修改一个形式参数的值,而且你想这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数。
在形式参数定义开始的时候在前边添加一个 inout关键字可以定义一个输入输出形式参数。输入输出形式参数有一个能输入给函数的值,函数能对其进行修改,还能输出到函数外边替换原来的值。

调用时,你只能把变量作为输入输出形式参数的实际参数。你不能用常量或者字面量作为实际参数,因为常量和字面量不能修改。在将变量作为实际参数传递给输入输出形式参数的时候,直接在它前边添加一个(&)符号来明确可以被函数修改。

这里有一个 swapTwoInts(_:_:)函数,它有两个输入输出整数形式参数 a和 b:

1
2
3
4
5
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}

你可以通过两个Int类型的变量来调用函数 swapTwoInts(_:_:)去调换它们两个的值,需要注意的是 someInt的值和 anotherInt的值在传入函数 swapTwoInts(_:_:)时都添加了’和‘符号。

1
2
3
4
5
var someInt = 3
var anotherInt = 107
swap(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// prints "someInt is now 107, and anotherInt is now 3"

输入输出形式参数与函数的返回值不同。上边的 swapTwoInts没有定义返回类型和返回值,但它仍然能修改 someInt和 anotherInt的值。输入输出形式参数是函数能影响到函数范围外的另一种替代方式。

函数类型

每一个函数都有一个特定的函数类型,它由形式参数类型,返回类型组成。
栗子🌰:

1
2
3
4
5
6
func addTwoInts(_ a: Int, _ b: Int) -> Int {
return a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
return a * b
}

上边的栗子定义了两个简单的数学函数 addTwoInts和 multiplyTwoInts 。这两个函数每个传入两个 Int值,返回一个 Int值,就是函数经过数学运算得出的结果。

这两个函数的类型都是 (Int, Int) -> Int 。也读作:
“有两个形式参数的函数类型,它们都是 Int类型,并且返回一个 Int类型的值。”

下边的另外一个栗子,一个没有形式参数和返回值的函数。

1
2
3
4
5
func printHelloWorld() {
print("hello, world")
func printHelloWorld() {
print("hello, world")
}

这个函数的类型是 () -> Void,或者 “一个没有形式参数的函数,返回 Void。”

使用函数类型

你可以像使用 Swift 中的其他类型一样使用函数类型。例如,你可以给一个常量或变量定义一个函数类型,并且为变量指定一个相应的函数。

1
var mathFunction: (Int, Int) -> Int = addTwoInts

读作:“定义一个叫做 mathFunction的变量,它的类型是‘一个能接受两个 Int值的函数,并返回一个 Int值。’将这个新的变量指向 addTwoInts函数。”

可以利用名字 mathFunction来调用指定的函数。

1
2
print("Result: \(mathFunction(2, 3))")
// prints "Result: 5"

同样也适用于multiplyTwoInts(_:_:)

函数类型作为形式参数类型

利用使用一个函数的类型例如 (Int, Int) -> Int作为其他函数的> Int`。这允许你预留函数的部分实现从而让函数的调用者在调用函数的时候提供。

下面的栗子打印出了上文中数学函数执行后的结果:

1
2
3
4
5
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
print("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// Prints "Result: 8"

函数 printMathResult(_:_:_:)的作用就是当调用一个相应类型的数学函数的时候打印出结果。它并不关心函数在实现过程中究竟做了些什么,它只关心函数是不是正确的类型。这使得函数 printMathResult(_:_:_:)以一种类型安全的方式把自身的功能传递给调用者。

函数类型作为返回类型

利用函数的类型作为另一个函数的返回类型。写法是在函数的返回箭头( ->)后立即写一个完整的函数类型。
下边的栗子定义了两个简单函数叫做 stepForward(_:)和 stepBackward(_:)。函数 stepForward(_:)返回一个大于输入值的值,而 stepBackward(_:)返回一个小于输入值的值。这两个函数的类型都是 (Int) -> Int:

1
2
3
4
5
6
func stepForward(_ input: Int) -> Int {
return input + 1
}
func stepBackward(_ input: Int) -> Int {
return input - 1
}

这里有一个函数 chooseStepFunction(backward:),它的返回类型是“一个函数的类型 (Int) -> Int”。函数 chooseStepFunction(backward:)返回了 stepForward(_:)函数或者一个基于叫做 backwards 的布尔量形式参数的函数 stepBackward(_:):

1
2
3
4
5
6
func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
return backwards ? stepBackward : stepForward
}
var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the stepBackward() function

内嵌函数

到目前为止,你在本章中遇到的所有函数都是全局函数,都是在全局的范围内进行定义的。你也可以在函数的内部定义另外一个函数。这就是内嵌函数。

内嵌函数在默认情况下在外部是被隐藏起来的,但却仍然可以通过包裹它们的函数来调用它们。包裹的函数也可以返回它内部的一个内嵌函数来在另外的范围里使用。

你可以重写上边的栗子 chooseStepFunction(backward:)来使用和返回内嵌函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1}
func stepBackward(input: Int) -> Int { return input - 1}
return backward ? stepBackward : stepForward
}
currentValue = -4
moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
while currentValue != 0 {
print("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!

摘自:swift 官网
所有代码在Xcode9 Swift4 环境编译没问题,代码戳这里 https://github.com/keshiim/LearnSwift4

Swift:控制流

发表于 2017-11-09 | 分类于 学习 , Swift | | 阅读次数

Swift 提供所有多样化的控制流语句。包括 while 循环来多次执行任务; if , guard 和 switch语句来基于特定的条件执行不同的代码分支;还有比如 break 和 continue 语句来传递执行流到你代码的另一个点上。

Swift 同样添加了 for-in循环,它让你更简便地遍历数组、字典、范围和其他序列。

Swift 的 switch 语句同样比 C 中的对应语句多了不少新功能。比如说 Swift 中的 switch 语句不再“贯穿”到下一个情况当中,这就避免了 C 中常见的 break 语句丢失问题。情况可以匹配多种模式,包括间隔匹配,元组和特定的类型。 switch 中匹配的值还能绑定到临时的常量和变量上供情况中代码使用,并且可以为每一个情况写 where 分句表达式来应用复杂条件匹配。

For-in 循环

使用 for-in 循环来遍历序列,比如一个范围的数字,数组中的元素或者字符串中的字符。

1
2
3
4
5
6
7
8
9
//这个例子使用 for-in 循环来遍历数组中的元素:
let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names {
print("Hello, \(name)!")
}
// Hello, Anna!
// Hello, Alex!
// Hello, Brian!
// Hello, Jack!

The same as Dictionary,访问它的键值对。当字典遍历时,每一个元素都返回一个 (key, value) 元组,你可以在for-in 循环体中使用显式命名常量来分解 (key, value) 元组成员,当然不想要的值使用_来舍弃值或键。

1
2
3
4
5
6
7
let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
print("\(animalName)s have \(legCount) legs")
}
// ants have 6 legs
// cats have 4 legs
// spiders have 8 legs

for-in 循环同样能遍历数字区间。这个栗子打印了乘五表格的前几行:

1
2
3
4
5
6
7
8
for index in 1...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 是一个常量,它的值在每次遍历循环开始的时候被自动地设置。因此,它不需要在使用之前声明。它隐式地在循环的声明中声明了,不需要再用 let 声明关键字。
如果你不需要序列的每一个值,你可以使用 下划线 ( _ )来取代遍历名以忽略值。

使用半开区间运算符( ..< )来包含最小值但不包含最大值

1
2
3
4
let minutes = 60
for tickMark in 0..<minutes {
// render the tick mark each minute (60 times)
}

有些用户可能想要在他们的UI上少来点 分钟标记。比如说每 5 分钟一个标记吧。使用 stride(from:to:by:) 函数来跳过不想要的标记。

1
2
3
4
let minuteInterval = 5
for tickMark in stride(from: 0, to: minutes, by: minuteInterval) {
// render the tick mark every 5 minutes (0, 5, 10, 15 ... 45, 50, 55)
}

闭区间也同样适用,使用 stride(from:through:by:) 即可:

1
2
3
4
5
6
let hours = 12
let hourInterval = 3
for tickMark in stride(from: 3, through: hours, by: hourInterval) {
print(tickMark, terminator: " > ")
}
//3 > 6 > 9 > 12 >

While 循环

while 循环执行一个合集的语句指导条件变成 false 。这种循环最好在第一次循环之后还有未知数量的遍历时使用。Swift 提供了两种 while 循环:

  • while 在每次循环开始的时候计算它自己的条件;
  • repeat-while 在每次循环结束的时候计算它自己的条件。

While

while 循环通过判断单一的条件开始。如果条件为 true ,语句的合集就会重复执行直到条件变为 false 。

1
2
3
4
//while 循环的通用格式:
while condition {
statements
}

玩一个玩蛇与梯子(也叫滑梯与梯子)的简单栗子:
<: style=’margin: .auto’ src=’snakesAndLadders_2x.png’ alt=’snakesAndLadders_2x’ title=’滑梯与梯子’>

下边是游戏的规则:

  • 棋盘拥有 25 个方格,目标就是到达或者超过第 25 号方格;
  • 每一次,你扔一个六面色子,安装方格的数字移动,依据水平的线路,如图安装上边虚线箭头标注的路线;
  • 如果你停留在了梯子的下边,你就可以顺着梯子爬上去;
  • 如果你停留在了蛇的头上,你就要顺着蛇滑下来。

游戏棋盘用 Int 值的数组来表示。它的大小基于一个叫做 finalSquare 的常量,它被用来初始化数组同样用来检测稍后的胜利条件。棋盘使用 26 个零 Int 值初始化,而不是 25 个(从 0 到 25 ):

1
2
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)

随后设置为拥有更多特定的值比如蛇和梯子。有梯子的方格有一个正数来让你移动到棋盘的上方,因此有蛇的方格有一个负数来让你从棋盘上倒退:

1
2
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

玩家从“零格”开始,它正好是棋盘的左下角。第一次扔色子总会让玩家上到棋盘上去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var square = 0
var diceRoll = 0
while square < finalSquare {
//roll the dice
diceRoll += 1
if diceRoll == 7 {
diceRoll = 1
}
//move by the rolled amount
square += diceRoll
if square < board.count {
// if we`re still on the board, move up or down for a snake or a ladder
square += board[square];
}
print("now: \(square)", terminator: "> ")
}
print("Game over!")
//print now: 1> now: 11> now: 4> now: 8> now: 13> now: 8> now: 18> now: 20> now: 23> now: 27> Game over!

当前的 while 循环执行结束,并且循环条件已经检查来看循环是否应该再次执行。如果玩家已经移到或者超出了第 25 号方格,循环评定为 false ,游戏就结束了。

while 循环在这个情况当中合适是因为开始 while 循环之后游戏的长度并不确定。循环会一直执行下去直到特定的条件不满足。

Repeat-While

while 循环的另一种形式,就是所谓的 repeat-while 循环,在判断循环条件之前会执行一次循环代码块。然后会继续重复循环直到条件为 false 。

repeat-while 循环的通用形式:

1
2
3
repeat {
statements
} while condiation

再次回顾蛇与梯子的栗子,使用 repeat-while 循环而不是 while 循环。 finalSquare , board , square , 和 diceRoll的值初始化的方式与 while 循环完全相同:

1
2
3
4
5
6
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0

在这个版本的游戏中,第一次循环中的动作是用来检查梯子或者蛇的。没有梯子能直接把玩家带到25格,因此不可能通过梯子赢得游戏。也就是说,在循环一开始就检查蛇还是梯子是安全的。
游戏一开始,玩家在“零格”。 board[0] 总是等于 0 的,并且没有效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
square = 0
diceRoll = 0
repeat {
// move up or down for a snake or ladder
square += board[square]
// roll the dice
diceRoll += 1
if diceRoll = 7 {
diceRoll = 1
}
// move by the rolled amount
square += diceRoll
print("now: \(square)", terminator: "> ")
} while square < finalSquare
//now: 1> now: 3> now: 14> now: 8> now: 13> now: 19> now: 9> now: 20> now: 23> now: 27> Game over!

在检查是蛇还是梯子的代码之后,就是要色子了,玩家按照 diceRoll 数量的格数前进。当前循环执行结束。
循环条件 ( while square < finalSquare) 与之前的相同,但是这次它会在第一次循环结束之后才会被判定。 repeat-while 循环的结构要比前边栗子里的 while 循环更适合这个游戏。在上边的 repeat-while 循环中, square += board[square] 总是会在循环的 while 条件确定 square 仍在棋盘上之后立即执行。这个行为就去掉了早期游戏版本中对数组边界检查的需要。

条件语句

很多时候根据特定的条件来执行不同的代码是很有用的。你可能想要在错误发生时运行额外的代码,或者当值变得太高或者太低的时候显示一条信息。要达成这个目的,你可以让你的那部分代码有条件地执行。

Swift 提供了两种方法来给你的代码添加条件分支,就是所谓的 if 语句和 switch 语句。总的来说,你可以使用 if 语句来判定简单的条件,比如少量的可能性。 switch 语句则适合更复杂的条件,比如多个可能的组合,并且在模式匹配的情况下更加有用,可以帮你选择一段合适的代码分支来执行。

If

最简单的形式中, if 语句有着一个单一的 if 条件。它只会在条件为 true 的情况下才会执行语句的集合:

1
2
3
4
5
6
7
var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
print("It`s very cold. Consider wearing a scarf.")
} else {
print("It`s not that cold. Wear a t-shirt.")
}
//prints "It's not that cold. Wear a t-shirt."

先前的栗子检测了温度是否小于等于 32 华氏温度(水的冰点)。如果是,就打印一个信息。否则,没有信息打印,并且执行 if 语句的大括号后边的代码,如果温度大于32华氏温度,会执行else 分句。

Switch

switch 语句会将一个值与多个可能的模式匹配。然后基于第一个成功匹配的模式来执行合适的代码块。 switch 语句代替 if 语句提供了对多个潜在状态的响应。

在其自身最简单的格式中, switch 语句把一个值与一个或多个相同类型的值比较:

1
2
3
4
5
6
7
8
9
switch some 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
}
没有隐式贯穿

相比 C 和 Objective-C里的 switch 语句来说,Swift 里的 switch 语句不会默认从每个情况的末尾贯穿到下一个情况里。相反,整个 switch 语句会在匹配到第一个 switch 情况执行完毕之后退出,不再需要显式的 break 语句。这使得 switch 语句比 C 的更安全和易用,并且避免了意外地执行多个 switch 情况。

尽管 break 在 Swift 里不是必须的,你仍然可以使用 break 语句来匹配和忽略特定的情况,或者在某个情况执行完成之前就打断它。

每一个情况的函数体必须包含至少一个可执行的语句。下面的代码就是不正确的,因为第一个情况是空的:

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")
}
// this will report a compile-time error

与 C 中的 switch 语句不同,这个 switch 语句没有同时匹配 ”a” 和 ”A” 。相反它会导致一个编译时错误 case “a”:没有包含任何可执行语句 。这可以避免意外地从一个情况贯穿到另一个情况中,并且让代码更加安全和易读。

在一个 switch 情况中匹配多个值可以用逗号分隔,并且可以写成多行,如果列表太长的话:

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")
}
// Prints "The letter A"
区间匹配

switch情况的值可以在一个区间中匹配。这个栗子使用了数字区间来为语言中的数字区间进行转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let approximateCount = 62
let countedThings = "moons orbiting Saturn"
var naturalCount: String
switch approximateCount {
case 0:
naturalCount = "no"
case 1..<5:
naturalCount = "a few"
case 5..<12:
naturalCount = "several"
case 12..<100:
naturalCount = "dozens of"
case 100..<1000:
naturalCount = "hundreds of"
default:
naturalCount = "many"
}
print("There are \(naturalCount) \(countedThings).")

在上面的栗子中, approximateCount 在 switch 语句中进行评定。每个 case 都与数字或者区间进行对比。由于 approximateCount 的值在12和100之间, naturalCount 被赋值 “dozens of”,并且执行结果传递出了 switch 语句。

元组

你可以使用元组来在一个 switch 语句中测试多个值。每个元组中的元素都可以与不同的值或者区间进行匹配。另外,使用下划线(_)来表明匹配所有可能的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let somePoint = (1, 1)
switch somePoint {
case (0, 0):
print("(0, 0) is at the origin")
case (_, 0):
print("(\(somePoint.0), 0) is on the x-axis")
case (0, _):
print("(0, \(somePoint.1)) is on the y-axis")
case (-2...2, -2...2):
print("(\(somePoint.0), \(somePoint.1)) is inside the box")
default:
print("(\(somePoint.0), \(somePoint.1)) is outside the box")
}
// prints "(1, 1) is inside the box"

switch 语句决定坐标是否在原点 (0,0) ;在红色的 x 坐标轴;在橘黄色的 y坐标轴;在蓝色的4乘4以原点为中心的方格里;或者在方格外边。

coordinateGraphSimple_2x

值绑定

switch 情况可以将匹配到的值*临时绑定为一个常量或者变量,来给情况的函数体使用。这就是所谓的值绑定*,因为值是在情况的函数体里“绑定”到临时的常量或者变量的。

下边的栗子接收一个 (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)")
case let (x, y):
print("somewhere else at (\(x), \(y))")
}
// print:on the x-axis with an x value of 2

coordinateGraphMedium_2x

switch 语句决定坐标是否在在红色的x坐标轴,在橘黄色的y坐标轴;还是其他地方;或不在坐标轴上。

三个 switch 情况都使用了常量占位符 x 和 y ,它会从临时 anotherPoint 获取一个或者两个元组值。第一个情况, case(let x, 0),匹配任何 y 的值是 0 并且赋值坐标的x到临时常量 x 里。类似地,第二个情况, case(0,let y) ,匹配让后 x 值是 0 并且把 y 的值赋值给临时常量 y 。

在临时常量被声明后,它们就可以在情况的代码块里使用。这里,它们用来输出点的分类。

注意这个 switch 语句没有任何的 default 情况。最后的情况, case let (x,y) ,声明了一个带有两个占位符常量的元组,它可以匹配所有的值。结果,它匹配了所有剩下的值,然后就不需要 default 情况来让 switch 语句穷尽了。

Where

switch 情况可以使用 where 分句来检查额外的情况。
下边的栗子划分 (x,y) 坐标到下边的图例中:

1
2
3
4
5
6
7
8
9
10
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
print("(\(x), \(y)) is on the line x == -y")
case let (x, y):
print("(\(x), \(y)) is just some arbitrary point")
}
// prints "(1, -1) is on the line x == -y"

coordinateGraphComplex_2x

三个 switch 情况声明了占位符常量 x 和 y ,它从 yetAnotherPoint 临时接收两个元组值。这个常量使用 where 分句,来创建动态过滤。 switch 情况只有 where 分句情况评定等于 true 时才会匹配这个值。

和前边的栗子一样,最后的情况匹配了余下所有可能的值,所以不需要 default 情况这个 switch 也是全面的。

复合情况

多个 switch 共享同一个函数体的多个情况可以在 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")
}
// Prints "e is a vowel"

复合情况同样可以包含值绑定。所有复合情况的模式都必须包含相同的值绑定集合,并且复合情况中的每一个绑定都得有相同的类型格式。这才能确保无论复合情况的那部分匹配了,接下来的函数体中的代码都能访问到绑定的值并且值的类型也都相同。

1
2
3
4
5
6
7
8
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")
}
// Prints "On an axis, 9 from the origin"

上边的 case 拥有两个模式: (let distance, 0) 匹配 x 轴的点以及 (0, let distance) 匹配 y 轴的点。两个模式都包含一个 distance 的绑定并且 distance 在两个模式中都是整形——也就是说这个 case 函数体的代码一定可以访问 distance 的值。

控制转移语句

控制转移语句在你代码执行期间改变代码的执行顺序,通过从一段代码转移控制到另一段。Swift 拥有五种控制转移语句:

  • continue
  • break
  • fallthrough
  • return
  • throw

continue , break , 和 fallthrough 语句在下边有详细描述。 return 语句在函数中描述,还有 throw 语句在使用抛出函数传递错误中描述。

Continue

continue 语句告诉循环停止正在做的事情并且再次从头开始循环的下一次遍历。它是说“我不再继续当前的循环遍历了”而不是离开整个的循环。
举个栗子🌰:

1
2
3
4
5
6
7
8
9
10
11
12
let puzzleInput = "great minds think alike"
var puzzleOutput = ""
for character in puzzleInput.characters {
switch character {
case "a", "e", "i", "o", "u", " ":
continue
default:
puzzleOutput.append(character)
}
}
print(puzzleOutput)
// prints "grtmndsthnklk"

上面的代码在匹配到元音或者空格的时候调用了 continue 关键字,导致遍历的当前循环立即结束并直接跳到了下一次遍历的开始。这个行为使得 switch 代码块匹配(和忽略)只有元音和空格的字符,而不是请求匹配每一个要打印的字符。

Break

break 语句会立即结束整个控制流语句。当你想要提前结束 switch 或者循环语句或者其他情况时可以在 switch 语句或者循环语句中使用 break 语句。

循环语句中的 Break

当在循环语句中使用时, break 会立即结束循环的执行,并且转移控制到循环结束花括号( } )后的第一行代码上。当前遍历循环里的其他代码都不会被执行,并且余下的遍历循环也不会开始了。

Switch 语句里的 Break

当在switch语句里使用时, break 导致 switch 语句立即结束它的执行,并且转移控制到 switch 语句结束花括号( } )之后的第一行代码上。

这可以用来在一个 switch 语句中匹配和忽略一个或者多个情况。因为 Swift 的 switch 语句是穷尽且不允许空情况的,所以有时候有必要故意匹配和忽略一个匹配到的情况以让你的意图更加明确。要这样做的话你可以通过把 break 语句作为情况的整个函数体来忽略某个情况。当这个情况通过 switch 语句匹配到了,情况中的 break 语句会立即结束 switch 语句的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let numberSymbol: Character = "三" // Simplified Chinese for the number 3
var possibleIntegerValue: Int?
switch numberSymbol {
case "1", "١", "一", "๑":
possibleIntegerValue = 1
case "2", "٢", "二", "๒":
possibleIntegerValue = 2
case "3", "٣", "三", "๓":
possibleIntegerValue = 3
case "4", "٤", "四", "๔":
possibleIntegerValue = 4
default:
break
}
if let integerValue = possibleIntegerValue {
print("The integer value of \(numberSymbol) is \(integerValue).")
} else {
print("An integer value could not be found for \(numberSymbol).")
}
// prints "The integer value of 三 is 3."

在上边的例子中,列举所有可能的 Character 值是不实际的,所以default情况就提供了一个匹配所有没有匹配到的字符的功能。这个 default 情况不需要执行任何动作,所以因此就写了一个 break 语句作为函数体。一旦 default 情况匹配到了, break 语句结束 switch 语句的执行,然后代码从 if let 语句继续执行。

Fallthrough

swift贯穿。
如果你确实需要 C 风格的贯穿行为,你可以选择在每个情况末尾使用 fallthrough 关键字。下面的栗子使用了 fallthrough 来创建一个数字的文字描述:

1
2
3
4
5
6
7
8
9
10
11
let integerToDescribe = 5
var description = "The number \(integerToDescribe) is"
switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
description += " a prime number, and also"
fallthrough
default:
description += " an integer."
}
print(description)
// prints "The number 5 is a prime number, and also an integer."

这个栗子声明了一个新的 String 变量叫做 description 并且赋值给它一个初始值。然后函数使用一个 switch 语句来判断 integerToDescribe 。如果 integerToDescribe 是一个列表中的质数,函数就在 description 的末尾追加文字,来标记这个数字是质数。然后它使用 fallthrough 关键字来“贯穿到” default 情况。 default 情况添加额外的文字到描述的末尾,接着 switch 语句结束。

给语句打标签

你可以内嵌循环和条件语句到其他循环和条件语句当中以在 Swift 语言中创建一个复杂的控制流结构。总之,循环和条件语句都可以使用 break 语句来提前结束它们的执行。因此,显式地标记那个循环或者条件语句是你想用 break 语句结束的就很有必要。同样的,如果你有多个内嵌循环,显式地标记你想让 continue 语句生效的是哪个循环就很有必要了。

要达到这些目的,你可以用语句标签来给循环语句或者条件语句做标记。在一个条件语句中,你可以使用一个语句标签配合 break 语句来结束被标记的语句。在循环语句中,你可以使用语句标签来配合 break 或者 continue 语句来结束或者继续执行被标记的语句。

通过把标签作为关键字放到语句开头来用标签标记一段语句,后跟冒号。这里是一个对 while 循环使用标签的栗子,这个原则对所有的循环和 switch 语句来说都相同:

1
2
3
(label name): while condition {
statements
}

下边的栗子为你之前章节看过的蛇与梯子游戏做了修改,在 while 循环中使用了标签来配合 break 和 continue 语句。这次,这个游戏有一个额外的规则:

  • 要赢得游戏,你必须精确地落在第25格上。
    如果特定的点数带你超过了第25格,你必须再次掷色子直到你恰好得到了落到第25格的点数。

游戏棋盘与之前的一样:
snakesAndLadders_2x(1)
finalSquare , board , square 和 diceRoll 的值也用和之前一样的方式来初始化:

1
2
3
4
5
6
let finalSquare = 25
var board = [Int](count: finalSquare + 1, repeatedValue: 0)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0

这个版本的游戏使用了一个 while 循环和一个 switch 语句来实现游戏的逻辑。 while 循环有一个叫做 gameLoop 的标签,来表明它是蛇与梯子游戏的主题循环。

while 循环条件是 while square != finaleSquare ,用来反映你必须精确地落在第25格上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gameLoop: while square != finalSquare {
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
switch square + diceRoll {
case finalSquare:
// diceRoll will move us to the final square, so the game is over
break gameLoop
case let newSquare where newSquare > finalSquare:
// diceRoll will move us beyond the final square, so roll again
continue gameLoop
default:
// this is a valid move, so find out its effect
square += diceRoll
square += board[square]
}
}
print("Game over!")

每次循环,都会扔色子。使用一个 switch 语句来考虑移动的结果而不是立即移动玩家,然后如果移动允许的话就工作:

  • 如果扔的色子将把玩家移动到最后的方格,游戏就结束。 break gameLoop 语句转移控制到 while 循环外的第一行代码上,它会结束游戏。

  • 如果扔的色子点数将会把玩家移动超过最终的方格,那么移动就是不合法的,玩家就需要再次扔色子。 continue gameLoop 语句就会结束当前的 while 循环遍历并且开始下一次循环的遍历。

  • 在其他所有的情况中,色子是合法的。玩家根据 diceRoll 的方格数前进,并且游戏的逻辑会检查蛇和梯子。然后循环结束,控制返回到 while 条件来决定是否要再次循环。

提前退出

guard 语句,类似于 if 语句,基于布尔值表达式来执行语句。使用 guard 语句来要求一个条件必须是真才能执行 guard 之后的语句。与 if 语句不同, guard 语句总是有一个 else 分句—— else 分句里的代码会在条件不为真的时候执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func greet(_ person: [String: String]) {
guard let name = person["name"] else {
return
}
print("Hello \(name)!")
guard let location = person["location"] else {
print("I hope the weather is nice near you.")
return
}
print("I hope the weather is nice in \(location).")
}
greet(["name": "John"])
// prints "Hello John!"
// prints "I hope the weather is nice near you."
greet(["name": "Jane", "location": "Cupertino"])
// prints "Hello Jane!"
// prints "I hope the weather is nice in Cupertino."

如果 guard 语句的条件被满足,代码会继续执行直到 guard 语句后的花括号。任何在条件中使用可选项绑定而赋值的变量或者常量在 guard 所在的代码块中随后的代码里都是**可用的**。

如果这个条件没有被满足,那么在 else 分支里的代码就会被执行。这个分支必须转移控制结束 guard 所在的代码块。要这么做可以使用控制转移语句比如 return , break , continue 或者 throw ,或者它可以调用一个不带有返回值的函数或者方法,比如 fatalError() 。

检查API的可用性

Swift 拥有内置的对 API 可用性的检查功能,它能够确保你不会悲剧地使用了对部属目标不可用的 API。

编译器在 SDK 中使用可用性信息来确保在你项目中明确的 API 都是可用的。如果你尝试使用一个不可用的 API 的话,Swift 会在编译时报告一个错误。

你可以在 if 或者 guard 语句中使用一个可用性条件来有条件地执行代码,基于在运行时你想用的哪个 API 是可用的。当验证在代码块中的 API 可用性时,编译器使用来自可用性条件里的信息来检查。

1
2
3
4
5
if #available(iOS 10, macOS 10.12, *) {
// Use iOS 10 APIs on iOS, and use macOS 10.12 APIs on macOS
} else {
// Fall back to earlier iOS and macOS APIs
}

上边的可用性条件确定了在 iOS 平台, if 函数体只在 iOS 10 及以上版本才会执行;对于 macOS 平台,在只有在 macOS 10.12 及以上版本才会运行。最后一个实际参数, * ,它需求并表明在其他所有平台, if 函数体执行你在目标里明确的最小部属。

摘自:swift 官网
所有代码在Xcode9 Swift4 环境编译没问题,代码戳这里 https://github.com/keshiim/LearnSwift4

Swift:集合类

发表于 2017-10-30 | 分类于 学习 , Swift | | 阅读次数

Swift 提供了三种主要的集合类型,所谓的数组、合集还有字典,用来储存值的集合。数组是有序的值的集合。合集是唯一值的无序集合。字典是无序的键值对集合。
CollectionTypes_intro_2x.png

Swift 中的数组、合集和字典总是明确能储存的值的类型以及它们能储存的键。就是说你不会意外地插入一个错误类型的值到集合中去。它同样意味着你可以从集合当中取回确定类型的值。

Swift 的数组、合集和字典是以泛型集合实现的。

集合的可变性

如果你创建一个数组、合集或者一个字典,并且赋值给一个变量,那么创建的集合就是可变的。这意味着你随后可以通过添加、移除、或者改变集合中的元素来改变(或者说异变)集合。如果你把数组、合集或者字典赋值给一个常量,则集合就成了不可变的,它的大小和内容都不能被改变。

集合不需要改变的情况下创建不可变集合是个不错的选择。这样做可以允许 Swift 编译器优化你创建的集合的性能。

数组

数组以有序的方式来储存相同类型的值。相同类型的值可以在数组的不同地方多次出现。

Swift 的 Array类型被桥接到了基础框架的 NSArray类上。

数组类型简写语法

Swift 数组的类型完整写法是Array<Element>, Element是数组允许存入的值的类型。你同样可以简写数组的类型为 [Element]。尽管两种格式功能上相同,我们更推荐简写并且涉及到数组类型的时候都会使用简写。

创建一个空数组

1
2
3
4
var someInts = Array<Int>()//完整数组声明
var someInts = [Int]() //简写数组声明
print("someInts is of type[Int] with \(somInts.count) items.")
//prints "someInts is of type [Int] with 0 items."

注意 someInts变量的类型通过初始化器的类型推断为 [Int]。

相反,如果内容已经提供了类型信息, 比如说作为函数的实际参数或者已经分类了的变量或常量,你可以通过空数组字面量来创建一个空数组,它写作[ ](一对空方括号) :

1
2
3
4
someInts.append(2)
//someInts now contains 1 value of type Int
someInts = [] //数组的字面量复制
//someInts is now an emtpy array, but is still of type [Int]

使用默认值创建数组

Swift 的 Array类型提供了初始化器来创建确定大小且元素都设定为相同默认值的数组。你可以传给初始化器对应类型的默认值(叫做 repeating)和新数组元素的数量(叫做 count):

1
2
3
var threeDoubles = Array(repeating: 0.0, count: 3)
print(threeDoubles)
// threeDoubles is of type [Double], and equals [0.0, 0.0, 0.0]

通过连接两个数组来创建数组

你可以通过把两个兼容类型的现存数组用加运算符(+)加在一起来创建一个新数组。新数组的类型将从你相加的数组里推断出来:

1
2
3
4
5
var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
// anotherThreeDoubles is of type [Double], and equals [2.5, 2.5, 2.5]
var sixDoubles = threeDoubles + anotherThreeDoubles
// sixDoubles is inferred as [Double], and equals [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]

使用数组字面量创建数组

你同样可以使用数组字面量来初始化一个数组,它是一种以数组集合来写一个或者多个值的简写方式。数组字面量写做一系列的值,用逗号分隔,用方括号括起来:

1
2
[value 1, value 2, value 3]
var shoppingList: [String] = ["Eggs", "Milk"]

shoppingList变量被声明为“字符串值的数组”写做 [String] 。由于这个特定的数组拥有特定的String值类型,它就只能储存 String值。这里, shoppingList被两个 String值( "Eggs"和 "Milk")初始化,写在字符串字面量里。

依托于 Swift 的类型推断,如果你用包含相同类型值的数组字面量初始化数组,就不需要写明数组的类型。shoppingList的初始化可以写得更短:

1
var shoppingList = ["Egg", "Milk"]

因为数组字面量中的值都是相同的类型,Swift 就能够推断 [String]是 shoppingList变量最合适的类型。

访问和修改数组

你可以通过数组的方法和属性来修改数组,或者使用下标脚本语法。要得出数组中元素的数量,检查只读的 count属性:

1
2
print("The shopping list contains \(shoppingList.count) items.")
// prints "The shopping list contains 2 items."

使用布尔量 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.")
}
// prints "The shopping list is not empty."

你可以通过 append(_:)方法给数组末尾添加新的元素:

1
2
shoppingList.append("Flour")
// shoppingList now contains 3 items, and someone is making pancakes

另外,可以使用加赋值运算符 (+=)来在数组末尾添加一个或者多个同类型元素:

1
2
3
4
shoppingList += ["Baking Powder"]
// shoppingList now contains 4 items
shoppingList += ["Chocolate Spread", "Cheese", "Butter"]
//// shoppingList now contains 7 items

通过下标脚本语法来从数组当中取回一个值,在紧跟数组名后的方括号内传入你想要取回的值的索引:

1
var fristItem = shoppingList[0]

你可以使用下标脚本语法来改变给定索引中已经存在的值:

1
2
shoppingList[0] = "Six eggs"
// the first item in the list is now equal to "Six eggs" rather than "Eggs"

你同样可以使用下标脚本语法来一次改变一个范围的值,就算替换与范围长度不同的值的合集也行。下面的栗子替换用 “Bananas”和 “Apples”替换 “Chocolate Spread”, “Cheese”, and “Butter”:

1
2
shoppingList[4...6] = ["Bananas", "Apples"]
// shoppingList now contains 6 items

调用 insert(_:at:)方法插入了一个新元素值为 "Maple Syrup"到 shopping list 的最前面,通过明确索引位置为 0 .
类似地,你可以使用remove(at:)方法来移除一个元素。这个方法移除特定索引的元素并且返回它(尽管你不需要的话可以无视返回的值):

1
2
shoppingList.insert("Maple Syrup", at: 0)
let mapleSyrup = shoppingList.remove(at: 0)

遍历一个数组

你可以用 for-in循环来遍历整个数组中值的合集

1
2
3
4
5
6
7
8
9
for item in shoppingList {
print(item)
}
//Six eggs
//Milk
//Flour
//Baking Powder
//Bananas
//Apples

如果你需要每个元素以及值的整数索引,使用 enumerated()方法来遍历数组。 enumerated()方法返回数组中每一个元素的元组,包含了这个元素的索引和值。你可以分解元组为临时的常量或者变量作为遍历的一部分:

1
2
3
4
5
6
7
8
for (index, value) in shoppingList.enumerated() {
print("Item \(index + 1): \(value)")
}
Item 2: Milk
Item 3: Flour
Item 4: Baking Powder
Item 5: Bananas
Item 6: Apples

合集

合集将同一类型且不重复的值、无序地储存在一个集合当中。当元素的顺序不那么重要的时候你就可以使用合集来代替数组,或者你需要确保元素不会重复的时候。
注意:Swift 的 Set类型桥接到了基础框架的 NSSet类上。

Set 类型的哈希值

为了能让类型储存在合集当中,它必须是可哈希的——就是说类型必须提供计算它自身哈希值的方法。哈希值是Int值且所有的对比起来相等的对象都相同,比如 a == b,它遵循 a.hashValue == b.hashValue。

所有 Swift 的基础类型(比如 String, Int, Double, 和 Bool)默认都是可哈希的,并且可以用于合集或者字典的键。没有关联值的枚举成员值(如同枚举当中描述的那样)同样默认可哈希。

你可以使用你自己自定义的类型作为合集的值类型或者字典的键类型,只要让它们遵循 Swift 基础库的 Hashable协议即可。遵循 Hashable协议的类型必须提供可获取的叫做 hashValue的 Int属性。通过 hashValue属性返回的值不需要在同一个程序的不同的执行当中都相同,或者不同程序。

因为 Hashable协议遵循 Equatable,遵循的类型必须同时一个“等于”运算符 ( ==)的实现。 Equatable协议需要任何遵循 ==的实现都具有等价关系。就是说, ==的实现必须满足以下三个条件,其中 a, b, 和 c是任意值:

  • a == a (自反性)
  • a == b 意味着b == a (对称性)
  • a == b && b == c 意味着 a == c (传递性)

合集类型语法

Swift 的合集类型写做 Set<Element>,这里的 Element是合集要储存的类型。不同与数组,合集没有等价的简写。

创建并初始化一个空合集

你可以使用初始化器语法来创建一个确定类型的空合集:

1
2
3
var letters = Set<Character>()
print("letters is of type Set<Character> with \(letters.count) items.")
// prints "letters is of type Set<Character> with 0 items."

另外,如果内容已经提供了类型信息,比如函数的实际参数或者已经分类的变量常量,你就可以用空的数组字面量来创建一个空合集:

1
2
3
4
5
6
letters.insert("a")
// letters now contains 1 value ('a') of type Charactor
print(letters)
letters = []
print(letters)
// letters is now an empty set, but is still of type Set<Character>

使用数组字面量创建合集

你同样可以使用数组字面量来初始化一个合集,算是一种写一个或者多个合集值的快捷方式。

1
2
var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]
// favroiteGenres has been initialized with three initial items

favoriteGenres变量被声明为“ String值的合集”,写做 Set<String>。由于这个合集已经被明确值类型为 String,它只允许储存 String值。这时,合集 favoriteGenres用三个写在数组字面量中的 String值 ( "Rock", "Classical", 和 "Hip hop")初始化。

由于 Swift 的类型推断,你不需要在使用包含相同类型值的数组字面量初始化合集的时候写合集的类型。 favoriteGenres 的初始化可以写的更短一些:

1
2
var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]
//Swift 就可以推断 Set<String>是 favoriteGenres变量的正确类型

访问和修改合集

Swift提供一些列API来访问和修改集合:count、isEmpty、insert(_:)、remove(_:)、removeAll()、contains(_:)

遍历合集

你可以在 for-in循环里遍历合集的值。
Swift 的 Set类型是无序的。要以特定的顺序遍历合集的值,使用 sorted()方法,它把合集的元素作为使用 < 运算符排序了的数组返回

1
2
3
4
5
6
7
8
9
10
11
12
for genre in favoriteGenres {
print(genre)
}
//Hip hop
//Rock
//Classical
for genre in favoriteGenres.sorted() {
print(genre)
}
//Classical
//Hip hop
//Rock

执行合集操作

下边的示例描述了两个合集—— a和 b——在各种合集操作下的结果,用阴影部分表示。
setVennDiagram_2x (1)

  • 使用 intersection(_:)方法来创建一个只包含两个合集共有值的新合集;
  • 使用 symmetricDifference(_:)方法来创建一个只包含两个合集各自有的非共有值的新合集;
  • 使用 union(_:)方法来创建一个包含两个合集所有值的新合集;
  • 使用 subtracting(_:)方法来创建一个两个合集当中不包含某个合集值的新合集。

下面的示例描述了三个合集—— a, b和 c——用重叠区域代表合集之间值共享。合集 a是合集 b的超集,因为 a包含 b的所有元素。相反地,合集 b是合集 a的子集,因为 b的所有元素被 a包含。合集 b和合集 c是不相交的,因为他们的元素没有相同的。

setEulerDiagram_2x(2)

  • 使用“相等”运算符 ( == )来判断两个合集是否包含有相同的值;
  • 使用 isSubset(of:) 方法来确定一个合集的所有值是被某合集包含;
  • 使用 isSuperset(of:)方法来确定一个合集是否包含某个合集的所有值;
  • 使用 isStrictSubset(of:) 或者 isStrictSuperset(of:)方法来确定是个合集是否为某一个合集的子集或者超集,但并不相等;
  • 使用 isDisjoint(with:)方法来判断两个合集是否拥有完全不同的值。
1
2
3
4
5
6
let houseAnimals: Set = ["🐶", "🐱"]
let farmAnimals: Set = ["🐮", "🐔", "🐑", "🐶", "🐱"]
let cityAnimals: Set = ["🐦", "🐭"]
print(houseAnimals.isSubset(of: farmAnimals)) //true
print(farmAnimals.isSuperset(of: houseAnimals)) //true
print(farmAnimals.isDisjoint(with: cityAnimals))//true

字典

字典储存无序的互相关联的同一类型的键和同一类型的值的集合。每一个值都与唯一的键相关联,它就好像这个值的身份标记一样。不同于数组中的元素,字典中的元素没有特定的顺序。当你需要查找基于特定标记的值的时候使用字典,很类似现实生活中字典用来查找特定字的定义。

字典类型简写语法

Swift 的字典类型写全了是这样的: Dictionary<Key, Value>,其中的 Key是用来作为字典键的值类型, Value就是字典为这些键储存的值的类型。

字典的 Key类型必须遵循 Hashable协议,就像合集的值类型

简写形式:[Key: Value]

创建一个空字典

就像数组,你可以用初始化器语法来创建一个空 Dictionary:

1
var nameOfIntegers = [Int: String]()

如果内容已经提供了信息,你就可以用字典字面量创建空字典了,它写做 [:](在一对方括号里写一个冒号):

1
2
3
4
nameOfIntegers[16] = "sixteen"
// namesOfIntegers now contains 1 key-value pair
nameOfIntegers = [:]
// namesOfIntegers is once again an empty dictionary of type [Int: String]

用字典字面量创建字典

你同样可以使用字典字面量来初始化一个字典,它与数组字面量看起来差不多。字典字面量是写一个或者多个键值对为 Dictionary集合的快捷方式。

键值对由一个键和一个值组合而成,每个键值对里的键和值用冒号分隔。键值对写做一个列表,用逗号分隔,并且最终用方括号把它们括起来:

[key 1: value 1, key 2: value 2, key 3: value 3]

1
2
//airports字典被声明为 [String: String]类型,它意思是“一个键和值都是 String的 Dictionary”。
var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]

与数组一样,如果你用一致类型的字典字面量初始化字典,就不需要写出字典的类型了。 airports的初始化就能写的更短:

1
2
3
//由于字面量中所有的键都有相同的类型,同时所有的值也是相同的类型,
//Swift 可以推断 [String: String]就是 airports字典的正确类型。
var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]

访问和修改字典

你可以通过字典自身的方法和属性来访问和修改它,或者通过使用下标脚本语法。

如同数组:

  • 你可以使用 count只读属性来找出 Dictionary拥有多少元素
1
print("The airports dictionary contains \(airports.count) 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.")
}
// prints "The airports dictionary is not empty."
  • 你可以用下标脚本给字典添加新元素。使用正确类型的新键作为下标脚本的索引,然后赋值一个正确类型的值
1
2
airports["LHR"] = "London"
// the airports dictionary now contains 3 items
  • 你同样可以使用下标脚本语法来改变特定键关联的值:
1
2
airports["LHR"] = "London Heathrow"
// the value for "LHR" has been changed to "London Heathrow"
  • updateValue(_:forKey:)。不同于下标脚本, updateValue(_:forKey:)方法在执行更新之后返回旧的值。这允许你检查更新是否成功。

updateValue(_:forKey:)方法返回一个字典值类型的可选项值。比如对于储存 String值的字典来说,方法会返回 String?类型的值,或者说“可选的 String”。这个可选项包含了键的旧值如果更新前存在的话,否则就是 nil:

1
2
3
4
if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
print("The old value for DUB was \(oldValue).")
}
// prints "The old value for DUB was Dublin."
  • 你可以使用下标脚本语法给一个键赋值 nil来从字典当中移除一个键值对:
1
2
3
4
airports["APL"] = "Apple International"
// "Apple International" is not the real airport for APL, so delete it
airports["APL"] = nil
// APL has now been removed from the dictionary
  • 另外,使用 removeValue(forKey:)来从字典里移除键值对。这个方法移除键值对如果他们存在的话,并且返回移除的值,如果值不存在则返回 nil:
1
2
3
4
5
6
if let 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.")
}
// Prints "The removed airport's name is Dublin Airport."

遍历字典

你可以用 for-in循环来遍历字典的键值对。字典中的每一个元素返回为(key, value)元组,你可以解开元组成员到临时的常量或者变量作为遍历的一部分:

1
2
3
4
5
for (airportCode, airportName) in airports {
print("\(airportCode): \(airportName)")
}
// YYZ: Toronto Pearson
// LHR: London Heathrow

你同样可以通过访问字典的 keys和 values属性来取回可遍历的字典的键或值的集合:

1
2
3
4
5
6
7
8
9
10
11
for airportCode in airports.keys {
print("Airport code: \(airportCode)")
}
// Airport code: YYZ
// Airport code: LHR
for airportName in airports.values {
print("Airport name: \(airportName)")
}
// Airport name: Toronto Pearson
// Airport name: London Heathrow

如果你需要和接收 Array实例的 API 一起使用字典的键或值,就用 keys或 values属性来初始化一个新数组:

1
2
3
4
let airportCodes = [String](airports.keys)
// airportCodes is ["YYZ", "LHR"]
let airportNames = [String](airports.values)
// airportNames is ["Toronto Pearson", "London Heathrow"]

Swift 的 Dictionary类型是无序的。要以特定的顺序遍历字典的键或值,使用键或值的 sorted()方法。

摘自:swift 官网
所有代码在Xcode9 Swift4 环境编译没问题,代码戳这里 https://github.com/keshiim/LearnSwift4

Swift:字符串和字符

发表于 2017-10-27 | 分类于 学习 , Swift | | 阅读次数

字符串是一系列的字符,比如说 "hello, world"或者 "albatross"。Swift 的字符串用 String类型来表示。 String的内容可以通过各种方法来访问到,包括作为 Character值的集合。

Swift 的 String 和 Character 类型提供了一种快速的符合 Unicode 的方式操作你的代码。字符串的创建和修改语法非常轻量易读,使用与 C 类似的字符串字面量语法。字符串串联只需要使用 +运算符即可,字符串的可修改能力通过选择常量和变量来进行管理,就如同 Swift 语言中的其他值。你同样可以使用字符串来 插入常量、变量、字面量以及表达式到更长的字符串当中 ,这就是所谓的字符串插值。这样让创建自定义字符串值来显示、储存和打印值变得更加简单。

别看语法简单,Swift 的 String类型仍旧是快速和现代的字符串实现。每一个字符串都是由 Unicode 字符的独立编码组成,并且提供了多种 Unicode 表示下访问这些字符的支持。

Swift 的 String类型桥接到了基础库中的 NSString类。Foundation 同时也扩展了所有 NSString 定义的方法给 String 。也就是说,如果你导入 Foundation ,就可以在 String 中访问所有的 NSString 方法,无需转换格式。

字符串字面量

你可以在你的代码中插入预先写好的 String值作为字符串字面量。字符串字面量是被双引号包裹的固定顺序文本字符( “)。
使用字符串字面量作为常量或者变量的初始值:

1
let someString = "Some string literal value."

注意 Swift 会为 someString常量推断类型为 String,因为你用了字符串字面量值来初始化。

如果你需要很多行的字符串,使用多行字符串字面量。多行字符串字面量是用三个双引号引起来的一系列字符:

1
2
3
4
5
6
7
8
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."
"""
print(quotation)

如同上面展示的那样,由于多行用了三个双引号而不是一个,你可以在多行字面量中使用单个双引号 “ 。要在多行字符串中包含 """ ,你必须用反斜杠( \ )转义至少其中一个双引号。举例来说:

1
2
3
4
5
let threeDoubleQuotes = """
Escaping the first quote \"""
Escaping all three quote \"\"\"
"""
print(threeDoubleQuotes)

在这个多行格式中,字符串字面量包含了双引号包括的所有行。字符串起始于三个双引号( """ )之后的第一行,结束于三个双引号( """ )之前的一行,也就是说双引号不会开始或结束带有换行。下面的两个字符串是一样的:

1
2
3
4
let singleLineString = "These are the same"
let multilineString = """
There are the same
"""

要让多行字符串字面量开始或结束带有换行,写一个空行作为第一行或者是最后一行。比如:

1
2
3
4
5
6
"""
This string starts with a line feed.
It also ends with a line feed.
"""

行字符串可以缩进以匹配周围的代码。双引号( """ )前的空格会告诉 Swift 其他行前应该有多少空白是需要忽略的。比如说,尽管下面函数中多行字符串字面量缩进了,但实际上字符串不会以任何空白开头。

1
2
3
4
5
6
7
8
9
10
11
12
func generateQuotation() -> 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."
"""
return quotation
}
print(generateQuotation())
print(quotation == generateQuotation()) //true

总而言之,如果你在某行的空格超过了结束的双引号( """ ),那么这些空格会被包含。也就是说以结束的"""左右标尺衡量。如图:
多行尾双引号为准

初始化一个空字符串

为了绑定一个更长的字符串,要在一开始创建一个空的 String值,要么赋值一个空的字符串字面量给变量,要么使用初始化器语法来初始化一个新的 String实例:

1
2
var emptyString = ""
var anotherEmptyString = String()

通过检查布尔量 isEmpty属性来确认一个 String值是否为空:

1
2
3
if emptyString.isEmpty {
print("Nothing to see here")
}

字符串可变性

你可以通过把一个 String设置为变量(这里指可被修改),或者为常量(不能被修改)来指定它是否可以被修改(或者改变):

1
2
3
4
5
var variableString = "Horse"
variableString += " and carriage"
let constantString = "Highlander"
constantString += "and another Highlander" //compile-time error

字符串是值类型

Swift 的 String类型是一种值类型。如果你创建了一个新的 String值, String值在传递给方法或者函数的时候会被复制过去,还有赋值给常量或者变量的时候也是一样。每一次赋值和传递,现存的 String值都会被复制一次,传递走的是拷贝而不是原本。

Swift 的默认拷贝 String行为保证了当一个方法或者函数传给你一个 String值,你就绝对拥有了这个 String值, 无需关心它从哪里来。 你可以确定你传走的这个字符串除了你自己就不会有别人改变它。

操作字符

你可以通过在 for-in循环里遍历 characters属性访问 String 中的每一个独立的 Character值:

1
2
3
4
5
6
7
8
for character in "Dog!🐶".characters {
print(character)
}
// D
// o
// g
// !
// 🐶

另外,你可以通过提供 Character类型标注来从单个字符的字符串字面量创建一个独立的 Character常量或者变量:

1
let exclamationMark: Character = "!"

String值可以通过传入 Character值的字符串作为实际参数到它的初始化器来构造:

1
2
3
4
let catCharacters: [Character] = ["c", "a", "t", "!", "🐱"]
let catString = String(catCharacters)
print(catString);
//print "cat!🐱"

连接字符串和字符

String值能够被加起来(或者说连接),使用加运算符(+)来创建新的String值,你同样也可以使用加赋值符号(+=)在已经存在的 String值末尾追加一个 String值:

1
2
3
4
5
6
let string1 = "hello"
let string2 = " there"
var welcome = string1 + string2 // welcome now equals "hello there"
var instruction = "look over"
instruction += string2 // instruction now equals "look over there"

你使用 String类型的 append()方法来可以给一个 String变量的末尾追加 Character值:

1
2
let exclamationMark: Character = "!"
welcome.append(exclamationMark)

字符串插值

字符串插值是一种从混合常量、变量、字面量和表达式的字符串字面量构造新 String值的方法。每一个你插入到字符串字面量的元素都要被一对圆括号包裹,然后使用反斜杠前缀:

1
2
3
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的真实值代替。

Unicode

Unicode 是一种在不同书写系统中编码、表示和处理文本的国际标准。它允许你表示几乎标准化格式的任何语言中的任何字符,并且为外部源比如文本文档或者网页读写这些字符。如同这节中描述的那样,Swift 的 String和 Character类型是完全 Unicode 兼容的。

Unicode 标量

面板之下,Swift 的原生 String 类型建立于 Unicode 标量值之上。一个 Unicode 标量是一个为字符或者修饰符创建的独一无二的21位数字,比如 LATIN SMALL LETTER A (" a")的 U+0061 ,或者FRONT-FACING BABY CHICK ( "🐥" )的 U+1F425 。

Unicode 标量码位位于 U+0000到 U+D7FF或者 U+E000到 U+10FFFF之间。Unicode 标量码位不包括从 U+D800到 U+DFFF的16位码元码位。

字符串字面量中的特殊字符

字符串字面量能包含以下特殊字符:

  • 转义特殊字符 \0 (空字符), \\ (反斜杠), \t (水平制表符), \n (换行符), \r(回车符),\" (双引号) 以及 \' (单引号);
  • 任意的 Unicode 标量,写作 \u{n},里边的 n是一个 1-8 个与合法 Unicode 码位相等的16进制数字。

下边的代码展示了这些特殊字符的四个栗子。 wiseWords常量包含了两个转义双引号字符。 dollarSign, blackHeart和 sparklingHeart常量展示了 Unicode 标量格式:

1
2
3
4
5
let wiseWords = "\"Imagination is more improtant 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 标量 é ( 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转换为 é来输出。

1
2
3
let eAcute: Character = "\u{e9}" // é
let combinedEAcute: Character = "\u{65}\u{301}"
// eAcute is é, combinedEAcute is é

扩展字形集群是一种非常灵活的把各种复杂脚本字符作为单一 Character值来表示的方法。比如说韩文字母中的音节能被表示为复合和分解序列两种。这两种表示在 Swift 中都完全合格于单一 Character值:

1
2
3
let precomposed: Character = "\u{d55c}" //한
let decomposeed: Character = "\u{1112}\u{1161}\u{11ab}" // ᄒ, ᅡ, ᆫ
// precomposed is 한, decomposed is 한

扩展字形集群允许封闭标记的标量 (比如 COMBINING ENCLOSING CIRCLE, 或者说 U+20DD) 作为单一 Character值来圈住其他 Unicode 标量:

1
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):

1
let regionalIndicatorForUS: Character = "\u{1f1fa}\u{1f1f8}" // regionalIndicatorForUS is 🇺🇸

字符统计

要在字符串中取回 Character值的总数,使用字符串 characters属性中的的 count属性:

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

注意 Swift 为 Character值使用的扩展字形集群*意味着字符串的创建和修改可能不会总是影响字符串的字符统计数。
比如说,如果你使用四个字符的cafe来初始化一个新的字符串,然后追加一个 C
OMBINING ACUTE ACCENT* ( U+0301)到字符串的末尾,字符串的字符统计结果将仍旧是4,但第四个字符是é而不是 e:

1
2
3
4
5
6
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+301
print("the number of characters in \(word) is \(word.characters.count)")
//prints "the number of characters in café is 4"

注意:

扩展字形集群能够组合一个或者多个 Unicode 标量。这意味着不同的字符——以及相同字符的不同表示——能够获得不同大小的内存来储存。因此,Swift 中的字符并不会在字符串中获得相同的内存空间。所以说,字符串中字符的数量如果不遍历它的扩展字形集群边界的话,是不能被计算出来的。如果你在操作特殊的长字符串值,要注意 characters属性为了确定字符串中的字符要遍历整个字符串的 Unicode 标量。

通过 characters属性返回的字符统计并不会总是与包含相同字符的 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:) 方法来访问给定索引的前后。要访问给定索引更远的索引,你可以使用 index(_:offsetBy:)方法而不是多次调用这两个方法。

你可以使用下标脚本语法来访问 String索引中的特定 Character。

1
2
3
4
5
6
7
8
9
10
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如果索引位置在字符串范围之外,就会触发运行时错误。

1
2
greeting[greeting.endIndex] //运行时报错
greeting.index(after: greeting.endIndex) //运行时报错

使用 characters属性的 indices属性来创建所有能够用来访问字符串中独立字符的索引范围 Range。

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

注意:

你可以在任何遵循了 Indexable 协议的类型中使用 startIndex 和 endIndex 属性以及 index(before:) , index(after:)和 index(_:offsetBy:)方法。这包括这里使用的 String ,还有集合类型比如 Array , Dictionary 和 Set。

插入和删除

要给字符串的特定索引位置插入字符,使用 insert(_:at:)方法,另外要冲入另一个字符串的内容到特定的索引,使用 insert(contentsOf:at:)方法。

1
2
3
4
var wellcome = "hello"
wellcome.insert("!", at: wellcome.endIndex) //"hello!"
wellcome.insert(contentsOf: " there".characters, at: wellcome.index(before: wellcome.endIndex))
//"hello there!"

要从字符串的特定索引位置移除字符,使用 remove(at:)方法,另外要移除一小段特定范围的字符串,使用 removeSubrange(_:) 方法

1
2
wellcome.remove(at: wellcome.index(before: wellcome.endIndex)) //"hello there"
wellcome.removeSubrange(wellcome.index(wellcome.endIndex, offsetBy: -6)..<wellcome.endIndex) //"hello"

注意:

你可以在任何遵循了 RangeReplaceableIndexable 协议的类型中使用 insert(_:at:) , insert(contentsOf:at:) , remove(at:) 方法。这包括了这里使用的 String ,同样还有集合类型比如 Array , Dictionary 和 Set 。

字符串比较

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

字符串和字符相等性

如同比较运算符中所描述的那样,字符串和字符相等使用“等于”运算符( ==) 和“不等”运算符 ( !=)进行检查:

1
2
3
4
5
let quotation1 = "We`re a lot alike, you and I."
let sameQuotation = "We`re a lot alike, you and I."
if quotation1 == 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)的。这两个扩展字形集群都是表示字符é的合法方式,所以它们被看做规范化相等:

1
2
3
4
5
6
7
// "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 combinedEAuteQuestion = "Voulez-vous un caf\u{65}\u{301}"
if eAcuteQuestion == combinedEAuteQuestion {
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,或者说 “А”)的。字符看上去差不多,但是它们拥有不同的语言意义,所以不相等。

前缀和后缀相等性

要检查一个字符串是否拥有特定的字符串前缀或者后缀,调用字符串的 hasPrefix(_:)和 hasSuffix(_:)方法,它们两个都会接受一个 String 类型的实际参数并且返回一个布尔量值。

举例,你可以使用 hasPrefix(_:)方法操作 romeoAndJuliet数组来计算第一场场景的数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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"]
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(_:)方法来计算后缀可等性。

如同字符串和字符相等性一节所描述的那样, hasPrefix(_:)和 hasSuffix(_:)方法只对字符串当中的每一个扩展字形集群之间进行了一个逐字符的规范化相等比较。

字符串的 Unicode 表示法

当一个 Unicode 字符串写入文本文档或者其他储存里边的时候,这个字符串的 Unicode 标量会被编码为一个或者一系列 Unicode 定义的编码格式。每一种格式都把字符串编码成所谓码元的小块。这些包括 UTF-8 编码格式(它把字符串以8 码元编码),UTF-16 编码格式(它把字符串按照 16位 码元 编码),以及 UTF-32 编码格式(它把字符串以32位码元编码)。

Swift 提供了几种不同的方法来访问字符串的 Unicode 表示。你可以使用 for-in语句来遍历整个字符串,来访以 Unicode 扩展字形集群的方式,访问单独的 Character值。

或者,你也可以用以下三者之一的其他 Unicode 兼容表示法来访问 String值:

  • 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)组成:

1
let dogString = "Dog‼🐶"

UTF-8 表示法

你可以通过遍历 utf8属性来访问一个 String的 UTF-8 表示法。这个属性的类型是 String.UTF8View,它是非负8位( UInt8)值,在字符串的 UTF-8 表示法中每一个字节的内容:
UTF-8

1
2
3
4
5
6
let dogString = "Dog‼🐶"
for codeUnit in dogString.utf8 {
print("\(codeUnit)", terminator: " ")
}
print("")
// 68 111 103 226 128 188 240 159 144 182

上文中的栗子,前三个十进制 codeUnit值 ( 68, 111, 103)表示了字符 D , o , 和 g ,它们的 UTF-8 表示法与它们的 ASCII 表示法相同。接下来的三个十进制 codeUnit值 ( 226, 128, 188)是 DOUBLE EXCLAMATION MARK字符的三字节 UTF-8表示法。最后四个 codeUnit值 ( 240, 159, 144, 182)是 DOG FACE字符的四字节 UTF-8 表示法。

UTF-16 表示法

你可以通过遍历 utf16属性来访问 String的 UTF-16表示法。这个属性的类型是String.UTF16View,它是非负 16位( UInt16)值,在字符串 UTF-16 表示法中每一个 16位 的内容:
UTF16

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

再一次,前三个 codeUnit值 ( 68, 111, 103)表示了字符 D , o , 和 g,它们的 UTF-16 码元与字符串 UTF-8 表示法中的值相同(因为这些 Unicode 标量表示 ASCII 字符)。

第四个 codeUnit值( 8252)是与十六进制值 203C相等的十进制数字,它表示了 DOUBLE EXCLAMATION MARK字符的 Unicode 标量 U+203C。这个字符可以在 UTF-16 中表示为单个码元了。

第五和第六个 codeUnit值 ( 55357和 56374)是 UTF-16 16位码元对表示的 DOG FACE字符。这些值是高16位码元值 U+D83D(十进制值为 55357)和低16位码元值 U+DC36(十进制值为 56374)。

Unicode 标量表示法

你可以通过遍历 unicodeScalars属性来访问 String值的 Unicode 标量表示法。这个属性的类型是 UnicodeScalarView,它是 UnicodeScalar类型值的合集。
每一个 UnicodeScalar都有值属性可以返回一个标量的21位值,用 UInt32值表示:
UnicodeScalar_2x.png

1
2
3
4
5
for codeUint in dogString.unicodeScalars {
print("\(codeUint.value)", terminator: " ")
}
print("")
// 68 111 103 8252 128054

前三个 UnicodeScalar值的 value属性 ( 68, 111, 103)还是表示了字符 D, o, 和 g。

第四个 codeUnit值 ( 8252)还是等于十六进制值 203C的十进制值,它表示了DOUBLE EXCLAMATION MARK字符的 Unicode 标量 U+203C。

第五个和最后一个 UnicodeScalar的 value属性, 128054,是一个等于十六进制值 1F436的十进制数字,它表示了 DOG FACE字符的 Unicode 标量 U+1F436。

作为查询它们 value属性的替代方法,每一个 UnicodeScalar值同样可以用来构造新的 String值,比如说使用字符串插值:

1
2
3
4
5
6
7
8
for scalar in dogString.unicodeScalars {
print("\(scalar)")
}
//D
//o
//g
//‼
//🐶

摘自:swift 官网
所有代码在Xcode9 Swift4 环境编译没问题,代码戳这里 https://github.com/keshiim/LearnSwift4

Swift:基础运算符

发表于 2017-10-26 | 分类于 学习 , Swift | | 阅读次数

运算符是一种用来检查、改变或者合并值的特殊符号或组合符号。举例来说,加运算符( + )能够把两个数字相加(比如 let i = 1 + 2 )。更复杂的栗子包括逻辑与运算 && 比如 if enteredDoorCode && passedRetinaScan 。

Swift 在支持 C 中的大多数标准运算符的同时也增加了一些排除常见代码错误的能力。赋值符号( = )不会返回值,以防它被误用于等于符号( == )的意图上。算数符号( + , - , * , / , % 以及其他)可以检测并阻止值溢出,以避免你在操作比储存类型允许的范围更大或者更小的数字时得到各种奇奇怪怪的结果。你可以通过使用 Swift 的溢出操作符来选择进入值溢出行为模式。

Swift 提供了两种 C 中没有的区间运算符( a..<b 和 a...b ),来让你便捷表达某个范围的值。

这个章节叙述了 Swift 语言当中常见的运算符。高级运算符 则涵盖了 Swift 中的高级运算符,同时描述了如何定义你自己的运算符以及在你自己的类当中实现标准运算符。

专门用语

运算符包括一元、二元、三元:

  • 一元运算符对一个目标进行操作(比如-a)。一元前缀 运算符在目标之前直接添加(比如!b),同事一元后缀运算符直接在目标末尾添加(比如c!)。
  • 二元运算符对两个目标金鑫操作(比如2 + 3)同时因为他们出现在两个目标之间,所有是中缀。
  • 三元运算符操作三个目标。如同 C,Swift语言也仅有一个三元运算符,三元条件运算符( a ? b : c )。

受到运算符影响的值叫做操作数。在表达式 1 + 2 中, + 符号是一个二元运算符,其中的两个值 1 和 2 就是操作数。

赋值运算符

赋值运算符( a = b )可以初始化或者更新 a 为 b 的值:

1
2
3
let b = 10
var a = 4
a = b // a的值现在是10

如果赋值符号右侧是拥有多个值的元组,它的元素将会一次性地拆分成常量或者变量:

1
2
let (x, y) = (1, 2)
// x 等于 1, 同时 y 等于 2

与 Objective-C 和 C 不同,Swift 的赋值符号自身不会返回值。下面的语句是不合法的:

1
2
3
if x = y {
//这里不合法,因为x = y并不会返回任何值
}

这个特性避免了赋值符号 (=) 被意外地用于等于符号 (==) 的实际意图上。Swift 通过让 if x = y 非法来帮助你避免这类的错误在你的代码中出现。

算术运算符

Swift 对所有的数字类型支持四种标准算术运算符:
加 ( + )
减 ( - )
乘 ( * )
除 ( / )
与 C 和 Objective-C 中的算术运算符不同,Swift 算术运算符默认不允许值溢出。你可以选择使用 Swift 的溢出操作符(比如 a &+ b)来行使溢出行为。
加法运算符同时也支持 String 的拼接:“hello” + "world"

余数运算符

余数运算符( a % b )可以求出多少个 b 的倍数能够刚好放进 a 中并且返回剩下的值(就是我们所谓的余数)。
当 b为负数时它的正负号被忽略掉了。这意味着 a % b 与 a % -b 能够获得相同的答案。

1
2
3
4
9 % 4 // equals -1
-9 % 4 // equals -1
//9 % -4 和 9 % 4 相等

一元减号运算符

数字值的正负号可以用前缀 – 来切换,我们称之为 一元减号运算符:

1
2
3
let three = 3
let minusThree = -three // minusThree equals -3
let plusThree = -minusThree // plusThree equals 3, or "minus minus three"

一元加号运算符

一元加号运算符 ( + )直接返回它操作的值,不会对其进行任何的修改:
尽管一元加号运算符实际上什么也不做,你还是可以对正数使用它来让你的代码对一元减号运算符来说显得更加对称。感觉没什么用!!

1
2
let minusSix = -6
let alsoMinusSix = +minusSix // alsoMinusSix equals -6

组合赋值符号

如同 C ,Swift 提供了由赋值符号( = )和其他符号组成的 组合赋值符号 。一个加赋值符号的栗子 ( += ):

1
2
var c = 1
c += 2 // c is now equal to 3

比较运算符

Swift 支持所有 C 的标准比较运算符:

  • 相等 ( a == b )
  • 不相等 ( a != b )
  • 大于 ( a > b )
  • 小于 ( a < b )
  • 大于等于 ( a >= b )
  • 小于等于( a <= b )

Swift 同时也提供两个等价运算符(=== 和 !== ),你可以使用它们来判断两个对象的引用是否相同。

比较运算符通常被用在条件语句当中,比如说 if 语句:

1
2
3
4
5
6
let name = "world"
if name == "world" {
print("Hello, world")
} else {
print("I`m sorry \(name), but I don`t recongnize you")
}

你同样可以比较拥有同样数量值的元组,只要元组中的每个值都是可比较的。比如说, Int 和 String 都可以用来比较大小,也就是说 (Int,String) 类型的元组就可以比较。一般来说, Bool 不能比较,这意味着包含布尔值的元组不能用来比较大小。

元组以从左到右的顺序比较大小,一次一个值,直到找到两个不相等的值为止。如果所有的值都是相等的,那么就认为元组本身是相等的。比如说:

1
2
3
(1, "zebra") < (2, "apple") // true because 1 is less than 2
(3, "apple") < (3, "bird") // true because 3 is equal to 3, and "apple" is less than "bird"
(4, "dog") == (4, "dog") // true because 4 is equal to 4, and "dog" is equal to "dog"

注意:
Swift 标准库包含的元组比较运算符仅支持小于七个元素的元组。要比较拥有七个或者更多元素的元组,你必须自己实现比较运算符。

三元条件运算符

三元条件运算符是一种有三部分的特殊运算,它看起来是这样的: question ? answer1 : answer2。这是一种基于 question 是真还是假来选择两个表达式之一的便捷写法。如果 question 是真,则会判断为 answer1 并且返回它的值;否则,它判断为 answer2 并且返回它的值。
举个简单例子:

1
2
3
4
5
if question {
answer1
} else {
answer2
}

合并空值运算符

合并空值运算符 ( a ?? b)如果可选项 a 有值则展开,如果没有值,是 nil ,则返回默认值 b。表达式 a 必须是一个可选类型。表达式 b 必须与 a 的储存类型相同。

合并空值运算符是下边代码的缩写:a != nil ? a! : b

上边的代码中,三元条件运算符强制展开( a! )储存在 a 中的值,如果 a 不是 nil 的话,否则就返回b 的值。合并空值运算符提供了更加优雅的方式来封装这个条件选择和展开操作,让它更加简洁易读。

区间运算符

Swift 包含了两个 区间运算符 ,他们是表示一个范围的值的便捷方式。

闭区间运算符

闭区间运算符( a...b )定义了从 a 到 b 的一组范围,并且包含 a 和 b 。 a 的值不能大于 b 。

遍历你需要用到的所有数字时,使用闭区间运算符是个不错的选择,比如说在 for-in 循环当中:

1
2
3
4
5
6
7
8
for index in 1...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

半开区间运算符

半开区间运算符( a..<b )定义了从 a 到 b 但不包括 b 的区间,即 半开 ,因为它只包含起始值但并不包含结束值。(注:其实就是左闭右开区间。)如同闭区间运算符, a 的值也不能大于 b ,如果a 与 b 的值相等,那返回的区间将会是空的。

半开区间在遍历基于零开始序列比如说数组的时候非常有用,它从零开始遍历到数组长度(但是不包含):

1
2
3
4
5
6
7
8
9
let names = ["Anna", "Alex", "Brian", "Jack"]
let count = names.count
for i in 0..<count {
print("Person \(i + 1) is called \(names[i])")
}
// Person 1 is called Anna
// Person 2 is called Alex
// Person 3 is called Brian
// Person 4 is called Jack

单侧区间

闭区间有另外一种形式来让区间朝一个方向尽可能的远——比如说,一个包含数组所有元素的区间,从索引 2 到数组的结束。在这种情况下,你可以省略区间运算符一侧的值。因为运算符只有一侧有值,所以这种区间叫做单侧区间。比如说:

1
2
3
4
5
6
7
8
9
10
for name in names[2...] {
print(name)
//Brian
//Jack
}
for name in names[1...2] {
print(name)
//Alex
//Brian
}

半开区间运算符同样可以有单侧形式,只需要写它最终的值。和你两侧都包含值一样,最终的值不是区间的一部分。举例来说:

1
2
3
4
5
for name in names[..<2] {
print(name)
//Anna
//Alex
}

单侧区间也可以 在其他上下文中使用 ,不仅仅是下标。你不能遍历省略了第一个值的单侧区间,因为遍历根本不知道该从哪里开始。你可以遍历省略了最终值的单侧区间;总之,由于区间无限连续,你要确保给循环添加一个显式的条件。你同样可以检测单侧区间是否包含特定的值,就如下面的代码所述。

1
2
3
4
let range = ...5
range.contains(9) //false
range.contains(4) //true
range.contains(-1)//true

逻辑运算符

逻辑运算符可以修改或者合并布尔逻辑值 true 和 false 。Swift 支持三种其他基于 C 的语言也包含的标准逻辑运算符

  • 逻辑 非(!a)
  • 逻辑 与(a && b)
  • 逻辑 或(a || b)

逻辑非运算符

逻辑非运算符( !a )会转换布尔值,把 true 变成 false , 把 false 变成 true

逻辑与运算符

逻辑与运算符( a && b )需要逻辑表达式的两个值都为 true ,整个表达式的值才为 true 。

如果任意一个值是 false ,那么整个表达式的结果会是 false 。事实上,如果第一个值是 false ,那么第二个值就会被忽略掉了,因为它已经无法让整个表达式再成为 true 。这就是所谓的 短路计算 。

逻辑或运算符

逻辑或运算符( a || b )是一个中缀运算符,它由两个相邻的管道字符组成。你可以使用它来创建两个值之间只要有一个为 true 那么整个表达式就是 true 的逻辑表达式。

如同上文中的逻辑与运算符,逻辑或运算符也使用短路计算来判断表达式。如果逻辑或运算符左侧的表达式为 true , 那么右侧则不予考虑了,因为它不会影响到整个逻辑表达式的结果。
你可以组合多个逻辑运算符来创建一个更长的组合表达式:

混合逻辑运算

你可以组合多个逻辑运算符来创建一个更长的组合表达式:

1
2
3
4
5
if enteredDoorCode && passedRetinaScan || hasDoorKey || knowsOverridePassword {
print("Welcome!")
} else {
print("ACCESS DENIED")
}

这个栗子使用了多个 && 和 || 运算符来创建组合表达式。不过, && 和 || 仍旧只能够操作两个值,它实际上是三个更小的表达式链接而成。这个栗子可以读作:

如果我们输入了正确的密码并通过了视网膜扫描,或者如果我们有合法的钥匙或者我们知道紧急超驰密码,就允许进入。

注意:
Swift 语言中逻辑运算符 && 和 ||是左相关的,这意味着多个逻辑运算符组合的表达式会首先计算最左边的子表达式。

显式括号

很多时候虽然不被要求,但使用括号还是很有用的,这能让复杂的表达式更容易阅读。在上文当中的门禁栗子里,把前边部分的表达式用圆括号括起来就会让整个组合表达式的意图更加明显:

1
2
3
4
5
6
if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword {
print("Welcome!")
} else {
print("ACCESS DENIED")
}
// prints "Welcome!"

圆括号把前边的两个值单独作为一部分来考虑,这样使整个表达式的意图清晰明显。组合表达式的输出并没有改变,但是整个意图变得清晰易读。 可读性永远是第一位的 ;当需要的时候,使用圆括号让你的意图更加明确

摘自:swift 官网
所有代码在Xcode9 Swift4 环境编译没问题,代码戳这里 https://github.com/keshiim/LearnSwift4

Swift:基础内容

发表于 2017-10-26 | 分类于 学习 , Swift | | 阅读次数

前序

Swift 为所有 C 和 Objective-C 的类型提供了自己的版本,包括整型值的 Int ,浮点数值的 Double 和 Float ,布尔量值的 Bool ,字符串值的 String 。如同集合类型中描述的那样, Swift 同样也为三个主要的集合类型提供了更高效的版本, Array , Set 和 Dictionary 。

和 C 一样,Swift 用变量存储和调用值,通过变量名来做区分。Swift 中也大量采用了值不可变的变量。它们就是所谓的常量,但是它们比 C 中的常量更加给力。当你所处理的值不需要更改时,使用常量会让你的代码更加安全、简洁地表达你的意图。

除了我们熟悉的类型以外,Swift 还增加了 Objective-C 中没有的类型,比如元组。元组允许你来创建和传递一组数据。你可以利用元组在一个函数中以单个复合值的形式返回多个值。

Swift 还增加了可选项,用来处理没有值的情况。可选项意味着要么“这里有一个值,它等于 x”要么“这里根本没有值”。可选项类似于 Objective-C 中的 nil 指针,但是不只是类,可选项也可以用在所有的类型上。可选项比 Objective-C 中的 nil 指针更安全、更易读,他也是 Swift 语言中许多重要功能的核心。

可选项充分证明了 Swift 是一门类型安全的语言。Swift 帮助你明确代码可以操作值的类型。如果你的一段代码预期得到一个 String ,类型会安全地阻止你不小心传入 Int 。在开发过程中,这个限制能帮助你在开发过程中更早地发现并修复错误。

常量和变量

常量和变量是把名字和一个特定类型的值关联起来。常量:值一旦设置好不能更改,然而,变量:可以在将来被设置为不同的值

声明常量和变量

常量和变量必须在使用前被声明,使用关键字 let 来声明常量,使用关键字 var 来声明变量。举一个如何利用常量和变量记录用户登录次数的栗子:

1
2
let maximumNumberOfLoginAttempts = 10
var currentLoginAttempt = 0

这段代码可以读作:
“声明一个叫做 maximumNumberOfLoginAttempts 的新常量,并设置值为 10 。然后声明一个叫做 currentLoginAttempt 的新变量, 并且给他一个初始值 0。”

你可以在一行中声明多个变量或常量,用逗号分隔:

1
var x = 0.0, y = 0.0, z = 0.0 //一行声明

注意:在你的代码中,如果存储的值不会改变,请用 let 关键字将之声明为一个常量。只有储存会改变的值时才使用变量。

类型标注

你可以在声明一个变量或常量的时候提供类型标注,来明确变量或常量能够储存值的类型。添加类型标注的方法是在变量或常量的名字后边加一个冒号,再跟一个空格,最后加上要使用的类型名称。
下面声明一个叫welcomeMessage的变量,并明确指定变量存储String类型的值。

1
var welcomeMessage: String //明确指定String类型的变量welcomeMessage

现在这个 welcomeMessage 变量就可以被设置到任何字符串中而不会报错了:

1
welcomeMessage = "Hello"

你可以在一行中定义多个相关的变量为相同的类型,用逗号分隔,只要在最后的变量名字后边加上类型标注。

1
var red, green, blue: Double

命名常量和变量

常量和变量的名字几乎可以使用任何字符,甚至包括 Unicode 字符:

1
2
3
let π = 3.141592653
let 你好 = "你好啊swift"
let 🐂🐶 = "cowdog"

常量和变量的名字不能包含空白字符、数学符号、箭头、保留的(或者无效的)Unicode 码位、连线和制表符。也不能以数字开头,尽管数字几乎可以使用在名字其他的任何地方。

一旦你声明了一个确定类型的常量或者变量,就不能使用相同的名字再次进行声明,也不能让它改存其他类型的值。常量和变量之间也不能互换。
如果你需要使用 Swift 保留的关键字来给常量或变量命名,可以使用反引号( ` )包围它来作为名称。总之,除非别无选择,避免使用关键字作为名字除非你确实别无选择。

1
2
let `let` = "let"
print(`let`)

你可以把现有变量的值更改为其他相同类型的值。在这个栗子中 friendlyWelcome 的值从 “Hello!” 改变为 “Bonjour!”。不同于变量,常量的值一旦设定则不能再被改变。尝试这么做将会在你代码编译时导致报错:

1
2
3
4
5
var friendlyWelcome = "Hello!"
friendlyWelcome = "Bonjour!" // friendlyWelcome 现在是 "Bonjour!"
let languageName = "Swift"
languageName = "Swift++" // this is a compile-time error - languageName cannot be changed

输出常量和变量

你可以使用 print(_:separator:terminator:) 函数来打印当前常量和变量中的值。

1
print(friendlyWelcome) // 输出 “Bonjour!”

Swift 使用字符串插值 的方式来把常量名或者变量名当做占位符加入到更长的字符串中,然后让 Swift 用常量或变量的当前值替换这些占位符。将常量或变量名放入圆括号中并在括号前使用反斜杠将其转义:

1
2
print("The current value of friendlyWelcome is \(friendlyWelcome)")
// 输出 "The current value of friendlyWelcome is Bonjour!"

注释

Swift注释和其他语言形式一样,略了不谈

分号

和许多其他的语言不同,Swift 并不要求你在每一句代码结尾写分号( ; ),当然如果你想写的话也没问题。总之,如果你想在一行里写多句代码,分号还是需要的。

1
2
let cat = "🐱"; print(cat)
// 输出 "🐱"

整数

数就是没有小数部分的数字,比如 42 和 -23 。整数可以是有符号(正,零或者负),或者无符号(正数或零)。

Swift 提供了 8,16,32 和 64 位编码的有符号和无符号整数,这些整数类型的命名方式和 C 相似,例如 8 位无符号整数的类型是 UInt8 ,32 位有符号整数的类型是 Int32 。与 Swift 中的其他类型相同,这些整数类型也用开头大写命名法。

整数范围

你可以通过 min 和 max 属性来访问每个整数类型的最小值和最大值:

1
2
let minValue = UInt8.min //0
let maxValue = UInt8.max //255

Int

在大多数情况下,你不需要在你的代码中为整数设置一个特定的长度。Swift 提供了一个额外的整数类型: Int ,它拥有与当前平台的原生字相同的长度。

  • 在32位平台上, Int 的长度和 Int32 相同。
  • 在64位平台上, Int 的长度和 Int64 相同。

除非你需操作特定长度的整数,否则请尽量在代码中使用 Int 作为你的整数的值类型。这样能提高代码的统一性和兼容性,即使在 32 位的平台上, Int 也可以存 -2,147,483,648 到 2,147,483,647 之间的任意值,对于大多数整数区间来说完全够用了。

UInt

Swift 也提供了一种无符号的整数类型, UInt ,它和当前平台的原生字长度相同。

  • 在32位平台上, UInt 长度和 UInt32 长度相同。
  • 在64位平台上, UInt 长度和 UInt64 长度相同。

只在的确需要存储一个和当前平台原生字长度相同的无符号整数的时候才使用 UInt 。其他情况下,推荐使用 Int ,即使已经知道存储的值都是非负的。如同类型安全和类型推断中描述的那样,统一使用 Int 会提高代码的兼容性,同时可以避免不同数字类型之间的转换问题,也符合整数的类型推断。

浮点数

浮点数是有小数的数字,比如 3.14159 , 0.1 , 和 -273.15 。
浮点类型相比整数类型来说能表示更大范围的值,可以存储比 Int 类型更大或者更小的数字。Swift 提供了两种有符号的浮点数类型。

  • Double代表 64 位的浮点数。
  • Float 代表 32 位的浮点数。

注:Double 有至少 15 位数字的精度,而 Float 的精度只有 6 位。具体使用哪种浮点类型取决于你代码需要处理的值范围。在两种类型都可以的情况下,推荐使用 Double 类型。

类型安全和类型推断

Swift 是一门类型安全的语言。类型安全的语言可以让你清楚地知道代码可以处理的值的类型。如果你的一部分代码期望获得 String ,你就不能错误的传给它一个 Int 。

因为 Swift 是类型安全的,他在编译代码的时候会进行类型检查,任何不匹配的类型都会被标记为错误。这会帮助你在开发阶段更早的发现并修复错误。

当你操作不同类型的值时,类型检查能帮助你避免错误。当然,这并不意味着你得为每一个常量或变量声明一个特定的类型。如果你没有为所需要的值进行类型声明,Swift 会使用类型推断的功能推断出合适的类型。通过检查你给变量赋的值,类型推断能够在编译阶段自动的推断出值的类型。

因为有了类型推断,Swift 和 C 以及 Objective-C 相比,只需要少量的类型声明。其实常量和变量仍然需要明确的类型,但是大部分的声明工作 Swift 会帮你做。

在你为一个变量或常量设定一个初始值的时候,类型推断就显得更加有用。它通常在你声明一个变量或常量同时设置一个初始的字面量(文本)时就已经完成。(字面量就是会直接出现在你代码中的值,比如下边代码中的 42 和 3.14159。)

举个栗子,如果你给一个新的常量设定一个 42 的字面量,而且没有说它的类型是什么,Swift 会推断这个常量的类型是 Int ,因为你给这个常量初始化为一个看起来像是一个整数的数字。

1
2
let meaningOfLife = 42
// meaningOfLife is inferred to be of type Int

同样,如果你没有为一个浮点值的字面量设定类型,Swift 会推断你想创建一个 Double 。

1
2
let pi = 3.14159
// pi is inferred to be of type Double

Swift 在推断浮点值的时候始终会选择 Double (而不是 Float )。

1
2
let anotherPi = 3 + 0.14159
// anotherPi is also inferred to be of type Double

这字面量 3 没有显式的声明它的类型,但因为后边有一个浮点类型的字面量,所以这个类型就被推断为 Double。

数值型字面量

整数型字面量可以写作:

  • 一个十进制数,没有前缀
  • 一个二进制数,前缀是 0b
  • 一个八进制数,前缀是 0o
  • 一个十六进制数,前缀是 0x

例子:下面的这些所有整数字面量的十进制值都是 17

1
2
3
4
let decimalInteger = 17
let binaryInteger = 0b10001 // 17 in binary notation
let octalInteger = 0o21 // 17 in octal notation
let hexadecimalInteger = 0x11 // 17 in hexadecimal notation

浮点字面量可以是十进制(没有前缀)或者是十六进制(前缀是 0x )。小数点两边必须有至少一个十进制数字(或者是十六进制的数字)。十进制的浮点字面量还有一个可选的指数,用大写或小写的 e 表示;十六进制的浮点字面量必须有指数,用大写或小写的 p 来表示。

十进制数与 exp 的指数,结果就等于基数乘以 $10^{exp}$:

  • 1.25e2 意味着 1.25 x $10^2$, 或者 125.0 .
  • 1.25e-2 意味着 1.25 x $10^{-2}$, 或者 0.0125 .

下面的这些浮点字面量的值都是十进制的 12.1875 :

1
2
3
let decimalDouble = 12.1875
let exponentDouble = 1.21875e1
let hexadecimalDouble = 0xC.3p0 //小数部分的换算是:16 x 0.1875 = 3

数值型字面量也可以增加额外的格式使代码更加易读。整数和浮点数都可以添加额外的零或者添加下划线来增加代码的可读性。下面的这些格式都不会影响字面量的值。

1
2
3
let paddedDouble = 000123.456
let oneMillon = 1_000_000
let justOverOneMillion = 1_000_000.000_000_1

数值类型转换

通常来讲,即使我们知道代码中的整数变量和常量是非负的,我们也会使用 Int 类型。经常使用默认的整数类型可以确保你的整数常量和变量可以直接被复用并且符合整数字面量的类型推测。

只有在特殊情况下才会使用整数的其他类型,例如需要处理外部长度明确的数据或者为了优化性能、内存占用等其他必要情况。在这些情况下,使用指定长度的类型可以帮助你及时发现意外的值溢出和隐式记录正在使用数据的本质。

整数转换

不同整数的类型在变量和常量中存储的数字范围是不同的。 Int8 类型的常量或变量可以存储的数字范围是 -128~127,而 UInt8 类型的常量或者变量能存储的数字范围是 0~255 。如果数字超出了常量或者变量可存储的范围,编译的时候就会报错:

1
2
3
4
5
let cannotBeNegative: UInt8 = -1
// UInt8 cannot store negative numbers, and so this will report an error
let tooBig: Int8 = Int8.max + 1
// Int8 cannot store a number larger than its maximum value,
// and so this will also report an error

因为每个数值类型可存储的值的范围不同,你必须根据不同的情况进行数值类型的转换。这种选择性使用的方式可以避免隐式转换的错误并使你代码中的类型转换意图更加清晰。

例子:要将一种数字类型转换成另外一种类型,你需要用当前值来初始化一个期望的类型。在下面的栗子中,常量 twoThousand 的类型是 UInt16 ,而常量 one 的类型是 UInt8 。他们不能直接被相加在一起,因为他们的类型不同。所以,这里让 UInt16 (one ) 创建一个新的 UInt16 类型并用 one 的值初始化,这样就可以在原来的地方使用了。

1
2
3
let twoThousand: UInt16 = 2_000
let one: UInt8 = 1
let twoThousandAndOne = twoThousand + UInt16(one)

因为加号两边的类型现在都是 UInt16 ,所以现在是可以相加的。输出的常量( twoThousandAndOne )被推断为 UInt16 类型,因为他是两个 UInt16 类型的和。

SomeType(ofInitialValue) 是调用 Swift 类型初始化器并传入一个初始值的默认方法。在语言的内部, UInt16 有一个初始化器,可以接受一个 UInt8 类型的值,所以这个初始化器可以用现有的 UInt8来创建一个新的 UInt16 。这里需要注意的是并不能传入任意类型的值,只能传入 UInt16 内部有对应初始化器的值。不过你可以扩展现有的类型来让它可以接收其他类型的值(包括自定义类型)。

整数和浮点数转换

整数和浮点数类型的转换必须显式地指定类型:

1
2
3
4
5
6
7
let three = 3
let pointOneFourOneFiveNine = 0.14159
let pi = Double(three) + pointOneFourOneFiveNine
// pi equals 3.14159, and is inferred to be of type Double
let integerPi = Int(pi)
// integerPi equals 3, and is inferred to be of type Int

浮点转换为整数也必须显式地指定类型。一个整数类型可以用一个 Double 或者 Float 值初始化。
在用浮点数初始化一个新的整数类型的时候,数值会被截断。也就是说4.75 会变成 4 , -3.9会变为 -3。

结合数字常量和变量的规则与结合数字字面量的规则不同,字面量 3 可以直接和字面量0.14159 相加,因为数字字面量本身没有明确的类型。它们的类型只有在编译器需要计算的时候才会被推测出来。

类型别名

类型别名可以为已经存在的类型定义了一个新的可选名字。用 typealias 关键字定义类型别名。

当你根据上下文的语境想要给类型一个更有意义的名字的时候,类型别名会非常高效,例如处理外部资源中特定长度的数据时:

1
typealias AudioSample = UInt16

一旦为类型创建了一个别名,你就可以在任何使用原始名字的地方使用这个别名。

布尔值

Swift 有一个基础的布尔量类型,就是 Bool ,布尔量被作为逻辑值来引用,因为他的值只能是真或者假。Swift为布尔量提供了两个常量值, true 和 false 。

1
2
3
4
5
6
7
let orangesAreOrange = true
let turnipsAreDelicious = false
if trunipsAreDelicious {
print("Mmm, tasty turnips!")
} else {
print("Eww, turnips are horrible.")
}

Swift 的类型安全机制会阻止你用一个非布尔量的值替换掉 Bool 。下面的栗子中报告了一个发生在编译时的错误:

1
2
3
let i = 1
if i { // 编译报错
}

元组

元组把多个值合并成单一的复合型的值。元组内的值可以是任何类型,而且可以不必是同一类型。
在下面的示例中, (404, "Not Found") 是一个描述了 HTTP 状态代码 的元组。HTTP 状态代码是当你请求网页的时候 web 服务器返回的一个特殊值。当你请求不存在的网页时,就会返回 404 Not Found

1
2
let http404Error = (404, "Not Found")
// http404Error is of type (Int, String), and equals (404, "Not Found")

(404, "Not Found") 元组把一个 Int 和一个 String 组合起来表示 HTTP 状态代码的两种不同的值:数字和人类可读的描述。他可以被描述为“一个类型为 (Int, String) 的元组”

任何类型的排列都可以被用来创建一个元组,他可以包含任意多的类型。例如 (Int, Int, Int) 或者 (String, Bool) ,实际上,任何类型的组合都是可以的。

1
2
3
4
5
6
let (statusCode, statusMessage) = http404Error
print("The status code is \(statusCode)")
print("The status message is \(statusMessage)")
//另外一种方法就是利用从零开始的索引数字访问元组中的单独元素
print("The status code is \(http404Error.0)")
print("The status message is \(http404Error.1)")

当你分解元组的时候,如果只需要使用其中的一部分数据,不需要的数据可以用下滑线( _ )代替:

1
2
let (justTheStatusCode, _) = http404Error
print("The status code is \(justTheStatusCode)")

你可以在定义元组的时候给其中的单个元素命名:

1
2
3
let http200Status = (statusCode: 200, description: "OK")
print("The status code is \(http200Status.statusCode)")
print("The status description is \(http200Status.1)")

作为函数返回值时,元组非常有用。一个用来获取网页的函数可能会返回一个 (Int, String) 元组来描述是否获取成功。相比只能返回一个类型的值,元组能包含两个不同类型值,他可以让函数的返回信息更有用。更多内容请参考多返回值的函数。

可选项

可以利用可选项来处理值可能缺失的情况。可选项意味着:
· 这里有一个值,他等于x
或者
· 这里根本没有值

在 C 和 Objective-C 中,没有可选项的概念。在 Objective-C 中有一个近似的特性,一个方法可以返回一个对象或者返回 nil 。 nil 的意思是“缺少一个可用对象”。然而,他只能用在对象上,却不能作用在结构体,基础的 C 类型和枚举值上。对于这些类型,Objective-C 会返回一个特殊的值(例如 NSNotFound )来表示值的缺失。这种方法是建立在假设调用者知道这个特殊的值并记得去检查他。然而,Swift 中的可选项就可以让你知道任何类型的值的缺失,他并不需要一个特殊的值。

下面的栗子演示了可选项如何作用于值的缺失,Swift 的 Int 类型中有一个初始化器,可以将 String 值转换为一个 Int 值。然而并不是所有的字符串都可以转换成整数。字符串 “123” 可以被转换为数字值 123 ,但是字符串 "hello, world" 就显然不能转换为一个数字值。

1
2
let possibleNumber = "123"
let convertedNumber = Int(possibleNumber) // convertedNumber is inferred to be of type "Int?", or "optional Int"

因为这个初始化器可能会失败,所以他会返回一个可选的 Int ,而不是 Int 。可选的 Int 写做 Int?,而不是 Int 。问号明确了它储存的值是一个可选项,意思就是说它可能包含某些 Int 值,或者可能根本不包含值。(他不能包含其他的值,例如 Bool 值或者 String 值。它要么是 Int 要么什么都没有。)

nil

你可以通过给可选变量赋值一个nil来将之设置为没有值:

1
2
var serverResponseCode: Int? = 404
serverResponseCode = nil

注意:nil 不能用于非可选的常量或者变量,如果你的代码中变量或常量需要作用于特定条件下的值缺失,可以给他声明为相应类型的可选项。

如果你定义的可选变量没有提供一个默认值,变量会被自动设置成 nil 。

1
2
var surveyAnswer: String?
// surveyAnswer is automatically set to nil

注意:Swift 中的 nil 和Objective-C 中的 nil 不同,在 Objective-C 中 nil 是一个指向不存在对象的指针。在 Swift中, nil 不是指针,他是值缺失的一种特殊类型,任何类型的可选项都可以设置成 nil 而不仅仅是对象类型。

If 语句以及强制展开

你可以利用 if 语句通过比较 nil 来判断一个可选中是否包含值。利用相等运算符 ( == )和不等运算符( != )。
如果一个可选有值,他就“不等于” nil,一旦你确定可选中包含值,你可以在可选的名字后面加一个感叹号 ( ! ) 来获取值,感叹号的意思就是说“我知道这个可选项里边有值,展开吧。”这就是所谓的可选值的强制展开。

1
2
3
4
5
if convertedNumber != nil {
print("convertedNumber contains some integer value.")
print("convertedNumber has an integer value of \(convertedNumber!)")
}
// prints "convertedNumber contains some integer value."

注意:
使用 ! 来获取一个不存在的可选值会导致运行错误,在使用!强制展开之前必须确保可选项中包含一个非 nil 的值。

可选项绑定

可以使用可选项绑定来判断可选项是否包含值,如果包含就把值赋给一个临时的常量或者变量。可选绑定可以与 if 和 while 的语句使用来检查可选项内部的值,并赋值给一个变量或常量。

在 if 语句中,这样书写可选绑定:

1
2
3
4
5
6
if let actualNumber = Int(possibleNumber) {
print("\'\(possibleNumber)\' has an integer value of \(actualNumber)")
} else {
print("\'\(possibleNumber)\' could not be converted to an integer")
}
// prints '123' has an integr value of 123

如果转换成功,常量 actualNumber 就可以用在 if 语句的第一个分支中,他早已被可选内部的值进行了初始化,所以这时就没有必要用 ! 后缀来获取里边的值。在这个栗子中 actualNumber 被用来输出转换后的值。

常量和变量都可以使用可选项绑定,如果你想操作 if 语句中第一个分支的 actualNumber 的值,你可以写 if var actualNumber 来代替,可选项内部包含的值就会被设置为一个变量而不是常量。

你可以在同一个 if 语句中包含多可选项绑定,用逗号分隔即可。如果任一可选绑定结果是 nil 或者布尔值**为 false ,那么整个 if 判断会被看作 false 。下面的两个 if 语句是等价的:**

1
2
3
4
5
6
7
8
9
10
11
if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
print("\(firstNumber) < \(secondNumber) < 100")//prints "4 < 42 < 100"
}
if let firstNumber = Int("4") {
if let secondNumber = Int("42") {
if firstNumber < secondNumber && secondNumber < 100 {
print("\(firstNumber) < \(secondNumber) < 100")//prints "4 < 42 < 100"
}
}
}

注意:
使用 if 语句创建的常量和变量只在if语句的函数体内有效。相反,在 guard 语句中创建的常量和变量在 guard 语句后的代码中也可用。

隐式展开可选项

可选项明确了常量或者变量可以“没有值”。可选项可以通过 if 语句来判断是否有值,如果有值的话可以通过可选项绑定来获取里边的值。

有时在一些程序结构中可选项一旦被设定值之后,就会一直拥有值。在这种情况下,就可以去掉检查的需求,也不必每次访问的时候都进行展开,因为它可以安全的确认每次访问的时候都有一个值。

这种类型的可选项被定义为隐式展开可选项。通过在声明的类型后边添加一个叹号( String! )而非问号( String? ) 来书写隐式展开可选项。在可选项被定义的时候就能立即确认其中有值的情况下,隐式展开可选项非常有用,隐式展开可选项主要被用在 Swift 类的初始化过程中。

隐式展开可选项是后台场景中通用的可选项, 但是同样可以像非可选值那样来使用, 每次访问的时候都不需要展开。下面的栗子展示了在访问被明确为 String 的可选项展开值时,可选字符串和隐式展开可选字符串的行为区别:

1
2
3
4
5
let possibleString: String? = "An optional string." //可选项
let forcedString: String = possibleString!
let assumedString: String! = "An implicitly unwrapped optional string." //隐式可选项
let implicitString: String = assumedString

你可以把隐式展开可选项当做在每次访问它的时候被给予了 自动进行展开的权限 ,你可以在声明可选项的时候添加一个叹号而不是每次调用的时候在可选项后边添加一个叹号。

注意:如果你在隐式展开可选项没有值的时候还尝试获取值,会导致运行错误。结果和在没有值的普通可选项后面加一个叹号一样。string!

你可以像对待普通可选一样对待隐式展开可选项来检查里边是否包含一个值,你也可以使用隐式展开可选项通过可选项绑定在一句话中检查和展开值:

1
2
3
4
5
6
if assumedString != nil {
print(assumedString)
}
if let definiteString = assumedString {
print(definiteString)
}

不要在一个变量将来会变为 nil 的情况下使用隐式展开可选项。如果你需要检查一个变量在生存期内是否会变为 nil ,就使用普通的可选项。

错误处理

在程序执行阶段,你可以使用错误处理机制来为错误状况负责。相比于可选项的通过值是否缺失来判断程序的执行正确与否, 而错误处理机制能允许你判断错误的形成原因 ,在必要的情况下,还能将你的代码中的错误传递到程序的其他地方。

当一个函数遇到错误情况,他会抛出一个错误,这个函数的访问者会捕捉到这个错误,并作出合适的反应。

1
2
3
func canThrowAnError() throws {
// this function may or may not throw an error
}

通过在函数声明过程当中加入 throws 关键字来表明这个函数会抛出一个错误。当你调用了一个可以抛出错误的函数时,需要在表达式前预置 try 关键字。
Swift 会自动将错误传递到它们的生效范围之外,直到它们被 catch 分句处理。

1
2
3
4
5
6
do {
try canThrowAnError()
// no error was thrown
} catch {
//an error was thrown
}

do 语句创建了一个新的容器范围,可以让错误被传递到到不止一个的 catch 分句里。
下面的栗子演示了如何利用错误处理机制处理不同的错误情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func makeASandwich() throws {
// ...
}
do {
try makeASandwich()
eatASandwich()
} catch Error.OutOfCleanDishes {
washDishes()
} catch Error.MissingIngredients(let ingredients) {
buyGroceries(ingredients)
}
/*
在上面的栗子中,在没有干净的盘子或者缺少原料的情况下,方法 makeASandwich()
就会抛出一个错误。由于 makeASandwich() 的抛出,
方法的调用被包裹在了一个 try 的表达式中。通过将方法的调用包裹在 do 语句中,
任何抛出来的错误都会被传递到预先提供的 catch 分句中。
如果没有错误抛出,方法 eatASandwich() 就会被调用,
如果有错误抛出且满足 Error.OutOfCleanDishes 这个条件,
方法 washDishes() 就会被执行。如果一个错误被抛出,
而它又满足 Error.MissingIngredients 的条件,那么 buyGroceries(_:)
就会协同被 catch 模式捕获的 [String] 值一起调用。
*/

断言和先决条件

断言和先决条件用来检测运行时发生的事情。你可以使用它们来保证在执行后续代码前某必要条件是满足的。如果布尔条件在断言或先决条件中计算为 true ,代码就正常继续执行。如果条件计算为 false ,那么程序当前的状态就是非法的;代码执行结束,然后你的 app 终止。

断言和先决条件的不同之处在于他们什么时候做检查:断言只在 debug 构建的时候检查,但先决条件则在 debug 和生产构建中生效。在生产构建中,断言中的条件不会被计算。这就是说你可以在开发的过程当中随便使用断言而无需担心影响生产性能。

使用断言进行调试

断言会在运行的时候检查一个逻辑条件是否为 true 。顾名思义,断言可以“断言”一个条件是否为真。你可以使用断言确保在运行其他代码之前必要的条件已经被满足。如果条件判断为 true,代码运行会继续进行;如果条件判断为 false,代码运行结束,你的应用也就中止了。

如果你的代码在调试环境下触发了一个断言,例如你在 Xcode 中创建并运行一个应用,你可以明确的知道不可用的状态发生在什么地方,还能检查断言被触发时你的应用的状态。另外,断言还允许你附加一条调试的信息:

1
2
3
4
let age = -3
assert(age >= 0, "A person`s age cannot be less than zero.")
//also like this
assert(age >= 0)

在这个例子当中,代码执行只要在 if age >= 0 评定为 true 时才会继续,就是说,如果 age 的值非负。如果 age 的值是负数,在上文的代码当中, age >= 0 评定为 false ,断言就会被触发,终止应用。

如果代码已经检查了条件,你可以使用 assertionFailure(_:file:line:) 函数来标明断言失败,比如

1
2
3
4
5
6
7
if age > 10 {
print("You can ride the roller-coaster or the ferris wheel.")
} else if age > 0 {
print("You can ride the ferris wheel.")
} else {
assertionFailure("A person`s age can`t be less than zero.")
}

强制先决条件

在你代码中任何条件可能潜在为假但必须肯定为真才能继续执行的地方使用先决条件。比如说,使用先决条件来检测下标没有越界,或者检测函数是否收到了一个合法的值。

你可以通过调用 precondition(_:_:file:line:) 函数来写先决条件。给这个函数传入表达式计算为 true 或 false ,如果条件的结果是 false 信息就会显示出来。比如说:

1
2
// In the implementation of a subscript...
precondition(index > 0, "Index must be greater than zero.")

注意:
如果你在不检查模式编译( -Ounchecked ),先决条件不会检查。编译器假定先决条件永远为真,并且它根据你的代码进行优化。总之, fatalError(_:file:line:) 函数一定会终止执行,无论你优化设定如何。

你可以在草拟和早期开发过程中使用 fatalError(_:file:line:)函数标记那些还没实现的功能,通过使用 fatalError("Unimplemented")来作为代替。由于致命错误永远不会被优化,不同于断言和先决条件,你可以确定执行遇到这些临时占位永远会停止。

摘自:swift 官网
所有代码在Xcode9 Swift4 环境编译没问题,代码戳这里 https://github.com/keshiim/LearnSwift4

Swift:初探

发表于 2017-10-20 | 分类于 学习 , Swift | | 阅读次数

自从swift流行起来之后,一直没有认真学习过Swift,还是杀下心来按照Swift AppleDoc系统学习一次。依照传统,先用Swift在屏幕上打印“Hellow, world!”

1
print("Hello, world!")

如果你曾使用 C 或者 Objective-C 写代码,那么 Swift 的语法不会让你感到陌生——在 Swift 语言当中,这一行代码就是一个完整的程序!你不需要为每一个功能导入单独的库比如输入输出和字符串处理功能。写在全局范围的代码已被用来作为程序的入口,所以你不再需要 main()函数。同样,你也不再需要在每句代码后边写分号。

简单值

使用 let来声明一个常量,用 var来声明一个变量。常量的值在编译时并不要求已知,但是你必须为其赋值一次。这意味着你可以使用常量来给一个值命名,然后一次定义多次使用。

不需要总是显式地写出类型,在声明一个常量或者变量的时候直接给它们赋值就可以让编译器推断它们的类型。如果初始值并不能提供足够的信息(或者根本没有提供初始值),就需要在变量的后边写出来了,用冒号分隔。

1
2
3
let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70

值绝对不会隐式地转换为其他类型。如果你需要将一个值转换为不同的类型,需要使用对应的类型显示地声明。

1
2
3
let label = "The width is "
let width = 94
let widthLabel = label + String(width)

其实还有一种更简单的方法来把值加入字符串:将值写在圆括号里,然后再在圆括号的前边写一个反斜杠 ( \) ,举个栗子:

1
2
3
4
let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."

使用方括号( [])来创建数组或者字典,并且使用方括号来按照序号或者键访问它们的元素。

1
2
3
4
5
6
7
8
var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[1] = "bottle of water"
var occupations = [
"Malcolm": "Captain",
"Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"

数组和字典可以有两种初始化方式分别如下:

1
2
3
4
5
6
7
//方式一 - 显示声明
let emptyArray = [String]()
let emptyDictionary = [String: Float]()
//方式二 - 隐式
shoppingList = []
occupations = [:]

控制流

使用 if和 switch来做逻辑判断,使用 for-in, for, while,以及 repeat-while来做循环,不在强制使用圆括号,但仍旧需要使用花括号来括住代码块。

1
2
3
4
5
6
7
8
9
10
let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
if score > 50 {
teamScore += 3
} else {
teamScore += 1
}
}
print(teamScore)

if语句当中,条件必须是布尔表达式,注意和Objective-C的区别。
当然要达到Objective-C的if可判断布尔表达式和真假值,你可以一起使用 if和 let来操作那些可能会丢失的值。这些值使用可选项表示。可选的值包括了一个值或者一个 nil来表示值不存在。在一个值的类型后边使用问号( ?)来把某个值标记为可选的。

1
2
3
4
5
if let v = optionValue {
//使用v
} else {
//v 为 nil
}

如果可选项的值为 nil,则条件为 false并且花括号里的代码将会被跳过。否则,可选项的值就会被展开且赋给 let后边声明的常量,这样会让展开的值对花括号内的代码可用。

高级用法:

1
2
3
4
5
6
7
guard let name = json["name"] as? String,
let coordinatesJSON = json["coordinates"] as? [String: Double],
let latitude = coordinatesJSON["lat"],
let longitude = coordinatesJSON["lng"],
let mealsJSON = json["meals"] as? [String] else {
return nil
}

另一种处理可选值的方法是使用 ?? 运算符提供默认值。如果可选值丢失,默认值就会使用。

1
2
let nickName: String? = nil
let informalGreeting = "Hi \(nickName ?? "You")"

Switch 选择语句支持任意类型的数据和各种类型的比较操作——它不再限制于整型和测试相等上。在执行完 switch 语句里匹配到的 case 之后,程序就会从 switch 语句中退出。执行并不会继续跳到下一个 case 里,所以完全没有必要显式地在每一个 case 后都标记 break 。

1
2
3
4
5
6
7
8
9
10
11
12
13
var vegetable: String = "red pepper"
switch vegetable {
case "celery":
print("Add come raisions and make ants on a log.")
case "cucumber", "watercress":
print("That would make a good tea sandwich.")
case let y where y.hasPrefix("red") :
print("\(y)<<<")
case let x where x.hasSuffix("pepper"):
print("Is it a spicy \(x)?")
default:
print("Everything tastes good in soup.")
}

注意 let 可以用在模式里来指定匹配的值到一个常量当中。

你可以使用 for-in来遍历字典中的项目,这需要提供一对变量名来储存键值对。字典使用无序集合,所以键值的遍历也是无序的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let interestingNumbers = [
"Prime": [2, 3, 5, 1, 2],
"Fibonacci": [1, 1, 2, 3, 5, 8],
"Square": [1, 4, 9, 16, 25],
]
var largest = 0
for (key, numbers) in interestingNumbers {
for number in numbers {
largest = number > largest ? number : largest
// if number > largest {
// largest = number
// }
}
}
print("largest = \(largest)")

使用 while来重复代码快直到条件改变。循环的条件可以放在末尾,这样可以保证循环至少运行了一次。

1
2
3
4
5
6
7
8
9
10
var n = 2
while n < 100 {
n = n * 2
}
print(n)
repeat {
m = m * 2
} while m < 100
print(m)

使用 ..<来创建一个不包含最大值的区间,使用 … 来创造一个包含最大值和最小值的区间。

1
2
3
4
5
var total = 0
for i in 0..<4 {
total += i
}
print(total)

函数和闭包

使用 func来声明一个函数。通过在名字之后在圆括号内添加一系列参数来调用这个方法。使用 ->来分隔形式参数名字类型和函数返回的类型。
默认情况下,函数使用他们的形式参数名来作为实际参数标签。在形式参数前可以写自定义的实际参数标签,或者使用 _ 来避免使用实际参数标签。

1
2
3
4
func greet(_ person: String, on day: String) -> String {
return "Hello \(person), today is \(day)."
}
greet(person: "keshiim", on: "Thursday")

使用元组来创建复合值——比如,为了从函数中返回多个值。元组中的元素可以通过名字或者数字调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
var min = scores[0]
var max = scores[0]
var sum = 0
for score in scores {
if score > max {
max = score
} else if score < min {
min = score
}
sum += score
}
return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])
print(statistics.sum)
print(statistics.2)

注意:返回值元素取成员可直接用成员名称,也可以用以元素成员从下标为0顺序的取值例如statistics.2

函数同样可以接受多个参数,然后把它们存放进数组当中。

1
2
3
4
5
6
7
8
9
func sumOf(numbers: Int...) -> Int {
var sum = 0
for number in numbers {
sum += number
}
return sum
}
sumOf()
sumOf(numbers: 42, 597, 12)

函数可以内嵌。内嵌的函数可以访问外部函数里的变量。你可以通过使用内嵌函数来组织代码,以避免某个函数太长或者太过复杂。

1
2
3
4
5
6
7
8
9
func returnFifteen() -> Int {
var y = 10
func add() {
y += 5
}
add()
return y
}
returnFifteen()

同时,函数是一等类型,这意味着函数可以把函数作为值来返回。

1
2
3
4
5
6
7
8
func makeincrementer() -> (Int) -> Int {
func addOne(number: Int) -> Int {
return 1 + number
}
return addOne
}
let increment = makeincrementer()
increment(7)

函数也可以把另外一个函数作为其自身的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
func hasAnyMatchs(list: [Int], condition: (Int) -> Bool) -> Bool {
for item in list {
if condition(item) {
return true
}
}
return false
}
func lessThanTen(number: Int) -> Bool {
return number < 10
}
var numbers = [20, 19, 7, 12];
hasAnyMatchs(list: numbers, condition: lessThanTen)

闭包:函数其实就是闭包的一种特殊形式:一段可以被随后调用的代码块。闭包中的代码可以访问其生效范围内的变量和函数,就算是闭包在它声明的范围之外被执行——你已经在内嵌函数的栗子上感受过了。你可以使用花括号({ })括起一个没有名字的闭包。在闭包中使用 in来分隔实际参数和返回类型。

1
2
3
4
5
6
//例子中将[Int]类型数组中每个值x2转成Sting类型,返回数组
var numbers = [20, 19, 7, 12];
print(numbers.map { (number: Int) -> String in
let result = 2 * number
return String(result)
})

你有更多的选择来把闭包写的更加简洁。当一个闭包的类型已经可知,比如说某个委托的回调,你可以去掉它的参数类型,它的返回类型,或者都去掉。

1
2
let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)

你可以调用参数通过数字而非名字——这个特性在非常简短的闭包当中尤其有用。当一个闭包作为函数最后一个参数出入时,可以直接跟在圆括号后边。如果闭包是函数的唯一参数,你可以去掉圆括号直接写闭包。

1
2
let sortedNumbers = numbers.sorted { $0 > $1 }
print(sortedNumbers)

对象和类

通过在class后接类名称来创建一个类。在类里边声明属性与声明常量或者变量的方法是相同的,唯一的区别的它们在类环境下。同样的,方法和函数的声明也是相同的写法。

1
2
3
4
5
6
class Shape {
var numberOfSides = 0
func simpleDescription() -> String {
return "A Shape with \(numberOfSides) sides."
}
}

通过在类名字后边添加一对圆括号来创建一个类的实例。使用点语法来访问实例里的属性和方法。

1
2
3
var shape = Shape()
shape.numberOfSides = 4
shape.simpleDescription()

为Shape类的添加一个重要的东西:一个用在创建实例的时候来设置类的初始化器。使用 init来创建一个初始化器。

1
2
3
4
5
6
7
8
9
10
11
12
class NamedShape {
var numberOfSides: Int = 0
var name: String
init(name: String) {
self.name = name
}
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}

注意使用 self来区分 name属性还是初始化器里的 name参数。创建类实例的时候给初始化器传参就好像是调用方法一样。每一个属性都需要赋值——要么在声明的时候(比如说 numberOfSides),要么就要在初始化器里赋值(比如说 name),使用 deinit来创建一个反初始化器,如果你需要在释放对象之前执行一些清理工作的话。

子类的方法如果要重写父类的实现,则需要使用 override——不使用 override关键字来标记则会导致编译器报错。编译器同样也会检测使用 override的方法是否存在于父类当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Square: NamedShape {
var sideLength: Double
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 4
}
func area() -> Double {
return sideLength * sideLength
}
override func simpleDescription() -> String {
return "A square with sides of length \(sideLength)."
}
}
let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()

除了存储属性,你也可以拥有带有 getter 和 setter 的计算属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class equilateralTriangle: NamedShape {
var sideLength: Double = 0.0
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 3
}
var perimeter: Double {
get {
return 3.0 * sideLength
}
set {
sideLength = newValue / 3.0
}
}
override func simpleDescription() -> String {
return "An equilateral triangle with sides of length \(sideLength)."
}
}
var triangle = equilateralTriangle(sideLength: 3.1, name: "a triangle")
print(triangle.perimeter)
triangle.perimeter = 9.9
print(triangle.sideLength)

在 perimeter的 setter 中,新值被隐式地命名为 newValue。你可以提供一个显式的名字放在 set 后边的圆括号里。

设置一个新值的前后执行代码,使用 willSet和 didSet。比如说,下面的类确保三角形的边长始终和正方形的边长相同.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TriangleAndSquare {
var triangle: EquilateralTriangle {
willSet {
square.sideLength = newValue.sideLength
}
}
var square: Square {
willSet {
triangle.sideLength = newValue.sideLength
}
}
init(size: Double, name: String) {
square = Square(sideLength: size, name: name)
triangle = EquilateralTriangle(sideLength: size, name: name)
}
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
print(triangleAndSquare.square.sideLength)
print(triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 50, name: "large square")
print(triangleAndSquare.triangle.sideLength)

当你操作可选项的值的时候,你可以在可选值后边使用 ?比如方法,属性和下标脚本。如果 ?前的值是 nil,那 ?后的所有内容都会被忽略并且整个表达式的值都是 nil。否则,可选项的值将被展开,然后 ?后边的代码根据展开的值执行。在这两种情况当中,表达式的值是一个可选的值。

1
2
let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength

枚举和结构体

使用 enum来创建枚举,类似于类和其他所有的命名类型,枚举也能够包含方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum Rank: Int {
case ace = 1
case two, three, four, five, six, seven, eight, nine, ten
case jack, queen, king
func simpleDescription() -> String {
switch self {
case .ace:
return "ace"
case .jack:
return "jack"
case .queen:
return "queen"
case .king:
return "king"
default:
return String(self.rawValue)
}
}
}
let ace = Rank.ace
let aceRowValue = ace.rawValue

默认情况下,Swift 从零开始给原始值赋值后边递增,但你可以通过指定特定的值来改变这一行为。在上边的栗子当中,原始值的枚举类型是 Int,所以你只需要确定第一个原始值。剩下的原始值是按照顺序指定的。你同样可以使用字符串或者浮点数作为枚举的原始值。使用 rawValue 属性来访问枚举成员的原始值。

使用 init?(rawValue:) 初始化器来从一个原始值创建枚举的实例。

1
2
3
4
if let convertRank = Rank(rawValue: 1) {
let aceDescription = convertRank.simpleDescription()
print(aceDescription)
}

枚举成员的值是实际的值,不是原始值的另一种写法。事实上,在这种情况下没有一个有意义的原始值,你根本没有必要提供一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
enum Suite {
case spades, hearts, diamonds, clubs
func simpleDescription() -> String {
switch self {
case .spades:
return "spades"
case .hearts:
return "hearts"
case .diamonds:
return "diamonds"
case .clubs:
return "clubs"
}
}
func color() -> String {
switch self {
case .spades, .clubs:
return "black"
case .hearts, .diamonds:
return "red"
}
}
}
let hearts = Suite.hearts
hearts.simpleDescription()
hearts.color()

注意有两种方法可以调用枚举的 hearts成员:当给 hearts指定一个常量时,枚举成员 Suit.Hearts会被以全名的方式调用因为常量并没有显式地指定类型。在 Switch 语句当中,枚举成员可以通过缩写的方式 .hearts被调用,因为 self已经明确了是 suit。总之你可以在任何值的类型已经明确的场景下使用使用缩写。

1
2
let heartss: Suite = .hearts
heartss.simpleDescription()

enum高阶用法:如果枚举拥有原始值,这些值在声明时确定,就是说每一个这个枚举的实例都将拥有相同的原始值。另一个选择是让case与值关联——这些值在你初始化实例的时候确定,这样它们就可以在每个实例中不同了。比如说,考虑在服务器上请求日出和日落时间的case,服务器要么返回请求的信息,要么返回错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum ServerResponse {
case result(String, String)
case failure(String)
func simpleDescription() -> String {
switch self {
case let .result(sunrise, sunset):
return "Sunrise is at \(sunrise) and sunset is at\(sunset)"
case let .failure(message):
return "Failure:.. \(message)"
}
}
}
let success = ServerResponse.result("6:0 am", "8:0 pm")
let failure = ServerResponse.failure("Out of cheese.")
success.simpleDescription()
failure.simpleDescription()

注意现在日出和日落时间是从 ServerResponse 值中以switch case 匹配的形式取出的

使用 struct来创建结构体。结构体提供很多类似与类的行为,包括方法和初始化器。其中最重要的一点区别就是结构体总是会在传递的时候拷贝其自身(传值),而类则会传递引用。

1
2
3
4
5
6
7
8
9
struct Card {
var rank: Rank
var suit: Suite
func simpleDecription() -> String {
return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
}
}
let threeOfSpades = Card(rank: .three, suit: .spades)
let threeOfSpadesDescription = threeOfSpades.simpleDecription()

协议和扩展

使用 protocol来声明协议。类,枚举以及结构体都兼容协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//protocol
protocol ExampleProtocol {
var simpleDescription: String { get }
mutating func adjust()
}
//class
class SimpleClass: ExampleProtocol {
var simpleDescription: String = "A very simle class."
var anotherProperty: Int = 6999
func adjust() {
simpleDescription += " Now 100% adjueted."
}
}
var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription
//Struct
struct SimpleStruct: ExampleProtocol {
var simpleDescription: String = "A very simple struct."
mutating func adjust() {
simpleDescription += " (adjusted)."
}
}
let b = SimpleStruct()
b.adjust()
let bDescription = b.simpleDescription
//Enum
enum SimpleEnum: ExampleProtocol {
case test(String)
var simpleDescription: String {
get {
switch self {
case let .test(text):
return text
default:
return "A very simiple Enum."
}
}
}
func adjust() {
print(" Now (adjusted).")
}
}
var c = SimpleEnum.test("hello")
c.simpleDescription
c.adjust()

注意使用 mutating关键字来声明在 SimpleStructure中使方法可以修改结构体。在 SimpleClass中则不需要这样声明,因为类里的方法总是可以修改其自身属性的。

Extension: 使用 extension来给现存的类型增加功能,比如说新的方法和计算属性。你可以使用扩展来使协议到别处定义类型,或者你导入的其他库或框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extension Int: ExampleProtocol {
var simpleDescription: String {
get {
return "The number \(self)"
}
}
mutating func adjust() {
self += 42
}
}
print(7.simpleDescription)
extension Double {
var absoluteValue: Double {
return fabs(self)
}
}
print((-4.2).absoluteValue)

你可以使用协议名称就像其他命名类型一样——比如说,创建一个拥有不同类型但是都遵循同一个协议的对象的集合。当你操作类型是协议类型的值的时候,协议外定义的方法是不可用的。

1
2
let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)

尽管变量 protocolValue有 SimpleClass的运行时类型,但编译器还是把它看做 ExampleProtocol。这意味着你不能访问类在这个协议中扩展的方法或者属性

错误处理

你可以用任何遵循 Error 协议的类型来表示错误。使用 throw 来抛出一个错误并且用 throws 来标记一个可以抛出错误的函数。如果你在函数里抛出一个错误,函数会立即返回并且调用函数的代码会处理错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum PrinterError: Error {
case outOfPaper
case noToner
case onFire
}
hrow 来抛出一个错误并且用 throws 来标记一个可以抛出错误的函数。如果你在函数里抛出一个错误,函数会立即返回并且调用函数的代码会处理错误。
func send(job: Int, toPrinter printerName: String) throws -> String {
if printerName == "Never Has Toner" {
throw PrinterError.noToner
}
return "Job sent"
}
func send(job: Int, toPrinter printerName: String) throws -> String {
if printerName == "Never Has Toner" {
throw PrinterError.noToner
}
return "Job sent"
}

有好几种方法来处理错误。一种是使用 do-catch 。
处理方式1:在 do 代码块里,你用 try 来在能抛出错误的函数前标记。在 catch 代码块,错误会自动赋予名字 error ,如果你不给定其他名字的话。

1
2
3
4
5
6
do {
let printerResponse = try send(job: 1040, toPrinter: "Bei jing")//or "Never Has Toner"
print(printerResponse) //Bei jing
} catch {
print(error) //PrinterError.noToner
}

处理方式2:你可以提供多个 catch 代码块来处理特定的错误。你可以在 catch 后写一个模式,用法和 switch 语句里的 case 一样。

1
2
3
4
5
6
7
8
9
10
11
//catch 模式匹配
do {
let printerResponse = try send(job: 1110, toPrinter: "Gutenberg")
print(printerResponse)
} catch PrinterError.onFire {
print("I`ll just put this over here, with the rest of the fire.")
} catch let printerError as PrinterError {
print("Printer error: \(printerError).")
} catch {
print(error)
}

处理方式3:另一种处理错误的方法是使用 try? 来转换结果为可选项。如果函数抛出了错误,那么错误被忽略并且结果为 nil 。否则,结果是一个包含了函数返回值的可选项。

1
2
let printerSuccess = try? send(job: 1882, toPrinter: "Mergenthaler") //Job sent
let printerFailure = try? send(job: 1002, toPrinter: "Never Has Toner") //nil

defer: 使用 defer 来写在函数返回之前最后要执行的代码块,无论是否错误被抛出。你甚至可以在没有错误处理的时候使用 defer ,来简化需要在多处地方返回的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]
func fridgeContains(_ food: String) -> Bool {
fridgeIsOpen = true //1
defer {
fridgeIsOpen = false //3
}
let result = fridgeContent.contains(food) //2
return result //4
}
fridgeContains("banana")
print(fridgeIsOpen) // false

泛型

把名字写在尖括号里来创建一个泛型方法或者类型。
泛型方法:

1
2
3
4
5
6
7
8
func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
var result = [Item]()
for _ in 0..<numberOfTimes {
result.append(item)
}
return result
}
makeArray(repeating: "knock", numberOfTimes: 4)

泛型枚举、结构体创建泛型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum OptionalValue<Wrapped> {
case none
case some(Wrapped)
func simpleDescription() -> Any {
switch self {
case .none:
return self
case let .some(val):
return val
}
}
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger.simpleDescription()
possibleInteger = .some(100)

where:在类型名称后紧接 where来明确一系列需求——比如说,来要求类型实现一个协议,要求两个类型必须相同,或者要求类必须继承自特定的父类。写<T: Equatable>和 <T where T: Equatable>是同一回事

1
2
3
4
5
6
7
8
9
10
func anyCommonElements<T: Sequence, U:Sequence>(_ lhs: T, _ rhs: U) -> Bool where T.Iterator.Element: Equatable, T.Iterator.Element == U.Iterator.Element {
for lhsItem in lhs {
for rhsItem in rhs {
if lhsItem == rhsItem {
return true
}
}
}
return false
}

摘自:swift 官网
所有代码在Xcode9 Swift4 环境编译没问题,代码戳这里 https://github.com/keshiim/LearnSwift4

iOS 获取当前正在显示的ViewController

发表于 2017-09-12 | 分类于 iOS | | 阅读次数

iOS 获取当前正在显示的ViewController,方法有如下几种


1. 从UIWindow中获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#import "UIWindow+SHHelper.h"
@implementation UIWindow (SHHelper)
- (UIViewController*)sh_topMostController
{
// getting rootViewController
UIViewController *topController = [self rootViewController];
// Getting topMost ViewController
while ([topController presentedViewController]) topController = [topController presentedViewController];
// Returning topMost ViewController
return topController;
}
- (UIViewController*)sh_currentViewController;
{
UIViewController *currentViewController = [self sh_topMostController];
while ([currentViewController isKindOfClass:[UINavigationController class]] && [(UINavigationController*)currentViewController topViewController])
currentViewController = [(UINavigationController*)currentViewController topViewController];
return currentViewController;
}
@end

2. 从UIView里面获取

1
2
3
4
5
6
7
8
9
10
11
//满足一个日常的需求:在UITableviewcell里面的UIView模块里面,调用self.navigationcontroller pushviewcontroller推入一个新的viewcontroller,需要获取其上层的UIViewcontroller, 可以使用下面的方法:
- (UIViewController *)sh_viewController
{
UIResponder *responder = self;
while ((responder = [responder nextResponder])){
if ([responder isKindOfClass: [UIViewController class]]){
return (UIViewController *)responder;
}
}
return nil;
}

3. 从UIViewController中获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#import "UIViewController+SHHelper.h"
@implementation UIViewController (SHHelper)
- (UIViewController*)sh_topMostController
{
UIViewController *topController = self ;
while ([self presentedViewController])
topController = [topController presentedViewController];
return topController;
}
- (UIViewController*)sh_currentViewController;
{
UIViewController *currentViewController = [self sh_topMostController];
while ([currentViewController isKindOfClass:[UINavigationController class]] && [(UINavigationController*)currentViewController topViewController])
currentViewController = [(UINavigationController*)currentViewController topViewController];
return currentViewController;
}
//我们在非视图类中想要随时展示一个view时,需要将被展示的view加到当前view的子视图,或用当前view presentViewController,或pushViewContrller,这些操作都需要获取当前正在显示的ViewController。
//获取当前view的UIViewController
+ (UIViewController *)sh_currentViewControllerFromcurrentView{
UIViewController *result = nil;
// 1. get current window
UIWindow * window = [[UIApplication sharedApplication] keyWindow];
if (window.windowLevel != UIWindowLevelNormal) {
NSArray *windows = [[UIApplication sharedApplication] windows];
for(UIWindow * tempWindow in windows) {
if (tempWindow.windowLevel == UIWindowLevelNormal) {
window = tempWindow;
break;
}
}
}
// 2. get current View Controller
UIView *frontView = [[window subviews] objectAtIndex:0];
id nextResponder = [frontView nextResponder];
if ([nextResponder isKindOfClass:[UIViewController class]]) {
result = nextResponder;
} else {
result = window.rootViewController;
}
return result;
}
//获取当前屏幕中present出来的viewcontroller。
- (UIViewController *)getPresentedViewController
{
UIViewController *appRootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
UIViewController *topVC = appRootVC;
if (topVC.presentedViewController) {
topVC = topVC.presentedViewController;
}
return topVC;
}
@end
1…345
Zheng keshiim

Zheng keshiim

一个☝程序猿的播种天地

45 日志
7 分类
16 标签
RSS
GitHub Twitter Weibo 知乎
© 2017 - 2018 Zheng keshiim
由 Hexo 强力驱动
主题 - NexT.Mist