任何命令式高级编程语言都有控制流语句。控制流分为两大类——分支和循环。
在Swift编程语言中,将分支语句称作为条件语句(Conditional Statements)。无论是条件语句还是循环语句,这些控制流语句中的条件表达式都必须是布尔类型(Bool)。
do语句块
Swift编程语言中可使用 do 语句块作为类似C语言中的一般语句块进行使用。与C语言的语句块类似,Swift中的 do 语句块单独作为一个作用域,该语句块中声明的局部对象在其外部将无法被访问。如果在 do 语句块中声明了对象,该对象标识符与其外部已有的对象标识符相同,那么在该语句块内部所访问的标识符都是它内部声明的。也就是说,语句块内声明的对象会将语句块外部的同名对象给覆盖掉。
另外与C语言的语句块类似的是,Swift中的 do 语句块也允许嵌套。当然,嵌套的语句块能够访问其外部声明的局部对象,但是也是会将外部同名对象给覆盖掉。而外部语句块作用域则无法访问嵌套语句块中声明的局部对象。下面我们来看一些简单的例子:
let a = 1, b = 2
do {
let a = 10
let b = 20
let c = 5
do {
let a = 30
// 这里可以访问其外部语句块中声明的局部对象
let b = 40 + c
// 这里输出:inner a + b = 75
print("inner a + b = \(a + b)")
}
// 这里输出:a + b = 30
print("a + b = \(a + b)")
}
// 这里输出:global a + b = 3
print("global a + b = \(a + b)")
Swift中如果不用 do 引出,而单单用花括号 { } ,那么它默认表达的是一个闭包表达式,而不是一个语句块。
if 语句
Swift编程语言中,if语句的表达跟其他编程语言差不多,用 if 关键字引出条件表达式,然后后面再跟条件成立时的语句块。其中,条件表达式可以用圆括号包起来,也可以不加圆括号。这里各位要注意的是,任何条件语句块的花括号 { } 不能缺省,即便 { } 中只包含一条语句。这同样也应用于后面将介绍的选择语句以及所有循环语句。下面举一些简单例子:
var a = 0, b = 1
// 这里表示如果a的值等于0,
// 那么执行{ }里的语句
if a == 0 {
print("a is equal to zero!")
}
// 这里的条件语句使用圆括号围了起来,
// 当然,这里不加圆括号也是可以的
if (a + b < 10) {
print("a + b is less than ten!")
}
// 下面的 { }中的语句不会执行,
// 因为a > 1的结果为假(false)
if a > 1 {
print("a is greater than one!")
}
如果我们要表达当 if 语句的条件不成立时则执行其它语句的逻辑,那么我们可以使用 else 语句块。else 语句块必须紧跟在 if 语句块的下面,而不能单独使用。另外, else 后面也可以再跟 if 语句,表示在上一句条件不成立的情况下再判断当前条件是否成立。我们下面看一些例子
var a = 1, b = 2
// 这里的a + b < 1显然不成立,
// 执行else语句块
if a + b < 1 {
print("less than one!")
}
else {
print("greater than one!")
}
// 下面这组条件语句中将执行中间的else if语句块
if a + b < 1 {
print("less than one!")
}
else if a * b > 1 {
print("greater than one!")
}
else {
print("other conditions")
}
三目条件操作符
Swift编程语言中的三目条件操作符与C语言中的差不多,形式为:condition ? expr1 : expr2。这里,condition 是一个布尔表达式,如果它的值为真,则执行表达式 expr1,否则执行表达式 expr2。
正由于这里的整个 condition ? expr1 : expr2 就是一个表达式,所以它的表达比if-else语句要灵活得多。我们看以下例子:
var a = 1, b = 2
// 我们这里判断a是否大于0,
// 如果啊大于0,则执行表达式a + b,
// 否则执行表达式a - b。
// 这里我们可以看到,
// 应该执行的表达式为:a + b。
// 所以就相当于:a = a + b
a = a > 0 ? a + b : a - b
// 这里判定b是否大于0,
// 如果大于0就执行:a += b + 1,
// 否则执行:a += a - 1
a += b > 0 ? b + 1 : a - 1
// 这里需要为表达式a += b和b += a添加上圆括号,
// 因为 += 操作符的优先级低于 ? : 操作符
a + b < 0 ? (a += b) : (b += a)
print("a = \(a), b = \(b)")
// 我们下面用三目条件表达式来实现一个if语句的功能:
a + b > 10 ? (_ = (a += 1, b += 1)) : ()
// 我们这里充分利用了Swift中灵活的空元组特性!
// 这里,(_ = (a += 1, b += 1))表达式相当于:
// _ = ((), ())
// 因为a += 1与b += 1的返回类型都是Void,
// 相当于空元组。
// 所以,整个(a += 1, b += 1)就是一个包含2个空元组的元组,
// 即:((), ())。
// 而最后一个()就是一个显式的空元组,
// 表示不执行任何命令
print("a = \(a), b = \(b)")
// 上述代码等同于:
if a + b > 10 {
a += 1
b += 1
}
这里各位要注意的是,? 要与表达式condition 之间要至少有一个空格分隔,否则 ?会被当作[optional-chaining操作符]。此外,: 也最好与 expr1 和 expr2 之间留有空格。
switch 语句
switch语句在C语言中又称为选择语句,它由关键字 switch 引出,后面跟一个表达式。Swift编程语言中神奇的是,switch 后面的表达式可以是 Void 类型。switch 语句块中由一个或多个 case 标签构成。一般 case 后面跟着的是与 switch 后的表达式的类型兼容的表达式。Swift编程语言中,一般情况下在 switch 语句块中需要包含一个 default 标签,以处理在当前 switch 语句块中没有被所有 case 覆盖到的情况。当然,如果 switch 后面的表达式的类型是 Void、Bool 或枚举等包含有限个数的值的类型,且这些值在 switch 语句块中被所有 case 覆盖到,那么 default 标签可省。
在Swift编程语言中,switch 后面的表达式可以是表示任何类型的表达式,甚至还可以是一个元类型。但 case 后面的表达式必须是表示值类型的表达式,这就意味着通常来说,我们用 switch 语句的时候,往往判定的是一个表示结构体或枚举类型的表达式。我们看以下例子:
class Test {
var a = 0, b = 100
}
let t = Test()
let q = Test()
// 这里使用了我们后面会描述的值绑定,
// 这是合法的
switch t {
case let obj:
print("value = \(obj.a + t.b)")
}
switch type(of: t) {
case let type:
print("type is: \(type)")
}
// 但以下的case语句都是非法的,
// 两者都会引发编译错误
switch t {
// 编译错误:类型Test的表达式模式无法匹配类型Test的值
case q:
print("q")
}
switch type(of: t) {
// 编译错误:Test.Type的表达式模式无法匹配Test.Type的值
case type(of: q):
print("type")
}
此外,所有 case 包括 default 情况下,都至少需要包含一条语句。如果我们在某些情况下想跳出当前 case 的执行,则可以使用 break 语句。当我们在某条 case 中使用了 break 语句,那么执行将会跳出整个 switch 语句块。我们下面来看一些例子:
// 这里直接声明一个类型为Void的常量c
let c: Void = ()
// 这里不需要default情况,
// 因为空元组就一种“值”
switch c {
case ():
print("void")
}
// 这里声明一个布尔类型变量b
var b = true
// 这里不需要default情况,
// 因为表示布尔的就true和false两种值,
// 并且以下switch语句块內全都覆盖到了
switch b {
case true:
// 由于b的值为true,
// 所以将执行这条打印语句
print("true")
case false:
print("false")
}
// 声明变量a为Int8类型
var a: Int8 = 10
// 这里需要加上default情况
switch a {
case 0:
// 这里表示如果b为true,
// 那么直接跳出整个switch语句块,
// 而不会执行打印语句
if b {
break
}
print("zero")
case 1:
// 由于每条case和default都必须含有至少一条语句,
// 如果我们对于当前情况不需要做任何动作,
// 那么可以放以下这条空语句
_ = ()
case 10:
print("ten")
default:
// 对于不需要处理的情况,
// 我们也可以直接放break语句跳出整个switch语句块
break
}
我们从上面代码中可以看到,一旦 switch 后面的表达式与其语句块内的某一 case 对应的值匹配,那么就执行该 case 中的语句。一条 case 或 default 标签下面可以含有多条语句,这就好比冒号后面跟着的是一个 do 语句块。而且Swift编程语言跟C语言不同的是,Swift允许在 case 与 default 后面的作用域内声明局部对象,而C语言则不行。在C语言中必须使用语句块做显式隔离。就从这一点我们可以看到,Swift中的 case 与 default 后面的作用域就已经好比是一个局部的语句块作用域了。此外,Swift跟C语言还有一点不同的是,默认情况下,一条 case 执行完成之后就自动退出整个 switch 语句块了,不需要显式添加 break 语句。如果要直通到下一条 case ,那么需要使用 fallthrough 语句。我们下面来看一个简单的例子:
var a = 10
switch a {
case 0:
print("zero")
case 10:
print("ten!")
a += 5
print("a = \(a)")
// Swift中,case后面的表达式允许存在相同的值,
// 只不过一旦前一个case匹配到了值之后就会先执行
case 10:
// 上一条case就好比:
do {
print("ten!")
a += 5
print("a = \(a)")
break
}
default:
break
}
我们已经知道,Swift编程语言中每条 case 下面必须至少要有一条语句,所以以下这种情况是错误的:
switch 0 {
// 这里会发出编译错误:
// switch中的一个'case'标签应该至少需要一条可执行的语句
case 0:
case 1:
print("zero")
default:
break
}
然而,Swift编程语言却提供了非常丰富的 case 匹配模式。Swift编程语言的 switch 后面的表达式类型不仅限于基本类型,只要是遵循 Equatable 协议的类型的对象都能作为有效的 switch 表达式,这一点比其他类C语言要宽松得多。下面举一个简单例子:
truct MyObject: Equatable {
var a = 0
var b = 1
public static func == (lhs: MyObject, rhs: MyObject) -> Bool {
return lhs.a == rhs.a && lhs.b == rhs.b
}
}
var obj = MyObject(a: 3, b: 4)
var obj2 = MyObject(a: 3, b: 3)
switch obj {
case MyObject():
print("dummy object")
case obj2:
print("obj2")
case MyObject(a: 3, b: 4):
print("Correct object")
default:
print("No match!")
}
我们这里还能看到,case 后面的表达式也不仅限于常量和字面量,变量也是合法的。所以综合来看,Swift中的 switch-case 语句相当灵活,很多情况下可以替代复杂而又长长的 if-else 语句。
case匹配模式
Swift编程语言中的 case 匹配非常丰富,除了前面介绍的常规匹配方式之外还有以下匹配模式:复合 case 匹配,区间匹配,值绑定匹配形式以及 where 从句引出的谓词逻辑匹配。
我们先看复合 case 匹配。我们在前面已经看到了,Swift编程语言中不能连续写两条 case 体,但是我们可以用复合 case 来列出能与当前 case 匹配的多个值。我们看以下例子。
switch a {
// 一条case语句可以包含多个表达式,
// 这也就被称为`复合case匹配`。
// 各个表达式之间直接用逗号分隔。
// 这些表达式中只要有一个能与switch表达式匹配上,
// 那么就执行该case标签下的语句
case 0, 1, 2:
print("zero")
// case后面的表达式不需要一定是常量和字面量,
// 也可以是变量。
// 这里哪怕用的是紧跟在switch之后的表达式也毫无问题
case a - 1, a, a + 1:
print("Matched")
// 我们这里还能对变量a进行修改
a += 1
print("a = \(a)")
default:
break
}
上述代码中,像 case 0, 1, 2: 这条 case 标签语句就类似于:
if a == 0 || a == 1 || a == 2 {}
然后我们再来看看 case 的区间匹配。 case 后面可以跟一个范围表达式来做区间匹配。此外,该范围表达式甚至可以直接是一个 Range、ClosedRange、CountableRange 或 CountableClosedRange 等类型的对象。如果用了 case 区间匹配(假定区间用的是 CountableRange 或 Range 类型的对象),那么这条 case 标签语句的作用就相当于:
if condition >= range.lowerBound && condition < range.upperBound {
}
当然,我们在使用区间匹配的时候也可以用逗号来分隔多个表达式以形成一条符合 case 匹配,并且各个表达式可以是一个范围表达式,也可以是其他类型的表达式。我们下面来看两个具体的例子:
var a: Int8 = 10
// 声明一个CountableRange<Int8>类型的对象
let range = Int8(1) ..< Int8(100)
switch a {
// 这里直接使用范围表达式进行匹配
case -5 ... 0:
print("zero and below!")
// 我们甚至可以直接用CountableRange<Int8>对象作为匹配表达式
case range:
print("one to ninety-nine!")
// 这里使用了复合case匹配,
// 由三个表达式构成。
// 前后两个都是范围表达式,
// 而中间一个表达式则为基本表达式(一个整数字面量)
case 100 ... 110, 112, 113 ... 120:
print("others")
// 这里使用了单边范围,这也是没问题的。
// 这里要注意的是,(-100)必须用圆括号包围起来,
// 因为它前面的 ..< 也是一个前缀操作符,
// 两者之间必须用圆括号进行分隔,
// 并且(-100)表达式也必须紧贴 ..< 操作符
case 123... , ..<(-100):
print("One sided range!")
// 对于整数类型,
// 即便上面的case通过范围表达式全都覆盖到了所有情况也必须添加default标签
default:
print("out of range")
}
let str = "abc"
// 这里声明了一个Range<String>类型的常量对象
let strRange = "bbb" ..< "ccc"
switch str {
case "a" ... "abb", "aba", "abd":
print("a to abb")
case "aa" ..< "cc", strRange, "ddd" ... "fff":
print("String matched!")
default:
print("Others")
}
我们这里还要注意的是,上述代码中的 case 100 ... 110, 112, 113 ... 127 这个表达式中,范围操作符的左右操作数都必须使用在[-128, 127]区间内的整数。因为 Int8 类型的对象作为范围操作符的操作数时也需要满足该类型的值范围约束。
Swift编程语言的 case 匹配有一个非常强大的功能——值绑定。在一般情况下,我们可以直接在 case 后面直接跟 let 或 var 声明一个局部对象,以捕获 switch 后面表达式的值。该对象只能在当前的 case 体中可见。如果我们在一般情况下使用了值绑定,那么我们就无需再写 default 处理了,因为值绑定的情况是直接获取 switch 表达式的值,这也就意味着已经覆盖了所有选择情况。我们先看以下简单的例子:
let a = 8
switch a {
case 0, 1, 2 ... 5:
print("normal")
// 这里在case后面直接声明了局部对象v,
// 用做值绑定
case let v:
print("v = \(v)")
// 这里不再需要default标签
}
switch a {
// 这里在case后面直接声明了一个变量v,
// 我们还能在这条case语句下修改变量v的值
case var v:
v += 1
print("v = \(v)")
// 这里会发出警告,
// 因为这里一旦用了值绑定,
// 那么该case必定会先执行,
// 所以后面的case体都会被忽略,
// 除非在执行的case体中使用了直通语句
case 100:
print("No reached!")
}
然而,值绑定模式用得更多的场合是在匹配元组对象的时候。
最后要提到的是Swift编程语言中 case 语句最最强大的匹配模式——基于值绑定基础上使用 where 从句。这里,where 后面直接跟一个布尔表达式,表示当该布尔表达式为真的时候则匹配当前的 case,这么一来,值绑定也无法覆盖到所有 case 情况了,所以需要添加 default 处理。
let a = 8
switch a {
// 这里在做值绑定的同时限制了一个范围区间
case let v where v > 0 && v < 3:
print("normal")
// 使用带有where从句的值绑定时,
// 可以出现多条值绑定的case,
// 并且也允许每条case声明的对象标识符完全相同
case let v where v >= 3 && v <= 10:
print("v = \(v)")
default:
print("Others")
}
这里对于值绑定的 case 表达式不能使用复合 case 匹配模式。不过值绑定的复合 case 匹配模式可用于匹配元组对象时使用。
case匹配模式用于元组对象
在Swift编程语言中,当元组对象作为 switch 语句的表达式时,则能看到其 case 匹配模式的强大魔力。由于元组本身仅仅是对三大基本类型对象的组装,所以匹配表达上能够具有非常丰富灵活的特征,case 匹配可以针对某一特定元素做各种丰富的约束。下面我们先看一个例子,这个例子展示了匹配元组对象时可采用的使用通配符匹配、复合 case 匹配以及区间匹配模式。
let tuple = (8, 9)
switch tuple {
// 这里使用了单一的元组对象进行匹配
case (0, 0):
print("case zero")
// 这里使用了最简单的复合case匹配模式
case (1, 2), (3, 4):
print("case one")
// 这里使用了通配符匹配结合复合case匹配模式。
// 通配符的位置默认就算匹配上了,
// 因此当前case就直接比较非通配符位置上的元组元素即可。
case (7, _), (_, 8):
print("case 2")
// 这里使用了区间匹配,
// 可以指定元组的每个元素的匹配范围
case (1 ... 3, 5 ..< 10), (_, 4 ... 9):
print("Matched case!")
default:
print("Others")
}
然后我们介绍一下元组对象的匹配模式中含有值绑定匹配的情况。对于元组对象的值绑定匹配有两种形式:一种是类似元组元素萃取的方式,另一种则是与上一节提到的值绑定类似的方式。这两种形式都能混合使用复合 case 匹配、区间匹配以及 where 从句。
当我们混合使用值绑定与复合 case 匹配时需要注意,对于 case 后面的所有子表达式而言,如果在其中一个子表达式中声明了某一对象,那么该对象标识符必须出现在当前 case 后面的每一个子表达式中,但摆放的位置可以任意挑选。这一点其实既是Swift编程语言灵活的地方,也是她严谨的地方。如果说,某一子表达式声明了一个对象,而在另一个表达式中没有该对象出现,这就意味着倘若匹配了那个没有对象出现的子表达式,那么此声明的对象是未被初始化的。
不过当前Swift编程语言的实现也有一个bug。如果我们用 let 声明了一个值绑定元组,而在它后面却可以再用 var 去声明另一个元组,而不会有任何编译警告。然而,值绑定的对象依然是常量,即便匹配了后面用 var 声明的那个元组表达式。我们看以下代码示例:
// 这里声明了一个元组常量对象tuple,
// 其类型为:(a: Int, b: Int, c: Int)
let tuple = (a: 8, b: 9, c: 10)
// 元组自身的标签不会对case匹配产生任何影响
switch tuple {
// 这里使用了类似元组元素萃取的值绑定匹配模式,
// 然后对第一个子表达式动用了where从句。
// 这里各位需要注意,第一个子表达式中出现了局部对象x,
// 那么后续所有表达式中都需要包含局部对象x,
// 但位置可以任意摆放
case (let x, 10, _) where x < 10,
(5, _, let x):
print("case one")
// 这里使用了元组对象声明方式的值绑定匹配模式,
// 然后这里还用了区间匹配模式。
// 这里各位需要注意的是,
// 即便第二个子表达式使用了var对元组局部对象进行声明,
// 但第二个子表达式的x和y仍然属于常量,
// 也就是说,所有子表达式中的对象是常量还是变量,
// 全都参照第一个子表达式的声明情况
case let (x, y, 7 ... 9),
var (0 ..< 5, x, y):
print("case two")
case var (x, 5, 6),
var (5, x, 6):
// 这里使用了var声明元组对象,
// 因此可以对其中的x进行修改
x += 10
print("x = \(x)")
// 这里也使用了元组对象声明方式的值匹配模式,
// 然后这里的两条子表达式都用了where从句
case let (x, _, y) where x > 8 && y < 11,
let (_, y, x) where x > 8 && y < 10:
print("Matched case")
// 由于这里匹配了第二个子表达式,
// 所以输出:x = 8, y = 10
print("x = \(x), y = \(y)")
default:
print("Others")
}
switch语句中的直通
switch 语句中的 case 的执行在完成之后会默认跳出当前整个 switch 语句块,这就好比编译器在每个 case 体的末尾都默认添加了 break 语句。不过Swift编程语言没有设计得那么僵硬,它提供了 fallthrough 语句可直接从当前的 case 体直通到下一个 case 体。
因为 fallthrough 语句的逻辑非常简单,我们直接看以下代码先体会一下。
let a = 10
switch a {
case 0:
print("zero")
case 9 ... 11:
print("ten")
fallthrough
case 8 ..< 12:
print("pass 1")
if a > 10 {
fallthrough
}
default:
print("Over!")
}
上述代码将直接会输出:
ten
pass 1
如果我们把常量a的值修改为11,那么程序输出将是:
ten
pass 1
Over!
也就是说,对于通过 fallthrough 语句而直通到达的 case 体,如果不再次通过 fallthrough 语句,那么也无法到达其下面的 case 体。所以对于每一个 case 体而言,如果想让当前的 case 直通到下一个 case,那么都需要通过执行 fallthrough。此外,如果当前 case 是最后一个情况,那么在该 case 体中就不能出现 fallthrough 语句了,否则编译器将直接报错。此外,如果当前 case 下面的 case 使用了值绑定匹配模式,那么当前 case 体中也不能使用 fallthrough 语句。
Swift编程语言与C语言不同,default 处理必须放在整个 switch 语句块中的最下面,而C语言中的 default 标签可以放在最前面,或是插在任意两个 case 之间。因为这两个编程语言处理 case 的方式是大相径庭的。
网友评论