Swift 5.1 温故而知新笔记系列之第三天

属性

  • 有等号=肯定是存储属性
  • 没有等号如果是get``set就是计算属性,
  • willSet或者didSet就是存储属性,属性观察器 ,而且不能和计算属性的get,set一起出现,就矛盾了
存储属性
  • 类比成成员变量
  • 存储在实例的内存中
  • 结构体、类可以定义存储属性
  • 枚举不可以定义存储属性,因为枚举的内存结构是用来存储case的关联值或者原始值的,底层看来就不能存储属性内存
  • 在创建类或者结构体实例时,必须为所有的存储属性设置一个合适的初始值
延迟存储属性
class Car {
    init() {
        print("Car init")
    }
    func run() {
        print("Car is Running")
    }
}



class People {
    
    lazy var car = Car()
    init() {
        print("People init")
    }
    
    func play() {
        car.run()
        print("People play")
    }
}


var p = People()
print("_____________")
p.play()


//People init
//_____________
//Car init
//Car is Running
//People play
  • lazy属性必须是var,不能是let
  • let必须在实例的初始化方法完成之前就拥有值
  • 如果多条线程同时第一次访问lazy属性,不是线程安全的
  • 当结构体包含一个延迟存储属性时,只有var才能访问延迟存储属性
计算属性
  • 本质就是方法 getter setter
  • 不占实例内存
  • 既然是方法,那就可以在枚举,结构体,类中定义计算属性
  • 定义计算属性只能用var,不能用let
struct Circle {
    var radius: Double
    var diameter: Double {
        set {
            radius = newValue / 2
        }
        
        get {
            return radius
        }
    }
}
var circle = Circle(radius: 100) // 100
print(circle.diameter)

circle.diameter = 50 // 25
print(circle.diameter)
属性观察器
class People {
    var name: String = "Mikejing" {
        willSet {
            print("willSet", newValue)
        }
        
        didSet {
            print("didSet", oldValue, self.name)
        }
    }
    init() {
        print("People init")
    }
}

var p = People()
p.name = "MiQiShu"

// People init
// willSet MiQiShu
// didSet Mikejing MiQiShu
  • willSet会传递新值,默认叫newValue
  • didSet会传递旧的,默认叫oldValue
  • 初始化其中设置的属性不会触发
  • 属性定义是设置初始值也不会触发
类型属性
  • 只有一个内存,类似全局变量
  • 通过static关键字定义,如果是类,可以用class
struc Car {
	static var count: Int = 0
	init() {
		Car.count += 1
	}
}

Car()
Car()
Car()
print(Car.count) // 3

1.不同于实例属性,不需要设定初始值
2.存储类型属性,默认是lazy的,会在第一次使用的时候初始化,就算被多个线程访问,都是安全的,类型存储属性可以是let
枚举类型也可以定义类型存储属性(计算或者存储),因为类型存储属性不和定义的类型存在一个内存空间,也就是不会破坏原本枚举的内存空间,就可以,和计算属性类似,一个方法,一个全局静态地址
因此可以搞一个单例,默认lazy

class FileManager {
    public static var shared = FileManager()
    private init() { }
    
    func open() {
        
    }
}



class ThemeManager {
    
    public static var shared = {
        // ...
        // ...
        return ThemeManager()
    }()
    private init() { }
    
    func colored() {
        
    }
    
}

FileManager.shared.open()

ThemeManager.shared.colored()

类型属性汇编窥探

var a = 10
var b = 11
var c = 12

可以看到这三个全部变量的地址如下


a -> 0x1000065A0

b -> 0x1000065A8

c -> 0x1000065B0

再来看这个


var a = 10
class People {
    static var age = 1
}
People.age = 100
var c = 12

汇编如下

a -> 0x1000066F0

People.age - > 0x1000066F8

c -> 0x100006700

这边可以看到,类型属性其实不在类的内存中存储,从汇编看,大概率是存储在全局区,和全局区变量挨在一起。

现在在static var age = 1People.age打上断点,进入汇编
在这里插入图片描述
在第一个callq的时候si进去,可以看到swift_once
在这里插入图片描述
然后继续next就会来到我们实际初始化的代码,因为static在类属性中默认就是lazy的,所以就会调用一段初始化函数,然后看汇编断在哪里

Swift01`globalinit_33_F38A6FD0F3BC15DFA2D78EFB3C1D4722_func0:
    0x1000017e0 <+0>:  pushq  %rbp
    0x1000017e1 <+1>:  movq   %rsp, %rbp
->  0x1000017e4 <+4>:  movq   $0x1, 0x4f09(%rip)        ; Swift01.a : Swift.Int + 4
    0x1000017ef <+15>: popq   %rbp
    0x1000017f0 <+16>: retq   

可以看到首地址0x1000017e0就是我们上面swift_once传递的函数地址,然后看到movq $0x1, 0x4f09(%rip),把1赋值给全局变量 0x4f09 + 0x1000017ef = 0x1000066F8,这个地址就和我们上面看到的第一幅图函数返回的rax地址一致。

结论:
类型属性本质就是全局变量,只不过类型添加了命名空间访问,限制了访问。

方法

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

  • 实例方法(Instance Method):通过实例对象调用
  • 类型方法(Type Method):通过类型调用,用static或者class关键字定义,默认是lazy

多态

多态的实现原理

  • OC: Runtime
  • C++:虚表

先看下结构体

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

var animal = Animal()
animal.speak()
animal.eat()
animal.sleep()

结构体不存在继承,重写,所以函数在编译完成后已经确定了

Swift01`main:
    0x1000017f0 <+0>:  pushq  %rbp
    0x1000017f1 <+1>:  movq   %rsp, %rbp
    0x1000017f4 <+4>:  subq   $0x10, %rsp
    0x1000017f8 <+8>:  movl   %edi, -0x4(%rbp)
    0x1000017fb <+11>: movq   %rsi, -0x10(%rbp)
    0x1000017ff <+15>: callq  0x100001a70               ; Swift01.Animal.init() -> Swift01.Animal at main.swift:135
    0x100001804 <+20>: callq  0x100001820               ; Swift01.Animal.speak() -> () at main.swift:136
->  0x100001809 <+25>: callq  0x100001910               ; Swift01.Animal.eat() -> () at main.swift:140
    0x10000180e <+30>: callq  0x1000019c0               ; Swift01.Animal.sleep() -> () at main.swift:144
    0x100001813 <+35>: xorl   %eax, %eax
    0x100001815 <+37>: addq   $0x10, %rsp
    0x100001819 <+41>: popq   %rbp
    0x10000181a <+42>: retq   

可以看到,函数地址已经确定了 callq 0x100001910

然后看一下最简单的继承关系

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 = Dog()
animal.speak()
animal.eat()
animal.sleep()
多态汇编完整分析

在第二个animal.speak()打上断点,然后汇编进行完整的分析
可以看到这里的调用关系如下
在这里插入图片描述
首先这里的调用函数就是callq *0x50(%rcx),向上推断rcx就是rax相关,rax就是0x1342(%rip)相关

1.全局变量赋值

0x100001107 <+519>: movq   0x1342(%rip), %rax        ; Swift02.animal : Swift02.Animal

核心代码 0x100002450 就是 0x1342(%rip),也就是全局变量var animal的地址,然后取出该内存地址中前八个字节,也就是堆空间对象的首地址0x100705660,此时rax存储的就是0x100705660

2.中间一系列寄存器变化

0x10000110e <+526>: movq   %rax, %rcx

rax存储的值给到rcx此时rcxrax都是0x100705660

0x100001114 <+532>: movq   %rax, -0x120(%rbp)
0x100001130 <+560>: movq   -0x120(%rbp), %rax

这两句话相当于rax值没变

3.rcx赋值
如果没有()就是把寄存器rax存储的值给到rcx寄存器,但是如果有()的意思是取出寄存器rax存储的地址中,前8个字节给到rcx寄存器,结果如下

(lldb) register read rax
     rax = 0x0000000100705660
(lldb) register read rcx
     rcx = 0x0000000100002310  type metadata for Swift02.Dog

4.函数调用
取出rcx寄存器中的值,偏移0x50,调用函数

0x10000113d <+573>: callq  *0x50(%rcx)

整体流程如下:
在这里插入图片描述
根据上面分析的汇编和这个流程图,找到对应的内存地址进行View Memory查看,多态函数调用的底层逻辑。

比如

(lldb) register read rcx
     rcx = 0x0000000100002310  type metadata for Swift02.Dog

看到对应的值进行分析在这里插入图片描述
高亮的就是0x50个字节的偏移,拿到后面第一个组八个字节

20 16 00 00 01 00 00 00

lldb调用查看

(lldb) image lookup --address 0x0100001620
      Address: Swift02[0x0000000100001620] (Swift02.__TEXT.__text + 1824)
      Summary: Swift02`Swift02.Dog.speak() -> () at main.swift:26

可以看到对应的就是speak函数的地址。

5.验证Meta Data存储位置
在这里插入图片描述

从图中可以看到,代码区的内存地址最小,随后全局区,堆和栈,我们Meta Data对应的内存处于代码区和全局区之间,不过可以通过image lookup --address xxx查看具体的段,或者用Mach-O View查看即可。

(lldb) register read rcx
     rcx = 0x0000000100002310  type metadata for Swift02.Dog
(lldb) image lookup --address 0x0000000100002310
      Address: Swift02[0x0000000100002310] (Swift02.__DATA.__data + 224)
      Summary: type metadata for Swift02.Dog

结论:
可以看到类对象的前八个字节数据也是存储在全局区__DATA段的

初始化器

初始化两段式和安全检查
init(parameters) {
	statements
}
convenience init(parameters) {
	statements
}
  • 每个类至少有一个指定初始化器,指定初始化器是类的主要初始化器
  • 默认初始化器总是类的指定初始化器
  • 类偏向于少量的指定初始化器,一个类通常只有一个指定初始化器

初始化器的相互调用规则
1.指定初始化器必须从它的直系父类调用指定初始化器
2.便捷初始化器必须从相同的类里调用另一个初始化器
3.便捷初始化器最终必须调用一个指定初始化器

总结一下就是:指定构造器必须总是向上代理(去父类);便利构造器必须总是横向代理(在本类)
在这里插入图片描述
Swift 中类的构造过程包含两个阶段。
第一个阶段:给类中的每个存储属性赋初始值。只要每个存储属性初始值被赋值
第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步自定义它们的存储属性。

Swift 通过4步安全检查来确定构造器两个阶段的成功执行:

  • 安全检查1:指定构造器必须在完成本类所有存储属性赋值之后,才能向上代理到父类的构造器。
class Animal {
    var head = 1
}

class Dog: Animal {
    var foot: Int
    override init() {
        super.init()
        foot = 4
    }
}
// 报错
// 修改如下
override init() {
    foot = 4
    //这句也可以省略,它默认是隐式调用的。
    super.init()
}
  • 安全检查2:指定构造器必须在为继承的属性设置新值之前向上代理调用父类构造器。
//这时,你必须显式的调用super.init(),因为你要修改继承属性- head 的值
override init() {
    foot = 4
    super.init()
    head = 2
}
  • 安全检查3:便利构造器必须先调用其他构造器,再为任意属性(包括所有同类中定义的)赋新值。
convenience init(foot: Int) {
    //先调用其他构造器,如果此处不调用会编译出错
    self.init()
    //再为任意属性(包括所有同类中定义的)赋新值
    self.foot = foot
    head = 3
}
  • 安全检查4:构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self 作为一个值。
class Dog: Animal {
    var foot: Int
    override init() {
        foot = 4
        super.init()
        head = 2
        // 如果上面的未完成,是不能调用run()的,因为self还没有完整的创建
        run()
    }
    
    func run() {
        //do something
    }
}

现在看一下阶段一和阶段二的完整流程:

阶段 1 - 自下而上

  • 类的某个指定构造器或便利构造器被调用。
  • 完成类的新实例内存的分配,但此时内存还没有被初始化。
  • 指定构造器确保其所在类引入的所有存储型属性都已赋初值。
    存储型属性所属的内存完成初始化。
  • 指定构造器切换到父类的构造器,对其存储属性完成相同的任务。
  • 这个过程沿着类的继承链一直往上执行,直到到达继承链的最顶部。
  • 当到达了继承链最顶部,而且继承链的最后一个类已确保所有的存储型属性都已经赋值,
    这个实例的内存被认为已经完全初始化。此时阶段 1 完成。
    在这里插入图片描述

阶段 2 - 自上而下

  • 从继承链顶部往下,继承链中每个类的指定构造器都有机会进一步自定义实例。
    构造器此时可以访问 self、修改它的属性并调用实例方法等等。
  • 最终,继承链中任意的便利构造器有机会自定义实例和使用 self。
    在这里插入图片描述
初始化器的继承和重写

继承 默认情况下子类是不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被子类自动继承。

规则 1
如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。
规则 2
如果子类提供了所有父类指定构造器的实现——无论是通过规则 1
继承过来的,还是提供了自定义实现——它将自动继承父类所有的便利构造器。

class Animal {
    let head = 1
    var name = ""
    
    init(name: String) {
        self.name = name
    }
    
    convenience init() {
        self.init(name: "animal")
    }
}

class Dog: Animal {
    let foot  = 4
}
//自动继承父类所有的指定构造
let d1 = Dog(name: "dog") // d1.name dog
//自动继承父类所有的便利构造器
let d2 = Dog() // d2.name animal

重写

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}
可失败构造器
struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty {
            return nil
        }
        self.species = species
    }
    // 可失败构造器不能与其他非可失败构造器(指定构造器、便利构造器)的参数和类型相同
    //所以下面这个指定构造器是非法的。
    //init(species: String) { }
}
  • 不允许同时定义参数标签、参数个数、参数类型相同的可失败初始化器和非可失败初始化器
  • 可以用init!定义隐式解包的可失败初始化器
  • 可失败初始化器可以调用非可失败初始化器,非可失败初始化器调用可失败初始化器需要进行解包
  • 如果初始化器调用一个可失败初始化器导致初始化失败,那么整个初始化过程都失败,并且之后的代码都停止执行
  • 可以用一个非可失败初始化器重写一个可失败初始化器,但反过来是不行的
必要构造器

我们可以通过required关键字来实现必要构造器,子类必须实现父类的必要构造器。

class Animal {
    var name: String
    required init(name: String) {
        self.name = name
    }
}

class Dog: Animal {
    var foot: Int
    //在重写父类必要构造器的时候不需要加override
    required init(name: String) {
        foot = 4
        super.init(name: name)
    }
}

Dog(name: "dog")

可选链

class Car {
    var price = 0
    
}
class Dog {
    var weight = 0
    
}
class Person {
    var name: String = ""
    var dog: Dog = Dog()
    var car: Car? = Car()
    @discardableResult
    func age() -> Int { 18 }
    func eat() { print("Person eat") }
    subscript(index: Int) -> Int { index }
}

var person: Person? = Person()
person?.age() // Int?
person?.eat() // ()?
person?.name // String?
person?[6] // Int?
  • 如果可选项为nil,调用方法,下标,属性失败,结果为nil
  • 如果可选项不为nil,调用方法,下标,属性成功,结果会被包装成可选项
  • 如果结果本身就是可选项,不会再次包装

如何判断可选链的方式被调用

if let _ = person?.eat(){
    print("调用成功")
} else {
    print("调用失败")
}
  • ?的意思是,如果person有值,就会继续调用,如果没有结束调用
  • 多个?可以链接在一起
  • 如果链中某一个节点为nil,那么整个链就会调用失败
var dog = person?.dog // Dog?
var weight = person?.dog.weight // Int?
var price = person?.car?.price // Int?

//var num1: Int? = nil
// 代表num有值就改为20 num为nil就不赋值
//num1? = 20
//print(num1 ?? 0)
// 0


var num1: Int? = 200
num1? = 20 
print(num1 ?? 0)
// 20


var scores = [
    "Jack": [100,200,300],
    "Rose": [10,20,30]
]

scores["Jack"]?[0]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 1 University students can understand innovation through learning from the past. 2. Students can better review by breaking down complex concepts into smaller components and studying the material in an organized way. 3. When learning from the past to understand innovation, it is important to focus on understanding the big picture and to not get bogged down in the details. ### 回答2: 1. 大学生如何理解温故而知新温故而知新是一种学习方法,它要求我们在学习新知识之前先回顾和巩固已经学过的知识。大学生理解温故而知新意味着要在学习新知识之前,先回顾和复习以前学过的相关知识或基础知识。通过温故,我们能够加深对已有知识的理解和记忆,从而更好地理解和掌握新的知识。 2. 学生如何更好地去复习? 学生要更好地复习,可以采取以下策略: 首先,制定一个合理的复习计划,将要复习的内容分配到不同的时间段,确保每个科目都有足够的时间。 其次,采用多种复习方法,如阅读教材、做练习题、参加讨论等,以帮助加深理解和牢固记忆。 另外,与同学或老师一起讨论复习内容,通过讲解和互动来加深理解。 此外,保持良好的学习习惯,比如及时复习、做好笔记等,能够帮助学生更好地掌握和复习知识。 3. 温故而知新的过程需要注意什么? 在温故而知新的过程中,需要注意以下几点: 首先,要有针对性,根据自己的学习需求和复习目标,选择性地回顾和复习相关知识点。 其次,要有系统性,将复习内容进行分类整理,形成一个清晰的知识框架,有助于加深理解和记忆。 另外,要关注重难点,重点复习那些相对较难或容易遗忘的知识点,加强对这些内容的学习和理解。 还要有耐心和恒心,温故而知新一个持续的过程,需要长期坚持和不断巩固。 最后,要善于总结和归纳,通过整理和回顾复习过程中的笔记和练习,提炼出关键概念和思维模式,便于记忆和应用。 ### 回答3: 1. 大学生如何理解温故而知新? 大学生可以理解为通过回顾过去的知识和经验,来获取新的见解和理解。温故是指回顾已经学过的知识,了解其中的原理、概念和重要点。而知新则是指通过对新知识的学习,扩展和更新自己的知识体系。温故而知新相辅相成,是一个持续学习和发展的过程。 2. 学生如何更好地去复习? 学生可以通过以下方式更好地进行复习: - 制定合理的复习计划:根据时间安排和课程难度,合理分配复习时间,确保每个学科都有足够的复习时间。 - 多种复习方法结合:采用不同的学习方式,如阅读教材、做练习题、参与讨论、制作思维导图等,帮助巩固记忆和理解知识。 - 主动参与课堂:积极参与讨论和提问,与同学和老师交流,加深对知识的理解和记忆。 - 不断反思和总结:及时检查自己的复习情况,发现不足和问题,并及时调整学习方法和计划。 3. 温故而知新的过程需要注意什么? 在温故而知新的过程中,学生需要注意以下几点: - 有目的性地温故:针对具体的知识点或者问题进行回顾,明确自己的学习目标和重点。 - 理解和记忆结合:不仅要理解概念和原理,还要通过多次的复习和记忆,帮助信息在大脑中形成长期记忆。 - 理论联系实际:将学到的知识应用到实际情境中,加深对知识的理解和记忆。 - 及时巩固复习成果:通过做练习题、整理笔记、与同学讨论等方式,巩固复习的成果,确保知识掌握得更牢固。 - 长期持续学习:温故而知新一个持续的过程,要保持学习的热情和动力,不断更新自己的知识体系。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值