Swift之class与struct

222 阅读5分钟

*注: 基于swift 5

一. class与struct的区别

Swift 中结构体和类有很多共同点。两者都可以:

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义下标操作用于通过下标语法访问它们的值
  • 定义构造器用于设置初始值
  • 通过扩展以增加默认实现之外的功能
  • 遵循协议以提供某种标准功能

与结构体相比,类还有如下的附加功能:

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 析构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用

类支持的附加功能是以增加复杂性为代价的。作为一般准则,优先使用结构体,因为它们更容易理解,仅在适当或必要时才使用类。实际上,这意味着你的大多数自定义数据类型都会是结构体和枚举。

apple文档中建议在添加新的数据结构时通过以下四点判断选择类型

-   Use structures by default.
-   Use classes when you need Objective-C interoperability.
-   Use classes when you need to control the identity of the data you're modeling.
-   Use structures along with protocols to adopt behavior by sharing implementations.

google翻译+个人理解

  • 默认使用结构。
  • 当您需要 Objective-C 交互时使用类。
  • 当您需要通过标识运算符(===)区别两个值相同,内存地址却不相同的实例时,请使用类
  • 可以通过结构体现实协议的方式满足继承时,优先使用结构体

更多详细的比较参见 在结构和类之间进行选择

二. class与struct的内存布局

class YYPerson {
    let age: Int
    let isTeacher: Bool
    
    init(_ age: Int, _ isTeacher: Bool) {
        self.age = age
        self.isTeacher = isTeacher
    }
}
print(MemoryLayout<YYPerson>.size)        // 8
print(MemoryLayout<YYPerson>.stride)      // 8
print(MemoryLayout<YYPerson>.alignment)   // 8
struct YYPerson {
    let age: Int
    let isTeacher: Bool
}
print(MemoryLayout<YYPerson>.size)        // 8 + 1 
print(MemoryLayout<YYPerson>.stride)      // 16
print(MemoryLayout<YYPerson>.alignment)   // 8

通过MemoryLayout的方式可以查看类型的内存布局,其中size指实际数据占用的字节大小,stride指系统给该类型分配的实际字节大小,也叫做步幅,alignment指内存对齐字节大小,系统给该类型分配的实际大小要能被alignment整除。

在上面的打印信息里,发现classsize竟然是8,而strut的是9,是不是感觉到奇怪?Int在64位的系统中占8个字节,Bool占1个字节,classsize至少也应该大于等于9,为什么呢?其实道理也比较简单,因为class是引用类型,使用MemoryLayout打印的是YYPerson类的在栈上的内存布局,栈上的实例(或者说是一个临时指针变量)仅包含一个指针类型的成员变量,因此size是8.于是疑问又来了,类的实例对象不是在堆上开辟的吗?的确,是在堆上开辟的实例对象,这里具体什么原因我也不清楚,暂时留个疑问点。我们可以使用runtime中提供的一个方法class_getInstanceSize来打印实例对象在堆上的内存布局

print(class_getInstanceSize(YYPerson.self)) // 32

发现是32,难道不是8+1=9,再内存对齐,应该是16啊?具体原因就要看swift源码中的class的内存布局,我先说一个没有验证的结论,另外16个字节分代表OC中的"isa"和引用计数指针

我们把思路拉回到classMemoryLayout.size是8的疑问,前面我说过MemoryLayout打印的是栈上的内存布局,我们用个好理解的方式来验证下。

class YYPerson {
    let age: Int
    let isTeacher: Bool
    
    init(_ age: Int, _ isTeacher: Bool) {
        self.age = age
        self.isTeacher = isTeacher
    }
}

var p1 = YYPerson(1, true)
var p2 = p1

withUnsafePointer(to: &p1){print($0)}
withUnsafePointer(to: &p2){print($0)}

image.png

发现p1(0x0000000100008338)和p2(0x0000000100008340)的内存地址正好相差8个字节,说明p1的stride就是8,p1和p2指针指向的内容又均是0x1007311e0,是不是同样说明class是引用类型,似乎也能理解MemoryLayout.size是8的疑问(难道打印的仅仅是class在栈中的一个引用堆上实例的变量的内存布局,换句话说所有的swift中的class也有类似于oc中的元类,此处可能打印的是YYPerson元类的实例,也就是类类型的内存布局,因此我猜测YYPerson.self是一个结构体,有一个指针成员变量,专门用来存储class的实例的地址。来,我们验证下)

这里有个一个工具,可以用来打印当前地址是在堆或者栈上。工具暂时就不外放。

    withUnsafePointer(to: YYPerson.self, { print($0)})
    withUnsafePointer(to: YYPerson.self, { print($0)})
    withUnsafePointer(to: YYPerson.self, { print($0)})

image.png

  • 从打印可以看出,每一次调用YYPerson.self,它的地址都不一样,是否说明是YYPerson.self是一个需要实例化的struct
  • YYPerson.self的内存地址都是在stack上。

似乎能验证上面的猜测了,剩下的就要去寻找真相了,答案就在swift标准库的源码中。

待续....尴尬,看了一天也就理解到这里。

1. 初始化

2. 内存分配

a. swift中的class

b. swift中的struct

c. OC中的class

3. 内存回收

三. 总结