一、概述
本系列文章旨在复习Swift5
核心语法且适当进行底层原理探索,属于阶段性复习和巩固,以供日后进一步探索Swift
语言的底层原理做铺垫。
整个系列文章如下,每一文章知识点独立成篇,欢迎各位按需或按兴趣点击阅读:
- 01-📝Swift5常用核心语法|了解Swift【Swift简介、Swift的版本、Swift编译原理】
- 02-📝Swift5常用核心语法|基础语法【Playground、常量与变量、常见数据类型、字面量、元组、流程控制、函数、枚举、可选项、guard语句、区间】
- 03-📝Swift5常用核心语法|面向对象【闭包、结构体、类、枚举】
- 04-📝Swift5常用核心语法|面向对象【属性、inout、类型属性、单例模式、方法、下标、继承、初始化】
- 05-📝Swift5常用核心语法|高级语法【可选链、协议、错误处理、泛型、String与Array、高级运算符、扩展、访问控制、内存管理、字面量、模式匹配】
- 06-📝Swift5常用核心语法|编程范式与Swift源码【从OC到Swift、函数式编程、面向协议编程、响应式编程、Swift源码分析】
二、Playground
1. HelloWorld
print("Hello World")
不用编写main函数
,Swift将全局范围内的首句可执行代码作为程序入口- 通过反汇编我们可以看到底层会执行
main函数
- 通过反汇编我们可以看到底层会执行
- 一句代码尾部可以省略分号(
;
),多句代码写到同一行时必须用分号(;
)隔开
2. Playground
Playground
可以 快速预览代码效果,是学习语法的好帮手Command + Shift + Enter
运行整个PlaygroundShift + Enter
运行截止到某一行代码- Playground - View
- Playground - ViewController
- Playground – 多Page
- 添加Page|方法1
- 添加Page|方法2
三、常量和变量
1. 常量
- 1.用
let
定义常量(常量只能赋值一次)
不用特意指明类型,编译器能自动推断出变量/常量的数据类型let a: Int = 10 let b = 20
- 2.它的值不要求在编译过程中确定,但使用之前必须赋值一次
这样写确定了a的类型,之后再去赋值,也不会报错let a: Int a = 10
-
- 用函数给常量赋值也可以,函数是在运行时才会确定值的,所以只要保证使用之前赋值了就行
func getNumber() -> Int { return 10 } let a: Int a = getNumber()
-
- 常量、变量在初始化之前,都不能使用
-
- 如果没有给a确定类型,也没有一开始定义的时候赋值,就会像下面这样报错
2. 变量
-
- 用
var
定义变量
- 用
var b = 20
b = 30
-
- 常量、变量在初始化之前,都不能使用
3. 注释
- 1.Swift中有单行注释和多行注释
注释之间嵌套也没有问题
// 单行注释
/*
多行注释
*/
/*
1
/* 释嵌套 */
2
*/
- 2.
Playground
里的注释支持Markup
语法(同Markdown)
Markup
语法只在Playground
里有效,在项目中无效
//: # 一级标题
/*:
## 基础语法
*/
可以通过Editor -> Show Raw Markup
来预览
预览的效果如下
4. 标识符
- 标识符(比如常量名、变量名、函数名)几乎可以使用任何字符
let 📒 = 5 var 😁 = 10 func 👽() { }
标识符
不能
以数字开头
不能
包含空白字符
、制表符
、箭头
等特殊字符
四、常见数据类型
1. 值类型
- 枚举(enum):
- Optional
- 结构体(struct):
- 非集合类型:
- Bool
- Double
- Float
- Int
- Character
- String
- 集合类型:
- Array
- Dictionary
- Set
- 非集合类型:
2. 引用类型
- 类(class)
可以通过command+control
进入到该类型的API中查看
例如Int类型
3. 整数类型
- 整数类型:
Int8
、Int16
、Int32
、Int64
、UInt8
、UInt16
、UInt32
、UInt64
- 在32bit平台,
Int
等于Int32
;在64bit平台,Int
等于Int64
- 整数的最值:
UInt8.max
,Int16.min
一般情况下,都是直接使用Int
即可
let a: Int8 = 5
4. 浮点类型
- Float:32位,精度只有6位
- Double:64位,精度至少15位
浮点型不指明类型默认就是
Double
let a: Float = 2.0
let b = 3.0
五、字面量
字面量就是指这个量本身,就是一个固定值的表示法
下面这些都是字面量
1. Bool布尔
一般用Bool
类型来表示是否的判断,是为true
,否为false
//布尔
let bool = true //取反是false
2. 字符串、字符
字符串的写法:
let string = "hello"
字符类型要写上Character
,否则会被认为是字符串
字符可存储ASCII字符、Unicode字符
字符写法:
let character: Character = "a"
3. 整数
不同进制的表示法:
- 二进制以
0b
开头 - 八进制以
0o
开头 - 十六进制以
0x
开头
let intDecimal = 17 // 十进制
let intBinary = 0b10001 // 二进制
let intOctal = 0o21 // 八进制
let intHexadecimal = 0x11 // 十六进制
4. 浮点数
let doubleDecimal = 125.0 // 十进制
let doubleDecimal2 = 1.25e2 // 也是125.0的另一种写法,表示1.25乘以10的二次方
let doubleDecimal3 = 0.0125
let doubleDecimal4 = 1.25e-2 // 也是0.0125的另一种写法,表示1.25乘以10的负二次方
let doubleHexadecimal1 = 0xFp2 // 十六进制,意味着15*2^2(15乘以2的二次方),相当于十进制的60
let doubleHexadecimal2 = 0xFp-2 //十六进制,意味着15*2^-2(15乘以2的负二次方),相当于十进制的3.75
整数和浮点数可以添加额外的零或者下划线来增强可读性
let num = 10_0000
let price = 1_000.000_000_1
let decimal = 000123.456
5. 数组
let array = [1, 2, 3, 4]
6. 字典
let dictionary = ["age" : 18, "height" : 1.75, "weight" : 120]
7. 类型转换
整数转换:
let int1: UInt16 = 2_000
let int2: UInt8 = 1
let int3 = int1 + UInt16(int2)
整数、浮点数转换:
let int = 3
let double = 0.1415926
let pi = Double(int) + double
let intPi = Int(pi)
字面量可以直接相加,因为数字字面量本身没有明确的类型:
let result = 3 + 0.14159
六、元组(tuple)
元组是可以多种数据类型组合在一起
let http404Error = (404, "Not Found")
print("The status code is (http404Error.0)")
// 可以分别把元组里的两个值分别进行赋值
let (statusCode, statusMsg) = http404Error
print("The status code is (statusCode)")
// 可以只给元组里的某一个值进行赋值
let (justTheStatusCode, _) = http404Error
// 可以在定义的时候给元组里面的值起名
let http200Status = (statusCode: 200, description: "ok")
print("The status code is (http200Status.statusCode)")
七、流程控制
1. 条件分支语句|if-else
Swift里的if else
后面的条件是可以省略小括号的,但大括号不可以省略
let age = 10
if age >= 22 {
print("Get married")
} else if age >= 18 {
print("Being a adult")
} else if age >= 7 {
print("Go to school")
} else {
print("Just a child")
}
if else
后面的条件只能是Bool类型
2. 循环语句|while
/repeat-while
while
:
var num = 5
while num > 0 {
print("num is (num)")
// 打印了五次
num -= 1
}
repeat-while
:
repeat-while
相当于C语言中的do-while
先执行一次,再判断条件循环
var num = -1
repeat {
print("num is (num)")
// 打印了一次
} while num > 0
这里不用num--
是因为
Swift3
开始,已经去掉了自增(++)、自减(--)运算符
3. 循环语句|for
-
1.闭区间运算符:
a...b
,相当于a <= 取值 <= b
// 第一种写法 let names = ["Anna", "Alex", "Brian", "Jack"] for i in 0...3 { print(names[i]) }// Anna Alex Brian Jack // 第二种写法 let range = 0...3 for i in range { print(names[i]) }// Anna Alex Brian Jack // 第三种写法 let a = 1 let b = 3 for i in a...b { }// Alex Brian Jack
-
- 循环里的
i
默认是let
,如需要更改加上var
for var i in 1...3 { i += 5 print(i) }// 6 7 8
- 循环里的
-
- 不需要值的时候用
_
来表示
for _ in 0...3 { print("for") }// 打印了3次
- 不需要值的时候用
for – 区间运算符用在数组上
-
4.半开区间运算符:
a..<b
,相当于a <= 取值 < b
for i in 0..<3 { print(i) }//0 1 2
-
5.单侧区间:让一个区间朝一个方向尽可能的远区间运算符还可以用在数组上)
let names = ["Anna", "Alex", "Brian", "Jack"] for name in names[0...3] { print(name) } // Anna Alex Brian Jack for name in names[2...] { print(name) } // Brian Jack for name in names[...2] { print(name) } // Anna Alex Brian for name in names[..<2] { print(name) } // Anna Alex let range = ...5 range.contains(7) // false range.contains(4) // true range.contains(-3) // true
4. 条件分支语句|switch
使用同C语言的switch
,不同的是:
-
case、default
后面不写大括号{}
var number = 1 switch number { case 1: print("number is 1") break case 2: print("number is 2") break default: print("number is other") break }
-
- 默认不写
break
,并不会贯穿到后面的条件
var number = 1 switch number { case 1: print("number is 1") case 2: print("number is 2") default: print("number is other") }
- 默认不写
fallthrough
使用fallthrough
可以实现贯穿效果
var number = 1
switch number {
case 1:
print("number is 1")
fallthrough
case 2:
print("number is 2")
default:
print("number is other")
}
// 会同时打印number is 1,number is 2
switch注意点
-
switch
必须要保证能处理所有情况 注意:像判断number的值,要考虑到所有整数的条件,如果不要判断全部情况,加上default
就可以了
-
case、default
后面至少要有一条语句
如果不想做任何事,加个break
即可
var number = 1 switch number { case 1: print("number is 1") case 2: break default: break }
-
- 如果能保证已处理所有情况,也可以不必使用
default
enum Answer { case right, wrong } let answer = Answer.right switch answer { case .right: print("right") case .wrong: print("wrong") }
- 如果能保证已处理所有情况,也可以不必使用
复合条件
-
switch
也支持Character
和String
类型
let string = "Jack" switch string { case "Jack": fallthrough case "Rose": print(string) default: break }//Jack let character: Character = "a" switch character { case "a", "A": print(character) default: break }
-
switch
可以同时判断多个条件
let string = "Jack" switch string { case "Jack", "Rose": print(string) default: break }// Right person
-
switch
也支持区间匹配和元组匹配
let count = 62 switch count { case 0: print("none") case 1..<5: print("a few") case 5..<12: print("several") case 12..<100: print("dozens of") default: print("many") }
-
- 可以使用下划线
_
忽略某个值
关于case
匹配问题,属于模式匹配(Pattern Matching)的范畴
let point = (1, 1) switch point: { case (2, 2): print("1") case (_, 0): print("2") case (-2...2, 0...): print("3") }
- 可以使用下划线
值绑定:
- 8.值绑定,必要时
let
也可以改成var
let point = (2, 0) switch point: { case (let x, 0): print("on the x-axis with an x value of \(x)") case (0, let y): print("on the y-axis with a y value of \(y)") case let (x, y): print("somewhere else at (\(x), \(y))") } // on the x-axis with an x value of 2
5. where
一般where
用来结合条件语句进行过滤
let point = (1, -1)
switch point {
case let (x, y) where x == y:
print("on the line x == y")
case let (x, y) where x == -y:
print("on the line x == -y")
case let (x, y):
print("(x), (y) is just some arbitrary point")
}// on the line x == -y
// 将所有正数加起来
var numbers = [10, 20, -10, -20, 30, -30]
var sum = 0
for num in numbers where num > 0 { // 使用where来过滤num
sum += num
}
print(sum) // 60
6. 标签语句
用outer
来标识循环跳出的条件
outer: for i in 1...4 {
for k in 1...4 {
if k == 3 {
continue outer
}
if i == 3 {
break outer
}
print("i == \(i), k == \(k)")
}
}
八、函数
1. 函数的定义
a.) 有返回值的函数
形参默认是let
,也只能是let
func sum(v1: Int, v2: Int) -> Int {
return v1 + v2
}
b.) 无返回值的函数
返回值Void的本质就是一个空元组
// 三种写法相同
func sayHello() -> Void {
print("Hello")
}
func sayHello() -> () {
print("Hello")
}
func sayHello() {
print("Hello")
}
2. 隐式返回(Implicit Return)
如果整个函数体是一个单一的表达式,那么函数会隐式的返回这个表达式
func sum(v1: Int, v2: Int) -> Int { v1 + v2 }
sum(v1: 10, v2: 20)//30
3. 返回元组,实现多返回值
func calculate(v1: Int, v2: Int) -> (sum: Int, difference: Int, average: Int) {
let sum = v1 + v2
return (sum, v1 - v2, sum >> 1)
}
let result = calculate(v1: 20, v2: 10)
result.sum // 30
result.difference // 10
result.average // 15
print(result.sum, result.difference, result.average)
4. 函数的文档注释
可以通过一定格式书写注释,方便阅读
/// 求和【概述】
///
/// 将2个整数相加【更详细的描述】
///
/// - Parameter v1: 第1个整数
/// - Parameter v2: 第2个整数
/// - Returns: 2个整数的和
///
/// - Note:传入2个整数即可【批注】
///
func sum(v1: Int, v2: Int) -> Int {
v1 + v2
}
5. 参数标签(Argument Label)
-
- 可以修改参数标签
func gotoWork(at time: String) {
print("this time is \(time)")
}
gotoWork(at: "8:00")// this time is 08:00
-
- 可以使用下划线
_
省略参数标签,为了阅读性一般不建议省略
- 可以使用下划线
func sum(_ value1: Int, _ value2: Int) -> Int {
value1 + value2
}
sum(5, 5)
6. 默认参数值(Default Parameter Value)
-
- 参数可以有默认值
func check(name: String = "nobody", age: Int, job: String = "none") {
print("name=(name), age=(age), job=(job)")
}
check(name: "Jack", age: 20, job: "Doctor")// name=Jack, age=20, job=Doctor
check(name: "Jack", age: 20)// name=Jack, age=20, job=none
check(age: 20, job: "Doctor")// name=nobody, age=20, job=Doctor
check(age: 20)// name=nobody, age=20, job=none
-
C++
的默认参数值有个限制:必须从右往左设置;由于Swift
拥有参数标签,因此没有此类限制
-
- 但是在省略参数标签时,需要特别注意,避免出错
// 这里的middle不可以省略参数标签
func test(_ first: Int = 10, middle: Int, _ last: Int = 30) { }
test(middle: 20)
7. 可变参数(Variadic Parameter)
-
- 一个函数
最多只能有一个
可变参数
func sum(_ numbers: Int...) -> Int { var total = 0 for number in numbers { total += number } return total } sum(1, 2, 3, 4)
- 一个函数
-
- 紧跟在可变参数 后面的参数不能省略参数标签
// 参数string不能省略标签 func get(_ number: Int..., string: String, _ other: String) { } get(10, 20, string: "Jack", "Rose")
Swift自带的print函数 我们可以参考下
Swift
自带的print函数
print(1, 2, 3, 4, 5)
print(1, 2, 3, 4, 5, separator: " ", terminator: "\n")
8. 输入输出参数(In-Out Parameter)
-
可以用
inout
定义一个输入输出参数:可以在函数内部修改外部实参的值
func swapValues(_ v1: inout Int, _ v2: inout Int) { let tmp = v1 v1 = v2 v2 = tmp } var num1 = 10 var num2 = 20 swapValues(&num1, &num2)
-
官方自带
swap
的交换函数就是使用的inout
- 可以利用元组来进行参数交换
func swapValues(_ v1: inout Int, _ v2: inout Int) { (v1, v2) = (v2, v1) } var num1 = 10 var num2 = 20 swapValues(&num1, &num2)
-
- 可变参数不能标记为
inout
- 可变参数不能标记为
-
inout
参数不能有默认值
-
inout
参数只能传入可以被多次赋值的
- 常量只能在定义的时候赋值一次,所以下面会报错
-
inout
参数的本质是地址传递
- 我们新建个项目,通过反汇编来观察其本质
leaq
表示的就是地址传递,可以看出在调用函数之前先将两个变量的地址放到了寄存器中
9. 函数重载(Function Overload)
-
- 函数重载的规则
- 函数名相同
- 参数个数不同
||
参数类型不同||
参数标签不同
func sum(value1: Int, value2: Int) -> Int { value1 + value2 } // 参数个数不同 func sum(_ value1: Int, _ value2: Int, _ value3: Int) -> Int { value1 + value2 + value3 } // 参数标签不同 func sum(_ a: Int, _ b: Int) -> Int {a + b} // 参数类型不同 func sum(_ a: Double, _ b: Double) -> Int { a + b }
函数重载注意点
-
- 返回值类型和函数重载无关
-
- 默认参数值和函数重载一起使用产生二义性时,编译器并不会报错(C++中会报错)
// 不建议的写法
func sum(_ value1: Int, _ value2: Int, _ value3: Int = 5) -> Int { v1 + v2 + v3 }
func sum(_ value1: Int, _ value2: Int) -> Int { v1 + v2 }
//会调用sum(v1: Int, v2: Int)
sum(10, 2)
-
- 可变参数、省略参数标签、函数重载一起使用产生二义性时,编译器有可能会报错
10. 内联函数(Inline Function)
如果开启了编译器优化(Release模式
默认会开启优化),编译器会自动将某些函数变成内联函数
- 将函数调用展开成函数体
我们分别来观察下更改Debug模式下的优化选项,编译器做了什么
1.我们新建一个项目,项目代码如下
2. 然后我们先通过反汇编观察没有被优化时的编译器做了什么
可以看到会调用test函数
,然后test函数
里面再执行打印
- 现在我们开启
Debug
模型下的优化选项,然后运行程序 发现print
打印直接就在main函数
里执行了,没有了test函数
的调用过程
相当于print函数
直接放到了main函数
中,编译器会将函数调用展开成函数体
哪些函数不会被内联
- 函数体比较长
- 包含递归调用
- 包含动态派发(运行时的多态调用(OC、Swift混编的时候才会有运行时
,纯粹的Swift项目是没有runtime的
))
@inline
我们可以使用@inline
关键字,来主动控制编译器是否做进行优化
-
@inline(nerver)
:永远不会被内联,即使开启了编译器优化
@inline(nerver) func test() {}
-
@inline(__alaways)
:开启编译器优化后,即使代码很长,也会被内联(递归调用和动态派发除外)
@inline(__alaways) func test() {}
-
- 在
Release模式下
,编译器已经开启优化,会自动决定哪些函数需要内联,因此没必要使用@inline
- 在
11. 函数类型(Function Type)
-
- 每一个函数都是有类型的,函数类型由
形参类型
、返回值类型
组成
func test() {} // () -> Void 或 () -> () func sum(a: Int, b: Int) -> Int { a + b }// (Int, Int) -> Int // 定义变量 var fn: (Int, Int) -> Int = sum fn(5, 3) //8 调用时不需要参数标签
- 每一个函数都是有类型的,函数类型由
-
- 函数类型作为
函数参数
func sum(v1: Int, v2: Int) -> Int { v1 + v2 } func difference(v1: Int, v2: Int) -> Int { v1 - v2 } func printResult(_ mathFn: (Int, Int) -> Int, _ a: Int, _ b: Int) { mathFn(a, b) } printResult(difference, 5, 2)// Result: 3 printResult(sum, 5, 2)// Result: 7
- 函数类型作为
-
- 函数类型作为
函数返回值
返回值是函数类型的函数叫做高阶函数(Higher-Order Function
)
func next(_ input: Int) -> Int { input + 1 } func previous(_ input: Int) -> Int { input - 1 } func forward(_ forward: Bool) -> (Int) -> Int { forward ? next : previous } forward(true)(3)//4 forward(false)(3)//2
- 函数类型作为
12. 嵌套函数(Nested Function)
-
- 将函数定义在函数内部
func forward(_ forward: Bool) -> (Int) -> Int { func next(_ input: Int) -> Int { input + 1 } func previous(_ input: Int) -> Int { input - 1 } forward ? next : previous } forward(true)(3)//4 forward(false)(3)//2
九、枚举
1. 枚举的基本用法
enum Direction {
case north
case south
case east
case west
}
// 简便写法
enum Direction {
case north, south, east, west
}
var dir = Direction.west
dir = Direction.east
dir = .north
print(dir) // north
switch dir {
case .north:
print("north")
case .south:
print("south")
case .east:
print("east")
case .west:
print("west")
}
2. 关联值(Associated Values)
有时会将枚举的成员值
和其他类型的值
关联 存储在一起
,会非常有用
enum Score {
case points(Int)
case grade(Character)
}
var score = Score.points(96)
score = .grade("A")
switch score {
case let .points(i):
debugPrint(i)
case let .grade(i):
debugPrint(i)
}
enum Date {
case digit(year: Int, month: Int, day: Int)
case string(String)
}
var date = Date.digit(year: 2020, month: 12, day: 5)
date = .string("2022-07-10")
//必要时【let】也可以改为【var】
switch date {
case .digit(let year, let month, let day):
debugPrint(year, month, day)
case let .string(value):
debugPrint(value)
}
关联值举例
enum Password {
case number(Int, Int, Int, Int)
case gesture(String)
}
var pwd = Password.number(5, 6, 4, 7)
pwd = .gesture("12369")
switch pwd {
case let .number(n1, n2, n3, n4):
print("number is ", n1, n2, n3, n4)
case let .gesture(str):
print("gesture is ", str)
}
必要时,使用了枚举关联值的switch-case
语句 里面的 let
也可以改成var
3. 原始值(Raw Values)
枚举成员可以使用相同类型
的默认值预先关联,这个默认值叫做原始值
enum PokerSuit: String {
case spade = "♠"
case heart = "♥"
case diamond = "♦"
case club = "♣"
}
let suit = PokerSuit.heart
debugPrint(suit)// heart
debugPrint(suit.rawValue)// ♥
debugPrint(PokerSuit.spade.rawValue)// ♠
enum Grade : String {
case perfect = "A"
case great = "B"
case good = "C"
case bad = "D"
}
print(Grade.perfect.rawValue) // A
print(Grade.great.rawValue) // B
print(Grade.good.rawValue) // C
print(Grade.bad.rawValue) // D
注意:
- 原始值不占用枚举变量的内存
- 原始值只是关联上了枚举变量,所以原始值占用内存的大小并不是枚举变量的大小
- 底层实现是通过计算属性/函数来获取原始值的
4. 隐式原始值(Implicitly Assigned Raw Values)
如果枚举的原始值类型是Int
、String
,Swift会自动分配原始值
字符串默认分配的原始值就是其变量名
enum Direction: String {
case north = "north"
case south = "south"
case east = "east"
case west = "west"
}
// 等价于上面
enum Direction: String {
case north, south, east, west
}
print(Direction.north) // north
print(Direction.north.rawValue) // north
Int类型
默认分配的原始值是从0开始递增的数字
enum Season: Int {
case spring, summer, autumn, winter
}
print(Season.spring.rawValue) // 0
print(Season.summer.rawValue) // 1
print(Season.autumn.rawValue) // 2
print(Season.winter.rawValue) // 3
如果有指定原始值的,下一个就会按照已经指定的值递增分配
enum Season: Int {
case spring = 1, summer, autumn = 4, winter
}
print(Season.spring.rawValue) // 1
print(Season.summer.rawValue) // 2
print(Season.autumn.rawValue) // 4
print(Season.winter.rawValue) // 5
5. 递归枚举(Recursive Enumeration)
-
- 递归枚举要用
indirect
关键字来修饰enum
,不然会报错
indirect enum ArithExpr { case number(Int) case sum(ArithExpr, ArithExpr) case difference(ArithExpr, ArithExpr) } 或者 enum ArithExpr { case number(Int) indirect case sum(ArithExpr, ArithExpr) indirect case difference(ArithExpr, ArithExpr) } let five = ArithExpr.number(5) let four = ArithExpr.number(4) let sum = ArithExpr.sum(five, four) let two = ArithExpr.number(2) let difference = ArithExpr.difference(sum, two) func calculate(_ expr: ArithExpr) -> Int { switch expr { case let .number(value): return value case let .sum(left, right): return calculate(left) + calculate(right) case let .difference(left, right): return calculate(left) - calculate(right) } } calculate(difference)
- 递归枚举要用
6. MemoryLayout
-
- 可以使用
MemoryLayout
获取数据类型占用的内存大小
64bit
的Int类型
占8个字节
let age = 10 MemoryLayout<Int>.stride // 8, 分配占用的空间大小 MemoryLayout<Int>.size // 8, 实际用到的空间大小 MemoryLayout<Int>.alignment // 8, 内存对齐参数 等同于 MemoryLayout.size(ofValue: age) MemoryLayout.stride(ofValue: age) MemoryLayout.alignment(ofValue: age)
- 可以使用
关联值和原始值的区别:
-
关联值类型会存储到枚举变量里面
-
原始值因为一开始就会知道默认值是多少,所以只做记录,不会存储
enum Password { case number(Int, Int, Int, Int) case other } MemoryLayout<Password>.stride // 40,分配占用的空间大小 MemoryLayout<Password>.size // 33,实际用到的空间大小 MemoryLayout<Password>.alignment // 8,对齐参数
enum Session: Int { case spring, summer, autnmn, winter } MemoryLayout<Session>.stride // 1,分配占用的空间大小 MemoryLayout<Session>.size // 1,实际用到的空间大小 MemoryLayout<Session>.alignment // 1,对齐参数
思考下面枚举变量的内存布局: 案例1:
enum TestEnum {
case test1, test2, test3
}
var t = TestEnum.test1
t = .test2
t = .test3
MemoryLayout<TestEnum>.stride // 1,分配占用的空间大小
MemoryLayout<TestEnum>.size // 1,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 1,对齐参数
案例2:
enum TestEnum : Int {
case test1 = 1, test2 = 2, test3 = 3
}
var t = TestEnum.test1
t = .test2
t = .test3
MemoryLayout<TestEnum>.stride // 1,分配占用的空间大小
MemoryLayout<TestEnum>.size // 1,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 1,对齐参数
案例3:
enum TestEnum {
case test1(Int, Int, Int)
case test2(Int, Int)
case test3(Int)
case test4(Bool)
case test5
}
var e = TestEnum.test1(1, 2, 3)
e = .test2(4, 5)
e = .test3(6)
e = .test4(true)
e = .test5
MemoryLayout<TestEnum>.stride // 32,分配占用的空间大小
MemoryLayout<TestEnum>.size // 25,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 8,对齐参数
案例4:
//注意!!!! 枚举选项只有一个,所以实际用到的内存空间 为0,但是要存储一个成员值 所以对其参数为1,给其分配一个字节
enum TestEnum {
case test
}
var t = TestEnum.test
MemoryLayout<TestEnum>.stride // 1,分配占用的空间大小
MemoryLayout<TestEnum>.size // 0,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 1,对齐参数
案例5:
enum TestEnum {
case test(Int)
}
var t = TestEnum.test(10)
MemoryLayout<TestEnum>.stride // 8,分配占用的空间大小
MemoryLayout<TestEnum>.size // 8,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 8,对齐参数
案例6:
enum TestEnum {
case test(Int)
}
var t = TestEnum.test(10)
MemoryLayout<TestEnum>.stride // 8,分配占用的空间大小
MemoryLayout<TestEnum>.size // 8,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 8,对齐参数
案例7:
enum TestEnum {
case test0
case test1
case test2
case test4(Int)
case test5(Int, Int)
case test6(Int, Int, Int, Bool)
}
var t = TestEnum.test(10)
MemoryLayout<TestEnum>.stride // 32,分配占用的空间大小
MemoryLayout<TestEnum>.size // 25,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 8,对齐参数
案例8:
enum TestEnum {
case test0
case test1
case test2
case test4(Int)
case test5(Int, Int)
case test6(Int, Int, Bool, Int)
}
var t = TestEnum.test(10)
MemoryLayout<TestEnum>.stride // 32,分配占用的空间大小
MemoryLayout<TestEnum>.size // 32,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 8,对齐参数
案例9:
enum TestEnum {
case test0
case test1
case test2
case test4(Int)
case test5(Int, Int)
case test6(Int, Bool, Int)
}
var t = TestEnum.test(10)
MemoryLayout<TestEnum>.stride //32,分配占用的空间大小
MemoryLayout<TestEnum>.size //25,实际用到的空间大小
MemoryLayout<TestEnum>.alignment //8,对齐参数
7. 枚举变量的内存布局
我们知道通过MemoryLayout
可以获取到枚举占用内存的大小,那枚举变量分别占用多少内存呢?
要想知道枚举变量的大小,我们需要通过查看枚举变量的内存布局来进行分析
枚举变量的分析准备
我们可以需要通过Xcode
里的View Memory
选项来查看详细的内存布局
1.可以在运行程序时,通过控制台打印的枚举变量右键选择View Memory of *
进入到内存布局的页面
2.还可以从Xcode
标题栏中选择Debug -> Debug Workflow -> View Memory
进入到内存布局的页面
3.进入到该页面,然后通过输入变量的内存地址来查看
4.我们可以下载一个小工具来获取到变量的内存地址
5.然后将下载好的这个文件Mems.swift
拖到自己的Xcode
中
调用这个函数就可以了
print(Mems.ptr(ofVal: &t))
我们来分析下面的枚举变量的情况
enum TestEnum {
case test1, test2, test3
}
var t = TestEnum.test1
print(Mems.ptr(ofVal: &t))
t = TestEnum.test2
t = TestEnum.test3
print(MemoryLayout<TestEnum>.stride) // 1
print(MemoryLayout<TestEnum>.size) // 1
print(MemoryLayout<TestEnum>.alignment) // 1
分别将断点打在给枚举变量t
赋值的三句代码上,然后运行程序观察每次断点之后的内存布局有什么变化
通过上图可以看到,每个枚举变量都分配了一个字节的大小,并且存储的值分别是0、1、2,我们可以知道这一个字节的大小就是用来存储枚举成员值
的
我们再来分析一个枚举变量的情况
enum TestEnum: Int {
case test1 = 1, test2 = 2, test3 = 3
}
var t = TestEnum.test1
print(Mems.ptr(ofVal: &t))
t = TestEnum.test2
t = TestEnum.test3
print(MemoryLayout<TestEnum>.stride) // 1
print(MemoryLayout<TestEnum>.size) // 1
print(MemoryLayout<TestEnum>.alignment) // 1
通过上图可以看到,每个枚举变量存储的值也是0、1、2,并且分配了一个字节的大小
可以证明枚举变量的内存大小和原始值类型无关,而且枚举变量里存储的值和原始值也无关
我们再来分析一个枚举变量的情况
enum TestEnum {
case test1(Int, Int, Int) // 24
case test2(Int, Int) // 16
case test3(Int) // 8
case test4(Bool) // 1
case test5 // 1
}
var t = TestEnum.test1(1, 2, 3)
print(Mems.ptr(ofVal: &t))
t = TestEnum.test2(4, 5)
t = TestEnum.test3(6)
t = TestEnum.test4(true)
t = TestEnum.test5
MemoryLayout<TestEnum>.size // 25
MemoryLayout<TestEnum>.stride // 32
MemoryLayout<TestEnum>.alignment // 8
我们先通过打印了解到枚举类型总共分配了32个字节
,然后我们通过断点分别来观察枚举变量的内存布局
执行完第一句我们可以看到,前面24个字节分别用来存储关联值1、2、3,第25个字节用来存储成员值0,之所以分配32个字节是因为内存对齐的原因
// 调整排版后的内存布局如下所示
01 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
03 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
执行完第二句我们可以看到,前面16个字节分半用来存储关联值4、5,然后第25个字节用来存储成员值1
// 调整排版后的内存布局如下所示
04 00 00 00 00 00 00 00
05 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
01 00 00 00 00 00 00 00
执行完第三句我们可以看到,前面8个字节分半用来存储关联值6,然后第25个字节用来存储成员值2
// 调整排版后的内存布局如下所示
06 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
执行完第四句我们可以看到,由于是Bool类型,那么只用了第一个字节来存储关联值1,然后第25个字节用来存储成员值3
// 调整排版后的内存布局如下所示
01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
03 00 00 00 00 00 00 00
执行完最后一句我们可以看到,由于没有关联值,那么只用了第25个字节存储成员值4
// 调整排版后的内存布局如下所示
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
04 00 00 00 00 00 00 00
总结:内存分配情况:一个字节存储成员值,n个字节存储关联值(n取占用内存最大的关联值),任何一个case的关联值都共有这n个字节
我们再来看几个情况
enum TestEnum {
case test
}
MemoryLayout<Session>.stride // 1,分配占用的空间大小
MemoryLayout<Session>.size // 0,实际用到的空间大小
MemoryLayout<Session>.alignment // 1,对齐参数
如果枚举里只有一个case
,那么实际用到的空间为0,都不用特别分配内存来进行存储
enum TestEnum {
case test(Int)
}
MemoryLayout<Session>.stride // 8,分配占用的空间大小
MemoryLayout<Session>.size // 8,实际用到的空间大小
MemoryLayout<Session>.alignment // 8,对齐参数
可以看到分配的内存大小就是关联值类型决定的,因为只有一个case
,所以都不需要再额外分配内存来存储是哪个case
了
十、可选项(Optional)
-
- 可选项,一般也叫可选类型,它允许将值设置为
nil
- 可选项,一般也叫可选类型,它允许将值设置为
-
- 在类型名称后面加个
问号
?
来定义一个可选项
var name: String? = nil
- 在类型名称后面加个
-
- 如果可选类型定义的时候没有给定值,默认值就是
nil
var age: Int? 等价于 var age: Int? = nil
- 如果可选类型定义的时候没有给定值,默认值就是
-
- 如果可选类型定义的时候赋值了,那么就是一个
Optional类型
的值
var name: String? = "Jack" // Optional(Jack)
- 如果可选类型定义的时候赋值了,那么就是一个
-
- 可选类型也
可以作为函数返回值
使用
var array = [1, 2, 3, 4] func get(_ index: Int) -> Int? { if index < 0 || index >= array.count { return nil } return array[index] }
- 可选类型也
1. 强制解包(Forced Unwrapping)
可选项是对其他类型的一层包装,可以理解为一个盒子
-
- 如果为
nil
,那么它就是个空盒子
- 如果为
-
- 如果不为
nil
,那么盒子里装的是:被包装类型的数据
var age: Int? age = 10 age = nil
- 可选关系的类型大致如下图:
- 如果不为
-
- 如果要从可选项中取出被包装的数据(将盒子里装的东西取出来),需要使用
感叹号
!
进行强制解包
var age: Int? = 10 var ageInt = age! ageInt += 10 // ageInt为Int类型
- 如果要从可选项中取出被包装的数据(将盒子里装的东西取出来),需要使用
-
- 如果对值为
nil
的可选项(空盒子)进行强制解包,将会产生运行时错误
- 如果对值为
2. 可选项绑定(Optional Binding)
-
- 我们可以判断可选项是否包含值
let number = Int("123") // number为Int? if number != nil { print(number!) }
-
- 还可以使用
可选项绑定
来判断可选项是否包含值
- 如果包含就
自动解包
,把值赋给一个临时的常量(let)或者变量(var)
,并返回true
,否则返回false
if let number = Int("123") { print("字符串转换整数成功:(number)") // number是强制解包之后的Int值 // number作用域仅限于这个大括号 } else { print("字符串转换整数失败") } // 字符串转换整数成功:123
- 还可以使用
-
- 如果判断条件有多个,可以合并在一起,用逗号
,
来分隔开
if let first = Int("4") { if let second = Int("42") { if first < second && second < 100 { print("(first) < (second) < 100") } } } 等于 if let first = Int("4"), let second = Int("42"), first < second && second < 100 { print("(first) < (second) < 100") }
- 如果判断条件有多个,可以合并在一起,用逗号
-
while循环
中使用可选项绑定
let strs = ["10", "20", "abc", "-20", "30"] var index = 0 var sum = 0 while let num = Int(strs[index]), num > 0 { sum += num index += 1 }
3. 空合并运算符(Nil-Coalescing Operator)
我们可以使用空合并运算符??
来对前一个值是否有值进行判断:
- 如果前一个值为
nil
,就会返回后一个值
详细用法如下:
- a
??
ba
是可选项b
是可选项或者不是可选项b
跟a
的存储类型必须相同- 如果
a
不为nil
,就返回a
- 如果
b
不是可选项,返回a
时会自动解包
- 如果
- 如果
a
为nil
,就返回b
结果的类型取决于
??
后面的值类型是什么
let a: Int? = 1
let b: Int = 2
let c = a ?? b // c是Int , 1
let a: Int? = nil
let b: Int = 2
let c = a ?? b // c是Int , 2
多个
??
一起使用
let a: Int? = 1
let b: Int? = 2
let c = a ?? b ?? 3
let a: Int? = nil
let b: Int? = 2
let c = a ?? b ?? 3
var a: Int??? = 10
var b: Int = 20
var c: Int? = 30
print(a ?? b) // Optional(Optional(10))
print(a ?? c) // Optional(Optional(10))
??
和if let
配合使用
let a: Int? = nil
let b: Int? = 2
if let c = a ?? b {
print(c)
}// 类似于if a != nil || b != nil
if let c = a, let d = b {
print(c)
print(d)
}// 类似于if a != nil && b != nil
4. 隐式解包(Implicitly Unwrapped Optional)
-
- 在某些情况下,可选项一旦被设定值之后,就会一直拥有值
-
- 在这种情况下,可以去掉检查,也不必每次访问的时候都进行解包,因为他能确定每次访问的时候都有值
-
- 可以在类型后面加个感叹号
!
定义一个隐式解包的可选项
let num1: Int! = 10 let num2: Int = num1 if num1 != nil { print(num1 + 6) } if let num3 = num1 { print(num3) }
- 可以在类型后面加个感叹号
如果对空值的可选项进行隐式解包,也会报错:
用隐式解包的可选项类型,大多数是希望别人要给定一个不为空的值
- 如果别人传的是个空值那就报错,目的就是制定你的规则,更多适用于做一个接口来接收参数;
- 更多还是建议不使用该类型
5. 字符串插值
-
- 可选项在字符串插值或者直接打印时,编译器会发出警告
-
- 至少有三种方法消除警告
var age: Int? = 10 print("My age is \(age!)") // My age is 10 print("My age is \(String(describing: age))") // My age is Optional(10) print("My age is \(age ?? 0)") // My age is 10
6. 多重可选项
-
- 看下面几个可选类型,可以用以下图片来解析
var num1: Int? = 10 var num2: Int?? = num1 var num3: Int?? = 10 print(num2 == num3) // true
-
- 可使用
lldb
指令frame variable -R
或者fr v -R
查看区别
- 可使用
-
- 看下面几个可选类型,可以用以下图片来解析
var num1: Int? = nil var num2: Int?? = num1 var num3: Int?? = nil print(num2 == num3) // false print(num3 == num1) // false(因为类型不同) (num2 ?? 1) ?? 2 // 2 (num3 ?? 1) ?? 2 // 1
-
- 不管是多少层可选项,一旦赋值为
nil
,就只有最外层一个大盒子
可使用lldb
指令frame variable -R
或者fr v -R
查看区别
- 不管是多少层可选项,一旦赋值为
十一、guard语句
-
- 当
guard语句
的条件为false
时,就会执行大括号里面的代码
- 当
-
- 当
guard语句
的条件为true
时,就会跳过guard语句
- 当
-
guard语句
适合用来“提前退出”
guard 条件 else { // do something.... 退出当前作用域 // return、break、continue、throw error }
-
- 当使用
guard语句
进行可选项绑定时,绑定的常量(let)、变量(var)
也能在外层作用域中使用
func login(_ info: [String : String]) { guard let username = info["username"] else { print("请输入用户名") return } guard let password = info["password"] else { print("请输入密码") return } // if username .... // if password .... print("用户名:(username)", "密码:(password)", "登录ing") } login(["username" : "jack", "password" : "123456"]) // 用户名:jack 密码:123456 登陆ing login(["password" : "123456"]) // 请输入密码 login(["username" : "jack"]) // 请输入用户名
- 在没有
guard
语句之前,用if-else条件分支语句代码如下(比对):
func login(_ info: [String : String]) { let username: String if let tmp = info["username"] { username = tmp } else { print("请输入用户名") return } let password: String if let tmp = info["password"] { password = tmp } else { print("请输入密码") return } // if username .... // if password .... print("用户名:\(username)", "密码:\(password)", "登陆ing") } login(["username" : "jack", "password" : "123456"]) // 用户名:jack 密码:123456 登陆ing login(["password" : "123456"]) // 请输入密码 login(["username" : "jack"]) // 请输入用户名
- 当使用
十二、typealias
用来给类型起别名
typealias Byte = Int8
typealias Short = Int16
typealias Long = Int64
typealias Date = (year: String, mouth: String, day: String)
func getDate(_ date: Date) {
print(date.day)
print(date.0)
}
getDate(("2011", "9", "10"))
typealias IntFn = (Int, Int) -> Int
func difference(v1: Int, v2: Int) -> Int {
v1 - v2
}
let fn: IntFn = difference
fn(20, 10)
func setFn(_ fn: IntFn) { }
setFn(difference)
func getFn() -> IntFn { difference }
按照Swift标准库
的定义,Void
就是空元组()
十三、区间
1. 区间的几种类型
闭区间 ClosedRange<Int>
1...3
半开区间 Range<Int>
1..<3
单侧区间 PartialRangeThrough<Int>
...3
2. 字符、字符串也能使用区间运算符,但默认不能用在for-in
中
let stringRange1 = "cc"..."ff"// ClosedRange<String>
stringRange1.contains("cd")// false
stringRange1.contains("dz") // true
stringRange1.contains("fg") // false
let stringRange2 = "a"..."f"
stringRange2.contains("d") // true
stringRange2.contains("h") // false
// \0到~囊括了所有可能要用到的ASCII字符
let characterRange:ClosedRange<Character> = "\0"..."~"
characterRange.contains("G")// true
3. 带间隔的区间值
let hours = 10
let hourInterval = 2
// tickmark的取值,从4开始,累加2,不超过10
for tickmark in stride(from: 4, through: hours, by: hourInterval) {
print(tickmark)
// 4,6,8,10
}
首先补充一下常用基础语法的知识点
十四、集合类型
1. 集合类型的定义
集合的定义:
- 集合就是用来存储一组数据的容器。
- 三种典型的集合类型:
数组
、集合
和字典
。
2. 集合和字典
集合和字典:
- 集合和字典类型也是存储了
相同类型数据
的集合,但是数据之间是无序
的。 集合不允许值重复
出现。- 字典中的值可以重复出现,但是每一个值都有唯一的键值与其对应。
2.1 集合
定义
- 集合中的元素是相同数据类型的,并且元素值是唯一的。
- 集合中的元素是无序的。
声明格式
Set<DataType>
a.集合的初始化
b.集合的为空判断和元素插入
c.删除元素
d.检索特定元素
e.遍历集合
f.集合排序
g.集合间的运算
2.2 字典
a. 字典的声明
b. 字典的初始化
c. 字典元素的更新
d. 字典元素的删除
e. 遍历字典
f. 字典的keys属性和values属性
2.3.数组
数组定义 数组是一种按照顺序来存储相同类型数据的集合,相同的值可以多次出现在一个数组中的不同位置
- 类型安全
- 数组是类型安全的,数组中包含的数据类型必须是明确的
- 声明格式
- 数组的声明格式为:
Array<DataType>
或[DataType]
- 数组的声明格式为:
2.3.1 常用函数
-
isEmpty
用来判断数组是否为空
-
append
用来向数组的末端添加一个元素
//实例
//创建了一个空的字符串数组,然后通过isEmpty来判断数组是否为空,再通过append来添加新的元素到数组中。
var animalArray = [String]()
if animalArray.isEmpty {
print("数组animalArray是空的 ")
}
animalArray.append("tiger")
animalArray.append("lion")
2.3.2 数组初始化
2.3.3 数组的相加和累加
2.3.4 数组的下标操作
2.3.5 插入和删除元素
2.3.6 数组的遍历
2.3.7 数组的片段
2.3.8 通过数组片段生成新数组
2.3.9 元素交换位置
2.3.10 数组排序
2.3.11 数组元素的检索
专题系列文章
1. 前知识
- 01-探究iOS底层原理|综述
- 02-探究iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM】
- 03-探究iOS底层原理|LLDB
- 04-探究iOS底层原理|ARM64汇编
2. 基于OC语言探索iOS底层原理
- 05-探究iOS底层原理|OC的本质
- 06-探究iOS底层原理|OC对象的本质
- 07-探究iOS底层原理|几种OC对象【实例对象、类对象、元类】、对象的isa指针、superclass、对象的方法调用、Class的底层本质
- 08-探究iOS底层原理|Category底层结构、App启动时Class与Category装载过程、load 和 initialize 执行、关联对象
- 09-探究iOS底层原理|KVO
- 10-探究iOS底层原理|KVC
- 11-探究iOS底层原理|探索Block的本质|【Block的数据类型(本质)与内存布局、变量捕获、Block的种类、内存管理、Block的修饰符、循环引用】
- 12-探究iOS底层原理|Runtime1【isa详解、class的结构、方法缓存cache_t】
- 13-探究iOS底层原理|Runtime2【消息处理(发送、转发)&&动态方法解析、super的本质】
- 14-探究iOS底层原理|Runtime3【Runtime的相关应用】
- 15-探究iOS底层原理|RunLoop【两种RunloopMode、RunLoopMode中的Source0、Source1、Timer、Observer】
- 16-探究iOS底层原理|RunLoop的应用
- 17-探究iOS底层原理|多线程技术的底层原理【GCD源码分析1:主队列、串行队列&&并行队列、全局并发队列】
- 18-探究iOS底层原理|多线程技术【GCD源码分析1:dispatch_get_global_queue与dispatch_(a)sync、单例、线程死锁】
- 19-探究iOS底层原理|多线程技术【GCD源码分析2:栅栏函数dispatch_barrier_(a)sync、信号量dispatch_semaphore】
- 20-探究iOS底层原理|多线程技术【GCD源码分析3:线程调度组dispatch_group、事件源dispatch Source】
- 21-探究iOS底层原理|多线程技术【线程锁:自旋锁、互斥锁、递归锁】
- 22-探究iOS底层原理|多线程技术【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】
- 23-探究iOS底层原理|内存管理【Mach-O文件、Tagged Pointer、对象的内存管理、copy、引用计数、weak指针、autorelease
3. 基于Swift语言探索iOS底层原理
关于函数
、枚举
、可选项
、结构体
、类
、闭包
、属性
、方法
、swift多态原理
、String
、Array
、Dictionary
、引用计数
、MetaData
等Swift基本语法和相关的底层原理文章有如下几篇:
- 01-📝Swift5常用核心语法|了解Swift【Swift简介、Swift的版本、Swift编译原理】
- 02-📝Swift5常用核心语法|基础语法【Playground、常量与变量、常见数据类型、字面量、元组、流程控制、函数、枚举、可选项、guard语句、区间】
- 03-📝Swift5常用核心语法|面向对象【闭包、结构体、类、枚举】
- 04-📝Swift5常用核心语法|面向对象【属性、inout、类型属性、单例模式、方法、下标、继承、初始化】
- 05-📝Swift5常用核心语法|高级语法【可选链、协议、错误处理、泛型、String与Array、高级运算符、扩展、访问控制、内存管理、字面量、模式匹配】
- 06-📝Swift5常用核心语法|编程范式与Swift源码【从OC到Swift、函数式编程、面向协议编程、响应式编程、Swift源码分析】
其它底层原理专题
1. 底层原理相关专题
2. iOS相关专题
- 01-iOS底层原理|iOS的各个渲染框架以及iOS图层渲染原理
- 02-iOS底层原理|iOS动画渲染原理
- 03-iOS底层原理|iOS OffScreen Rendering 离屏渲染原理
- 04-iOS底层原理|因CPU、GPU资源消耗导致卡顿的原因和解决方案