02-📝Swift5常用核心语法|基础语法【Playground、常量与变量、常见数据类型、字面量、元组、流程控制、函数、枚举、可选项、guard语句、区间】

702 阅读38分钟

image.png

一、概述

本系列文章旨在复习Swift5核心语法且适当进行底层原理探索,属于阶段性复习和巩固,以供日后进一步探索Swift语言的底层原理做铺垫。

整个系列文章如下,每一文章知识点独立成篇,欢迎各位按需或按兴趣点击阅读:

二、Playground

1. HelloWorld

image.png

print("Hello World")
  • 不用编写main函数,Swift将全局范围内的首句可执行代码作为程序入口
    • 通过反汇编我们可以看到底层会执行main函数 -w1084
  • 一句代码尾部可以省略分号(;),多句代码写到同一行时必须用分号(;)隔开

2. Playground

  • Playground可以 快速预览代码效果,是学习语法的好帮手
  • Command + Shift + Enter
    运行整个Playground
  • Shift + Enter
    运行截止到某一行代码
  • Playground - View image.png image.png
  • Playground - ViewController image.png image.png
  • Playground – 多Page
    • 添加Page|方法1
    • 添加Page|方法2

三、常量和变量

1. 常量

  • 1.用let定义常量(常量只能赋值一次)
    不用特意指明类型,编译器能自动推断出变量/常量的数据类型
    let a: Int = 10
    let b = 20
    
  • 2.它的值不要求在编译过程中确定,但使用之前必须赋值一次
    这样写确定了a的类型,之后再去赋值,也不会报错
    let a: Int
    a = 10
    
    1. 用函数给常量赋值也可以,函数是在运行时才会确定值的,所以只要保证使用之前赋值了就行
    func getNumber() -> Int {
        return 10
    }
    
    let a: Int
    a = getNumber()
    
    1. 常量、变量在初始化之前,都不能使用 image.png
    1. 如果没有给a确定类型,也没有一开始定义的时候赋值,就会像下面这样报错 -w643

2. 变量

    1. var定义变量
var b = 20
b = 30
    1. 常量、变量在初始化之前,都不能使用 -w644

3. 注释

  • 1.Swift中有单行注释和多行注释
    注释之间嵌套也没有问题 image.png
// 单行注释

/*
 多行注释
*/

/*
  1
 /* 释嵌套 */
 2 
*/ 
  • 2.Playground里的注释支持Markup语法(同Markdown)
    Markup语法只在Playground里有效,在项目中无效
//: # 一级标题

/*:
 ## 基础语法
 */

可以通过Editor -> Show Raw Markup来预览

-w299

预览的效果如下

-w369

4. 标识符

  1. 标识符(比如常量名、变量名、函数名)几乎可以使用任何字符
    let 📒 = 5
    var 😁 = 10
    
    func 👽() {
    
    }
    

标识符

  • 不能数字开头
  • 不能包含空白字符制表符箭头等特殊字符

-w649

四、常见数据类型

1. 值类型

  • 枚举(enum):
    • Optional
  • 结构体(struct):
    • 非集合类型:
      • Bool
      • Double
      • Float
      • Int
      • Character
      • String
    • 集合类型:
      • Array
      • Dictionary
      • Set

2. 引用类型

  • 类(class)

可以通过command+control进入到该类型的API中查看

例如Int类型

-w757

3. 整数类型

  • 整数类型:Int8Int16Int32Int64UInt8UInt16UInt32UInt64
  • 在32bit平台,Int等于Int32;在64bit平台,Int等于Int64
  • 整数的最值:UInt8.maxInt16.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类型

-w718

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
    
    1. 循环里的i默认是let,如需要更改加上var
    for var i in 1...3 {
        i += 5
        print(i)
    }// 6 7 8
    
    1. 不需要值的时候用_来表示
    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,不同的是:

    1. 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
    }
    
    1. 默认不写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注意点

    1. switch必须要保证能处理所有情况 注意:像判断number的值,要考虑到所有整数的条件,如果不要判断全部情况,加上default就可以了 -w722
    1. case、default后面至少要有一条语句
      如果不想做任何事,加个break即可
    var number = 1
    
    switch number {
    case 1:
        print("number is 1")
    case 2:
        break
    default:
        break
    }
    
    1. 如果能保证已处理所有情况,也可以不必使用default
    enum Answer { case right, wrong }
    
    let answer = Answer.right
    
    switch answer {
    case .right:
        print("right")
    case .wrong:
        print("wrong")
    }
    

复合条件

    1. switch也支持CharacterString类型
    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
    }
    
    1. switch可以同时判断多个条件
    let string = "Jack"
    
    switch string {
    case "Jack", "Rose":
        print(string)
    default:
        break
    }// Right person  
    
    1. 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")
    }
    
    1. 可以使用下划线_忽略某个值
      关于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
}

-w592

详细参照Apple官方的api设计准则

5. 参数标签(Argument Label)

    1. 可以修改参数标签
func gotoWork(at time: String) {
    print("this time is \(time)")
} 
gotoWork(at: "8:00")// this time is 08:00
    1. 可以使用下划线_省略参数标签,为了阅读性一般不建议省略
func sum(_ value1: Int, _ value2: Int) -> Int {
     value1 + value2
} 
sum(5, 5)

6. 默认参数值(Default Parameter Value)

    1. 参数可以有默认值
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
    1. C++的默认参数值有个限制:必须从右往左设置;由于Swift拥有参数标签,因此没有此类限制
    1. 但是在省略参数标签时,需要特别注意,避免出错
// 这里的middle不可以省略参数标签
func test(_ first: Int = 10, middle: Int, _ last: Int = 30) { }
test(middle: 20)

7. 可变参数(Variadic Parameter)

    1. 一个函数最多只能有一个可变参数
    func sum(_ numbers: Int...) -> Int {
        var total = 0 
        for number in numbers {
            total += number
        } 
        return total
    } 
    sum(1, 2, 3, 4)
    
    1. 紧跟在可变参数 后面的参数不能省略参数标签
    // 参数string不能省略标签
    func get(_ number: Int..., string: String, _ other: String) { }
    get(10, 20, string: "Jack", "Rose")
    

Swift自带的print函数 我们可以参考下Swift自带的print函数 -w828

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 -w674

    • 可以利用元组来进行参数交换
    func swapValues(_ v1: inout Int, _ v2: inout Int) {
            (v1, v2) = (v2, v1)
    }
    
    var num1 = 10
    var num2 = 20
    swapValues(&num1, &num2)
    
    1. 可变参数不能标记为inout -w708
    1. inout参数不能有默认值 -w704
    1. inout参数只能传入可以被多次赋值的
    • 常量只能在定义的时候赋值一次,所以下面会报错 -w712
    1. inout参数的本质是地址传递
    • 我们新建个项目,通过反汇编来观察其本质 -w671
    • leaq表示的就是地址传递,可以看出在调用函数之前先将两个变量的地址放到了寄存器中 -w1119

9. 函数重载(Function Overload)

    1. 函数重载的规则
    • 函数名相同
    • 参数个数不同 || 参数类型不同 || 参数标签不同
    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 }
    

函数重载注意点

    1. 返回值类型和函数重载无关 -w711
    1. 默认参数值和函数重载一起使用产生二义性时,编译器并不会报错(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)
    1. 可变参数、省略参数标签、函数重载一起使用产生二义性时,编译器有可能会报错 -w723

10. 内联函数(Inline Function)

如果开启了编译器优化(Release模式默认会开启优化),编译器会自动将某些函数变成内联函数

  • 将函数调用展开成函数体 -w829

我们分别来观察下更改Debug模式下的优化选项,编译器做了什么
1.我们新建一个项目,项目代码如下 -w551 2. 然后我们先通过反汇编观察没有被优化时的编译器做了什么 -w1059 可以看到会调用test函数,然后test函数里面再执行打印

-w1051

  1. 现在我们开启Debug模型下的优化选项,然后运行程序 -w619 发现print打印直接就在main函数里执行了,没有了test函数的调用过程
    相当于print函数直接放到了main函数中,编译器会将函数调用展开成函数体 -w1061

哪些函数不会被内联

  • 函数体比较长
  • 包含递归调用
  • 包含动态派发(运行时的多态调用(OC、Swift混编的时候才会有运行时,纯粹的Swift项目是没有runtime的))

@inline 我们可以使用@inline关键字,来主动控制编译器是否做进行优化

    1. @inline(nerver):永远不会被内联,即使开启了编译器优化
    @inline(nerver) func test() {}
    
    1. @inline(__alaways):开启编译器优化后,即使代码很长,也会被内联(递归调用和动态派发除外)
    @inline(__alaways) func test() {}
    
    1. Release模式下,编译器已经开启优化,会自动决定哪些函数需要内联,因此没必要使用@inline

11. 函数类型(Function Type)

    1. 每一个函数都是有类型的,函数类型由形参类型返回值类型组成
    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  调用时不需要参数标签
    
    1. 函数类型作为函数参数
    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
    
    1. 函数类型作为函数返回值
      返回值是函数类型的函数叫做高阶函数(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)

    1. 将函数定义在函数内部
    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)
}

关联值举例 image.png image.png

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)

如果枚举的原始值类型是IntString,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)

    1. 递归枚举要用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

    1. 可以使用MemoryLayout获取数据类型占用的内存大小
      64bitInt类型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 *进入到内存布局的页面

-w440

2.还可以从Xcode标题栏中选择Debug -> Debug Workflow -> View Memory进入到内存布局的页面

-w569

3.进入到该页面,然后通过输入变量的内存地址来查看

-w1129

4.我们可以下载一个小工具来获取到变量的内存地址

下载地址:github.com/CoderMJLee/…

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赋值的三句代码上,然后运行程序观察每次断点之后的内存布局有什么变化

-w1127

-w1124

-w1124

通过上图可以看到,每个枚举变量都分配了一个字节的大小,并且存储的值分别是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

-w1131

-w1126

-w1125

通过上图可以看到,每个枚举变量存储的值也是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个字节,然后我们通过断点分别来观察枚举变量的内存布局

-w773 -w1124

执行完第一句我们可以看到,前面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

-w719 -w1193

执行完第二句我们可以看到,前面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

-w563 -w1196

执行完第三句我们可以看到,前面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

-w665 -w1192

执行完第四句我们可以看到,由于是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

-w676 -w1191

执行完最后一句我们可以看到,由于没有关联值,那么只用了第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)

    1. 可选项,一般也叫可选类型,它允许将值设置为nil
    1. 在类型名称后面加个问号 ?来定义一个可选项
    var name: String? = nil
    
    1. 如果可选类型定义的时候没有给定值,默认值就是nil
    var age: Int?
    
    等价于
    var age: Int? = nil
    
    1. 如果可选类型定义的时候赋值了,那么就是一个Optional类型的值
    var name: String? = "Jack" // Optional(Jack)
    
    1. 可选类型也可以作为函数返回值使用
    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)

可选项是对其他类型的一层包装,可以理解为一个盒子

    1. 如果为nil,那么它就是个空盒子
    1. 如果不为nil,那么盒子里装的是:被包装类型的数据
    var age: Int?
    age = 10
    age = nil
    
    • 可选关系的类型大致如下图: -w606
    1. 如果要从可选项中取出被包装的数据(将盒子里装的东西取出来),需要使用感叹号 !进行强制解包
    var age: Int? = 10
    var ageInt = age!
    ageInt += 10 // ageInt为Int类型
    
    1. 如果对值为nil的可选项(空盒子)进行强制解包,将会产生运行时错误 -w668

2. 可选项绑定(Optional Binding)

    1. 我们可以判断可选项是否包含值
    let number = Int("123") // number为Int?
    
    if number != nil {
        print(number!)
    }
    
    1. 还可以使用可选项绑定来判断可选项是否包含值
    • 如果包含就自动解包,把值赋给一个临时的常量(let)或者变量(var),并返回true,否则返回false
    if let number = Int("123") {
         print("字符串转换整数成功:(number)")
         // number是强制解包之后的Int值
         // number作用域仅限于这个大括号
    } else {
        print("字符串转换整数失败")
    }
    // 字符串转换整数成功:123
    
    1. 如果判断条件有多个,可以合并在一起,用逗号,来分隔开
    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")
    }
    
    1. 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,就会返回后一个值 -w860 -w871

详细用法如下:

  • a ?? b
    • a可选项
    • b可选项或者不是可选项
    • ba存储类型必须相同
    • 如果a不为nil,就返回a
      • 如果b不是可选项,返回a时会自动解包
    • 如果anil,就返回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)

    1. 在某些情况下,可选项一旦被设定值之后,就会一直拥有值
    1. 在这种情况下,可以去掉检查,也不必每次访问的时候都进行解包,因为他能确定每次访问的时候都有值
    1. 可以在类型后面加个感叹号!定义一个隐式解包的可选项
    let num1: Int! = 10
    let num2: Int = num1
    
    if num1 != nil {
        print(num1 + 6)
    }
    
    if let num3 = num1 {
        print(num3)
    }
    

如果对空值的可选项进行隐式解包,也会报错: -w687

用隐式解包的可选项类型,大多数是希望别人要给定一个不为空的值

  • 如果别人传的是个空值那就报错,目的就是制定你的规则,更多适用于做一个接口来接收参数
  • 更多还是建议不使用该类型

5. 字符串插值

    1. 可选项在字符串插值或者直接打印时,编译器会发出警告 -w708
    1. 至少有三种方法消除警告
    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. 多重可选项

    1. 看下面几个可选类型,可以用以下图片来解析
    var num1: Int? = 10
    var num2: Int?? = num1
    var num3: Int?? = 10 
    
    print(num2 == num3) // true
    

    -w787

    1. 可使用lldb指令frame variable -R或者fr v -R查看区别 -w1124
    1. 看下面几个可选类型,可以用以下图片来解析
    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
    

-w784

    1. 不管是多少层可选项,一旦赋值为nil,就只有最外层一个大盒子
      可使用lldb指令frame variable -R或者fr v -R查看区别 -w1126

十一、guard语句

    1. guard语句的条件为false时,就会执行大括号里面的代码
    1. guard语句的条件为true时,就会跳过guard语句
    1. guard语句适合用来“提前退出”
    guard 条件 else {
        // do something....
        退出当前作用域
        // return、break、continue、throw error
    }
    
    1. 当使用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就是空元组()

-w314

十三、区间

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. 集合类型的定义

image.png 集合的定义:

  • 集合就是用来存储一组数据的容器。
  • 三种典型的集合类型:数组集合字典

2. 集合和字典

集合和字典:

  • 集合和字典类型也是存储了相同类型数据的集合,但是数据之间是无序的。
  • 集合不允许值重复出现。
  • 字典中的值可以重复出现,但是每一个值都有唯一的键值与其对应。

2.1 集合

定义

  • 集合中的元素是相同数据类型的,并且元素值是唯一的。
  • 集合中的元素是无序的。

声明格式

  • Set<DataType>

a.集合的初始化

image.png

b.集合的为空判断和元素插入

image.png

c.删除元素

image.png

d.检索特定元素

image.png

e.遍历集合

image.png

f.集合排序

image.png

g.集合间的运算

image.png

2.2 字典

a. 字典的声明

image.png

b. 字典的初始化

image.png

c. 字典元素的更新

image.png

d. 字典元素的删除

image.png

e. 遍历字典

image.png

f. 字典的keys属性和values属性

image.png

2.3.数组

数组定义 数组是一种按照顺序来存储相同类型数据的集合,相同的值可以多次出现在一个数组中的不同位置

  • 类型安全
    • 数组是类型安全的,数组中包含的数据类型必须是明确的
  • 声明格式
    • 数组的声明格式为: Array<DataType>[DataType]

2.3.1 常用函数

    1. isEmpty 用来判断数组是否为空
    1. append 用来向数组的末端添加一个元素
//实例
//创建了一个空的字符串数组,然后通过isEmpty来判断数组是否为空,再通过append来添加新的元素到数组中。
var animalArray = [String]()
if animalArray.isEmpty {
    print("数组animalArray是空的 ")
}
animalArray.append("tiger")
animalArray.append("lion")

2.3.2 数组初始化

image.png

2.3.3 数组的相加和累加

image.png

2.3.4 数组的下标操作

image.png

2.3.5 插入和删除元素

image.png

2.3.6 数组的遍历

image.png

2.3.7 数组的片段

image.png image.png

2.3.8 通过数组片段生成新数组

image.png

2.3.9 元素交换位置

image.png

2.3.10 数组排序

image.png

2.3.11 数组元素的检索

image.png

专题系列文章

1. 前知识

2. 基于OC语言探索iOS底层原理

3. 基于Swift语言探索iOS底层原理

关于函数枚举可选项结构体闭包属性方法swift多态原理StringArrayDictionary引用计数MetaData等Swift基本语法和相关的底层原理文章有如下几篇:

其它底层原理专题

1. 底层原理相关专题

2. iOS相关专题

3. webApp相关专题

4. 跨平台开发方案相关专题

5. 阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

6. Android、HarmonyOS页面渲染专题

7. 小程序页面渲染专题