04-📝Swift5常用核心语法|面向对象【属性、inout、类型属性、单例模式、方法、下标、继承、初始化】

1,490 阅读36分钟

一、概述

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

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

二、属性

1. 属性的基本概念

Swift中跟实例相关的属性可以分为2大类:

  • 存储属性(Stored Property)
    • 类似于成员变量的概念
    • 存储在实例的内存中
    • 结构体、类可以定义存储属性
    • 枚举不可以定义存储属性
  • 计算属性(Computed Property)
    • 本质就是方法(函数)
    • 不占用实例的内存
    • 枚举、结构体、类都可以定义计算属性

1.1 存储属性

关于存储属性,Swift有个明确的规定:

  • 在创建类或结构体的实例时,必须为所有的存储属性设置一个合适的初始值
  • 可以在初始化器里为存储属性设置一个初始值
    struct Point {
        // 存储属性
        var x: Int
        var y: Int
    } 
    let p = Point(x: 10, y: 10) 
    
  • 可以分配一个默认的属性值作为属性定义的一部分
    struct Point {
        // 存储属性
        var x: Int = 10
        var y: Int = 10
    } 
    let p = Point() 
    

1.2 计算属性

定义计算属性只能用var,不能用let

  • let代表常量,值是一直不变的
  • 计算属性的值是可能发生变化的(即使是只读计算属性)
    struct Circle {
        // 存储属性
        var radius: Double 
        // 计算属性
        var diameter: Double {
            set {
                radius = newValue / 2
            } 
            get {
                radius * 2
            }
        }
    }
    
    var circle = Circle(radius: 5)
    print(circle.radius) // 5.0
    print(circle.diameter) // 10.0
    
    circle.diameter = 12
    print(circle.radius) // 6.0
    print(circle.diameter) // 12.0
    
  • set传入的新值默认叫做newValue,也可以自定义
    struct Circle {
        // 存储属性
        var radius: Double 
        // 计算属性
        var diameter: Double {
            set(newDiameter) {
                radius = newDiameter / 2
            } 
            get {
                radius * 2
            }
        }
    } 
    var circle = Circle(radius: 5)
    circle.diameter = 12
    print(circle.diameter) 
    
  • 只读计算属性,只有get,没有set
    struct Circle {
        // 存储属性
        var radius: Double 
        // 计算属性
        var diameter: Double {
            get {
                radius * 2
            }
        }
    } 
    
    struct Circle {
        // 存储属性
        var radius: Double 
        // 计算属性
        var diameter: Double { radius * 2 }
        }
    } 
    
  • 打印Circle结构体的内存大小,其占用才8个字节,其本质是因为计算属性相当于函数
    var circle = Circle(radius: 5)
    print(Mems.size(ofVal: &circle)) // 8
    

我们可以通过反汇编来查看其内部做了什么

  • 可以看到内部会调用set方法去计算 -w723
  • 然后我们在往下执行,还会看到get方法的调用 -w722
  • 所以可以用此证明:计算属性只会生成gettersetter,不会开辟内存空间

注意:

  • 一旦将存储属性变为计算属性,初始化构造器就会报错,只允许传入存储属性的值
  • 因为存储属性是直接存储在结构体内存中的,如果改成计算属性则不会分配内存空间来存储 -w646 -w525
  • 如果只有setter也会报错 -w651
  • 只读计算属性:只有get,没有set
    struct Circle {
        var radius: Double 
        var diameter: Double { 
            get { 
                radius * 2 
            }
        } 
    }
    //可以简写成
    struct Circle {
        var radius: Double 
        var diameter: Double { radius * 2  } 
    }
    

2. 枚举rawValue原理(计算属性)

    1. 枚举原始值rawValue的本质也是计算属性,而且是只读的计算属性
    enum TestEnum: Int {
        case test1, test2, test3
    
        var rawValue: Int {
            switch self {
            case .test1:
                return 10
            case .test2:
                return 20
            case .test3:
                return 30
            }
        }
    } 
    print(TestEnum.test1.rawValue)//10
    
    1. 下面我们去掉自己写的rawValue,然后转汇编看下本质是什么样的
    • 可以看到底层确实是调用了getter
        enum TestEnum: Int {
            case test1, test2, test3
        }
    
        print(TestEnum.test1.rawValue)
    
    -w717

3. 延迟存储属性(Lazy Stored Property)

    1. 使用lazy可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化
    • 看下面的示例代码,如果不加lazy,那么Person初始化之后就会进行Car的初始化
    • 加上lazy,只有调用到属性的时候才会进行Car的初始化
    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()
    
    // 打印:
    // Person init!
    // ----
    // Car init!
    // Car is running!
    
    1. lazy属性必须是var,不能是let
      let必须在实例的初始化方法完成之前就拥有值
    class PhotoView {
        lazy var image: UIImage = {
            let url = "http://www.***.com/logo.png"
            let data = Data(url: url)
            return UIImage(data: data)
        }()
    } 
    
    1. 注意: lazy属性和普通的存储属性内存布局是一样的,不同的只是什么分配内存的时机,而且lazy属性可以通过闭包进行初始化
    1. 延迟存储属性的注意点
    • 1.如果多条线程同时第一次访问lazy属性,无法保证属性只被初始化一次
    • 2.当结构体包含一个延迟存储属性时,只有var才能访问延迟存储属性
      因为延迟存储属性初始化时需要改变结构体的内存 -w652

4. 属性观察器(Property Observer)

    1. 可以为非lazyvar存储属性设置属性观察器
    • 只有存储属性可以设置属性观察器
    • willSet会传递新值,默认叫newValue
    • didSet会传递旧值,默认叫oldValue
    struct Circle {
        // 存储属性
        var radius: Double {
            willSet {
                print("willSet", newValue)
            }
    
            didSet {
                print("didSet", oldValue, radius)
            }
        }
    
        init() {
            radius = 1.0
            print("Circle init!")
        }
    }
    
    var circle = Circle()
    circle.radius = 10.5
    
    // 打印
    // willSet 10.5
    // didSet 1.0 10.5 
    
    1. 在初始化器中设置属性值不会触发willSetdidSet
    struct Circle {
        // 存储属性
        var radius: Double {
            willSet {
                print("willSet", newValue)
            }
    
            didSet {
                print("didSet", oldValue, radius)
            }
        }
    
        init() {
            radius = 1.0
            print("Circle init!")
        }
    }
    
    var circle = Circle() 
    
    1. 在属性定义时设置初始值也不会触发willSetdidSet
    struct Circle {
        // 存储属性
        var radius: Double = 1.0 {
            willSet {
                print("willSet", newValue)
            }
    
            didSet {
                print("didSet", oldValue, radius)
            }
        }
    }
    
    var circle = Circle()
    
    1. 计算属性设置属性观察器会报错 -w657

5. 全局变量和局部变量

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

5.1 全局变量

var num: Int {
    get {
        return 10
    }
    
    set {
        print("setNum", newValue)
    }
}

num = 11 // setNum 11
print(num) // 10 

5.2 局部变量

func test() {
    var age = 10 {
        willSet {
            print("willSet", newValue)
        }
        
        didSet {
            print("didSet", oldValue, age)
        }
    }
        
    age = 11
    // willSet 11
    // didSet 10 11
}

test() 

二、inout

1. inout对属性的影响

看下面的示例代码,分别输出什么,为什么?

struct Shape { 
    var width: Int   
    var side: Int { 
        willSet { 
            print("willSet", newValue) 
        }
  
        didSet { 
            print("didSet", 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) 
test(&s.width) 
s.show()
 
print("--------------------")   
test(&s.side)

s.show() 
print("--------------------") 
test(&s.girth) 
s.show()
 

// 打印: 
//getGirth 
//width=20, side=4, girth=80 
//-------------------- 
//willSet 20 
//didSet 4 20 
//getGirth 
//width=20, side=20, girth=400 
//-------------------- 
//getGirth 
//setGirth 20 
//getGirth 
//width=1, side=20, girth=20 

第一段打印
初始化的时候会给width赋值为10,side赋值为4,并且不会调用side的属性观察器
然后调用test方法,并传入width的地址值,width变成20
然后调用show方法,会调用girth的getter,然后先执行打印,再计算,girth为80

下面我们通过反汇编来进行分析 -w963 -w963 -w965 -w807

第二段打印
现在width的值是20,side的值是4,girth的值是80
然后调用test方法,并传入side的地址值,side变成20,并且触发属性观察器,执行打印
然后调用show方法,会调用girth的getter,然后先执行打印,再计算,girth为400

下面我们通过反汇编来进行分析

-w960 -w351

将地址值存储到rdi中,并带入到test函数中进行计算

-w959 -w960 -w870

  • setter中才会真正的调用willSetdidSet方法
  • willSetdidSet之间的计算才是真正的将改变了的值覆盖了全局变量里的side
  • 真正改变了side的值的时候是调用完test函数之后,在内部的setter里进行的

第三段打印
现在width的值是20,side的值是20,girth的值是400
然后调用test方法,并传入girth的getter的返回值为400,然后将20赋值给girth的setter计算,width变为1
然后调用show方法,,会调用girth的getter,然后先执行打印,再计算,girth为20

下面我们通过反汇编来进行分析

-w962 -w371

-w961 -w963 -w425

-w958 -w399

-w961 -w675

-w963 -w614

-w960 -w822

-w961 -w958 -w837

再后面都是计算的过程了,这里就不详细跟进了

我们主要了解inout是怎么给计算属性进行关联调用的,从上面分析可以看出:

  • 从调用girth的getter开始,都会将计算的结果放入一个寄存器中
  • 然后通过这个寄存器的地址再进行传递
  • inout影响的也是修改这个寄存器中存储的值,然后再进一步传递到setter里进行计算

2. inout的本质总结

-w947

对于没有属性观察器的存储属性来说:

  • inout的本质就是传进来一个地址值,然后将值存储到这个地址对应的存储空间内

对于设置了属性观察器和计算属性来说:

  • inout会先将传进来的地址值放到一个局部变量中,然后改变局部变量地址值对应的存储空间

  • 再将改变了的局部变量值覆盖最初传进来的参数的值

    • 这时会对应触发属性观察器willSet、didSet和计算属性的setter、getter的调用
  • 如果不这么做,直接就改变了传进来的地址值的存储空间的话,就不会调用属性观察器了,而计算属性因为没有分配内存来存储值,也就没办法更改了

  • 总结:inout的本质就是引用传递(地址传递)

三、类型属性(Type Property)

1. 两类属性

严格来说,属性可以分为两大类:

  • 实例属性(Instance Property):只能通过实例去访问
    • 存储实例属性(Stored Instance Property):存储在实例的内存中,每个实例都有一份
    • 计算实例属性(Computed Instance Property)
  • 类型属性(Type Property):只能通过类去访问
    • 存储类型属性(Stored Type Property):整个程序运行过程中,就只有一份内存(类似于全局变量)
    • 计算类型属性(Computed Type Property)
    1. 可以通过static定义类型属性
    struct Car {
        static var count: Int = 0
        init() {
            Car.count += 1
        }
    } 
    
    1. 如果是类,也可以用关键字class修饰计算属性类型
    class Car {
        class var count: Int {
            return 10
        }
    } 
    print(Car.count) 
    
    1. 类里面不能用class修饰存储属性类型 -w642

2. 类型属性细节

  • 不同于存储实例属性存储类型属性必须设定初始值,不然会报错
  • 因为类型没有像实例那样的init初始化器来初始化存储属性 -w640
  • 存储类型属性可以用let
    struct Car {
        static let count: Int = 0 
    } 
    print(Car.count) 
    
  • 枚举类型也可以定义类型属性(存储类型属性计算类型属性
    enum Shape {
        static var width: Int = 0
        case s1, s2, s3, s4
    } 
    var s = Shape.s1
    Shape.width = 5 
    
  • 存储类型属性默认就是lazy,会在第一次使用的时候进行初始化
    • 就算被多个线程同时访问,保证只会初始化一次

通过反汇编来分析类型属性的底层实现

我们先通过打印下面两组代码来做对比,发现存储类型属性的内存地址和前后两个全局变量正好相差8个字节,所以可以证明存储类型属性的本质就是类似于全局变量,只是放在了结构体或者类里面控制了访问权限:

var num1 = 5
var num2 = 6
var num3 = 7

print(Mems.ptr(ofVal: &num1)) // 0x000000010000c1c0
print(Mems.ptr(ofVal: &num2)) // 0x000000010000c1c8
print(Mems.ptr(ofVal: &num3)) // 0x000000010000c1d0
var num1 = 5

class Car {
    static var count = 1
}

Car.count = 6

var num3 = 7

print(Mems.ptr(ofVal: &num1)) // 0x000000010000c2f8
print(Mems.ptr(ofVal: &Car.count)) // 0x000000010000c300
print(Mems.ptr(ofVal: &num3)) // 0x000000010000c308

然后我们通过反汇编来观察: -w1086 -w1086 -w1085

-w508 -w1086

通过调用我们可以发现最后会调用到GCDdispatch_once,所以存储类型属性才会说是线程安全的,并且只执行一次

并且dispatch_once里面执行的代码就是static var count = 1

四、单例模式

public class FileManager {
    public static let shared = FileManager()
    private init() { }
    
    public func openFile() {
        
    }
}

FileManager.shared.openFile()

五、方法(Method)

1. 基本概念

枚举、结构体、类都可以定义实例方法类型方法

  • 实例方法(Instance Method): 通过实例对象调用
  • 类型方法(Type Method): 通过类型调用
    • 实例方法调用
      class Car {
          var count = 0
      
          func getCount() -> Int {
              count
          }
      }
      
      let car = Car()
      car.getCo 
      
    • 类型方法用static或者class关键字定义
      class Car {
          static var count = 0
      
          static func getCount() -> Int {
              count
          }
      }
      
      Car.getCount() 
      
    • 类型方法中不能调用实例属性,反之实例方法中也不能调用类型属性 -w645 -w644
  • 不管是类型方法还是实例方法,都会传入隐藏参数self
  • self在实例方法中代表实例对象
  • self在类型方法中代表类型
    // count等于self.count、Car.self.count、Car.count
    static func getCount() -> Int {
        self.count
    } 
    

2. mutating

  • 结构体枚举值类型,默认情况下,值类型的属性不能被自身的实例方法修改
  • func关键字前面加上mutating可以允许这种修改行为
    struct Point {
        var x = 0.0, y = 0.0
    
        mutating func moveBy(deltaX: Double, deltaY: Double) {
            x += deltaX
            y += deltaY
        }
    } 
    
    enum StateSwitch {
        case low, middle, high
    
        mutating func next() {
            switch self {
            case .low:
                self = .middle
            case .middle:
                self = .high
            case .high:
                self = .low
            }
        }
    } 
    

3. @discardableResult

  • func前面加上@discardableResult,可以消除函数调用后返回值未被使用的警告
    struct Point {
        var x = 0.0, y = 0.0
    
        @discardableResult mutating func moveX(deltaX: Double) -> Double {
            x += deltaX
            return x
        }
    }
    
    var p = Point()
    p.moveX(deltaX: 10) 
    

六、下标(subscript)

1. 基本概念

    1. 使用subscript可以给任意类型(枚举结构体)增加下标功能
      有些地方也翻译成:下标脚本
    1. subscript的语法类似于实例方法、计算属性,本质就是方法(函数)
class Point {
    var x = 0.0, y = 0.0
    
    subscript(index: Int) -> Double {
        set {
            if index == 0 {
                x = newValue
            } else if index == 1 {
                y = newValue
            }
        }
        
        get {
            if index == 0 {
                return x
            } else if index == 1 {
                return y
            } 
            return 0
        }
    }
}

var p = Point()
p[0] = 11.1
p[1] = 22.2
print(p.x) // 11.1
print(p.y) // 22.2
print(p[0]) // 11.1
print(p[1]) // 22.2 
    1. subscript中定义的返回值类型决定了getter中返回值类型和setternewValue的类型
    1. subscript可以接收多个参数,并且类型任意
    class Grid {
        var data = [
            [0, 1 ,2],
            [3, 4, 5],
            [6, 7, 8]
        ]
    
        subscript(row: Int, column: Int) -> Int {
            set {
                guard row >= 0 && row < 3 && column >= 0 && column < 3 else { return }
                data[row][column] = newValue
            }
    
            get {
                guard row >= 0 && row < 3 && column >= 0 && column < 3 else { return 0 }
                return data[row][column]
            }
        }
    } 
    var grid = Grid()
    grid[0, 1] = 77
    grid[1, 2] = 88
    grid[2, 0] = 99 
    
    1. subscript可以没有setter,但必须要有getter,同计算属性
    class Point {
        var x = 0.0, y = 0.0 
        subscript(index: Int) -> Double {
            get {
                if index == 0 {
                    return x
                } else if index == 1 {
                    return y
                } 
                return 0
            }
        }
    } 
    
    1. subscript如果只有getter,可以省略getter
    class Point {
        var x = 0.0, y = 0.0 
        subscript(index: Int) -> Double {
            if index == 0 {
                return x
            } else if index == 1 {
                return y
            } 
            return 0
        }
    } 
    
    1. subscript可以设置参数标签
      只有设置了自定义标签的调用才需要写上参数标签
      class Point {
          var x = 0.0, y = 0.0 
          subscript(index i: Int) -> Double {
              if i == 0 {
                  return x
              } else if i == 1 {
                  return y
              }
      
              return 0
          }
      }
      
      var p = Point()
      p.y = 22.2
      print(p[index: 1]) // 22.2 
      
  • 8.subscript可以是类型方法
    class Sum {
        static subscript(v1: Int, v2: Int) -> Int {
            v1 + v2
        }
    } 
    print(Sum[10, 20]) // 30 
    

通过反汇编来分析

看下面的示例代码,我们将断点打到图上的位置,然后观察反汇编

-w710

看到其内部是会调用setter来进行计算

-w708 -w714

然后再将断点打到这里来看

-w552

看到其内部是会调用getter来进行计算 -w712 -w716

经上述分析就可以证明subscript本质就是方法调用

2. 结构体和类作为返回值对比

看下面的示例代码

struct Point {
    var x = 0, y = 0
}

class PointManager {
    var point = Point()
    subscript(index: Int) -> Point {
        set { point = newValue }
        get { point }
    }
}

var pm = PointManager()
pm[0].x = 11 // 等价于pm[0] = Point(x: 11, y: pm[0].y)
pm[0].y = 22 // 等价于pm[0] = Point(x: pm[0].x, y: 22) 

如果我们注释掉setter,那么调用会报错 -w644 但是我们将结构体换成类,就不会报错了 -w624

  • 原因还是在于结构体是值类型,通过getter得到的Point结构体只是临时的值(可以想成计算属性),并不是真正的存储属性point,所以会报错
    • 通过打印也可以看出来要修改的并不是同一个地址值的point -w716
  • 但换成了类,那么通过getter得到的Point类是一个指针变量,而修改的是指向堆空间中的Point的属性,所以不会报错

3.接收多个参数的下标

class Grid {
    var data = [
        [0, 1, 2], 
        [3, 4, 5], 
        [6, 7, 8] 
    ] 
    
    subscript(row: Int, column: Int) -> Int { 
        set { 
            guard row >= 0 && row < 3 && column >= 0 && column < 3 else {
                return 
            }
            data[row][column] = newValue 
        } 
    
        get { 
            guard row >= 0 && row < 3 && column >= 0 && column < 3 else { 
                return 0 
            } 
            return data[row][column] 
        }

    } 
}

var grid = Grid() 
grid[0,1] = 77 
grid[1,2] = 88 
grid[2,0] = 99 
print(grid.data)

七、继承(Inheritance)

1. 基本概念

  • 继承: 值类型(结构体、枚举)不支持继承,只有引用类型的类支持继承
  • 基类: 没有父类的类,叫做基类
  • Swift并没有像OC、Java那样的规定,任何类最终都要继承自某个基类 image.png
  • 子类可以重写从父类继承过来的下标方法属性。重写必须加上override
    class Car {
        func run() {
            print("run")
        }
    }
    
    
    class Truck: Car {
        override func run() {
    
        }
    } 
    

2.内存结构

看下面几个类的内存占用是多少

class Animal {
    var age = 0
}

class Dog: Animal {
    var weight = 0
}

class ErHa: Dog {
    var iq = 0
}

let a = Animal()
a.age = 10
print(Mems.size(ofRef: a)) // 32
print(Mems.memStr(ofRef: a))

//0x000000010000c3c8
//0x0000000000000003
//0x000000000000000a
//0x000000000000005f

let d = Dog()
d.age = 10
d.weight = 20
print(Mems.size(ofRef: d)) // 32
print(Mems.memStr(ofRef: d))

//0x000000010000c478
//0x0000000000000003
//0x000000000000000a
//0x0000000000000014

let e = ErHa()
e.age = 10
e.weight = 20
e.iq = 30
print(Mems.size(ofRef: e)) // 48
print(Mems.memStr(ofRef: e))

//0x000000010000c548
//0x0000000000000003
//0x000000000000000a
//0x0000000000000014
//0x000000000000001e
//0x0000000000000000 
    1. 首先类内部会有16个字节:存储类信息引用计数
    1. 然后才是成员变量/常量的内存(存储属性)
    1. 又由于堆空间分配内存,存在内存对齐的概念,其原则分配的内存大小为16的倍数且刚好大于或等于初始化一个该数据类型变量所需的字节数
    1. 基于前面的规则,最终得出结论:所分配的内存空间分别占用为323248
    1. Tips:子类会继承自父类的属性,所以内存会算上父类的属性存储空间

3. 重写实例方法、下标

class Animal {
    func speak() {
        print("Animal speak")
    }
    
    subscript(index: Int) -> Int {
        index
    }
}

var ani: Animal
ani = Animal()
ani.speak()
print(ani[6])

class Cat: Animal {
    override func speak() {
        super.speak()
        
        print("Cat speak")
    }
    
    override subscript(index: Int) -> Int {
        super[index] + 1
    }
}

ani = Cat()
ani.speak()
print(ani[7]) 
    1. class修饰的类型方法下标,允许被子类重写
    class Animal {
        class func speak() {
            print("Animal speak")
        }
    
        class subscript(index: Int) -> Int {
            index
        }
    }
    
    
    Animal.speak()
    print(Animal[6])
    
    class Cat: Animal {
        override class func speak() {
            super.speak()
    
            print("Cat speak")
        }
    
        override class subscript(index: Int) -> Int {
            super[index] + 1
        }
    }
    
    Cat.speak()
    print(Cat[7]) 
    
    1. static修饰的类型方法、下标,不允许被子类重写 -w571 -w646
    1. 但是被class修饰的类型方法、下标,子类重写时允许使用static修饰
      但再后面的子类就不被允许了
    class Animal {
        class func speak() {
            print("Animal speak")
        }
    
        class subscript(index: Int) -> Int {
            index
        }
    }
    
    
    Animal.speak()
    print(Animal[6])
    
    class Cat: Animal {
        override static func speak() {
            super.speak()
    
            print("Cat speak")
        }
    
        override static subscript(index: Int) -> Int {
            super[index] + 1
        }
    }
    
    Cat.speak()
    print(Cat[7]) 
    
    -w634

4. 重写属性

    1. 子类可以将父类的属性(存储、计算)重写为计算属性
    class Animal {
        var age = 0
    }
    
    class Dog: Animal {
        override var age: Int {
            set {
    
            }
    
            get {
                10
            }
        }
        var weight = 0
    } 
    
    1. 但子类不可以将父类的属性重写为存储属性 -w644 -w638
    1. 只能重写var属性,不能重新let属性 -w642
    1. 重写时,属性名、类型要一致 -w639
    1. 子类重写后的属性权限不能小于父类的属性权限
    • 如果父类属性是只读的,那么子类重写后的属性可以是只读的也可以是可读可写的
    • 如果父类属性是可读可写的,那么子类重写后的属性也必须是可读可写的

4.1 重写实例属性

class Circle {
    // 存储属性
    var radius: Int = 0

    // 计算属性
    var diameter: Int {
        set(newDiameter) {
            print("Circle setDiameter")
            radius = newDiameter / 2
        }

        get {
            print("Circle getDiameter")
            return radius * 2
        }
    }
}

class SubCircle: Circle {
    override var radius: Int {
        set {
            print("SubCircle setRadius")
            super.radius = newValue > 0 ? newValue : 0
        }

        get {
            print("SubCircle getRadius")
            return super.radius
        }
    }
    
    override var diameter: Int {
        set {
            print("SubCircle setDiameter")
            super.diameter = newValue > 0 ? newValue : 0
        }

        get {
            print("SubCircle getDiameter")
            return super.diameter
        }
    }
}

var c = SubCircle()
c.radius = 6
print(c.diameter)

c.diameter = 20
print(c.radius)

//SubCircle setRadius

//SubCircle getDiameter
//Circle getDiameter
//SubCircle getRadius
//12

//SubCircle setDiameter
//Circle setDiameter
//SubCircle setRadius

//SubCircle getRadius
//10 
    1. 从父类继承过来的存储属性都会分配内存空间不管 之后会不会被重写为计算属性
    1. 如果重写的方法里的settergetter不写super,那么就会死循环
    class SubCircle: Circle {
        override var radius: Int {
            set {
                print("SubCircle setRadius")
                radius = newValue > 0 ? newValue : 0
            }
    
            get {
                print("SubCircle getRadius")
                return radius
            }
        }    
    } 
    

4.2 重写类型属性

    1. class修饰的计算类型属性,可以被子类重写
class Circle {
    // 存储属性
    static var radius: Int = 0 
    // 计算属性
    class var diameter: Int {
        set(newDiameter) {
            print("Circle setDiameter")
            radius = newDiameter / 2
        } 
        get {
            print("Circle getDiameter")
            return radius * 2
        }
    }
} 
class SubCircle: Circle { 
    override static var diameter: Int {
        set {
            print("SubCircle setDiameter")
            super.diameter = newValue > 0 ? newValue : 0
        } 
        get {
            print("SubCircle getDiameter")
            return super.diameter
        }
    }
}

Circle.radius = 6
print(Circle.diameter)

Circle.diameter = 20
print(Circle.radius)

SubCircle.radius = 6
print(SubCircle.diameter)

SubCircle.diameter = 20
print(SubCircle.radius)

//Circle getDiameter
//12

//Circle setDiameter
//10

//SubCircle getDiameter
//Circle getDiameter
//12

//SubCircle setDiameter
//Circle setDiameter
//10 
    1. static修饰的类型属性(计算、存储),不可以被子类重写 -w861

5. 属性观察器

    1. 可以在子类中为父类属性(除了只读计算属性、let属性)增加属性观察器
      重写后还是存储属性,不是变成了计算属性
    class Circle {
        var radius: Int = 1
    }
    
    class SubCircle: Circle {
        override var radius: Int {
            willSet {
                print("SubCircle willSetRadius", newValue)
            }
    
            didSet {
                print("SubCircle didSetRadius", oldValue, radius)
            }
        }
    }
    
    var circle = SubCircle()
    circle.radius = 10
    
    //SubCircle willSetRadius 10
    //SubCircle didSetRadius 1 10 
    
    1. 如果父类里也有属性观察器:
    • 那么子类赋值时,会先调用自己的属性观察器willSet,然后调用父类的属性观察器willSet
    • 并且在父类里面才是真正的进行赋值
    • 然后先父类的didSet,最后再调用自己的didSet
      class Circle {
          var radius: Int = 1 {
              willSet {
                  print("Circle willSetRadius", newValue)
              }
      
              didSet {
                  print("Circle didSetRadius", oldValue, radius)
              }
          }
      }
      
      class SubCircle: Circle {
          override var radius: Int {
              willSet {
                  print("SubCircle willSetRadius", newValue)
              }
      
              didSet {
                  print("SubCircle didSetRadius", oldValue, radius)
              }
          }
      }
      
      var circle = SubCircle()
      circle.radius = 10
      
      //SubCircle willSetRadius 10
      //Circle willSetRadius 10
      //Circle didSetRadius 1 10
      //SubCircle didSetRadius 1 10 
      
    1. 可以给父类的计算属性增加属性观察器
    class Circle {
        var radius: Int {
            set {
                print("Circle setRadius", newValue)
            }
    
            get {
                print("Circle getRadius")
                return 20
            }
        }
    }
    
    class SubCircle: Circle {
        override var radius: Int {
            willSet {
                print("SubCircle willSetRadius", newValue)
            } 
            didSet {
                print("SubCircle didSetRadius", oldValue, radius)
            }
        }
    }
    
    var circle = SubCircle()
    circle.radius = 10
    
    //Circle getRadius
    //SubCircle willSetRadius 10
    //Circle setRadius 10
    //Circle getRadius
    //SubCircle didSetRadius 20 20 
    

上面打印会先调用一次Circle getRadius是因为在设置值之前会先拿到它的oldValue,所以需要调用getter一次

为了测试,我们将oldValue的获取去掉后,再打印发现就没有第一次的getter的调用了

-w717

6. final

    1. final修饰的方法下标属性,禁止被重写 -w643 -w644 -w640
    1. final修饰的类,禁止被继承 -w642

7. 方法调用的本质

    1. 我们先看下面的示例代码,分析结构体的调用方法区别是什么
    struct Animal {
        func speak() {
            print("Animal speak")
        }
    
        func eat() {
            print("Animal eat")
        }
    
        func sleep() {
            print("Animal sleep")
        }
    }
    
    var ani = Animal()
    ani.speak()
    ani.eat()
    ani.sleep() 
    
    1. 反汇编之后,发现结构体的方法调用就是直接找到方法所在地址直接调用
      结构体的方法地址都是固定的 -w715
    1. 接下来 我们在看换成之后反汇编的实现是怎样的
    class Animal {
        func speak() {
            print("Animal speak")
        }
    
        func eat() {
            print("Animal eat")
        }
    
        func sleep() {
            print("Animal sleep")
        }
    }
    
    var ani = Animal()
    ani.speak()
    ani.eat()
    ani.sleep() 
    
    1. 反汇编之后,会发现需要调用的方法地址不确定
      所以凡是调用固定地址都不会是类的方法的调用 -w1189 -w1192 -w1190 -w1186 -w1185 -w1187 -w1189
    • 而且上述的几个调用的方法地址都是从rcx往高地址偏移8个字节来调用的,也就说明几个方法地址都是连续的
    • 我们再来分析下方法调用前做了什么:
    • 通过反汇编我们可以看到:
      • 会从全局变量的指针找到其指向的堆内存中的类的存储空间
      • 然后再根据类的前8个字节里的类信息知道需要调用的方法地址
      • 类信息的地址进行偏移找到方法地址,然后调用 -w1140
    • 然后我们将示例代码修改一下,再观察其本质是什么
    class Animal {
        func speak() {
            print("Animal speak")
        }
    
        func eat() {
            print("Animal eat")
        }
    
        func sleep() {
            print("Animal sleep")
        }
    }
    
    class Dog: Animal {
        override func speak() {
            print("Dog speak")
        }
    
        override func eat() {
            print("Dog eat")
        }
    
        func run() {
            print("Dog run")
        }
    }
    
    var ani = Animal()
    ani.speak()
    ani.eat()
    ani.sleep()
    
    ani = Dog()
    ani.speak()
    ani.eat()
    ani.sleep() 
    
    • 增加了子类后,Dog的类信息里的方法列表会存有重写后的父类方法,以及自己新增的方法
    class Dog: Animal {
        func run() {
            print("Dog run")
        }
    } 
    
    • 如果子类里没有重写父类方法,那么类信息里的方法列表会有父类的方法,以及自己新增的方法

八、多态及实现原理

  • 面向对象语言三大特性:封装、继承、多态。
  • OC中多态是用Runtime实现的,在C++中用虚表实现多态,今天我们了解一下Swift中的多态及其原理
    • C++ 类似,都是使用虚表
  • 什么是多态?父类指针指向子类对象就是多态

1. 函数调用比较

1.1 结构体的函数

通过汇编分析可以看到:

  • 因为不存在继承重写行为,调用的函数地址都是在编译时期确定的。

1.2. 类的函数

  • speak函数调用栈:
  • eat函数调用栈:
  • sleep函数调用栈
  • 类生成的汇编代码非常多,相比结构体复杂了很多,并且通过函数调用发现:
    • 函数地址是动态变化的
    • 所以,如果没有继承行为或简单的类,建议使用结构体,效率更高。(机器指令越少,意味着要执行的代码越高效)
  • 类的函数调用地址之所以变化是为因为
    • 子类继承父类 会导致 函数实际调用地址 发生变化
    • 这也是多态的体现。

2、汇编分析类的继承

示例代码:

class Animal {
    func speak() {
        print("Animal speak")
    }
    func eat() {
        print("Animal eat")
    }
    func sleep() {
        print("Animal sleep")
    }
}

class Dog: Animal {
    override func speak() {
        print("Dog speak")
    }
    override func eat() {
        print("Dog eat")
    }
    func run() {
        print("Dog run")
    }
}
var animal = Animal()
animal.speak()
animal.eat()
animal.sleep()
/*
 输出:
 Animal speak
 Animal eat
 Animal sleep
 */

animal = Dog()
animal.speak()
animal.eat()
animal.sleep()
/*
 输出:
 Dog speak
 Dog eat
 Animal sleep
 */

汇编分析:

分析:

  • 类的实例前8个字节保存的是类的信息,所以上面的汇编代码会一值围绕着实例animal的前8个字节去查找函数地址。
  • animal最后一次指向的是对象Dog在堆空间的内存,所以最终调用的是Dog中的speak函数。
  • 其实就是虚表:
  • callq *0x50(%rcx)中的0x50就是偏移量,跳过0x50就是函数speak的地址。

总结起来其实很简单:

  • 先找到全局变量animal的地址;
  • animal地址保存的是堆空间Dog对象的内存地址;
  • Dog对象前8个字节保存的是对象类型信息地址;
  • 对象类型信息地址保存着类中函数的地址。

注意: 无论创建多少个同类型对象,对象的类型信息都指向同一块内存地址。对象类型信息保存在全局区。

九、初始化

1. 类的初始化器

    1. 结构体枚举都可以定义初始化器
    • 类有两种初始化器:
    • 指定初始化器(designated initializer
    • 便捷初始化器(convenience initializer
      // 指定初始化器
      init(parameters) {
          statements
      }
      
      // 便捷初始化器
      convenience init(parameters) {
          statements
      } 
      
    1. 每个类至少有一个指定初始化器
      指定初始化器是类的主要初始化器
    1. 默认初始化器总是类的指定初始化器
    class Size {
        init() {
    
        }
    
        init(age: Int) {
    
        }
    
        convenience init(height: Double) {
            self.init()
        }
    }
    
    var s = Size()
    s = Size(height: 180)
    s = Size(age: 10) 
    
    1. 类本身会自带一个指定初始化器
    class Size {
    
    }
    
    var s = Size() 
    
    1. 如果有自定义的指定初始化器,默认的指定初始化器就不存在了 -w644
    1. 类偏向于少量指定初始化器
      一个类通常只有一个指定初始化器
    class Size {
        var width: Double = 0
        var height: Double = 0
    
        init(height: Double, width: Double) {
            self.width = width
            self.height = height
        }
    
        convenience init(height: Double) {
            self.init(height: height, width: 0)
        }
    
        convenience init(width: Double) {
            self.init(height: 0,width: width)
        }
    }
    
    let size = Size(height: 180, width: 70) 
    

2. 初始化器的相互调用

初始化器的相互调用规则

  • 指定初始化器必须从它的直系父类调用指定初始化器
  • 便捷初始化器必须从相同的类里调用另一个初始化器
  • 便捷初始化器最终必须调用一个指定初始化器
    class Person {
        var age: Int
        init(age: Int) {
            self.age = age
        }
    
        convenience init() {
            self.init(age: 0)
    
            self.age = 10
        }
    }
    
    class Student: Person {
        var score: Int
    
        init(age: Int, score: Int) {
    
            self.score = score
            super.init(age: age)
    
            self.age = 30
        }
    
        convenience init(score: Int) {
            self.init(age: 0, score: score)
    
            self.score = 100
        }
    } 
    

这一套规则保证了:
使用任何初始化器,都可以完整地初始化实例 -w1211

3. 两段式初始化和安全检查

Swift在编码安全方面煞费苦心,为了保证初始化过程的安全,设定了两段式初始化安全检查

3.1 两段式初始化

第一阶段: 初始化所有存储属性

  • 外层调用 指定/便捷初始化器
  • 分配内存给实例,但未初始化
  • 指定初始化器 确保当前类定义的存储属性都初始化
  • 指定初始化器 调用父类的初始化器,不断向上调用,形成初始化器链

第二阶段: 设置新的存储属性值

  • 从顶部初始化器往下,链中的每一个指定初始化器都有机会进一步定制实例
  • 初始化器现在能够使用self(访问、修改它的属性、调用它的实例方法等)
  • 最终,链中任何便捷初始化器都有机会定制实例以及使用self

3.2 安全检查

  • 指定初始化器必须保证在调用父类初始化器之前, 其所在类定义的所有存储属性都要初始化完成
  • 指定初始化器必须先调用父类初始化器,然后才能为继承的属性设置新值
  • 便捷初始化器必须先调用同类中的其他初始化器,然后再为任意属性设置新值
  • 初始化器在第一阶段初始化完成之前,不能调用任何实例方法,不能读取任何实例属性的值,也不能引用self
  • 直到第一阶段结束,实例才算完全合法

3.3 重写

    1. 当重写父类的指定初始化器时,必须加上override(即使子类的实现的便捷初始化器
    1. 指定初始化器能纵向调用,可以被子类调用
    class Person {
        var age: Int
        init(age: Int) {
            self.age = age
        }
    
        convenience init() {
            self.init(age: 0)
    
            self.age = 10
        }
    }
    
    class Student: Person {
        var score: Int
    
        override init(age: Int) {
            self.score = 0
            super.init(age: age)
        }
    } 
    
    class Person {
        var age: Int
        init(age: Int) {
            self.age = age
        }
    
        convenience init() {
            self.init(age: 0)
    
            self.age = 10
        }
    }
    
    class Student: Person {
        var score: Int
    
        init(age: Int, score: Int) {
    
            self.score = score
            super.init(age: age)
        }
    
        override convenience init(age: Int) {
            self.init(age: age, score: 0)
        }
    } 
    
    1. 如果子类写了一个匹配父类便捷初始化器的初始化器,不用加override
    class Person {
        var age: Int
        init(age: Int) {
            self.age = age
        }
    
        convenience init() {
            self.init(age: 0)
        }
    }
    
    class Student: Person {
        var score: Int
    
        init(age: Int, score: Int) {
    
            self.score = score
            super.init(age: age)
        }
    
        convenience init() {
            self.init(age: 0, score: 0)
        }
    } 
    

    因为父类的便捷初始化器永远不会通过子类直接调用
    因此,严格来说,子类无法重写父类的便捷初始化器

    1. 便捷初始化器只能横向调用,不能被子类调用
      子类没有权利更改父类的便捷初始化器,所以不能叫重写
    class Person {
        var age: Int
        init(age: Int) {
            self.age = age
        }
    
        convenience init() {
            self.init(age: 0)
        }
    }
    
    class Student: Person {
        var score: Int
    
        init(age: Int, score: Int) {
    
            self.score = score
            super.init(age: age)
        }
    
        init() {
            self.score = 0
            super.init(age: 0)
        }
    } 
    

4. 自动继承

    1. 如果子类没有自定义任何指定初始化器,它会自动继承父类所有的指定初始化器
    class Person {
        var age: Int
    
        init(age: Int) {
            self.age = age
        }
    }
    
    class Student: Person {
    
    }
    
    var s = Student(age: 20) 
    
    class Person {
        var age: Int
    
        init(age: Int) {
            self.age = age
        }
    }
    
    class Student: Person {
    
        convenience init(name: String) {
            self.init(age: 0)
        }
    }
    
    var s = Student(name: "ray")
    s = Student(age: 20) 
    
    1. 如果子类提供了父类所有指定初始化器的实现(要不通过上一种方式继承,要不重新)
    class Person {
        var age: Int
    
        init(age: Int) {
            self.age = age
        }
    
        convenience init(sex: Int) {
            self.init(age: 0)
        }
    }
    
    class Student: Person {
    
        override init(age: Int) {
            super.init(age: 20)
        }
    }
    
    var s = Student(age: 30) 
    
    class Person {
        var age: Int
    
        init(age: Int) {
            self.age = age
        }
    
        convenience init(sex: Int) {
            self.init(age: 0)
        }
    }
    
    class Student: Person {
    
        init(num: Int) {
            super.init(age: 0)
        }
    
        override convenience init(age: Int) {
            self.init(num: 200)
        }
    }
    
    var s = Student(age: 30) 
    
    1. 如果子类自定义了指定初始化器,那么父类的指定初始化器便不会被继承
      子类自动继承所有的父类便捷初始化器 -w643
    1. 就算子类添加了更多的便捷初始化器,这些规则仍然适用
    class Person {
        var age: Int
    
        init(age: Int) {
            self.age = age
        }
    
        convenience init(sex: Int) {
            self.init(age: 0)
        }
    }
    
    class Student: Person {
    
        convenience init(isBoy: Bool) {
            self.init(age: 20)
        }
    
        convenience init(num: Int) {
            self.init(age: 20)
        }
    }
    
    var s = Student(age: 30)
    s = Student(sex: 24)
    s = Student(isBoy: true)
    s = Student(num: 6) 
    
    1. 子类以便捷初始化器的形式重新父类的指定初始化器,也可以作为满足第二条规则的一部分
    class Person {
        var age: Int
    
        init(age: Int) {
            self.age = age
        }
    
        convenience init(sex: Int) {
            self.init(age: 0)
        }
    }
    
    class Student: Person {
    
        convenience init(sex: Int) {
            self.init(age: 20)
        }
    }
    
    var s = Student(age: 30)
    s = Student(sex: 24) 
    

5. required

    1. required修饰指定初始化器,表明其所有子类必须实现该初始化器(通过继承或者重写实现)
class Person {
    var age: Int
    
    init(age: Int) {
        self.age = age
    }
    
    required init() {
        self.age = 0
    }
}

class Student: Person {
    
    
}

var s = Student(age: 30) 
    1. 如果子类重写了required初始化器,也必须加上required,不用加override
class Person {
    var age: Int
    
    init(age: Int) {
        self.age = age
    }
    
    required init() {
        self.age = 0
    }
}

class Student: Person {
    
    init(num: Int) {
        super.init(age: 0)
    }
    
    required init() {
        super.init()
    }
}

var s = Student(num: 30)
s = Student() 

6. 属性观察器

    1. 父类的属性在它自己的初始化器中赋值不会触发属性观察器
      但在子类的初始化器中赋值会触发属性观察器
    class Person {
        var age: Int {
            willSet {
                print("willSet", newValue)
            }
    
            didSet {
                print("didSet", oldValue, age)
            }
        }
    
        init() {
            self.age = 0
        }
    }
    
    class Student: Person {
    
        override init() {
            super.init()
    
            age = 1
        }
    }
    
    var s = Student() 
    

7. 可失败初始化器

    1. 结构体枚举都可以使用init?定义可失败初始化器
    class Person {
        var name: String
    
        init?(name: String) {
            if name.isEmpty {
                return nil
            }
    
            self.name = name
        }
    }
    
    let p = Person(name: "Jack")
    print(p) 
    
    • 下面这几个也是使用了可失败初始化器
    var num = Int("123")
    
    enum Answer: Int {
        case wrong, right
    }
    
    var an = Answer(rawValue: 1) 
    

    -w539

    1. 不允许同时定义参数标签参数个数参数类型相同可失败初始化器非可失败初始化器 -w644
    1. 可以用init!定义隐式解包的可失败初始化器
    class Person {
        var name: String
    
        init!(name: String) {
            if name.isEmpty {
                return nil
            }
    
            self.name = name
        }
    }
    
    let p = Person(name: "Jack")
    print(p) 
    
    1. 可失败初始化器可以调用非可失败初始化器
      非可失败初始化器调用可失败初始化器需要进行解包
    class Person {
        var name: String
    
        convenience init?(name: String) {
            self.init()
    
            if name.isEmpty {
                return nil
            }
    
            self.name = name
        }
    
        init() {
            self.name = ""
        }
    } 
    
    class Person {
        var name: String
    
        init?(name: String) {
    
            if name.isEmpty {
                return nil
            }
    
            self.name = name
        }
    
        convenience init() {
            // 强制解包有风险
            self.init(name: "")!
    
            self.name = ""
        }
    } 
    
    1. 如果初始化器调用一个可失败初始化器导致初始化失败,那么整个初始化过程都失败,并且之后的代码都停止执行
    class Person {
        var name: String
    
        init?(name: String) {
    
            if name.isEmpty {
                return nil
            }
    
            self.name = name
        }
    
        convenience init?() {
            // 如果这一步返回为nil,那么后面的代码就不会继续执行了
            self.init(name: "")!
    
            self.name = ""
        }
    }
    
    let p = Person()
    print(p) 
    
    1. 可以用一个非可失败初始化器重写一个可失败初始化器,但反过来是不行的
    class Person {
        var name: String
    
        init?(name: String) {
    
            if name.isEmpty {
                return nil
            }
    
            self.name = name
        }
    }
    
    class Student: Person {
    
        override init(name: String) {
            super.init(name: name)!
        }
    } 
    

    -w643

7. 反初始化器(deinit)

    1. deinit叫做反初始化器,类似于C++的析构函数,OC中的dealloc方法
    1. 当类的实例对象被释放内存时,就会调用实例对象的deinit方法
    class Person {
        var name: String
    
        init(name: String) {
            self.name = name
        }
    
        deinit {
            print("Person对象销毁了")
        }
    } 
    
    1. 父类的deinit能被子类继承
    1. 子类的deinit实现执行完毕后会调用父类的deinit
    class Person {
        var name: String 
        init(name: String) {
            self.name = name
        } 
        deinit {
            print("Person对象销毁了")
        }
    }
    
    class Student: Person { 
        deinit {
            print("Student对象销毁了")
        }
    }
    
    func test() {
        let stu = Student(name: "Jack")
    }
    
    test()
    
    // 打印
    // Student对象销毁了
    // Person对象销毁了 
    
    1. deinit不接受任何参数,不能写小括号,不能自行调用

专题系列文章

1. 前知识

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

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

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

其它底层原理专题

1. 底层原理相关专题

2. iOS相关专题

3. webApp相关专题

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

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

6. Android、HarmonyOS页面渲染专题

7. 小程序页面渲染专题