Swift进阶杂谈3:引用类型

515 阅读4分钟

  • 类的常用写法
//写法一
class PDTeacher {
    var age:Int
    func teach() {
        print("teach")
    }
    init(_ age: Int) {
        self.age = age
    }
    
}
var t = PDTeacher.init(20)

//写法二
class PDTeacher2 {
    var age:Int?;
    func teach() {
        print("teach")
    }
    
    init(_ age: Int) {
        self.age = age
    }
}
var t1 = PDTeacher2.init(12)
  • 在类中,如果属性没有赋值,也不是可选项,编译时会报错,需要自己实现init方法

为什么类时引用类型?

定义一个类,通过一个例子说明

   class CJLTeacher1 {
    var age: Int = 18
    var age2: Int = 20
}
var t1 = CJLTeacher1()

类初始化的对象t1,存储在全局区

  • 打印t1、t:po t1,从图中可以看出,t1内存空间中存放的是地址t中存储的是

  • 获取t1变量的地址,并查看其内存情况

  • 获取t1指针地址:po withUnsafePointer(to: &t1){print($0)}

  • 查看t1全局区地址内存情况:x/8g 0x0000000100008218

  • 查看t1地址中存储的堆区地址内存情况:x/8g 0x00000001040088f0

引用类型 特点

  • 1、地址中存储的是堆区地址
  • 2、堆区地址中存储的是

问题1:此时将t1赋值给t2,如果修改了t2,会导致t1修改吗?

  • 通过lldb调试得知,修改了t2,会导致t1改变,主要是因为t2t1地址中都存储的是 同一个堆区地址,如果修改,修改是同一个堆区地址,所以修改t2会导致t1一起修改,即浅拷贝

问题2:如果结构体中包含类对象,此时如果修改t1中的实例对象属性,t会改变吗?

代码如下所示

class CJLTeacher1 {
    var age: Int = 18
    var age2: Int = 20
}

struct CJLTeacher {
    var age: Int = 18
    var age2: Int = 20
    var teacher: CJLTeacher1 = CJLTeacher1()
}

var  t = CJLTeacher()

var t1 = t
t1.teacher.age = 30

//分别打印t1和t中teacher.age,结果如下
t1.teacher.age = 30 
t.teacher.age = 30

从打印结果中可以看出,如果修改t1中的实例对象属性,会导致t中实例对象属性的改变。虽然在结构体中是值传递,但是对于teacher,由于是引用类型,所以传递的依然是地址

同样可以通过lldb调试验证

  • 打印t的地址:po withUnsafePointer(to: &t){print($0)}
  • 打印t的内存情况: x/8g 0x0000000100008238
  • 打印t中teacher地址的内存情况:x/8g 0x000000010070e4a0 注意 在编写代码过程中,应该尽量避免值类型包含引用类型

查看当前的SIL文件,尽管CJLTeacher1是放在值类型中的,在传递的过程中,不管是传递还是赋值,teacher都是按照引用计数进行管理的 可以通过打印teacher的引用计数来验证我们的说法,其中teacher的引用计数为3

主要是是因为:

  • mainretain一次

  • teacher.getter方法中retain一次

  • teacher.setter方法中retain一次

mutating

通过结构体定义一个,主要有pushpop方法,此时我们需要动态修改栈中的数组

  • 如果是以下这种写法,会直接报错,原因是值类型本身是不允许修改属性
  • 将push方法改成下面的方式,查看SIL文件中的push函数
struct CJLStack {
    var items: [Int] = []
    func push(_ item: Int){
        print(item)
    }
}

从图中可以看出,push函数除了item,还有一个默认参数self,self是let类型,表示不允许修改

  • 尝试1:如果将push函数修改成下面这样,可以添加进去吗?
struct CJLStack {
    var items: [Int] = []
    func push(_ item: Int){
        var s = self
        s.items.append(item)
    }
}

打印结果如下 可以得出上面的代码并不能将item添加进去,因为s是另一个结构体对象,相当于值拷贝,此时调用push是将item添加到s的数组中了

  • 根据前文中的错误提示,给push添加mutating,发现可以添加到数组了
struct CJLStack {
    var items: [Int] = []
    mutating func push(_ item: Int){
        items.append(item)
    }
}

查看其SIL文件,找到push函数,发现与之前有所不同,push添加mutating(只用于值类型)后,本质上是给值类型函数添加了inout关键字,相当于在值传递的过程中,传递的是引用(即地址)

inout关键字

  • 一般情况下,在函数的声明中,默认的参数都是不可变的,如果想要直接修改,需要给参数加上inout关键字
  • 未加inout关键字,给参数赋值,编译报错
  • 添加inout关键字,可以给参数赋值

总结

  • 1、结构体中的函数如果想修改其中的属性,需要在函数前加上mutating,而类则不用

  • 2、mutating本质也是加一个 inout修饰的self

  • 3、Inout相当于取地址,可以理解为地址传递,即引用

  • 4、mutating修饰方法,而inout 修饰参数

总结

通过上述LLDB查看结构体 & 类的内存模型,有以下总结:

  • 值类型,相当于一个本地excel,当我们通过QQ传给你一个excel时,就相当于一个值类型,你修改了什么我们这边是不知道的

  • 引用类型,相当于一个在线表格,当我们和你共同编辑一个在先表格时,就相当于一个引用类型,两边都会看到修改的内容

  • 结构体中函数修改属性, 需要在函数前添加mutating关键字,本质是给函数的默认参数self添加了inout关键字,将self从let常量改成了var变量