2. 类与结构体

145 阅读4分钟

一、类与结构体初识

/// 父类
class Person {
}

/// 类
class Teacher: Person {
    var name: String
    var age: Int

    /// 析构方法
    deinit {
    }

    /// 构造方法
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    /// 方法
    func description() {
    }

    /// 下标
    subscript(index: Int) -> Int {
        index
    }
}

/// 拓展 Equatable 协议
extension Teacher: Equatable {
    static func == (lhs: Teacher, rhs: Teacher) -> Bool {
        lhs.name == rhs.name && lhs.age == rhs.age
    }
}

/// 结构体
struct Student: Equatable {
    var name: String
    var age: Int

    /// 构造方法
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    /// 下标
    subscript(index: Int) -> Int {
        index
    }
}

/// 拓展方法
extension Student {
    /// 方法
    func description() {
    }
}

相同点

  • 定义存储值的属性
  • 定义⽅法
  • 定义下标以使⽤下标语法提供对其值的访问
  • 定义初始化器
  • 使⽤ extension 来拓展功能
  • 遵循协议来提供某种功能

不同点:

  • 类有继承的特性,⽽结构体没有
  • 类型转换使您能够在运⾏时检查和解释类实例的类型
  • 类有析构函数⽤来释放其分配的资源
  • 引⽤计数允许对⼀个类实例有多个引⽤

二、引用类型与值类型

在使用类与结构体时,我们首先需要注意的是:

类是引用类型,一个类的变量并不直接储存具体的实例对象,而是对存储具体实例内存地址的引用;结构体是值类型。

1. 引用类型

var t1 = Teacher(name: "Tom", age: 3) b5b2aa81672a40debb78d1bc06b7e43c.png

var t2 = t1

884581d6df55424589ebc6f17f8c85fd.png

调试: a91d79f0632a4d05a4905c9320a9eb54.png

这里我们借助两个指令来查看当前变量的内存结构

po : p 和 po 的区别在于使用 po 只会输出对应的值,而 p 则会返回值的类型以及命令结果的引用名。 x/8g: 读取内存中的值(8g: 8字节格式输出)

通过调试可以发现 当 t2.age 被修改时,t1.age 也发生变化。我们可以通过 withUnsafePointer 来查看 t1 和 t2 的内存地址(注意 t1 和 t2 需要使用 var,如果使用 let 则无法使用 withUnsafePointer(to: &t1) { print($0) 来分析)。 t1 和 t2 在内存中的地址是相邻的,中间相差8字节;接下来我们通过x/8g来查看 t1 和 t2 的内存地址的存储信息都是 0x0000000100543620,这正是 Teacher 实例对象的内存地址。

结论: 我们创建一个 t1 来接收 Teacher 类的实例对象,但是 t1 并没有直接存储 Teacher 的实例对象,而是存储对 Teacher 实例对象内存地址的引用;同样的,如果我们将 t1 赋值给其他变量,比如 t2,那么 t1 和 t2 存储的都是 Teacher 实例变量内存地址的引用;任何对于 Teacher 实例的修改都会影响其它的引用方。

2. 值类型

与引用类型的变量中存储的地址相比,值类型存储的就是具体的实例或者说具体的值。 e38b406111124569822a69802bef99c7.png

调试: 6e236807c53644f48d40fe8e1511a4d3.png

通过调试可以发现 当 s2.age 被修改时,s1.age 并没有发生变化。通过 po s1/s2 发现它们存储的是 name 和 age 具体的值,并不是内存地址。

结论: 我们创建一个 s1 来接收 Student 类的实例对象,s1 存储的是 Student 的实例对象;如果我们将 s1 赋值给其他变量,比如 s2,那么 s1 和 s2 存储的是独立的实例对象;任何对实例对象 Student 的修改都是独立的,并不会影响到其它的 Student 实例对象。

三、性能对比

这里我们通过运行 StructVsClassPerformance 来做分析。 06dfbd4df475483b97e2e7bc62e9ed3e.png

这里对比的是 1个参数的 class 和 struct 与 10个参数的 class 和 struct 分别创建 1000万次消耗的时间。可以发现 struct 性能明显高于 class,class 比 struct 消耗的时间要多 50% 以上。*

1. 类的内存分布

LLDB 调试插件使用参考1. 内存结构

950e53c6c84547b392f63e022ac29dca.png 标识符 t1 在栈上,其存储的是指向堆空间的 Teacher 实例地址。

2. 结构体的内存分布

676bc037b39d48839d545da599215154.png 标识符 s1 在栈上,其存储的 Student 实例也是在栈上。

四、总结

基于 类 和 结构体 的特性差异,在实际开发中应选择合适的类型。结构体因为处在栈上,在内存分配上性能要高于类,但对其实例属性的修改相当于重新拷贝一份新的实例(类似于OC的深拷贝),在某些需要共享一个实例对象的业务场景并不适用。类处在堆上,使用时在遵循业务场景需要的基础上也要考虑性能问题,如果只是作为只读对象,应优先使用结构体。