首页 > 代码库 > 8.Swift教程翻译系列——控制流之条件

8.Swift教程翻译系列——控制流之条件

3.条件语句

经常会需要根据不同的情况来执行不同的代码。你可能想要在发生错误的时候执行一段额外的代码,或者当某个值变得太高或者太低的时候给他输出出来。要实现这些需求,你可以使用条件分支。

Swift提供两种方式来实现条件分支,也就是if语句和switch语句。一般来说If用在可能的情况比较少的简单条件中,当遇到复杂条件有很多种可能性的时候使用switch会更好,或者要根据模式匹配来判断要执行什么代码的时候switch也很有用。

if语句

if的最简单形式只有一个单独的if条件,只有当条件为true的时候才会执行if大括号中的代码:

var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
<span style="white-space:pre">	</span>println("It's very cold. Consider wearing a scarf.")
}
// prints "It's very cold. Consider wearing a scarf."
上面的例子检查温度是不是小于等于32度(水结冰的问题呢,不是摄氏度吧)。如果是,输出语句就会执行,如果不是就不会执行,接着执行If大括号后面的语句。

if语句还可以提供另外一个可选择的语句块,就是else分支,当if条件计算为false的时候将执行else的代码,下面是包含else的代码:

temperatureInFahrenheit = 40
if temperatureInFahrenheit <= 32 {
<span style="white-space:pre">	</span>println("It's very cold. Consider wearing a scarf.")
} else {
<span style="white-space:pre">	</span>println("It's not that cold. Wear a t-shirt.")
}
// prints "It's not that cold. Wear a t-shirt."

两个分支总有一种会执行。因为温度上升到40度了,已经比32度要高了,所以执行的是else分支。

你还可以将多个if链接起来,使用更多的分支:

temperatureInFahrenheit = 90
if temperatureInFahrenheit <= 32 {
<span style="white-space:pre">	</span>println("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
<span style="white-space:pre">	</span>println("It's really warm. Don't forget to wear sunscreen.")
} else {
<span style="white-space:pre">	</span>println("It's not that cold. Wear a t-shirt.")
}
// prints "It's really warm. Don't forget to wear sunscreen."
这里添加了一个if语句来响应特别热的温度。最后一个else还在,用来作为既不太热也不太冷的温度的响应。

不过最后一个else是可选的,如果说各种情况不用全部都处理:

temperatureInFahrenheit = 72
if temperatureInFahrenheit <= 32 {
<span style="white-space:pre">	</span>println("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
<span style="white-space:pre">	</span>println("It's really warm. Don't forget to wear sunscreen.")
}
这个例子中,温度既不会太冷而执行if,也不会太热而执行else if,所以什么都没有输出。

switch语句

switch语句是将某个值与若干个可能的情况进行匹配。然后执行第一个匹配上的情况对应的代码。switch语句为多种可能情况的if语句提供了另一种选择。

switch的最简单形式是某个简单类型的值跟几个同类型的值进行比较看是否相等:

switch some value to consider {
<span style="white-space:pre">	</span>case value 1 :
<span style="white-space:pre">		</span>respond to value 1
<span style="white-space:pre">	</span>case value 2 ,value 3 :
<span style="white-space:pre">		</span>respond to value 2 or 3
<span style="white-space:pre">	</span>default:
<span style="white-space:pre">		</span>otherwise, do something else
}
switch语句由多个可能匹配的case组成。除了比较特殊的值,swift还提供几种方式来给每种情况指定更复杂的匹配模式。这些内容在本章后面会介绍的。

switch的各个case都是独立的执行分支,switch语句决定哪条分支被执行。switch语句必须是完善的,也就是说所有要考虑的情况都必须跟一个case匹配。最好是给每一种可能的情况都提供一个case,你还可以定义一个默认的情况,如果前面所有情况都没匹配上就会执行这个默认的分支,默认分支使用default关键字,而且必须出来再所有的case之后。

下面例子使用switch语句来检查一个单独的小写字母someCharacter:

let someCharacter: Character = "e"
switch someCharacter {
<span style="white-space:pre">	</span>case "a", "e", "i", "o", "u":
<span style="white-space:pre">		</span>println("\(someCharacter) is a vowel")
<span style="white-space:pre">	</span>case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m", "n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
<span style="white-space:pre">		</span>println("\(someCharacter) is a consonant")
<span style="white-space:pre">	</span>default:
<span style="white-space:pre">		</span>println("\(someCharacter) is not a vowel or a consonant")
}
// prints "e is a vowel"
上面switch的第一个case匹配任意的元音字母,第二个case匹配任意的辅音字母。要写出其他全部的字母有点儿不太可能,所以最后使用了default来匹配除了元音和辅音字母以外的任意字符。加上这个default就保证了switch语句的完备性。

没有隐式顺序执行

与C或者OC里的switch对比,swift的switch语句默认不会再去一个个匹配已经匹配到的case后面的case,当程序匹配到一个case,执行case的代码块,switch就结束了,代码块末尾不需要使用break语句。这使得switch语句比C更安全简单,避免了因为忘记写break造成程序执行了好几个case。

NOTE 如果你觉得没有break看着别扭,你也可以加上。

每个case都必须包含至少一条语句,下面这样写就是错的,因为第一个case是空的:

let anotherCharacter: Character = "a"
switch anotherCharacter {
<span style="white-space:pre">	</span>case "a":
<span style="white-space:pre">	</span>case "A":
<span style="white-space:pre">		</span>println("The letter A")
<span style="white-space:pre">	</span>default:
<span style="white-space:pre">		</span>println("Not the letter A")
}
// this will report a compile-time error
这可跟C语言不一样了,switch语句不会同时匹配"a"和“A”,而是给case "a":报编译错误“does not contain any executeable statements(不包含任何可执行的语句)”。这样避免了不小心从一个case再执行到另一个case,使代码又安全又意图清晰。

如果要一个case匹配多种情况,就使用逗号分隔,而且很长的可以分行写:

switch some value to consider {
<span style="white-space:pre">	</span>case value 1 ,
<span style="white-space:pre">	</span>   value 2 :
<span style="white-space:pre">		</span>statements
}
NOTE 如果要让匹配命中后面的case顺序执行,可以使用fallthrough关键字,后面也会专门介绍。

范围匹配

switch的case匹配的值也可以使用范围。下面的例子使用数字范围来显示数字的自然语言形式

let count = 3_000_000_000_000
let countedThings = "stars in the Milky Way"
var naturalCount: String
switch count {
case 0:
    naturalCount = "no"
case 1...3:
    naturalCount = "a few"
case 4...9:
    naturalCount = "several"
case 10...99:
    naturalCount = "tens of"
case 100...999:
    naturalCount = "hundreds of"
case 1000...999_999:
    naturalCount = "thousands of"
default:
    naturalCount = "millions and millions of"
}
println("There are \(naturalCount) \(countedThings).")
// prints "There are millions and millions of stars in the Milky Way.”
元组

你可以在switch语句中使用元组来对多个值做判断。元组中的元素各自都能被单个值或者某个范围来比较。或者你也可以使用下划线来让他匹配任意值。

下面举例使用(Int, Int)类型的元组(x,y)来表示一个点,然后给点分类:

let somePoint = (1, 1)
switch somePoint {
case (0, 0):
    println("(0, 0) is at the origin")
case (_, 0):
    println("(\(somePoint.0), 0) is on the x-axis")
case (0, _):
    println("(0, \(somePoint.1)) is on the y-axis")
case (-2...2, -2...2):
    println("(\(somePoint.0), \(somePoint.1)) is inside the box")
default:
    println("(\(somePoint.0), \(somePoint.1)) is outside of the box")
}
// prints "(1, 1) is inside the box”

switch语句判断点是不是在原点(0,0),或者红色的x轴上,或者橘黄色的y轴上,或者是在蓝色区域内,或者是来蓝色区域以外。

和C不一样的是,Swift可以多个case使用重复的值,事实上(0,0)是可以匹配上面四个case的。但是就算多个case都能匹配,第一个被匹配的才会被执行。所以点(0,0)会匹配第一个(0,0)的情况,其他的就被忽略了。

值绑定

case可以将他匹配的值绑定到临时常量或者变量中,然后在case的代码块里使用。这就是值绑定,因为值只是在case代码块里被绑定给某个临时常量或者变量。

下面的例子跟上面差不多

let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
    println("on the x-axis with an x value of \(x)")
case (0, let y):
    println("on the y-axis with a y value of \(y)")
case let (x, y):
    println("somewhere else at (\(x), \(y))")
}
// prints "on the x-axis with an x value of 2”

switch语句判断点是否在x轴上,或者在y轴上,或者其他地方。

三个case生命了常量x和y,用来从元组anotherPoint中临时获取x或者y或者x和y的值。第一个case,caes (let x,0)匹配任何y坐标为0的点,并且把这个点得x坐标赋值给临时常量x,第二个case, case (0, let y)匹配所有x坐标为0的点,然后将这个点得y坐标赋值给临时常量y。

只要是临时常量被生命了,那在这个case的代码块里就可以使用这个常量。对于这个例子来说,他们被用来输出坐标的值。

注意这个switch是没有default的,因为最后一个case let(x, y)使用了两个占位符,那他就会匹配所有的点,所以也就不用再加default了,这个switch已经很完整了。

上面的例子中,x和y被定义成常量,那是因为我们在case里面没有必要去修改坐标的值。如果你想声明成变量就用var关键字,如果是这样的话,临时变量应该是已经被创建并且用合适的值初始化了得。任何对于该变量的修改都只能在这个case里面有效。

where

case还可以使用where语句来添加更多条件判断

下面的例子将对下图的点进行分类:

let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
    println("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
    println("(\(x), \(y)) is on the line x == -y")
case let (x, y):
    println("(\(x), \(y)) is just some arbitrary point")
}
// prints "(1, -1) is on the line x == -y”

switch判断点是否在绿色直线(x=y)上,或者在紫色直线(x = -y)上,或者其他地方。

三个case都声明了常量x和y,暂时存储了点得坐标值。这些常量也被用来作为case中where语句的一部分,来创建一个动态的过滤器。这个case只有当当前匹配的点满足where的表达式计算结果为true的时候才会被匹配。

和前一个例子一样,最后一个case会匹配剩下所有的点,所以这个switch也没有加default。


4.控制转移语句

控制转移语句可以将控制从一段代码转移到另一段代码,以此来改变代码的执行顺序。Swift有四种控制转移语句

continue

break

fallthrough

return

下面介绍前三种,最后一个return会在函数的章节里再介绍。

continue

continue语句告诉循环停止当前的循环,然后开始下一次循环。也就是说“本次循环任务已经执行完了”,而不会完全跳出循环。

NOTE 在for-condition-increment型的循环中,如果使用了continue语句,递增语句还是会被执行的。循环本身的顺序还是正常的,只是循环体被跳过了而已。

下面的例子将小写字符串中的元音字母和空格都去掉了,最后生成一个没什么含义的短语

let puzzleInput = "great minds think alike"
var puzzleOutput = ""
for character in puzzleInput {
    switch character {
    case "a", "e", "i", "o", "u", " ":
        continue
    default:
        puzzleOutput += character
    }
}
println(puzzleOutput)
// prints "grtmndsthnklk”
上面的代码只要碰到元音或者空格就使用continue,当前的循环就被终止,开始下一次循环了。这样可以让switch只匹配并忽略元音字母及空格,而不是要求代码块匹配所有需要输出的字符。

break

break语句终止当前的整个控制流。break可以用来switch或者循环当中来提前退出整个switch或者循环。

在循环中break

如果在循环语句中使用break,整个循环立马就被终止了,转移执行循环的大括号后面的代码。当前循环不会被执行了,下一次以及以后的循环也不会被执行了。

在switch中break

如果在switch中使用break,整个switch也立即被终止,开始执行switch以后的代码了。可以使用break来匹配并且忽略一种或多种情况。因为swift的switch是完备的,并且不允许空分支,有时为了故意匹配并且忽略某些情况,来使意图更明显。那么就把break作为想要忽略的case代码块的唯一一句代码。当那个case被匹配到了,整个switch语句立马就被终止了。

NOTE 如果case只有注释,编译器会报编译错误。注释不是语句,也不会是case被忽略。如果想让case被忽略掉就用break。

下面的例子通过switch来判断字符是否代表下面四种语言之一。为了简洁这边在一个case中匹配多个值:

let numberSymbol: Character = "三"  // Simplified Chinese for the number 3
var possibleIntegerValue: Int?
switch numberSymbol {
case "1", "?", "一", "?":
    possibleIntegerValue = http://www.mamicode.com/1>上面的例子检查numberSymbol来决定是否为拉丁文,阿拉伯数字,中文或者泰文的1,2,3,4。如果发现某个匹配的,那个case就给可选变量possibleIntegerValue赋值。

当switch执行完的时候,使用可选值绑定来判断possibleIntegerValue是否有值,因为这个可选变量隐式使用nil初始化的,所以如果只有当某个case给他赋值的时候下面的if才会通过。

要列出上面例子中所有可能的字符就不太实际了,所以用了个deault来匹配剩下的所有字符。这个分支不需要做任何操作,所以就只写了一个break语句。只要最后的default被匹配,整个switch就被break终止了,开始执行下面的if了。

fallthrough

swift的switch语句如果匹配到了某个case是不会再继续向下执行的,执行完这个case的代码整个switch也就结束了。但是在C里面是要求在case的结尾加一个break才能有这样的效果。避免了默认挂穿执行意味着swift的switch语句比C要更加简洁并且可预料,也避免了因为忘记写break而多执行了其他的case。

如果你确实需要像C那样贯穿的特性,你可以通过使用fallthrough关键字来满足。下面的自理使用fallthrough来创建数字的文本描述:

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."
}
println(description)
// prints "The number 5 is a prime number, and also an integer.”
例子中声明了一个字符串变量description并且给他赋初始值。然后使用switch来测试integerToDescribe的值。如果是第一个case中的某个素数,就在description后面加一段文字说明这个数字是素数。然后使用fallthrough关键字来让程序继续直接进入下一个case default,default case给description添加了其他的描述信息,然后整个switch才结束。

如果integerToDescribe没有在第一个case当中,第一个case也就不会被匹配。也没有其他特殊的分支了,所以就被default匹配了。

当switch语句执行完以后,数字的描述被使用println函数输出。这个例子中数字5被正确的认出是素数。

NOTE fallthrough不会检查下一个将要匹配的分支的条件,他只是简单地让程序直接进入下一个分支,就像C标准的switch语句一样。
标签语句

你可以通过在switch或者循环当中嵌套其他的switch或者循环来实现复杂的控制流结构。switch和循环都可以使用break语句来提前结束程序。但是有时候需要指定你想跳出的是哪个循环或者switch。比如说你嵌套了好几个循环,指明break应该跳出哪个循环就很需要了。

要想达到这个目的,你可以给循环或者switch设置一个标签,然后再break或者continue后面加上这个标签,就能让break或者continue影响这个循环或者switch了。

标签语句是通过在语句本身前面加上标签名称再加上冒号。下面有个给while语句使用标签的例子,这个规则对于所有的循环和switch都是通用的:

label name: while condition {
    statements
}
下面的例子使用带标签的break和continue语句重新实现上面写过的蛇与梯子的游戏。这次游戏规则要加一条:当且仅当你身在第25格的时候才算过关

如果某次掷色子然后跳到25格以外了,那就重新掷色子,一直到你站在25格上。游戏棋盘跟前面的一样:

finalSquare,borad,square以及diceRoll的初始化方式跟前面也是一样的:

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 != finalSquare,也就是说你必须站在第25格上才能过关:

gameLoop: while square != finalSquare {
    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]
    }
}
println("Game over!")
每次循环先掷色子。但是这次不是根据色子直接移动了,而是用switch来根据移动的结果来判断是否允许移动:

如果这个色子能把你移动到最后一格上,游戏就结束了,break gameLoop语句就把程序转移到while循环以后了,然后游戏也就结束了。

如果这个色子把你移动到棋盘外面了,那这个移动是非法的,你要重新掷色子。continue gameLoop语句结束了当前正在进行的循环,并且开始下次循环。
其他情况,移动是合法的且不会让你过关的。你就按照色子的数字移动,然后检查当前位置是不是蛇头或者梯子可以移动的。然后当前循环结束,返回到while条件语句判断是否需要进行下一次循环。

NOTE 如果上面的break语句不适用gameLoop标签,那break只会跳出switch而不会终止整个while循环,使用gameLoop标签可以清楚的指明需要结束哪个语句。

还要注意的时当在gameLoop循环里要使用continue进行下一次循环的时候可以不用gameLoop标签的。因为这个游戏里只有一个循环,所以没有必要指明continue对谁起作用。
但是你要用也无妨。这样可以使程序逻辑清晰易于理解。