Swift 进阶(七)方法、下标

431 阅读10分钟

方法(Method)

基本概念

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

  • 实例方法(Instance Method):通过实例对象调用
  • 类型方法(Type Method):通过类型调用

实例方法调用

class Car {
    var count = 0
    
    func getCount() -> Int {
        count
    }
}

let car = Car()
car.getCount()

类型方法用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
}

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

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

基本概念

使用subscript可以给任意类型(枚举、结构体、类)增加下标功能

有些地方也翻译成:下标脚本

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

subscript中定义的返回值类型决定了getter中返回值类型和setternewValue的类型

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

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

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

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

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本质就是方法调用

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

看下面的示例代码

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的属性,所以不会报错

继承(Inheritance)

基本概念

值类型(结构体、枚举)不支持继承,只有类支持继承

没有父类的类,叫做基类

Swift并没有像OC、Java那样的规定,任何类最终都要继承自某个基类

子类可以重新父类的下标、方法、属性,重写必须加上override

class Car {
    func run() {
        print("run")
    }
}

class Truck: Car {
    override func run() {
        
    }
}

内存结构

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

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

首先类内部会有16个字节存储类信息和引用计数,然后才是属性,又由于堆空间分配内存的原则是16的倍数,所以内存空间占用分别为32、32、48

子类会继承自父类的属性,所以内存会算上父类的属性存储空间

重写实例方法、下标

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

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

static修饰的类型方法、下标,不允许被子类重写

-w571 -w646

但是被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

重写属性

子类可以将父类的属性(存储、计算)重写为计算属性

class Animal {
    var age = 0
}

class Dog: Animal {
    override var age: Int {
        set {
            
        }
        
        get {
            10
        }
    }
    var weight = 0
}

但子类不可以将父类的属性重写为存储属性

-w644 -w638

只能重写var属性,不能重新let属性

-w642

重写时,属性名、类型要一致

-w639

子类重写后的属性权限不能小于父类的属性权限

  • 如果父类属性是只读的,那么子类重写后的属性也是只读的,也可以是可读可写的
  • 如果父类属性是可读可写的,那么子类重写后的属性也必须是可读可写的

重写实例属性

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

从父类继承过来的存储属性,都会分配内存空间,不管之后会不会被重写为计算属性

如果重写的方法里的settergetter不写super,那么就会死循环

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

        get {
            print("SubCircle getRadius")
            return radius
        }
    }    
}

重写类型属性

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

static修饰的类型属性(计算、存储),不可以被子类重写

-w861

属性观察器

可以在子类中为父类属性(除了只读计算属性、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

如果父类里也有属性观察器,那么子类赋值时,会先调用自己的属性观察器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

可以给父类的计算属性增加属性观察器

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

final

final修饰的方法、下标、属性,禁止被重写

-w643 -w644 -w640

final修饰的类,禁止被继承

-w642

方法调用的本质

我们先看下面的示例代码,分析结构体和类的调用方法区别是什么

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

反汇编之后,发现结构体的方法调用就是直接找到方法所在地址直接调用

结构体的方法地址都是固定的

-w715

下面我们在看换成类之后反汇编的实现是怎样的

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

反汇编之后,会发现需要调用的方法地址是不确定的,所以凡是调用固定地址的都不会是类的方法的调用

-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")
    }
}

如果子类里没有重写父类方法,那么类信息里的方法列表会有父类的方法,以及自己新增的方法