Swift 之流程控制、函数、枚举、可选类型、结构体和类、闭包

1、流程控制

1、if-else

1、if 后面的条件只能是 bool 类型。
2、if 后面的条件可以省略小括号,但条件后面的大括号不可以省略。

2、while

1、swift 中的 while 用法和 OC 类似。
2、repeat-while 相当于 C 语言中的 do-while。

3、for

闭区间运算符:a...b 即取值大于等于 a 且小于等于 b。
半开区间运算符:a.. 单侧区间:[a...]即取值从 a 到无限大;[.. 区间类型:

let range1: ClosedRange = 1...3
let range2: Range = 1..<3
let range3: PartialRangeThrough = ...5

字符、字符串:字符、字符串也能使用区间运算符,但默认不能用在 for-in 中。

//表示 str1 包含 a,b,c,d,e,f 
let str1 = "a"..."f"  

/*
表示 str2 包含如下:
第一个字符包含 c 到 f。
第二个字符有以下情况:
    第一个字符为 c 是,第二个字符为 c 到 z;
    第一个字符为 f 时,第二个字符为 a 到 f;
    否则第二个字符为 a 到 z。
*/
let str2 = "cc"..."ff"  

// \0到~囊括了所有可能要用到的ASCII字符
let characterRange: ClosedRange = "\0"..."~"

带间隔的区间值:

// t 的取值:从4开始,累加2,不超过11
for t in stride(from: 4, through: 11, by: 2) {
    print(tickMark) // 4 6 8 10
}
4、switch

1、默认可以不写 break,并不会贯穿到后面的条件。
2、使用 fallthrough 可以实现贯穿效果。
3、switch 必须要保证能处理所有情况。
4、case、default 后面至少要有一条语句,如果不想做任何事,加个 break 即可。
5、如果所有的条件都已处理,也可以不必使用 default。
6、switch 条件可以支持 Character、String、int、区间匹配、元组匹配。
7、switch 条件可以支持复合条件,每个条件用 ,分开。
8、值绑定

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
//必要时let也可以改为var
5、where:用来添加过滤条件
6、标签语句
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)")
    }
}

2、函数

函数的定义:func 函数名(参数类型) -> 返回值类型 {}

形参默认是 let

//无参且返回 int 类型
func config() -> Int {
    return 20
}

//有参且返回 Int 类型
func sum(v1: Int, v2: Int) -> Int{
    return v1 + v2
}
sum(v1: 10, v2: 5)

隐式返回
如果整个函数体是一个单表达式,那么函数会隐式返回这个表达式(隐藏 return)

func sum(v1: Int, v2: Int) -> Int{
   //会将 v1+v2 的值返回
    v1 + v2
}
sum(v1: 10, v2: 5)

返回元组:实现多返回值

//计算两个数的和、差、平均值
func sum(v1: Int, v2: Int) -> (sum: Int, difference: Int, average: Int){
    sum =  v1 + v2
    //sum >> 1:和右移一位就相当于除以2
    return (sum, v1-v2, sum >> 1)
}

let result = calculate(v1: 20, v2: 10)
result.0 //30
result.difference  //10
result.2  //15

函数文档注释:

    /// 求和【概述】
    ///
    /// 将2个整数相加【更详细的描述】
    ///
    /// - Parameter v1: 第1个整数
    /// - Parameter v2: 第2个整数
    /// - Returns: 2个整数的和
    ///
    /// - Note:传入2个整数即可【批注】
    ///
    func sum(v1: Int, v2: Int) -> Int {
        v1 + v2
    }

参数标签

func goToWork(at time: String) {
    print("this time is \(time)")
}
goToWork(at: "08:00")
// this time is 08:00
//省略参数标签
func sum(_ v1: Int, _ v2: Int) -> Int {
    v1 + v2
}
sum(10, 20)

默认参数值

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: "Rose", age: 18) // name=Rose, age=18, job=none
check(age: 10, job: "Batman") // name=nobody, age=10, job=Batman
check(age: 15) // name=nobody, age=15, job=none
//在省略参数标签时,需要特别注意,避免出错

可变参数

func sum(_ numbers: Int...) -> Int {
    var total = 0
    for number in numbers {
        total += number
    }
    return total
}
sum(10, 20, 30, 40) // 100
// 紧跟在可变参数后面的参数不能省略参数标签
// 参数 string 不能省略标签
func test(_ numbers: Int..., string: String, _ other: String) { }
test(10, 20, 30, string: "Jack", "Rose")

输入输出参数 inout
可以用 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)
func swapValues(_ v1: inout Int, _ v2: inout Int) {
    (v1, v2) = (v2, v1)
}

注:
1、可变参数不能标记为 inout
2、inout 参数不能有默认值
3、inout 参数只能传入可以被多次赋值的
4、如果实参有物理内存地址,且没有设置属性观察器,直接将实参的内存地址传入函数(实参进行引用传递)。
5、如果实参是计算属性或者设置了属性观察器,采取了 Copy In Copy Out 的做法。
a、调用该函数时,先复制实参的值,产生副本【get】。
b、将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值。
c、函数返回后,再将副本的值覆盖实参的值【set】。
总结:inout 的本质就是引用传递(地址传递)

函数重载
规则:函数名相同,参数个数不同或者参数类型不同或者参数标签不同。
1、返回值类型与函数重载无关。
2、默认参数值和函数重载一起使用产生二义性时,编译器并不会报错。
3、可变参数、省略参数标签、函数重载一起使用产生二义性时,编译器有可能会报错。

内联函数
内联函数可以将函数调用展开成函数体。如果开启了编译器优化(Release 模式默认会开启优化),编译器会自动将某些函数变成内联函数。
1、哪些函数不会被自动内联?
函数体比较长、包含递归调用、包含动态派发等。

2、@inline

// 永远不会被内联(即使开启了编译器优化)
@inline(never) func test() {
    print("test")
}

// 开启编译器优化后,即使代码很长,也会被内联(递归调用函数、动态派发的函数除外)
@inline(__always) func test() {
    print("test")
}

注:
在 Release 模式下,编译器已经开启优化,会自动决定哪些函数需要内联,因此没必要使用 @inline。

函数可以作为参数、返回值,此时的函数称为高阶函数

typealias 用来给类型起别名

嵌套函数:将函数定义在函数内部。

3、枚举

1、基本用法
enum Direction {
    case north
    case south
    case east
    case west
}

//等价于
enum Direction {
    case north, south, east, west
}
2、关联值

将枚举的成员值跟其他类型的值关联存储在一起。关联值是直接存储在枚举类型的内存中。

enum Score {
    case points(Int)
    case grade(Character)
}

var score = Score.points(96)
score = .grade("A")

switch score {
case let .points(i):
    print(i, "points")
case let .grade(i):
    print("grade", i)
} // grade A

enum Date {
    case digit(year: Int, month: Int, day: Int)
    case string(String)
}

var date = Date.digit(year: 2011, month: 9, day: 10)
date = .string("2011-09-10")
switch date {
case .digit(let year, let month, let day):
    print(year, month, day)
case let .string(value):
    print(value)
}
3、原始值

枚举成员可以使用相同类型的默认值预先对应,这个默认值叫做:原始值。原始值的 case 值是固定的,不可以更改。原始值不占用枚举的内存,也就是不会存储到枚举变量的内存中。

enum PokerSuit : Character {
  case spade = "♠"
  case heart = "♥"
  case diamond = "♦"
  case club = "♣"
}
4、隐式原始值

如果枚举的原始值类型是 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
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、递归枚举
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 two = ArithExpr.number(2)
let sum = ArithExpr.sum(five, four)
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)
问与答

1、以下枚举的内存

enum TestEnum {
    case test
}
print(MemoryLayout.stride)  //1
print(MemoryLayout.size)  //0, 
print(MemoryLayout.alignment)//1

分析:只有一个 case,无所谓用不用内存
enum TestEnum {
    case test1, test2, test3
}
print(MemoryLayout.stride)  //1
print(MemoryLayout.size)  //1
print(MemoryLayout.alignment)  //1

分析:用 1 个字节来区分 case
enum TestEnum : Int {
    case test1 = 1, test2 = 2, test3 = 3
}
print(MemoryLayout.stride)  //1
print(MemoryLayout.size)  //1
print(MemoryLayout.alignment)  //1

分析:初始值不存在枚举内存中,用 1 个字节来区分 case 即可
enum TestEnum {
    case test(Int)
}
print(MemoryLayout.stride)  //8
print(MemoryLayout.size)  //8
print(MemoryLayout.alignment)  //8

分析:关联值存储在枚举内存中,1 个 case 关联值类型为 int 用 8 个字节
enum TestEnum {
    case test1(Int, Int, Int)
    case test2(Int, Int)
    case test3(Int)
    case test4(Bool)
    case test5
}
print(MemoryLayout.stride)  //32
print(MemoryLayout.size)  //25
print(MemoryLayout.alignment)  //8

分析:
1 个字节用来区分 case。
N 个字节用来存储 case 占用内存最大的关联值。所有 case 的关联值共用这 N 个字节。
故:该枚举中实际分配 32 字节,占用 25 字节,内存对齐为 8 字节。
enum TestEnum {
    case test0
    case test1
    case test2
    case test4(Int)
    case test5(Int, Int)
    case test6(Int, Int, Int, Bool)
}
print(MemoryLayout.stride)  //32
print(MemoryLayout.size)  //25
print(MemoryLayout.alignment)  //8

分析:
enum TestEnum {
    case test0
    case test1
    case test2
    case test4(Int)
    case test5(Int, Int)
    case test6(Int, Int, Bool, Int)
}
print(MemoryLayout.stride)  //32
print(MemoryLayout.size)  //32
print(MemoryLayout.alignment)  //8

分析:
enum TestEnum {
    case test0
    case test1
    case test2
    case test4(Int)
    case test5(Int, Int)
    case test6(Int, Bool, Int)
}
print(MemoryLayout.stride)  //32
print(MemoryLayout.size)  //25
print(MemoryLayout.alignment)  //8

分析:

2、枚举的关联值和原始值
定义:
关联值:将枚举的成员值跟其他类型的值关联存储在一起。关联值是直接存储在枚举类型的内存中。
原始值:枚举成员可以使用相同类型的默认值预先对应,这个默认值叫做:原始值。

内存对比:
关联值:关联值是直接存储在枚举类型的内存中。枚举内存的大小 = N 个字节(枚举关联值类型中最大的 case) + 1 个字节(case 大于 1 个时,该字节用来区分 case,否则不需要该字节)。枚举中任何一个 case 都共用这 N 个字节。
原始值:原始值的 case 值是固定的,不可以更改。原始值不占用枚举变量的内存,也就是不会将值存储到枚举变量的内存中。

MemoryLayout

可以使用 MemoryLayout 获取数据类型占用的内存大小。

enum Password {
    case number(Int, Int, Int, Int)
    case other
}

MemoryLayout.stride // 40, 分配占用的空间大小
MemoryLayout.size // 33, 实际用到的空间大小
MemoryLayout.alignment // 8, 对齐参数

var pwd = Password.number(9, 8, 6, 4)
pwd = .other
MemoryLayout.stride(ofValue: pwd) // 40
MemoryLayout.size(ofValue: pwd) // 33
MemoryLayout.alignment(ofValue: pwd) // 8

4、可选类型

可选类型,它允许将值设置为 nil。在类型名称后面加个问号 ? 来定义一个可选项。Optional 是一个泛型枚举

enum Optional {
  case none
  case some(Wrapped)
}

Optional 类型表示:有值 / 没有值。

强行打开 - 不安全
let a: String = x!

隐式解包变量声明 - 在许多情况下不安全
var a = x!

可选链接 - 安全
let a = x?.count

无合并操作员 - 安全
let a = x ?? ""

可选绑定 - 安全
if let a = x {
  print("x was successfully unwrapped and is = \(a)")
}

警卫声明 - 安全
guard let a = x else {
  return
}

可选模式 - 安全
if case let a? = x {
  print(a)
}

5、结构体和类

1、结构体

在 Swift 标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分。比如 Bool、Int、Double、 String、Array、Dictionary 等常见类型都是结构体。所有的结构体都有一个编译器自动生成的初始化器。

结构体的初始化器:编译器会根据情况,可能会为结构体生成多个初始化器,其宗旨是:保证所有成员都有初始值。

自定义初始化器:一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器。

2、类

类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器。

类的初始化器:如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器。成员的初始化是在这个初始化器中完成的。

3、结构体和类的区别

结构体是值类型(枚举也是值类型),类是引用类型(指针类型)。

值类型:值类型赋值给 var、let 或者给函数传参,是直接将所有内容拷贝一份。产生了全新的文件副本,属于深拷贝。

值类型的写时复制
在 Swift 标准库中,为了提升性能,String、Array、Dictionary、Set 采取了 Copy On Write 的技术。比如仅当有操作时,才会真正执行拷贝操作。对于标准库值类型的赋值操作,Swift 能确保最佳性能,所以没必要为了保证最佳性能来避免赋值。 建议:不需要修改的,尽量定义成 let。

引用类型:引用赋值给 var、let 或者给函数传参,是将内存地址拷贝一份,属于浅拷贝。

嵌套类型:

struct Poker {
    enum Suit : Character {
        case spades = "♠", hearts = "♥", diamonds = "♦", clubs = "♣"
    }
    enum Rank : Int {
        case two = 2, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king, ace
    }
}

print(Poker.Suit.hearts.rawValue)
var suit = Poker.Suit.spades
suit = .diamonds
var rank = Poker.Rank.five
rank = .king

枚举、结构体、类都可以定义方法
一般把定义在枚举、结构体、类内部的函数,叫做方法。

class Size {
    var width = 10
    var height = 10
    func show() {
        print("width=\(width), height=\(height)")
    }
}
let s = Size()
s.show() // width=10, height=10
enum PokerFace : Character {
    case spades = "♠", hearts = "♥", diamonds = "♦", clubs = "♣"
    func show() {
        print("face is \(rawValue)")
    }
}
let pf = PokerFace.hearts
pf.show() // face is ♥
struct Point {
    var x = 10
    var y = 10
    func show() {
        print("x=\(x), y=\(y)")
    }
}
let p = Point()
p.show() // x=10, y=10

方法占用对象的内存么?
不占用。方法的本质就是函数,方法、函数都存放在代码段。

6、闭包

1、函数表达式

在 Swift 中,可以通过 func 定义一个函数,也可以通过闭包表达式定义一个函数。

//函数表达式 
func sum(_ v1: Int, _ v2: Int) -> Int { v1 + v2 }

//闭包表达式定义一个函数
var fn = {(v1: Int, v2: Int) -> Int in
    return v1 + v2
}
fn(10, 20)

//闭包表达式
var fn = {(参数列表) -> 返回值类型 in
    函数体代码
}
fn(X, Y)//函数调用(fn 为函数名)
2、闭包表达式的简写
//示例
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}

//调用简写过程
exec(v1: 10, v2: 20, fn: {(v1: Int, v2: Int) -> Int in
    return v1 + v2
})
exec(v1: 10, v2: 20, fn: {v1, v2 in 
    return v1 + v2
})
exec(v1: 10, v2: 20, fn: {v1, v2 in 
    v1 + v2
})
exec(v1: 10, v2: 20, fn: { $0 + $1 })
exec(v1: 10, v2: 20, fn: +)
3、尾随闭包

如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性。
1、尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式。
2、如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后边写圆括号。

func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}

exec(v1: 10, v2: 20) {$0 + $1}
func exec(fn: (Int, Int) -> Int) {
    print(fn(1, 2))
}

exec(fn: { $0 + $1 })
exec() { $0 + $1 }
exec { $0 + $1 }

示例:数组排序

func sort(by areInIncreasingOrder: (Element, Element) -> Bool)

/// 返回true: i1排在i2前面
/// 返回false: i1排在i2后面
func cmp(i1: Int, i2: Int) -> Bool {
    // 大的排在前面
    return i1 > i2
}

//使用
var nums = [11, 2, 18, 6, 5, 68, 45]
nums.sort(by: cmp)
// [68, 45, 18, 11, 6, 5, 2]
//闭包方式,以下所有方式等效
nums.sort(by: {(i1: Int, i2: Int) -> Bool in
    return i1 < i2
})
nums.sort(by: { i1, i2 in return i1 < i2 })
nums.sort(by: { i1, i2 in i1 < i2 })
nums.sort(by: { $0 < $1 })
nums.sort(by: <)
nums.sort() { $0 < $1 }
nums.sort { $0 < $1 }
//结果: [2, 5, 6, 11, 18, 45, 68]
4、自动闭包

示例:如果第1个数大于0,返回第一个数。否则返回第2个数

func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
    return v1 > 0 ? v1 : v2
}
getFirstPositive(10, 20) // 10
getFirstPositive(-2, 20) // 20
getFirstPositive(0, -4) // -4
//修改需求:当 v1 > 0 成立的时候返回 v1 并不执行 v2(), 当 v1 > 0 不成立的时候才执行 v2()
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int? {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive(-4) { 20 }
//自动闭包 @autoclosure :会将数据自动包装为闭包
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int? {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive(-4, 20)

//分析:
@autoclosure 会自动将 20 封装成 { 20 }
@autoclosure 只支持 () -> T 格式的参数
@autoclosure 并非只可以是最后一个参数
?? 运算符其实就是 @autoclosure 
有 @autoclosure  的函数和无 @autoclosure 的函数会构成函数重载
5、闭包

一个函数和它所捕获的变量\常量环境组合起来,称为闭包。一般指定义在函数内部的函数。一般它捕获的是外层函数的局部变量\常量。


可以把闭包想象成是一个类的实例对象,其同样包含类相关信息、引用计数、成员变量。
1、内存在堆空间
2、捕获的局部变量\常量就是对象的成员(存储属性)
3、组成闭包的函数就是类内部定义的方法

//闭包内容可以类比如下
class Closure {   //闭包可以比作为类
    var num = 0    //闭包捕获的常量/变量
    func plus(_ i: Int) -> Int {  //闭包方法
        num += i
        return num
    }
}
var cs1 = Closure()
var cs2 = Closure()
cs1.plus(1) // 1
cs2.plus(2) // 2
cs1.plus(3) // 4
cs2.plus(4) // 6
cs1.plus(5) // 9
cs2.plus(6) // 12

你可能感兴趣的:(Swift 之流程控制、函数、枚举、可选类型、结构体和类、闭包)