Swift Learning (2) - The Basics (Fully Ver.)

Swift Learning (2) - The Basics (Fully Ver.)

August 7, 2021·Jensen
Jensen

image

Swift包含了CObjective-C上所有基础数据类型,Int表示整型值;DoubleFloat表示浮点值;Bool是布尔型值;String是文本型数据。Swift还提供了三个基本的集合类型,ArraySetDictionary

就像C语言一样,Swift使用变量来进行存储并通过变量名来关联值。在Swift中,广泛的使用者着值不可变的变量,它们就是常量,而且比C语言中的常量更强大。在Swift中,如果要处理的值不需要改变,那使用常量可以让代码更加安全并且更清晰地表达意图。

除了我们熟悉的类型,Swift还增加了Objective-C中没有的高阶数据类型比如元组(Tuple)。元组可以让使用者创建或者传递一组数据,比如作为函数的返回值时,可以用一个元组返回多个值。

Swift还增加了一个可选(Optional)类型,用于处理值缺失的情况。可选便是“那儿有一个值,并且它等于x”或者“那儿没有值”。可选有点像在Objective-C中使用nil,但是它可以用在任何类型上,不仅仅是类。可选类型比Objective-C中的nil指针更加安全也更具有表现力,它是Swift许多强大特性的重要组成部分。

Swift是一门类型安全的语言,这意味着Swift可以让使用者清楚地知道值的类型。如果代码需要一个String参数,类型安全会阻止不小心传入的一个Int参数。同样的,如果代码需要的是一个String参数,类型安全会阻止意外传入一个可选的String。类型安全可以帮助开发者在开发阶段尽早地发现并修正错误。


常量和变量

常量和变量把一个名字(如testVal)和一个指定类型的值(如“Jensen”)关联起来。常量的值一旦设定就不能再改变,而变量的值可以随意地更改。

声明常量和变量

常量和变量必须在使用前声明,用let来声明常量,用var来声明变量。

let maximumNumberOfLoginAttempts = 10  // 常量记录一共可以进行的登录尝试次数
var currentLoginAttempt = 0  // 变量记录当前登陆尝试次数

上述两行代码可以被理解为:“声明一个名字是maximumNumberOfLoginAttempts的新常量,并给它一个值10 。然后,声明一个名字是currentLoginAttempt的变量并将它的值初始化为0”。

可以在一行中声明多个常量或者多个变量,用逗号隔开。

var x = 0.0, y = 0.0, z = 0.0

注意

如果代码中有不需要改变的值,请使用let关键字将它声明为常量。只将需要改变的值声明为变量即可。

类型注解

当声明常量或者变量的时候可以加上类型注解(Type Annotation),说明常量或者变量中要存储的值的类型。如果要添加类型注解,需要在常量或者变量名后面加上一个冒号和空格,然后加上类型名称。

var welcomeMessage: String // 给welcomeMessage变量增加了类型注解,表示这个变量可以存储String类型的值
// 上述声明中的冒号代表着“是 ... 类型”,welcomeMessage变量现在可以被设置成任意字符串。
welcomeMessage = "Hello!"

甚至可以在一行中定义多个同样类型的变量,用逗号分隔,并在最后一个变量名之后添加类型注解。

var red, green, yellow: Double

常量和变量的命名

常量和变量名几乎可以包含所有的字符,包括Unicode字符。

let π = 3.141592654
let 你好 = "你好世界!"
let 🐮🍺 = "666"

但是也存在例外,常量与变量名不能包含数学符号,箭头,保留的(或者非法的)Unicode码位,连线与制表符。也不能以数字开头,但是可以在常量与变量名的其他地方包含数字。

一旦给常量或者变量添加了类型注解,就不可以使用相同的名字再次进行声明,或者改变其存储的值的类型。同时,Swift也不允许将变量与变量进行互转。

注意

如果需要使用与Swift保留关键字相同的名称作为常量或者变量名,可以使用反引号(`)将关键字包围的方式将其作为名字使用。无论如何,开发者都应当避免使用保留关键字作为常量或变量名,除非别无选择。

可以更改现有的变量值为其他同类型的值。

var friendlyWelcome = "Hello!"
friendlyWelcome = "Benjour!"  // friendlyWelcome现在是“Bonjour!”
friendlyWelcome = "你好!" // friendlyWelcome现在是“欢迎!”
// 与变量不同,常量的值一旦被确定就不能更改了。尝试这样做会导致变异时报错。
let languageName = "Swift"
//languageName = "C++"  // 取消这行注释编译器会报Cannot assign to value: 'languageName' is a 'let' constant错误

输出常量和变量

print(friendlyWelcome)  // 输出:你好!
---
output: 你好!

separatorterminator参数具有默认值,因此调用这个函数的时候可以忽略它们。

默认情况下,输出函数通过添加换行符来结束当前行。如果不想换行,可以传递一个空字符串给terminator参数。

print("Line1", terminator: "")
print("Line2")
---
output: Line1Line2

Swift用字符串插值(String interpolation)的方式把常量名或者变量名当做占位符加入到长字符串中。Swift会用当前常量或者变量的值替换这些占位符。将常量或者变量名放入圆括号中,并在开括号钱使用反斜杠将其转义。

print("The current login attempt is \(currentLoginAttempt), you have \(maximumNumberOfLoginAttempts) login attempts in total.")
---
output: The current login attempt is 0, you have 10 login attempts in total.

注释

Swift中的注释与C语言的注释十分相似,单行注释以双正斜杠//作为起始标记。

也可以进行多行注释,其起始标记为单个正斜杠后跟随一个星号/*,终止标记为一个星号后跟单个正斜杠*/

/* 这是一个注释
 但是是多行注释 */

C语言不同的是,Swift的多行注释可以嵌套在其它的多行注释之中。

/* 这是外层注释的第一行
 /* 这是内层注释 */
 这是外层注释的最后一行 */

通过运用嵌套多行注释,能够快速方便的注释掉一大段代码,即便这段代码之中已经含有了多行注释块。


分号

与其他大部分编程语言不同,Swift并不强制要求开发者在每条语句的结尾处使用分号;。当然,开发者也可以根据自己的习惯添加分号,但是有一种情况下必须要使用分号,即在同一行内写多条独立语句。

let pig = "🐷"; print(pig)
---
output: 🐷

整数

整数就是没有小数部分的数字,比如43和-23。Swift提供了8、16、32和64位的有符号和无符号整数类型。这些整数类型和C语言的命名方式很像,比如8位无符号整数类型是UInt8,32位有符号整数类型是Int32。就像Swift的其他类型一样,整数类型采用大写命名法。

整数范围

可以访问不同整数类型的minmax属性来获取对应类型的最小值和最大值。

let minValue = UInt8.min
let maxValue = UInt8.max
print("UInt8类型的最小值是:\(minValue)\nUInt8类型的最大值是:\(maxValue)")
---
output: UInt8类型的最小值是:0
UInt8类型的最大值是:255

minmax所传回值的类型正是其所对的整数类型,如上例UInt8,所传回的值的类型也是UInt8

Int

一般来说,不需要专门指定整数的长度。Swift提供了一个特殊的整数类型Int,长度与当前平台的原生字长相同:

在32位平台上,Int与Int32长度相同

在64位平台上,Int和Int64长度相同

UInt

Int类型一样,Swift也提供了一个特殊的无符号类型UInt,长度与当前平台的原生字长相同:

在32位平台上,UInt和UInt32长度相同

在64位平台上,UInt和UInt64长度相同

注意

尽量不要使用UInt,除非真的需要存储一个和当前平台原生字长相同的无符号整数。否则最好使用Int,统一使用Int能够提高代码的可复用性,避免不同类型数字之间的转换,并且匹配数字的类型推断。


浮点数

浮点数就是有小数部分的数字,比如3.141592654、0.1和-98.09等等。

浮点类型比整数类型表示的范围要大,可以存储比Int类型更大或者更小的数字。Swift提供了两种有符号浮点数类型:

Double表示64位浮点数,当需要存储很大或者很高精度的浮点数时,请使用此类型

Float表示32位浮点数,当精度要求不高的话可以使用此类型

注意

Double精确度很高,至少有15位小数,而Float只有6位小数。选择哪种类型取决于你的代码需要处理的值的范围,在两种类型都匹配的情况下将优先选择Double。


类型安全和类型推断

Swift是一个类型安全(Type Safe)的语言。类型安全的语言可以开发者你清楚地知道代码要处理的值的类型。

由于Swift是类型安全的,所以它在编译代码时会进行类型检查(Type Checks),并把不匹配的类型标记为错误。能够让开发者尽早地发现错误。

当要处理不同类型的值时,类型检查可以帮助避免错误。然而,这并不是说每次声明常量和变量的时候都需要显式指定类型。如果没有显式指定类型,Swift会使用类型推断(Type Inference)来选择合适的类型。有了类型推断,编译器可以在编译代码的时候自动推断出表达式的类型。原理很简单,只要检查赋的值即可。

当声明常量或者变量并赋初值的时候类型推断非常有用。声明常量或者变量的时候赋给它们一个字面量(Literal Value或Literal)即可触发类型推断。(字面量就是在代码中直接出现的值,比如0.1和3.141592654等等)

let meaningOfLife = 42  // meaningOfLife会被类型推断推测为Int类型
let pi = 3.141592654  // pi会被类型推断推测为Double类型,当推断浮点数类型时,Swift总是会选择Double而不是Float

如果表达式中同时出现了整数和浮点数,总是会被推断为Double类型。

let anotherPi = 3 + 0.141592654  // 原始值3没有显式地声明类型,而后又出现了一个浮点字面量,所以表达式被推断为Double类型

数值型字面量

整数字面量可以被写作:

一个十进制数,没有前缀

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

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

let decimalInteger = 17
let binaryInteger = 0b10001  // 二进制的17
let octalInteger = 0o21  // 八进制的17
let hexadecimalInteger = 0x11  // 十六进制的17

小数点两边必须至少要有一个十进制数字或者是十六进制的数字。十进制浮点数也可以有一个可选的指数(Exponent),通过大写或者小写的e来指定;十六进制浮点数必须有一个指数,通过大写或者小写的p来指定。

十进制:1.25e2表示1.2510^2,等于125.0; 八进制:0xFp-2表示152^-2,等于3.75

下面的这些浮点字面量都等于十进制的12.1875。

let decimalDouble = 12.1875
let exponentDouble = 1.21875e1
let hexadecimalDouble = 0xC.3p0

数值类字面量可以包括额外的格式来增强可读性。整数和浮点数都可以添加额外的零并且包含下划线,并不影响字面量的值。

let paddedDouble = 000123.456
let oneMillion = 1_000_000
let justOverOneMillion = 1_000_000.000_000_1

数值型类型转换

通常来讲,即使代码中的整数常量和变量已知非负,也请使用Int类型。只有在必要的时候才使用其他整数类型,比如要处理外部的长度明确的数据或者为了优化性能、内存占用等等。使用显式指定长度的类型可以及时发现值溢出并且可以暗示正在处理特殊数据。

整数转换

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

// let cannotBeNegative: UInt8 = -1  // 取消该行注释后,会报错:Negative integer '-1' overflows when stored into unsigned type 'UInt8'
// let tooBig: Int8 = Int8.max + 1  // 取消该行注释后,会报错:Arithmetic operation '127 + 1' (on type 'Int8') results in an overflow

开发者需要根据不同情况选择使用数值型类型转换,这种选择性使用方式,可以预防隐式转换的错误并让代码中的类型转换意图变得清晰。

let twoThousand: UInt16 = 2_000
let one: UInt8 = 1
let twoThousandAndOne = twoThousand + UInt16(one)  // 该代码中UInt16(one)来创建一个新的UInt16数字并用one的值来初始化它,然后便可以使得两个无符号16位值相加
print("UInt8转换至UInt16: ", twoThousandAndOne)
---
output: UInt8转换至UInt16:  2001

SomeType(ofInitialValue)是调用Swift构造器并传入一个初始值的默认方法。在语言内部,UInt16有一个构造器,可以接受一个UInt8类型的值,所以这个构造器可以用现有的UInt8来创建一个新的UInt16

整数和浮点数转换

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

let three = 3
let pointOneFourOneFiveNine = 0.14159
let simplePi = Double(three) + pointOneFourOneFiveNine  // pi等于3.14159,所以被类型推断为Double类型
print("整数和浮点数转换:", simplePi)
---
output: 整数和浮点数转换: 3.14159

在上述例子中,常量three的值被用来创建一个Double类型的值,否则加号两边的数类型不同,无法相加。

浮点数到整数的反向转换同样行,整数类型可以用Double或者Float类型来初始化。

let roundPi = Int(simplePi)  // roundPi等于3,所以被推断为Int类型
print("浮点数转换至整数:", roundPi)
---
output: 浮点数转换至整数: 3

当用这种方式来初始化一个新的整数值时,浮点值会被截断,也就是说4.75会变成4,-3.9会变成-3。

注意

结合数字类常量和变量不同于结合数字类字面量。字面量3可以直接和字面量0.14159相加,因为数字字面量本身没有明确的类型。它们的类型只在编译器需要求值的时候被预测。

类型别名

类型别名(Type Aliases)就是给现有类型定义另一个名字。开发者可以使用Typealias关键字来定义类型别名。

当想要给现有类型起一个更具有意义的名字时,类型别名非常有用。

假设现在正在处理特定长度的外部资源的数据,定义了一个类型别名后,可以在任何使用原始名的地方使用别名。

typealias AudioSample = UInt16
var maxAmplitudeFound = AudioSample.min  // maxAmplitudeFound现在是0
print("maxAmplitudeFound现在的值是:", maxAmplitudeFound)
---
output: maxAmplitudeFound现在的值是: 0

上述例子中,AudioSample被定义为UInt16的一个别名,因为是别名,所以AudioSample.min实际上是UInt16.min

布尔值

Swift有一个基本的布尔(Boolean)类型,叫做Bool。布尔值指的是逻辑上的值,因为其只能是真或者假。Swift有两个布尔值常量,truefalse

let orangesAreOrange = true
let turnipsAreDelicious = false

orangesAreOrangeturnipsAreDelicious的类型会被推断为Bool,因为它们的初值是布尔字面量。就像之前提到的IntDouble一样,如果创建常/变量的时候就赋予truefalse,就不需要将常/变量声明为Bool类型。

编写条件语句特别是if条件语句时,布尔值就非常有用。

if turnipsAreDelicious {
    print("Haha, turnips are decilious!")
} else {
    print("Oh shit! Turnips are horrible!")
}
---
output: Oh shit! Turnips are horrible!

如果在需要使用Bool类型的地方使用了非布尔值,Swift的安全机制会报错。

let test = 1
//if test {}  // 如果取消该行注释,则编译器会报错:Type 'Int' cannot be used as a boolean; test for '!= 0' instead

同样的,下述例子是合法的。

let i = 1
if i == 1 {}  // i == 1的结果是Bool类型,所以可以通过Swift的类型检查

元组

元组(Tuples)把多个值组合成一个复合值。元组内的值可以是任意类型,并不要求是相同类型。

下述例子,(404, “Not Found”) 是一个描述HTTP状态码(HTTP status code)的元组。

let http404Error = (404, "Not Found")  // http404Error的类型是(Int, String)

(404, “Not Found”)元组把一个Int值和一个String值组合起来表示HTTP状态码的两部分。这个元组可以被描述成“一个类型为(Int, String)的元组”。

可以把任意顺序的类型组合成一个元组,这个元组可以包含所有类型。还可以将一个元组的内容分解(Decompose)成单独的常量和变量,然后就可以正常使用它们了。

let (statusCode, statusMessage) = http404Error
print("The http status code is \(statusCode).")
print("The http status message is \(statusMessage).")
---
output: The http status code is 404.
The http status message is Not Found.

如果只需要一部分元组值,分解的时候可以吧要忽略的部分用下划线(_)标记,类似于Python

let (httpStatusCode, _) = http404Error
print("The temp http status code is \(httpStatusCode).")
---
output: The temp http status code is 404.

还可以通过下标来访问元组中的单个元素,下标从零开始。

print("通过下标访问元组中的第一个元素:", http404Error.0)
print("通过下标访问元组中的第二个元素:\(http404Error.1)")
---
output: 通过下标访问元组中的第一个元素: 404
通过下标访问元组中的第二个元素:Not Found

可以在定义元组的时候给耽搁元素命名,然后通过名字来获取这些元素的值。

let http500Status = (statusCode: 500, description: "Server Error")
print("通过元组元素命名方式访问第一个元素:\(http500Status.statusCode)")
print("通过元组元素命名方式访问第二个元素:\(http500Status.description)")
---
output: 通过元组元素命名方式访问第一个元素:500
通过元组元素命名方式访问第二个元素:Server Error

当函数中需要返回多个值时,元组很有用。


可选类型

使用可选类型(Optionals)来处理值可能缺失的情况。可选类型表示有两种可能:

或者有值,开发者可以选择可选类型访问这个值;或者可能根本没有值

一个例子,SwiftInt类型有一种构造器,作用是将一个String值转换成一个Int值。然而,不是所有的字符串都可以转换成一个整数。字符串“123”可以被转换成数字123,但是字符串“Hello, world”不行。

let possibleNumber = "123"
var convertedNumber = Int(possibleNumber)
print("convertedNumber被推测为类型“Int?”,或者类型“optional Int”:", convertedNumber!)
let possibleName = "Jensen"
convertedNumber = Int(possibleName)
print("字符串\(possibleName)转成数字后有问题:", convertedNumber)
---
output: convertedNumber被推测为类型“Int?”,或者类型“optional Int”: 123
字符串Jensen转成数字后有问题: nil
---
warning: Expression implicitly coerced from 'Int?' to 'Any'

由于该构造器可能会失效,所以Int(xxx)返回的是一个可选类型(optional)的Int,而不是一个Int。可选的Int被写作Int?,问号暗示包含的值时可选类型,但不能包含其他任何值比如Bool``值或者String值,只能是Int`或者是什么都没有。

nil

可以给可选变量赋值为nil来表示没有值。

var serverResponseCode: Int? = 404  // serverResponseCode包含一个可选的Int值404
serverResponseCode = nil  // serverResponseCode现在不包含值
print("可选值serverResponseCode:", serverResponseCode)
---
output: 可选值serverResponseCode nil
---
warning: Expression implicitly coerced from 'Int?' to 'Any'

请注意,nil不能用于非可选的常量和变量,如果代码中有常量或者变量需要处理缺失值的情况,请把它们声明成对应的可选类型。如果声明一个可选常量或者变量但是没有赋值,它们会自动被设置为nil

var surveyAnswer: String?
print("没有赋值的可选常/变量的值为:", surveyAnswer)
---
output: 没有赋值的可选常/变量的值为: nil
---
warning: Expression implicitly coerced from 'String?' to 'Any'

if语句以及强制解析

可以使用if语句和nil比较来判断一个可选值是否包含值,可以使用“相等”(==)或“不等”(!=)来执行比较。

如果可选类型有值,它将不等于nil

if convertedNumber != nil {
    print("convertedNumber contains some integer value.")
} else {
    print("convertedNumber is nil.")
}
---
output: convertedNumber is nil.

当确定可选类型确实包含值之后,可以在可选的名字后面加上一个感叹号!来获取值。这个操作被称为可选值的强制解析(Forced Unwrapping)。

let optionalInteger: Int? = 10086
if optionalInteger != nil {
    print("The optional integer value is \(optionalInteger!)")  // 来获取一个不存在的可选值会导致运行错误,使用!来强制解析值之前,一定要确定可选包含一个非nil的值。
} else {
    print("The optional value is Nil.")
}
---
output: The optional integer value is 10086

可选绑定

可选绑定(Optional Binding)来判断可选类型是否包含值,如果包含就把值赋给一个临时常量或者变量。可选绑定可以用在ifwhile语句中。

if let actualNumber = Int(possibleNumber) {
    print("\'\(possibleNumber)\' has an integer value of \(actualNumber).")
} else {
    print("\'\(possibleNumber)\' could not be converted to an Integer.")
}
---
output: '123' has an integer value of 123.

上述代码可以被理解为“如果Int(possibleNumber)返回的可选Int包含一个值,创建一个叫做actualNumber的新常量并将可选Int包含的值赋给它。actualNumber已经被可选类型包含的值初始化过,所以不需要再使用!后缀来获取它的值”。

可以包含多个可选绑定或多个布尔条件在一个if语句中,只要使用逗号分开就行。只要有一个可选绑定的值为nil或者任意一个布尔条件为false,则整个if条件判断为false

if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
    print("\(firstNumber) > \(secondNumber) < 100")
}
// 等同下面的代码
if let firstNumber = Int("4") {
    if let secondNumber = Int("42") {
        if firstNumber < secondNumber && secondNumber < 100 {
            print("\(firstNumber) > \(secondNumber) < 100")
        }
    }
}
---
output: 4 > 42 < 100
4 > 42 < 100

注意

在if条件语句中使用常量和变量来创建一个可选绑定,仅在if语句的句中(body)中才能获取到值。相反,在guard语句中使用常量和变量来创建一个可选绑定,仅在guard语句外且在语句后才能获取到值。

隐式解析可选类型

有时候在程序架构中,第一次被赋值后,可以确定一个可选类型总会有值。在这种情况下, 每次进行判断和解析可选值是非常低效的。这种类型可选状态被定义为隐式解析可选类型(Implicitly Unwrappered Optionals)。把想要用作可选的类型的后面的问号String?改成感叹号String!来声明一个隐式解析可选类型。与其在使用时把感叹号放在可选类型的名称的后面,可以在定义它时,就直接把感叹号放在可选类型的后面。

当可选类型被第一次赋值之后就可以确定之后一直有值的时候,隐式解析可选类型非常有用。隐式解析可选类型主要被用在Swift中类的构造的过程中。

一个隐式解析可选类型其实就是一个普通的可选类型,但是可以被当成非可选类型来使用,并不需要每次都使用解析来获取可选值。

let possibleString: String? = "An optional String."
let forcedString = possibleString!
print("显式解析可选类型:", forcedString)

let assumedString: String! = "An implicitly unwrapped optional String."
let implicitString: String = assumedString
print("隐式解析可选类型:", implicitString)
---
output: 显式解析可选类型: An optional String.
隐式解析可选类型: An implicitly unwrapped optional String.

可以把隐式解析可选类型当做一个可以自动解析的可选类型。当使用一个隐式解析可选值时,Swift首先会把它当作普通的可选值。如果它不能被当成可选类型使用,Swift会强制解析可选值。在上述的代码中,可选值assumedString在把自己的值赋给implicitString之前会被强制解析,原因是因为implicit本身的类型就是非可选类型的String

下述代码optionalString并没有显示的数据类型,那么根据类型推断,其就是一个普通的可选类型。

let optionalString = assumedString

如果在隐式解析可选类型没有值的时候尝试取值,会触发错误。与在没有值的普通可选类型后面加一个感叹号一样。

可以吧隐式解析可选类型当做普通可选类型来判断其是否包含值。

if assumedString != nil {
    print("隐式解析可选类型取值:", assumedString!)
}
---
output: 隐式解析可选类型取值: An implicitly unwrapped optional String.

也可以在可选绑定中使用隐式解析可选类型来检查病解析其值。

if let definiteString = assumedString {
    print("隐式解析可选类型取值:", definiteString)
}
---
output: 隐式解析可选类型取值: An implicitly unwrapped optional String.

注意

如果一个变量之后可能变成nil的话请不要使用隐式解析可选类型。如果需要在变量的生命周期中判断是否是nil的话,请使用普通可选类型。


错误处理

可以使用错误处理(Error Handling)来应对程序执行中可能会遇到的错误条件。

相较于可选类型中运用值的存在与否来表达函数的成功与失败,错误处理可以推断失败的原因,并传播至程序的其他部分。

当一个函数遇到错误条件,它能报错。调用函数的地方能抛出错误消息并合理处理。

func canThrowAnError() throws {
    // 这个函数内部可能会抛出错误
}

一个函数可以通过在声明中添加throws关键词来抛出错误信息。当函数能够抛出错误消息时,应该在表达式中前置try关键词。

do {
    try canThrowAnError()
    // 没有错误消息抛出则执行后面代码
} catch {
    // 有错误消息抛出则执行后面代码
}

一个do语句创建了一个新的包含作用域,使得错误能被传播到一个或多个catch从句。下述伪代码展示了错误处理如何用来应对不同错误条件的例子。

func makeASandwich() throws {
    // function body
}

enum SandwichError: Error {
    case outOfCleanDishes
    case missingIngredients(ingredients: String)
}

do {
    try makeASandwich()
    // eatASandwich()
} catch SandwichError.outOfCleanDishes {
    // washDishes()
} catch SandwichError.missingIngredients(let ingredients) {
    // 如果匹配missingIngredients的错误被抛出,buyGroceries(_:)函数会被调用,并且使用catch所捕捉到的关联值[ingredients]作为参数。
    // buyGroceries(ingredients)
}
---
warning: Immutable value 'ingredients' was never used; consider replacing with '_' or removing it

上述例子中,makeASandwich()(做一个三明治)函数会抛出一个错误消息如果没有干净的盘子或者某个原料缺失。因为makeASandwich()抛出错误,函数调用被包裹在try表达式中。将函数包裹在一个do语句中,任何被抛出的错误会被传播到提供的catch从句中。


断言和先决条件

断言和先决条件是在运行时所做的检查。可以用它们来检查在执行后续代码之前是否一个必要的条件被满足了。如果断言或者先决条件中的布尔条件评估的结果为true(真),则代码像往常一样继续执行。如果布尔条件评估结果为假(false),程序的当前状态是无效的,则代码执行结束,应用程序中止。

断言帮助开发者在开发阶段找到错误和不正确的假设,先决条件帮助开发者在生产环境中探测到存在的问题。与错误处理不同,断言和先决条件并不是用来处理可以恢复的或者可预期的错误。因为一个断言失败表明了程序正处于一个无效的状态,没有办法去捕获一个失败的断言。

注意

使用断言和先决条件不是一个能够避免出现程序出现无效状态的编码方法。然而,如果一个无效状态程序产生了,断言和先决条件可以强制检查数据和程序状态,使得程序可预测的中止(不是系统强制的,被动的中止),并帮助使这个问题更容易调试。一旦探测到无效的状态,执行则被中止,防止无效的状态导致的进一步对于系统的伤害。

断言和先决条件的不同点是,它们什么时候进行状态检测:断言仅在调试环境运行,而先决条件则在调试环境和生产环境中运行。在生产环境中,断言的条件将不会进行评估。这个意味着可以使用很多断言在程序的开发阶段,但是这些断言在生产环境中不会产生任何影响。

使用断言进行调试

可以调用Swift标准库的assert(_:_:file:line:)函数来写一个断言。向这个函数传入一个结果为true或者false的表达式以及一条信息,当表达式的结果为false的时候这条信息会被显示。

let age = -3
assert(age >= 0, "A person's age cannot be less than zero.")  // 因为age < 0,所以断言会被触发

上述代码中,如果age的值是负数,就像代码中那样,age >= 0false,断言被触发,终止应用。

如果不需要断言信息,可以就想下述这样忽略掉。

assert(age >= 0)

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

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的时候这条信息会被显示。

var index = 0
precondition(index > 0, "Index must be greater than zero.")  // 索引需要从1开始

可以调用preconditionFailure(_:file:line:)方法来表明出现了一个错误,例如,switch进入了default分支,但是所有的有效值应该被任意一个其他分支(非default分支)处理。

注意

如果使用unchecked模式编译代码,先决条件将不会进行检查。编译器假设所有的先决条件总是为true(真),它将优化编写的代码。然而fatalError(_:file:line)函数总是中断执行,无论您怎么进行优化设定。

可以在设计原型和早期开发阶段使用fatalError(_:file:line)函数,此阶段只有方法的声明但是没有具体实现,可以在方法体中写上fatalError(“Unimplemented”)作为具体实现。因为fatalError不会像断言和先决条件那样被优化掉,可以确保代码执行到一个没有被实现的方法时,程序会中断。

Last updated on