【Swift】:面向对象(二)

一、继承(针对类)
二、多态(针对类)


三、协议(枚举、结构体、类都可以)
四、扩展(枚举、结构体、类都可以)


五、访问控制(枚举、结构体、类都可以)
六、内存管理(针对类)

写在前面:在Swift开发里,我们可以采用面向对象编程 + 面向协议编程的混合编程范式。

  • 优先考虑值类型(structenum),而不是引用类型(class),除非某个东西必须继承自别的东西时再使用引用类型,一来值类型更加轻量执行效率更高,二来它们脱离了继承体系可以强迫我们采用面向协议编程;
  • 需要抽取公共的属性和方法时,优先考虑抽取个协议来遵守,而不是抽取个父类来继承。


一、继承(针对类)


继承是指一个类拥有了另一个类的属性和方法,用:表示。枚举和结构体不支持继承,只有类才支持继承。

Swift并没有像OC那样规定所有的类都必须继承自某个基类,只要一个类没有父类,那它就是一个基类。

// 基类,Animal类
class Animal {
    var age: Int = 0
    
    func run() {
        print("\(age)岁就会跑了")
    }
}

// Dog类继承自Animal类
class Dog: Animal {}

let dog = Dog()
dog.run() // 0岁就会跑了

继承里一个很重要的关键词就是重写,无论是重写父类的属性还是方法,我们都必须显式地写上override关键字,否则会报错:

  • 1️⃣重写实例属性

子类可以把父类的存储属性和计算属性重写为计算属性,但是不能重写为存储属性。

class Animal {
    var age: Int = 0 // 存储属性
    var month: Int { // 计算属性
        set {
            print("Animal setMonth")
            
            age = newValue / 12
        }

        get {
            print("Animal getMonth")
            
            return age * 12
        }
    }
}

class Dog: Animal {
    override var age: Int { // 把父类的存储属性重写为计算属性
        set {
            print("Dog setAge")
            
            super.age = newValue
        }

        get {
            print("Dog getAge")
            
            return super.age
        }
    }
    
    override var month: Int { // 把父类的计算属性重写为计算属性
        set {
            print("Dog setMonth")
            
            super.month = newValue
        }

        get {
            print("Dog getMonth")
            
            return super.month
        }
    }
}

var animal = Dog()

// Dog setAge
animal.age = 11 
// Dog setMonth、Animal setMonth、Dog setAge
animal.month = 24 

// Dog getAge
print(animal.age) // 2 
// Dog getMonth、Animal getMonth、Dog getAge
print(animal.month) // 24
  • 2️⃣重写类属性

子类可以把父类用class定义的(不能是用static定义的)计算属性重写为计算属性,存储属性不支持重写。

class Animal {
    static var age: Int = 0 // 存储属性
    class var month: Int { // 用class定义的计算属性
        set {
            print("Animal setMonth")
            
            age = newValue / 12
        }

        get {
            print("Animal getMonth")
            
            return age * 12
        }
    }
}

class Dog: Animal {
    override class var month: Int { // 把父类用class定义的计算属性重写为计算属性
        set {
            print("Dog setMonth")
            
            super.month = newValue
        }
        get {
            print("Dog getMonth")
            
            return super.month
        }
    }
}

Dog.age = 11
// Dog setMonth、Animal setMonth
Dog.month = 24

print(Dog.age) // 11
// Dog getMonth、Animal getMonth
print(Dog.month) // 24
  • 3️⃣重写实例方法
class Animal {
    func speak() {
        print("Animal speak")
    }
}

class Dog: Animal {
    override func speak() {
        super.speak()
        
        print("Dog speak")
    }
}

var animal = Animal()
animal.speak() // Animal speak

animal = Dog()
animal.speak() // Animal speak、Dog speak
  • 4️⃣重写类方法

class定义的类方法才支持重写,用static定义的类方法不支持重写。

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

class Dog: Animal {
    override class func speak() {
        super.speak()
        
        print("Dog speak")
    }
}

Animal.speak() // Animal speak
Dog.speak() // Animal speak、Dog speak


二、多态(针对类)


多态是指父类指针指向子类对象。因为只有类才支持继承,所以也只有类才支持多态。

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() // animal变量为Animal类型,指向了一个Animal对象
animal.speak() // Animal speak
animal.eat() // Animal eat
animal.sleep() // Animal sleep

animal = Dog() // animal变量重新指向了一个Dog对象,变成了Dog类型
animal.speak() // Dog speak,虽然在编译时编译器依旧认为animal是Animal类型的,但只要编译能通过,运行时才会决定到底调用谁的方法
animal.eat() // Dog eat
animal.sleep() // Animal sleep
//animal.run() // 不能调用run,因为在编译时编译器依旧认为animal是Animal类型的,它没有run方法,编译都通不过,OC也是这样的

Swift方法的调用流程:

先简单说一下OC方法的调用流程:指针变量 --> 对象 --> 对象的isa指针/superclass指针 --> 对象所属的类/父类 --> 方法列表 --> 函数的地址 --> 调用函数。

Swift实例方法的调用流程:变量 --> 对象 --> 对象的前8个字节(指向该对象的类型信息) --> 对象的类型信息(里面存储着这个类所有方法的地址) --> 函数的地址 --> 调用函数。

Swift animal变量指向Animal对象时
Swift animal变量指向Dog对象时

可见有一个很明显的区别是:Swift里没有superclass指针这个东西,子类是直接把父类所有的方法地址都存储在自己的类型信息里。

另一个区别是:OC调用类方法照样是遵循这套调用流程的,而Swift调用类方法则是直接拿代码区的函数来调用,没有这套流程。


三、协议(枚举、结构体、类都可以)


协议一般用来定义一些属性的声明、方法的声明,而交给遵守该协议的东西去实现,而且默认是必须实现。枚举、结构体、类都可以遵守协议。

// 协议1
protocol Protocol1 {
    ...
}

// 协议2
protocol Protocol2 {
    ...
}

// 协议3
protocol Protocol3 {
    ...
}

// 父类:遵守协议1
class ParentClass: Protocol1 {
    ...
}

// 子类:继承自ParentClass,并遵守协议1、协议2、协议3
class SubClass: ParentClass, Protocol2, Protocol3 {
    ...
}

1、协议中的属性

  • 协议中声明的属性,不需要指定是存储属性还是计算属性,你只需要指定是实例属性还是类属性就可以了(但是为了枚举、结构体、类可以通用某个协议,类属性必须得用static定义,而不能用class定义),但必须用{ get set }来表明该属性可读可写,或者用{ get }来表明该属性只读
  • 协议中声明的属性,必须用var
protocol Drawable {
    var x: Int { get set } // 声明了一个可读可写的实例属性
    var y: Int { get } // 声明了一个只读的实例属性
}
  • 实现协议中声明的属性时,其访问权限不能小于声明时的那个权限
class Person: Drawable {
    var x: Int = 11 // 存储属性,肯定是可读可写的,不小于声明时的权限(可读可写)
    var y: Int = 12 // 存储属性,肯定是可读可写的,不小于声明时的权限(只读)
}
class Person: Drawable {
    var x: Int { // 计算属性,这里定义为可读可写的,不小于声明时的权限(可读可写)
        get { 11 }
        set {}
    }
    
    var y: Int { // 计算属性,这里定义为只读的,不小于声明时的权限(只读)
        get { 12 }
//        set {} // 当然也可以定义为可读可写的计算属性
    }
}

2、协议中的方法

协议中声明的方法,就是一个方法不写方法体而已(同样地,为了枚举、结构体、类可以通用某个协议,类方法必须得用static定义,而不能用class定义,当然你实现类方法的时候可以是static也可以是class,这要看你想不想这个方法被子类重写——static修饰的类方法不能被子类重写、class修饰的才行)。

protocol Drawable {
    func draw() // 声明了一个实例方法
    static func draw() // 声明了一个类型方法
}

class Person: Drawable {
    func draw() {
        print("实例方法draw")
    }
    
    static func draw() {
        print("类型方法draw")
    }
    
//    class func draw() {
//        print("类型方法draw")
//    }
}

如果我们想在协议里定义可选的方法,那就得给方法提供一个默认的实现,可以结合扩展来实现。

protocol Runnable {
    // run方法为可选的
    func run()
}

extension Runnable {
    // 为run方法提供默认实现
    func run() {}
}

补充:协议的继承

一个协议可以继承一个或多个其它协议。

// 父协议1
protocol ParentProtocol1 {
    ...
}

// 父协议2
protocol ParentProtocol2 {
    ...
}

// 父协议3
protocol ParentProtocol3 {
    ...
}

// 子协议
protocol SubProtocol: ParentProtocol1, ParentProtocol2, ParentProtocol3 {
    ...
}


四、扩展(枚举、结构体、类都可以)


类似于OC的分类,Swift的扩展也有两个作用:

  • 一般用来给一个已有的枚举、结构体、类扩展计算属性(计算属性本质就是方法嘛,不能扩展存储属性)、方法、协议;注意是扩展也就是说新增哦,不是重写,扩展里的名字要是跟本类里的名字一样,就会报重复定义的错,而OC分类里的名字是可以跟本类里的名字一样的,也就是说分类里的东西会重写掉本类里的东西,其实分类和本类里的东西都存在,只不过分类里的东西后编译,会被优先找到而已;
  • 把一个类里同一功能的函数聚合、不同功能的函数分散,编写到不同的扩展里去,实现代码分离,便于维护。

1、扩展计算属性、方法、协议

  • 扩展计算属性
extension Double {
    // 只读的计算属性
    var km: Double {
        get {
            return self / 1_000.0
        }
    }
    var m: Double {
        get {
            return self
        }
    }
    var dm: Double {
        get {
            return self * 10
        }
    }
    var cm: Double {
        get {
            return self * 100
        }
    }
    var mm: Double {
        get {
            return self * 1_000.0
        }
    }
}

var distance = 1000.0
print(distance.km) // 1km
print(distance.m) // 1000m
print(distance.dm) // 10000dm
print(distance.cm) // 100000cm
print(distance.mm) // 1000000mm
  • 计算属性 + 关联对象 = 实现扩展存储属性的效果
// objc_setAssociatedObject和objc_getAssociatedObject方法在Foundation框架里
import Foundation

class Person {}

extension Person {
    // 计算属性 + 关联对象 = 实现扩展存储属性的效果
    private static var HEIGHT_KEY: Void? // 用static修饰的一个类属性,本质就是个全局变量,保证地址的唯一性
    var height: Double {
        set(newValue) {
            objc_setAssociatedObject(self, &Person.HEIGHT_KEY, newValue, .OBJC_ASSOCIATION_ASSIGN)
        }
        get {
            guard let height = objc_getAssociatedObject(self, &Person.HEIGHT_KEY) as? Double else { return 0.0 }
            return height
        }
    }
}

var p = Person()
p.height = 180
print(p.height) // 180.0
  • 扩展方法
extension Int {
    // 方法
    func square() -> Int {
        return self * self
    }
}

print(10.square()) // 100
  • 扩展协议
class Person {
    var age: Int
    var name: String

    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}

// 遵守某个协议
extension Person: Equatable {
    static func == (lhs: Person, rhs: Person) -> Bool {
        return lhs.age == rhs.age
    }
}

var p1 = Person(age: 11, name: "张三")
var p2 = Person(age: 11, name: "张三")

print(p1 == p2) // true

2、实现代码分离

class HomeViewController: BaseViewController {
    lazy var tableView = UITableView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.dataSource = self
        tableView.delegate = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }
}

// MARK: - UITableViewDataSource
extension HomeViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        return cell
    }
}

// MARK: - UITableViewDelegate
extension HomeViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 44
    }
}


五、访问控制(枚举、结构体、类都可以)


  • 模块(Module):是指一个独立的项目或动态库(Xcode里TARGETS那个地方就是一个一个的模块 ),一个模块里可以通过import关键字导入另外一个模块。
  • 源文件(Source):是指我们编写的一个个.swift文件,它通常属于某个模块,而它内部又包含着多个类和函数等。
  • private:修饰的属性、方法只能在当前类内访问,修饰的类/结构体/枚举只能在当前源文件内访问(实际开发中我们一把都会把属性、方法写成private的,感觉就是在OC的.m文件里写代码一样,等需要时再把private去掉暴露出去)
  • fileprivate:修饰的属性、方法、类/结构体/枚举只能在当前源文件内访问
  • internal(默认):修饰的属性、方法、类/结构体/枚举能在(当前整个项目这个模块 - 项目导进来的动态库等其它模块)内访问(所以Swift项目里很少import其它文件)
  • public:修饰的属性、方法、类/结构体/枚举能在(当前整个项目这个模块 + 项目导进来的动态库等其它模块)内访问到它,但是项目导进来的动态库等其它模块不能继承、重写它们
  • open:修饰的属性、方法、类能在(当前整个项目这个模块 + 项目导进来的动态库等其它模块)内访问到它,而且项目导进来的动态库等其它模块能继承、重写它们open不能修饰结构体/枚举,如果想公开就用public吧)


六、内存管理(针对类)


和OC一样,Swift也采用基于引用计数的ARC来进行内存管理,我们这里说的一般指堆内存。

1、引用修饰符

Swift里有3种引用修饰符:

  • 强引用:默认情况下就是强引用
class Person {}
var p = Person() // 强引用
  • 弱引用:通过weak来定义弱引用,但是弱引用必须是可选项、还必须是var,因为ARC将来会把这个引用自动置为nil,不是可选项当然就不能置为nil,不是var当然就不能修改值
class Person {}
weak var p2: Person? = Person() // 弱引用
  • 无主引用:通过unowned来定义无主引用,类似于OC的unsafe_unretained,ARC将来不会把这个引用自动置为nil,所以它不是可选项也可以,不是var也可以
class Person {}
unowned var p = Person() // 无主引用

2、循环引用

2.1 使用weakunowned都可以解决循环引用
class Person {
    var apartment: Apartment?
    
    deinit {
        print("person对象销毁")
    }
}

class Apartment {
    var person: Person?
    
    deinit {
        print("apartment对象销毁")
    }
}

func test() {
    let person = Person()
    let apartment = Apartment()
    
    person.apartment = apartment
    apartment.person = person
}
test()

上面的代码就存在循环引用,PersonApartment对象都无法销毁,我们只需要把其中任意一个引用变为弱引用或无主 引用就可以了,例如:

class Apartment {
    weak var person: Person?
    
    deinit {
        print("Apartment对象销毁")
    }
}
2.2 闭包的循环引用

闭包表达式会对它内部访问的对象进行强引用,所以如果它内部用到的对象也对闭包表达式进行了强引用,这就会导致循环引用。(类似于OC的block

class Person {
    var age: Int = 11
    var fn: (() -> ())? // 属性,想要接收一个函数
    
    deinit {
        print("Person对象销毁")
    }
}

func test() {
    let person = Person()
    
    // fn又是person对象的一个属性,所以person对象也强引用着闭包表达式
    person.fn = { // 闭包表达式
        // 内部访问了person对象,所以会强引用person对象
        print(person.age)
    }
}
test()

上面的代码就存在循环引用,Person对象和闭包表达式都无法销毁,我们只需要在闭包表达式的捕获列表里用weakunowned声明一下是一个弱引用或者无主引用就可以了,例如:

func test() {
    let person = Person()
    
    person.fn = {
        [weak weakPerson = person] in // [...]为捕获列表
        print(weakPerson?.age) // 因为weak修饰的弱引用是可选项,所以得用可选链来访问
    }
}
test()

更简单的写法:

func test() {
    let person = Person()
    
    person.fn = {
        [weak person] in
        print(person?.age) // 因为weak修饰的弱引用是可选项,所以得用可选链来访问
    }
}
test()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容