Swift 学习札记(一) - 基础知识

  1. Swift 学习札记(一) - 基础知识

最早开始接触学习是在 2015年6月,当时也是跟着 Apple 的 “The Swift Programming Language” 学习。由于当时 Swift 处于十分不稳定的阶段,同时工作中也一直处于使用 Objective-C,所以很快,Swift 就被搁浅了。现在 Swift 已经到了 ~> 3.0 时代,并且 Swift 的开发团队也表示,即将发布的 Swift 3.1 将可与 Swift 3.0 实现源代码兼容。所以现在已经是一个重新学习和在实际工程中使用 Swift 的良好时机了。
我将会复习(或者说时重新学习)Swift,并根据学习的内容做一些摘录,摘录的顺序会按照 “The Swift Programming Language”,以下是第一部分。

基础知识

常量和变量

声明常量和变量

变量/常量 必须先声明,后使用。

声明常量

使用关键词 let 来声明常量。

1
let maximumNumberOfLoginAttempts = 10

声明变量

使用关键词 var 来声明变量。

1
var currentLoginAttempt = 0

同时声明多个常量/变量

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

类型声明

在声明一个变量/常量时,可以通过给其添加类型,来明确该变量/常量的数据类型。

添加类型声明

通过在变量/常量和加 “:” 再加类型的方式,来添加类型声明。

1
var welcomeMessage: String

同时声明,多了同类型的常量/变量

1
var red, green, blue: Double

常量/变量的命名

常量/变量的名称可以使用几乎所有的字符,包括 Unicode。

1
2
3
let π = 3.14159
let 你好 = "你好世界"
let 🐶🐮 = "dogcow"

变量和常量的名称不可以包含空格,算术运算符,箭头,私有(无效)的 Unicode,制表符。同时,也不可以用数字开头,但可以在其他位置使用数字。

可以通过给 Swift 关键字添加 ` 来将其作为我们的常量/变量的名称,但我们应该避免这么做。

1
let `switch` : String = "a Switch"

打印常量/变量

通过 print(_:separator:terminator:) 函数来打印常量/变量。

1
2
3
var friendlyWelcome = "Bonjour!"
print(friendlyWelcome)
// 打印出 "Bonjour!"

print(_:separator:terminator:) 是一个全局函数。separatorterminator 参数是有默认值的,所以在使用的时候,可以完全抛弃这两个参数。使用默认值的情况下,会在输出的行末添加换行,如果不需要在输出的行末添加换行,可以将 terminator 的值设为 “”(空字符串)。

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

1
2
3
var friendlyWelcome = "Bonjour!"
print("The current value of friendlyWelcome is \(friendlyWelcome)")
// 打印出 "The current value of friendlyWelcome is Bonjour!"

注释

注释的主要作用是作为所写代码的标注,注释是一段不可执行的文本,Swift 的编译器会在编译的时候自动忽略注释。

Swift 中的注释与 C 语言中的非常相似。 单行注释使用 “//“

1
// 这是一行注释

多行注释则是使用 “/*“ 开头,使用 “*/“ 结尾。与 C 语言不通的是,Swift 的多行注释是可以嵌套的。

1
2
/* 这是一段
多行注释 */
1
2
3
/* 这是多行注释的开始
/* 这是多行注释的嵌套部分 */
这是多行注释的结尾 */

分号

Swift 的语句结尾处是不需要写上 “;” 的(如果你执意要写的话,也没有问题)。但如果,要将多个语句写在同一行,则需要在不同的语句中间加上 “;”。

1
let cat = "🐱"; print(cat)

整型

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

Swift 提供了8位,16位,32位和64位的有符号整型和无符号整型。 这些整型的命名方式和 C 语言类似。例如8位无符号整型表示为 UInt8,32位有符号整型表示为 Int32。

整型范围

可以通过访问整型类型的 minmax 属性来获取该整型类型的最大值和最小值。

1
2
let minValue = UInt8.min
let maxValue = UInt8.max

Int

大多数情况下,我们并不需要为我们的数据挑选一个特定尺寸的整型类型。Swift 额外为我们提供了 Int 类型,它的长度与当前平台一个字的长度相等。

字是用来一次性处理事务的一个固定长度的位(bit)组。

  • 在 32位平台上,IntInt32 的长度一样。
  • 在 64位平台上,IntInt64 的长度一样。

UInt

同样的 Swift 也额外提供了一个无符号的整型类型 UInt, 它的长度与当前平台一个字的长度相等。

  • 在 32位平台上,UIntUInt32 的长度一样。
  • 在 64位平台上,UIntUInt64 的长度一样。

除非你真的需要一个与当前平台字长一样长的无符号整型(UInt),不如都建议使用 Int,即使你已经明确知道你的数据不可能为负数。这样做的目的是为了提高代码的兼容性,同时可以避免不同数字类型之间的转换问题,也符合整数的类型推断。

浮点型

浮点数是指包括小数部分的数字, 例如 3.141590.1-273.15

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

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

Double 类型可以有至少 15 位的精度,而 Float 只能保证6位的精度。具体使用哪一种浮点数类型主要取决与您的时机需求。但当两种都可以使用的时候,建议使用 Double

类型安全/类型推测

Swift 是一门 类型安全 的编程语言,会在编译的时候,对代码进行类型检查,任何类型不匹配的情况都会被标记为错误。

但这并不代表您需要为每个变量/常量标明类型,Swift 会使用 类型推断 来推断出合适的类型。通过检查声明时赋给变量/常量的值,类型推断能够在编译阶段自动的推断出值的类型。

在你为一个变量/常量设定一个初始值的时候,类型推断就显得更加便捷。在你声明一个变量/常量的同时设置一个初始的字面量(文本)时,它就能推断出该变量/常量的类型。

1
2
3
4
5
let meaningOfLife = 42
// meaningOfLife 被推断为整型 Int

let pi = 3.14159
// pi 被推断为 Double

Swift 在进行类型推动时,会选择奖浮点数推断为 Double

数字字面量

整型的字面量写法有:

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

下面列出的整型字母量都表示十进制数的 17

1
2
3
4
let decimalInteger = 17
let binaryInteger = 0b10001 // 用二进制表示的 17
let octalInteger = 0o21 // 用八进制表示的 17
let hexadecimalInteger = 0x11 // 用十六进制表示的 17

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

如果一个十进制数的指数是 exp,那么它的值就是基数乘以 10exp

  • 1.25e2 等于 1.25*102,也就是 125.0。
  • 1.25e-2 等于 1.25*10-2,也就是0.0123。

如果一个十六进制数的指数是 exp,那么它的值就是基数乘以 2exp

  • 0xFp2 等于 15 * 22,也就是 60.0。
  • 0xFp-2 等于 15 * 2-2,也就是 3.75。

为了方便数字的阅读,无论是整型还是浮点型,都可以额外的添加多个零,也可以添加下滑线:

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

数字类型转变

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

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

整型转换

要将一种数字类型转换成另外一种类型,你需要用当前值来初始化一个期望的类型。

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

可以通过 SomeType(ofInitialValue) 来调用一个 Swift 类型的初始化方法,并传入初始值。

整数和浮点数转换

整数和浮点数类型的互相转换必须是显式声明的:

1
2
3
4
5
let three = 3
let pointOneFourOneFiveNine = 0.14159
let pi = Double(three) + pointOneFourOneFiveNine

let integerPi = Int(pi)

通过浮点型来初始化一个整型时,采用截断浮点数的方式来获取整型。

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

类型别名

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

当你想根据当前的使用环境给类型一个更有意义的名字的时,类型别名就显得非常有用了。例如处理外部资源中特定长度的数据时:

1
typealias AudioSample = UInt16

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

1
var maxAmplitudeFound = AudioSample.min

布尔值

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

1
2
let orangeAreOrange = true
let turnipsAreDelicious = false

在使用条件语句的时候,例如 if 语句时,布尔值就会变得非常有用:

1
2
3
4
5
if turnipsAreDelicious {
print("Mmm, tasty turnips!")
} else {
print("Eww, turnips are horrible.")
}

因为 Swift 是类型安全的,一个非布尔值不可以用来替代 Bool。下面的例子会在编译的时候报错:

1
2
3
4
let i = 1
if i {
// some code
}

元组

元组 把多个值合并成单一的复合型的值。元组内的数据可以是任何类型,而且各个数据不需要是同一类型。

元组 (404, "Not Found") 用来描述一个 HTTP 的状态码。

1
let http404Error = (404, "Not Found")

元组 (404, "Not Found") 由一个 Int 类型和一个 String 类型的数据构成。该元组可以被描述为“一个 (Int, String) 类型的元组”。

任何类型的排列都可以被用来创建一个元组,他可以包含任意多的类型。

你也可以将一个元组的内容分解成单独的常量或变量,这样你就可以正常的使用它们了:

1
2
3
4
5
let (statusCode, statusMessage) = http404Error
print("The status code is \(statusCode)")
// 打印出 "The status code is 404"
print("The status message is \(statusMessage)")
// 打印出 "The status message is Not Found"

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

1
2
3
4
let (justTheStatusCode, _) = http404Error

print("The status code is \(justTheStatusCode)")
// 打印出 "The status code is 404"

另外一种方法就是利用从零开始的索引数字访问元组中的单独元素:

1
2
3
4
print("The status code is \(http404Error.0)")
// 打印出 "The status code is 404"
print("The status message is \(http404Error.1)")
// 打印出 "The status message is Not Found"

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

1
let http200Status = (statusCode: 200, description: "OK")

在命名之后,你就可以通过访问名字来获取元素的值了:

1
2
3
4
print("The status code is \(http404Error.statusCode)")
// 打印出 "The status code is 404"
print("The status message is \(http404Error.description)")
// 打印出 "The status message is Not Found"

作为函数返回值时,元组非常有用。

元组在临时的值组合中很有用,但是它们不适合创建复杂的数据结构。如果你的数据结构超出了临时使用的范围,那么请建立一个类或结构体来代替元组。

可选值

在可能出现值为空的情况时,就应该使用 可选值。可选值表示了两种可能:有值,可以通过解包来获取值,或者就是更不就没有值。

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

Swift 的 Int 类型中有一个初始化器,可以将 String 值转换为一个 Int 值。然而并不是所有的字符串都可以转换成整数。字符串 “123” 可以被转换为数字值 123 ,但是字符串 “hello, world” 就显然不能转换为一个数字值。

1
2
3
let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber 被推断为类型 "Int?", 也就是可选 Int 类型

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

nil

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

1
2
3
4
var serverResponseCode: Int? = 404
// serverResponseCode 时机上包含了一个 Int 值 404。
serverResponseCode = nil
// serverResponseCode 现在不饱和任何值

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

If 语句以及强制解包

你可以使用 if 语句通过比较 nil 来判断一个可选中是否包含值。

1
2
3
4
5
if convertedNumber != nil {
print("convertedNumber contain some integer value.")
}

// 打印出:“convertedNumber contain some integer value.”

一旦你确定可选中包含值,你可以在可选的名字后面加一个感叹号 ( ! ) 来获取值,这就是所谓的可选值的 强制展开

1
2
3
4
5
if convertedNumber != nil {
print("convertedNumber contain some integer value of \(convertedNumber!).")
}

// 打印出:“convertedNumber contain some integer value of 123.”

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

可选绑定

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

1
2
3
if let constantName = someOptional {
// 语句
}

可以把之前的例子重写:

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

// 打印出:“"123" has an integer value of 123”

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

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

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

if let firstNumber = Int("4") {
if let secondNumber = Int("42") {
if firstNumber < secondNumber && secondNumber < 100 {
print("\(firstNumber) < \(secondNumber) < 100")
}
}
}
// 打印出:"4 < 42 < 100

使用 if 语句进行 可选绑定 ,创建的常量和变量只在if语句的函数体内有。不同的是,在 guard 语句中创建的常量和变量在 guard 语句后的代码中也可用。

隐式解析可选类型

可选类型暗示了常量或者变量可以“没有值”。可选可以通过 if 语句来判断是否有值,如果有值的话可以通过可选绑定来解析值。

有时候在程序架构中,第一次被赋值之后,可以确定一个可选类型总会有值。在这种情况下,每次都要判断和解析可选值是非常低效的,因为可以确定它总会有值。

这种类型的可选状态被定义为隐式解析可选类型。把想要用作可选的类型的后面的问号(String?)改成感叹号(String!)来声明一个隐式解析可选类型。

一个隐式解析可选类型其实就是一个普通的可选类型,但是可以被当做非可选类型来使用,并不需要每次都使用解析来获取可选值。下面的例子展示了可选类型 String 和隐式解析可选类型 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 // 不需要感叹符号

你仍然可以把隐式解析可选类型当做普通可选类型来使用,对他进行是否有值的判断,或者是使用隐式解析。

错误处理

你可以使用 错误处理 来应对程序执行中可能会遇到的错误情况。

相对于可选值通过是否有值来表示函数执行是否成功,错误处理可以推断失败的原因,并且的在必要的情况下,将该信息传到程序的其他地方。

当一个函数执行时发生错误时,它能报错。调用函数的地方能够捕获这个错误消息并进行需要的处理。

1
2
3
func canThrowAnError() throws {
// 这个函数可能会抛出错误
}

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

Swift 会自动把错误传送出当前的作用域,直到这些错误被 catch 从句处理。

1
2
3
4
5
6
do {
try canThrowAnError()
// 没有错误抛出
} catch {
// 有错误抛出
}

一个 do 语句创建了一个新的包含作用域,使得错误能被传播到一个或多个 catch 从句中。

下面的例子演示了一个错误处理如何用来应对不同错误条件的例子。

1
2
3
4
5
6
7
8
9
10
11
12
func makeASandwich() throws {
// ...
}

do {
try makeASandwich()
eatASandwich()
} catch SandwichError.outOfCleanDishes {
washDishes()
} catch SandwichError.missingIngredients(let ingredients) {
buyGroceries(ingredients)
}

断言

在某些情况下,如果值缺失或者值并不满足特定的条件,你的代码可能没办法继续执行。这时,你可以在你的代码中触发一个断言来结束代码运行并通过调试来找到值缺失的原因。

使用断言进行调试

断言会在运行时判断一个逻辑条件是否为 true。从字面意思来说,断言“断言”一个条件是否为真。你可以使用断言来保证在运行其他代码之前,某些重要的条件已经被满足。如果断言条件判断为 true,代码会继续进行;如果断言条件判断为 false,代码执行结束,应用被终止。

如果你的代码在调试环境下触发了一个断言,比如你在 Xcode 中构建并运行一个应用,你可以清楚地看到不合法的状态发生在哪里并检查断言被触发时你的应用的状态。此外,断言允许你附加一条调试信息。

可以通过调用 Swift 的全局函数 assert(_:_:file:line:) 来写一个断言。你可以向这个函数传入一个表达式,来验证结果为 truefalse,以及一条用来在表达式为 false 的时候显示的信息:

1
2
3
let age = -3
assert(age >= 0, "A person's age cannot be less than zero")
// 因为 *age* 并不大于 0,所以该处会触发断言

当代码使用优化编译的时候,断言将会被禁用,例如在 Xcode 中,使用默认的 target Release 配置选项来 build 时,断言会被禁用。

何时使用断言

当条件可能为假,但是需要确保条件为真,,代码才能继续运行时,使用断言:

  • 整数类型的下标索引被传入一个自定义下标实现,但是下标索引值可能太小或者太大。
  • 需要给函数传入一个值,但是非法的值可能导致函数不能正常执行。
  • 一个可选值现在是 nil,但是后面的代码运行需要一个非 nil 值。

断言可能导致你的应用终止运行,所以你应当仔细设计你的代码来让非法条件不会出现。然而,在你的应用发布之前,有时候非法条件可能出现,这时使用断言可以快速发现问题。

0%