Swift第四章 闭包、属性

闭包

闭包表达式(Closure Expression)

  1. 在Swift中,可以通过func定义一个函数,也可以通过闭包表达式定义一个函数
  2. 闭包书写格式

     {
         (参数列表) -> 返回值类型 in
         函数体代码
     }
    
  3. 使用场景:一个函数返回值是一个函数
  4. 举例:

     //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)
    
  5. 闭包表达式的简写

     //定义函数,第三个参数是函数
     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: +)
    

尾随闭包

  1. 如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性
  2. 尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式

     func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
         print(fn(v1, v2))
     }
        
     //函数调用,即最后一个实参是闭包,将闭包表达式写到函数调用()外面,即用{}起来里面
     //看起来像函数定义,其实是函数调用,只是最后一个闭包实参放到了{}中
     exec(v1: 10, v2: 20) {
         $0 + $1
     }
    
  3. 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后边写圆括号

     func exec(fn: (Int, Int) -> Int) {
         print(fn(1, 2))
     }
     //方式1
     exec(fn: { $0 + $1 })
     //方式2
     exec() { $0 + $1 }
     //方式3,省略()
     exec { $0 + $1 }
    
  4. 示例:数组的排序

     //官方定义,有一个参数,且为函数
     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)重难点!!!

  1. 网上有各种关于闭包的定义,个人觉得比较严谨的定义是
  2. 一个函数和它所捕获的变量\常量环境组合起来,称为闭包
    1. 般指定义在函数内部的函数
    2. 一般它捕获的是外层函数的局部变量\常量
  3. 举例:

     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
    
  4. 可以把闭包想象成是一个类的实例对象
    1. 内存在堆空间
    2. 捕获的局部变量\常量就是对象的成员(存储属性)
    3. 组成闭包的函数就是类内部定义的方法
     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
    

注意

  1. 如果返回值是函数类型,那么参数的修饰要保持统一
//外层返回值是函数,返回的函数参数要求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. 案例

     // 如果第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)
    
    1. @autoclosure 会自动将 20 封装成闭包 { 20 }
    2. @autoclosure 只支持 () -> T 格式的参数,即函数类型无参,有返回值。
    3. @autoclosure 并非只支持最后1个参数
    4. 空合并运算符 ?? 使用了 @autoclosure 技术
    5. 有@autoclosure、无@autoclosure,构成了函数重载,调用方式不一样,所以可以重载
    6. 为了避免与期望冲突,使用了@autoclosure的地方最好明确注释清楚:这个值会被推迟执行。因为有可能一直都不会执行。

属性

  1. Swift中跟实例相关的属性可以分为2大类
    1. 存储属性(Stored Property)
      1. 类似于成员变量这个概念
      2. 存储在实例的内存中
      3. 结构体、类可以定义存储属性
      4. 枚举不可以定义存储属性
    2. 计算属性(Computed Property)
      1. 本质就是方法(函数)
      2. 不占用实例的内存
      3. 枚举、结构体、类都可以定义计算属性
  2. 举例

     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
    

存储属性

  1. 创建类 或 结构体的实例时,必须为所有的存储属性设置一个合适的初始值
  2. 可以在初始化器(init方法)里为存储属性设置一个初始值
  3. 可以分配一个默认的属性值作为属性定义的一部分

     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()
    

计算属性

  1. set传入的新值默认叫做newValue,也可以自定义

     struct Circle {
         var radius: Double
         var diameter: Double {
             //自定义newValue
             set(newDiameter) {
                 radius = newDiameter / 2
             }
             get {
                 radius * 2
             }
         }
     }
    
  2. 只读计算属性:只有get,没有set

     struct Circle {
         var radius: Double
         var diameter: Double {
             get {
                 radius * 2
             }
         }
     }
        
     //等价
     struct Circle {
         var radius: Double
         var diameter: Double { radius * 2 }
     }
    
  3. 定义计算属性只能用var,不能用let
    1. let代表常量:值是一成不变的
    2. 计算属性的值是可能发生变化的(即使是只读计算属性)
  4. 计算属性特点:要么有set、get方法,要么只有get方法,而且属性类型后面直接{}封装

总结(一定要分清楚)

  1. 存储属性
    1. 没有实现get、set方法
    2. 该属性在实例内存中有存储
  2. 计算属性:
    1. 该属性有实现set方法、set方法,或者只有get方法(注意get方法的简写)
    2. 该属性在实例内存中没有存储,属性值通过调用get方法得到

枚举rawValue原理

  1. 枚举原始值rawValue的本质是:只读计算属性
  2. 因此不占用内存
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)

  1. 使用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)
         }()
     }
    

注意点

  1. lazy属性必须是var,不能是let,因为let必须在实例的初始化方法完成之前就拥有
  2. 如果多条线程同时第一次访问lazy属性,无法保证属性只被初始化1次
  3. 当结构体包含一个延迟存储属性时,只有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)

  1. 可以为非lazyvar存储属性设置属性观察器

     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)
    
  2. willSet会传递新值,默认叫newValue
  3. didSet会传递旧值,默认叫oldValue
  4. 在初始化器中设置属性值不会触发willSet和didSet
  5. 在属性定义时设置初始值也不会触发willSet和didSet
  6. 计算属性不需要使用willSet、didSet,因为在set方法中就可以监听了

全局变量、局部变量

  1. 属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量身上

     //全局变量计算属性
     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的再次研究

  1. 案例

     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
    

总结

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

类型属性(Type Property)

  1. 严格来说,属性可以分为实例属性(Instance Property)、类型属性(Type Property)
  2. 实例属性(Instance Property):只能通过实例去访问
    1. 存储实例属性(Stored Instance Property):存储在实例的内存中,每个实例都有1份
    2. 计算实例属性(Computed Instance Property)
  3. 类型属性(Type Property):只能通过类型去访问
    1. 存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似于全局变量)
    2. 计算类型属性(Computed Type Property)
  4. 可以通过static定义类型属性
    1. 如果是类,也可以用关键字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

类型属性细节

  1. 不同于存储实例属性,必须给存储类型属性设定初始值
    1. 因为类型没有像实例那样的init初始化器来初始化存储属性
  2. 存储类型属性默认就是lazy,会在第一次使用的时候才初始化
    1. 就算被多个线程同时访问,保证只会初始化一次
    2. 存储类型属性可以是let
  3. 枚举类型也可以定义类型属性(存储类型属性、计算类型属性)

单例模式

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()
Table of Contents