Swift基础知识(二)
多元组
元组将多个值分组为一个复合值。元组中的值可以是任何类型,并且彼此不必是同一类型。
在本例中,(404,“Not Found”)是描述HTTP状态码的元组。HTTP状态码是web服务器在我们请求网页时返回的特殊值。如果我们请求的网页不存在,则返回404 Not Found状态码。
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)的元组,或者我们需要的任何其他排列组合。
我们可以将元组的内容分解为单独的常量或变量,然后像往常一样访问这些常量或变量:
let (statusCode, statusMessage) = http404Error
print("The status code is \(statusCode)")
// Prints "The status code is 404"
print("The status message is \(statusMessage)")
// Prints "The status message is Not Found"
如果我们只需要元组的一些值,则在分解元组时忽略带有下划线(\的元组部分:
let (justTheStatusCode, _) = http404Error
print("The status code is \(justTheStatusCode)")
// Prints "The status code is 404"
或者,使用从零开始的索引号访问元组中的各个元素值:
print("The status code is \(http404Error.0)")
// Prints "The status code is 404"
print("The status message is \(http404Error.1)")
// Prints "The status message is Not Found"
定义一个元组时,可以命名元组中的各个元素:
let http200Status = (statusCode: 200, description: "OK")
如果在元组中命名了元素,我们就可以使用元素名称访问这些元素的值:
print("The status code is \(http200Status.statusCode)")
// Prints "The status code is 200"
print("The status message is \(http200Status.description)")
// Prints "The status message is OK"
元组作为函数的返回值特别有用。尝试检索网页的函数可能返回(Int,String)元组类型来描述页面检索的成功或失败的状态。通过返回一个具有两个不同类型值的元组,该函数就可以提供了有关其结果的更多有用的信息,而不是仅返回单个类型的单个值。有关详细信息,请参见具有多个返回值的函数。
注意
元组对于一组简单的相关值很有用。它们不适合创建复杂的数据结构。如果我们的数据结构可能比较复杂的话,请将其建模为类或结构体,而不是元组。有关详细信息,请参见结构和类。
可选项
在可能缺少值的情况下,我们可以使用可选项。一个optional表示两种可能性:要么有一个值,那样我们可以打开optional来访问该值,要么根本没有值。
注意
optionals的概念在C或Objective-C中是不存在的。Objective-C中,和optional最接近的东西就是从一个方法中返回nil的能力,否则该方法将返回一个对象,nil的意思是“没有有效的对象”。然而,这只适用于对象,而不适用于结构体、基本C类型或枚举值。对于这些类型,Objective-C方法通常返回一个特殊的值(如NSNotFound),以表示没有值。这种方法假设其调用者知道有一个特殊的值要测试,并记得检查它。Swift的optionals允许我们指示任何类型可能没有值,而不需要一个特殊常量来表示。
下面是一个如何使用optionals来处理缺少值的例子。Swift的Int类型有一个初始化方法,它试图将字符串值转换为Int值。但是,并非每个字符串都可以转换为整数。字符串“123”可以转换为数值123,但是字符串“hello,world”没有明显的数值可以转换。
下面的示例使用初始化方法尝试将字符串转换为Int:
let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber is inferred to be of type "Int?", or "optional Int"
因为初始化方法可能会失败,所以它会返回一个可选的Int,而不是Int。可选的Int写为Int?,而不是Int。问号表示它包含的值是可选的,这意味着它可能是Int值,或者也可能根本不是任何Int值。(它不能表示任何其他内容,比如Bool值或字符串值。要么是Int,要么什么都不是。)
nil
通过为可选变量指定特殊值nil,可以将其设置为无值状态:
var serverResponseCode: Int? = 404
// serverResponseCode contains an actual Int value of 404
serverResponseCode = nil
// serverResponseCode now contains no value
注意
不能对非可选常量和变量设置为nil。如果代码中的常量或变量在某些情况下需要在没有值的情况下工作,请始终将其声明为适当类型的可选值。
如果定义可选变量而不提供默认值,则该变量将自动设置为nil:
var surveyAnswer: String?
// surveyAnswer is automatically set to nil
注意
Swift的nil与Objective-C中的nil不同。在Objective-C中,nil是指向不存在对象的指针。在Swift中,nil不是指针,而是缺少某种类型的值。任何类型的选项都可以设置为nil,而不仅仅是对象类型。
If语句和强制展开
我们可以使用if语句,通过比较optional和nil,来确定optional是否包含值。我们可以使用“等于”运算符(==)或“不等于”运算符(!=)。
如果optional有一个值,它被认为是“不等于”nil:
if convertedNumber != nil {
print("convertedNumber contains some integer value.")
}
// Prints "convertedNumber contains some integer value."
一旦确定optional确实有值,就可以通过添加感叹号(!)来访问其基础值到可选名称的末尾。感叹号(!)实际上是这样的意思:“我知道这个optional肯定有一个值;请使用它。” 这就是强制展开optional的值:
if convertedNumber != nil {
print("convertedNumber has an integer value of \(convertedNumber!).")
}
// Prints "convertedNumber has an integer value of 123."
有关if语句的更多信息,请参见控制流。
注意
如果使用!访问不存在的可选值,就可能会触发运行时错误。在使用!强制展开其值之前,请务必确保可选值包含非nil值。
可选绑定
我们可以使用可选绑定来确定optional是否包含值。如果包含值,就把该值作为临时常量或变量使用。可选绑定可以与if和while语句一起使用,来检查optional中的值,并把该值提取赋值到常量或变量中。if和while语句在控制流中有更详细的描述。
为if语句编写可选绑定,如下所示:
if let constantName = someOptional {
statements
}
我们可以在someOptional部分重写possibleNumber示例,以使用可选绑定而不是强制展开:
if let actualNumber = Int(possibleNumber) {
print("The string \"\(possibleNumber)\" has an integer value of \(actualNumber)")
} else {
print("The string \"\(possibleNumber)\" could not be converted to an integer")
}
// Prints "The string "123" has an integer value of 123"
此代码可以理解为:
如果Int(possibleNumber)返回的可选Int值,将它命名为叫作actualNumber的新常量。
如果转换成功,actualNumber常量将会在If语句的第一个分支中执行。它已经用optional中包含的值初始化了,因此不需要使用!后缀以访问其值。在本例中,actualNumber仅用于打印转换后的结果。
可以将常量和变量与可选绑定一起使用。如果要在If语句的第一个分支中操作actualNumber的值,可以改为 If var actualNumber
,这样optional中的值将作为变量,而不是常量来提供。
我们可以在一个if语句中,根据需要用逗号分隔来包含任意多个可选绑定和布尔条件。如果可选绑定中的任何值为nil或任何布尔条件的计算结果为false,则整个If语句的条件将被视为false。下列if语句是等效的:
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语句后面的代码行中可用,如Early Exit中所述。
隐式展开可选项
如上所述,Optionals表示允许常量或变量具有“无值”的情况。Optionals可以用if语句检查是否存在值,也可以用可选绑定有条件地展开以访问optional的值(如果存在)。
有时,从程序的结构可以清楚地看出,在第一次设置值之后,optional总是有一个值。在这些情况下,消除每次访问可选项时检查和打开其值的需要是很有用的,因为可以安全地假设它一直都有一个值。
这类可选项被定义为隐式展开可选项。我们可以通过放置感叹号(String!),而不是问号(String?)使其成为可选的类型之后,来编写隐式展开的可选项。在使用可选项时,不要在其名称后放置感叹号,而是在声明可选项时在其类型后放置感叹号。
当一个可选的值在第一次定义之后会立即确认为存在,并且可以肯定地假设在之后一直存在时,隐式展开可选项是非常有用的。Swift中隐式展开选项的主要用途,就是在类初始化期间,如无主引用和隐式展开可选属性中所述。
隐式展开可选项是一个正常的可选值,也可以像非可选值一样使用,而无需每次访问时都展开可选值。下面这个示例显示了可选字符串和隐式展开的可选字符串在作为显式字符串,访问其所代表的值时的行为差异:
let possibleString: String? = "An optional string."
let forcedString: String = possibleString! // requires an exclamation point
let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // no need for an exclamation point
我们可以将隐式展开可选项,视为在需要时授予强制展开可选项的权限。当使用隐式展开可选项时,Swift首先尝试将其用作普通可选值;如果不能将其用作可选值,Swift 就会强制展开该值。在上面的代码中,可选值assumedString在将其值赋给implicitString之前就被强制展开,这是因为implicitString有一个显式的、非可选的字符串类型。在下面的代码中,optionalString没有显式类型,因此它是一个普通的可选类型。
let optionalString = assumedString
// The type of optionalString is "String?" and assumedString isn't force-unwrapped.
如果隐式展开的optional为nil,并且我们尝试访问它的值时,就会触发运行时错误。造成的结果,就和在没有值的普通可选项后面放置感叹号完全相同。
可以检查隐式展开可选项是否为零,方法与检查普通可选项相同:
if assumedString != nil {
print(assumedString!)
}
// Prints "An implicitly unwrapped optional string."
我们还可以将隐式展开的可选项与可选项绑定一起使用,在单个语句中检查并展开提取其值:
if let definiteString = assumedString {
print(definiteString)
}
// Prints "An implicitly unwrapped optional string."
注意
当一个变量可能在以后变为nil时,不要使用隐式展开的可选变量。如果需要在变量的生存期内检查nil值,请始终使用普通可选类型。
错误处理
我们可以使用错误处理来响应程序在执行期间可能遇到的错误情况。
与可选项不同,可选项可以使用值的存在或不存在来传递函数的成功或失败,而错误处理允许我们确定失败的根本原因,并且在必要时将错误传递到程序的另一部分。
当函数遇到错误情况时,它会抛出一个错误。然后,该函数的调用者可以捕获到错误并做出适当的响应。
func canThrowAnError() throws {
// this function may or may not throw an error
}
函数通过在其声明中包含throws关键字来指示它可以抛出错误。调用可能引发错误的函数时,应在表达式前面加上try关键字。
Swift会自动将错误传递到当前范围之外,直到它们被catch子句处理为止。
do {
try canThrowAnError()
// no error was thrown
} catch {
// an error was thrown
}
do语句创建一个新的包含范围,允许将错误传递到一个或多个catch子句。
下面是一个如何使用错误处理来响应不同错误条件的示例:
func makeASandwich() throws {
// ...
}
do {
try makeASandwich()
eatASandwich()
} catch SandwichError.outOfCleanDishes {
washDishes()
} catch SandwichError.missingIngredients(let ingredients) {
buyGroceries(ingredients)
}
在本例中,如果没有干净的盘子或缺少任何配料,makeASandwich()函数将抛出一个错误。因为makeASandwich()可以抛出错误,所以函数调用被包装在try表达式中。通过在do语句中包装函数调用,抛出的任何错误都将传递到提供的catch子句。
如果没有抛出错误,则调用eatASandwich()函数。如果抛出一个错误为SandwichError.outOfCleanDishes
则调用washdisks()函数。如果抛出一个错误为SandwichError.MissingElements
,就会调用buyGroceries(_:)
。
错误处理中更详细地介绍了抛出、捕获和传递错误。
断言和前提条件
断言和前提条件是在运行时发生的检查。在执行任何进一步的代码之前,可以使用它们来确保基本条件得到满足。
如果断言或前提条件中的布尔条件的计算结果为true,则代码将照常继续执行。
如果条件的计算结果为false,则程序的当前状态无效;代码执行结束,应用程序终止。
在编写代码时,可以使用断言和前提条件来表示所做的假设和期望,这样就可以将它们作为代码的一部分。断言可以帮助我们在开发过程中发现错误和不正确的假设,前提条件可以帮助我们发现问题。
除了在运行时验证我们的期望之外,断言和前提条件也成为代码中一种有用的文档形式。与上面错误处理中讨论的错误条件不同,断言和前提条件不用于可恢复或预期的错误。因为失败的断言或先决条件表示程序状态无效,所以无法捕获失败的断言。
使用断言和前提条件并不能代替来让无效条件不可能出现。但是,使用它们来强制执行有效的数据和状态会使应用程序在出现无效状态时更容易终止,并有助于使问题更易于调试。一旦检测到无效状态就停止执行也有助于限制无效状态造成的损害。
断言和前提条件的区别在于它们被检查的时间:断言只在调试版本中被检查,而前提条件在调试版本和生产版本中都被检查。在产品构建中,断言中的条件不会被计算。这意味着我们可以在开发过程中使用任意数量的断言,而不会影响生产中的性能。
使用断言进行调试
我们可以通过调用 assert(_:_:file:line:)
函数来编写断言。向此函数传递一个计算结果为true或false的表达式,以及一条在条件的结果为false时显示的消息。例如:
let age = -3
assert(age >= 0, "A person's age can't be less than zero.")
// This assertion fails because -3 is not >= 0.
在本例中,如果age>=0的计算结果为true,即age的值为非负值,则继续执行代码。如果age的值为负(如上面的代码所示),那么age>=0的计算结果为false,断言失败,从而终止应用程序。
例如,当断言消息只是以普通形式重复条件时,可以省略断言消息。
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.")
}
强制执行前提条件
当条件有可能为false时,请使用一个前提条件。但一定要为true,代码才能继续执行。例如,使用一个前提条件来检查下标是否越界,或者检查函数是否传递了有效值。
我们可以通过调用前置条件来编写precondition(_:_:file:line:)
函数。向此函数传递一个计算结果为true或false的表达式,以及一条在条件的结果为false时显示的消息。例如:
// In the implementation of a subscript...
precondition(index > 0, "Index must be greater than zero.")
我们也可以调用preconditionFailure(_:file:line:)
函数来指示发生了故障,例如,如果采用了开关的默认情况,但所有有效的输入数据都应该由开关的其他情况之一处理。
注意
如果在未检查模式下编译(-Ounchecked),则不检查前提条件。编译器假定前提条件始终为真,并相应地优化代码。但是,无论优化设置如何,fatalError(_:file:line:)
函数始终停止执行。
我们可以在原型设计期间或早期,使用fatalError(_:file:line:)
来为尚未实现的功能创建存根。因为致命错误永远不会被优化掉,不像断言或前提条件,所以我们可以确定,如果遇到存根实现,执行总是会停止。