Swift第四章 闭包、属性
闭包
闭包表达式(Closure Expression)
- 在Swift中,可以通过func定义一个函数,也可以通过闭包表达式定义一个函数
-
闭包书写格式
{ (参数列表) -> 返回值类型 in 函数体代码 } - 使用场景:一个函数返回值是一个函数
-
举例:
//func 定义函数 func sum(_ v1: Int, _ v2: Int) -> Int { v1 + v2 } //闭包 var fn = { (v1: Int, v2: Int) -> Int in return v1 + v2 } //调用闭包不需要写参数标签 fn(10, 20) //定义闭包,直接调用 { (v1: Int, v2: Int) -> Int in return v1 + v2 }(10, 20) -
闭包表达式的简写
//定义函数,第三个参数是函数 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 }) //简写方式1 exec(v1: 10, v2: 20, fn: { v1, v2 in return v1 + v2 }) //简写方式2 exec(v1: 10, v2: 20, fn: { v1, v2 in v1 + v2 }) //简写方式3 exec(v1: 10, v2: 20, fn: { $0 + $1 }) //简写方式4 exec(v1: 10, v2: 20, fn: +)
尾随闭包
- 如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性
-
尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式
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)) } //方式1 exec(fn: { $0 + $1 }) //方式2 exec() { $0 + $1 } //方式3,省略() 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] //sort参数为函数 nums.sort(by: cmp) // [68, 45, 18, 11, 6, 5, 2] //sort参数为闭包 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]
忽略参数
func exec(fn: (Int, Int) -> Int) {
print(fn(1, 2))
}
//不管闭包的参数传值什么返回结果都是10,因此可以用_忽略参数
exec { _,_ in 10 } // 10
闭包(Closure)重难点!!!
- 网上有各种关于闭包的定义,个人觉得比较严谨的定义是
- 一个函数和它所捕获的变量\常量环境组合起来,称为闭包
- 般指定义在函数内部的函数
- 一般它捕获的是外层函数的局部变量\常量
-
举例:
typealias Fn = (Int) -> Int func getFn() -> Fn { var num = 0 func plus(_ i: Int) -> Int { num += i return num } return plus } // 返回的plus和num形成了闭包 //plus也可以写成闭包表达式 func getFn() -> Fn { var num = 0 //也可以将plus写成闭包表达式 return { num += $0 return num } } //1. 调用getFn函数之后,该函数内部的局部变量num会被释放 //本质:因为plus函数捕获了局部变量num,为了延长生命周期,因此将这个变量的值拷贝放到堆空间---通过汇编查看调用了alloc分配堆空间,这样就可以继续是用num的值 var fn1 = getFn() //每调用一次,就会重新分配一次num到堆空间 var fn2 = getFn() //理论上,若num被释放,下面的函数调用都会报错,但是却可以正常调用,为什么??? //根据打印可以看出,fn1、fn2捕获的num局部变量不是同一个 fn1(1) // 1 fn2(2) // 2 fn1(3) // 4 fn2(4) // 6 fn1(5) // 9 fn2(6) // 12 - 可以把闭包想象成是一个类的实例对象
- 内存在堆空间
- 捕获的局部变量\常量就是对象的成员(存储属性)
- 组成闭包的函数就是类内部定义的方法
class Closure { var num = 0 func plus(_ i: Int) -> Int { num += i return num } } //创建2个不同对象 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
注意
- 如果返回值是函数类型,那么参数的修饰要保持统一
//外层返回值是函数,返回的函数参数要求inout
func add(_ num: Int) -> (inout Int) -> Void {
//内层函数参数要保持一致为inout
func plus(v: inout Int) {
v += num
}
return plus
}
var num = 5
add(20)(&num)
print(num)
自动闭包
-
案例
// 如果第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,v2一定会用到,那么是否可以做到,v1如果大于0,v2直接不使用呢?----把v2变为函数类型,只有V1不满足,才需要通过调用V2,拿到数据。 // 改成函数类型的参数,可以让v2延迟加载 func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int? { return v1 > 0 ? v1 : v2() } //尾随闭包 getFirstPositive(-4) { 20 } //自动闭包,使用尾随闭包看起来还是有点不易读取 func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int? { return v1 > 0 ? v1 : v2() } //自动闭包调用方式 getFirstPositive(-4, 20)- @autoclosure 会自动将 20 封装成闭包 { 20 }
- @autoclosure 只支持 () -> T 格式的参数,即函数类型无参,有返回值。
- @autoclosure 并非只支持最后1个参数
- 空合并运算符 ?? 使用了 @autoclosure 技术
- 有@autoclosure、无@autoclosure,构成了函数重载,调用方式不一样,所以可以重载
- 为了避免与期望冲突,使用了@autoclosure的地方最好明确注释清楚:这个值会被推迟执行。因为有可能一直都不会执行。
属性
- Swift中跟实例相关的属性可以分为2大类
- 存储属性(Stored Property)
- 类似于成员变量这个概念
- 存储在实例的内存中
- 结构体、类可以定义存储属性
- 枚举不可以定义存储属性
- 计算属性(Computed Property)
- 本质就是方法(函数)
- 不占用实例的内存
- 枚举、结构体、类都可以定义计算属性
- 存储属性(Stored Property)
-
举例
struct Circle { // 存储属性,半径 var radius: Double // 计算属性:直径,这个属性没有存储,每次获取都是通过radius计算出来的 //定义一个diameter属性,同时定义他的set、get方法 var diameter: Double { set { radius = newValue / 2 } get { radius * 2 //等价 //return = radius * 2 } } } var circle = Circle(radius: 5) print(circle.radius) // 5.0 //本质调用diameter属性的get方法 print(circle.diameter) // 10.0 //本质调用diameter属性的set方法 circle.diameter = 12 print(circle.radius) // 6.0 print(circle.diameter) // 12.0 //打印内存 print(MemoryLayout<Circle>.stride) // 8
存储属性
- 在创建类 或 结构体的实例时,必须为所有的存储属性设置一个合适的初始值
- 可以在初始化器(init方法)里为存储属性设置一个初始值
-
可以分配一个默认的属性值作为属性定义的一部分
struct Poit { var x : Int = 11 var y : Int = 12 /* init () { x = 21 y = 22 } */ } //var p = Point(Int:10 , Int 12) var p = Point()
计算属性
-
set传入的新值默认叫做newValue,也可以自定义
struct Circle { var radius: Double var diameter: Double { //自定义newValue set(newDiameter) { radius = newDiameter / 2 } get { radius * 2 } } } -
只读计算属性:只有get,没有set
struct Circle { var radius: Double var diameter: Double { get { radius * 2 } } } //等价 struct Circle { var radius: Double var diameter: Double { radius * 2 } } - 定义计算属性只能用var,不能用let
- let代表常量:值是一成不变的
- 计算属性的值是可能发生变化的(即使是只读计算属性)
- 计算属性特点:要么有set、get方法,要么只有get方法,而且属性类型后面直接{}封装
总结(一定要分清楚)
- 存储属性
- 没有实现get、set方法
- 该属性在实例内存中有存储
- 计算属性:
- 该属性有实现set方法、set方法,或者只有get方法(注意get方法的简写)
- 该属性在实例内存中没有存储,属性值通过调用get方法得到
枚举rawValue原理
- 枚举原始值rawValue的本质是:只读计算属性
- 因此不占用内存
enum TestEnum : Int {
case test1 = 1, test2 = 2, test3 = 3
//本质是一个计算属性,get方法的省略写法,直接返回一个值
var rawValue: Int {
switch self {
case .test1:
return 10
case .test2:
return 11
case .test3:
return 12
}
}
}
print(TestEnum.test3.rawValue) // 12
延迟存储属性(Lazy Stored Property)
-
使用lazy可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化
class Car { init() { print("Car init!") } func run() { print("Car is running!") } } class Person { lazy var car = Car() init() { print("Person init!") } func goOut() { car.run() } } let p = Person() print("--------") p.goOut() //上面打印如下,说明Car是用到的时候才会初始化 Person init! -------- Car init! Car is running! class PhotoView { //此时image是存储属性,后面用的是“=”,直接等于闭包的表达式调用结果。当真正用到image属性的时候才会调用网络请求,获取图片数据。 lazy var image: Image = {//闭包表达式调用 let url = "https://www.520it.com/xx.png" let data = Data(url: url) return Image(data: data) }() }
注意点
- lazy属性必须是var,不能是let,因为let必须在实例的初始化方法完成之前就拥有
- 如果多条线程同时第一次访问lazy属性,无法保证属性只被初始化1次
-
当结构体包含一个延迟存储属性时,只有var才能访问延迟存储属性,因为延迟属性初始化时需要改变结构体的内存
//该结构体包含一个延迟存储属性 struct Point { var x = 0 var y = 1 lazy var z = 0 } //这里只能用var修饰 //let p = Point() var p = Point() //第一次访问这个延迟存储属性,此时z会被赋值为0,因此p必须用var修饰。 print(p.z)
属性观察器(Property Observer)
-
可以为非lazy的var存储属性设置属性观察器
struct Circle { //该属性为var修饰的存储属性,因为{}内部不是get、set方法 var radius: Double { willSet { print("willSet", newValue) } didSet { print("didSet", oldValue, radius) } } init() { self.radius = 1.0 print("Circle init!") } } // Circle init! var circle = Circle() // willSet 10.5 // didSet 1.0 10.5 circle.radius = 10.5 // 10.5 print(circle.radius) - willSet会传递新值,默认叫newValue
- didSet会传递旧值,默认叫oldValue
- 在初始化器中设置属性值不会触发willSet和didSet
- 在属性定义时设置初始值也不会触发willSet和didSet
- 计算属性不需要使用willSet、didSet,因为在set方法中就可以监听了
全局变量、局部变量
-
属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量身上
//全局变量计算属性 var num: Int { get { return 10 } set { print("setNum", newValue) } } num = 11 // setNum 11 print(num) // 10 func test() { //局部变量设置属性观察器 var age = 10 { willSet { print("willSet", newValue) } didSet { print("didSet", oldValue, age) } } age = 11 // willSet 11 // didSet 10 11 } test()
inout的再次研究
-
案例
struct Shape { //宽,存储属性 var width: Int //几条边,存储属性 var side: Int { willSet { print("willSetSide", newValue) } didSet { print("didSetSide", oldValue, side) } } //周长,计算属性 var girth: Int { set { width = newValue / side print("setGirth", newValue) } get { print("getGirth") return width * side } } func show() { print("width=\(width), side=\(side), girth=\(girth)") } } func test(_ num: inout Int) { num = 20 } var s = Shape(width: 10, side: 4) //获取对象内存width属性的地址传递 test(&s.width) s.show() print("----------") /* &s.side,是不是直接获取第二个存储属性的地址直接传递呢? 1. 首先获取s.side的值,然后赋值给临时空间 tem = 4 2. 然后将&tem传递给test,修改tem = 20 3. 调用side属性set方法,然后把临时存储空间的值20作为参数 疑问:为什么不直接拿到实例中side的属性地址直接传递呢? 以为side属性设置的有属性监控willSet、didSet,如果需要被触发,必须调用setter方法,因此,不能直接通过side的属性地址直接修改 */ test(&s.side) s.show() print("----------") /* s.girth是计算属性,对象内存中没有这个属性的地址,那么是如何修改呢? 1. 先调用girth属性get方法,然后将返回结果,放到临时存储空间tem = 20 2. 将临时存储空间的地址值&tem传递给test函数作为参数,执行test修改临时存储空间的值为20,即temp = 20 3. 调用girth属性set方法,然后把临时存储空间的值20作为参数 */ test(&s.girth) s.show() //打印如下: getGirth width=20, side=4, girth=80 ---------- willSetSide 20 didSetSide 4 20 getGirth width=20, side=20, girth=400 ---------- getGirth setGirth 20 getGirth width=1, side=20, girth=20
总结
- 如果实参有物理内存地址,且没有设置属性观察器
- 直接将实参的内存地址传入函数(实参进行引用传递)
- 如果实参是计算属性 或者 设置了属性观察器
- 采取了Copy In Copy Out的做法
- 调用该函数时,先复制实参的值,产生副本【get】
- 将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值
- 函数返回后,再将副本的值覆盖实参的值【set】
- 总结:inout的本质就是引用传递(地址传递)
类型属性(Type Property)
- 严格来说,属性可以分为实例属性(Instance Property)、类型属性(Type Property)
- 实例属性(Instance Property):只能通过实例去访问
- 存储实例属性(Stored Instance Property):存储在实例的内存中,每个实例都有1份
- 计算实例属性(Computed Instance Property)
- 类型属性(Type Property):只能通过类型去访问
- 存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似于全局变量)
- 计算类型属性(Computed Type Property)
- 可以通过static定义类型属性
- 如果是类,也可以用关键字class
struct Car {
//类型属性
static var count: Int = 0
init() {
Car.count += 1
}
}
let c1 = Car()
let c2 = Car()
let c3 = Car()
//创建3个实例,Car.count也累加
print(Car.count) // 3
类型属性细节
- 不同于存储实例属性,必须给存储类型属性设定初始值
- 因为类型没有像实例那样的init初始化器来初始化存储属性
- 存储类型属性默认就是lazy,会在第一次使用的时候才初始化
- 就算被多个线程同时访问,保证只会初始化一次
- 存储类型属性可以是let
- 枚举类型也可以定义类型属性(存储类型属性、计算类型属性)
单例模式
public class FileManager {
public static let shared = FileManager()
private init() { }
func open () {}
}
public class FileManager {
public static let shared = {
// ....,创建之前,做一些事
// ....
return FileManager()
}()
private init() { }
}
//调用
FileManager.shared.open()