*注: 基于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整除。
在上面的打印信息里,发现class的size竟然是8,而strut的是9,是不是感觉到奇怪?Int在64位的系统中占8个字节,Bool占1个字节,class的size至少也应该大于等于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"和引用计数指针。
我们把思路拉回到class的MemoryLayout.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)}
发现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)})
- 从打印可以看出,每一次调用
YYPerson.self,它的地址都不一样,是否说明是YYPerson.self是一个需要实例化的struct? YYPerson.self的内存地址都是在stack上。
似乎能验证上面的猜测了,剩下的就要去寻找真相了,答案就在swift标准库的源码中。
待续....尴尬,看了一天也就理解到这里。