本文主要来介绍 swift中结构体的一些性质。
构造方法
我们先来看如下代码
struct Person {
var age :Int?
func run(){
print("person - run")
}
}
class Animal {
var age: Int?
func run(){
print("animal - run")
}
}
let p = Person(age: 19)
let a = Animal()
从 构造方法来讲,结构体会默认生成一个包含所有属性的构造方法,类则不会。通过SIL
文件我们也可以看出来
struct Person {
@_hasStorage @_hasInitialValue var age: Int? { get set }
func run()
init(age: Int? = nil)
init()
}
class Animal {
@_hasStorage @_hasInitialValue var age: Int? { get set }
func run()
@objc deinit
init()
}
Person 结构体
有一个默认的初始化方法,Animal 类
则没有默认的初始化方法。
结构体是值类型
什么是值类型
我们通过以下示例来了解什么是值类型
func test() {
var age = 18
var age2 = age
age = 30
age2 = 45
print("age:\(age), age2:\(age2)")
}
test()
当我们创建age变量时,在栈区开辟了一段空间存储 age变量的值,我们使用 lldb
命令来查看age变量的内存地址值
(lldb) po withUnsafePointer(to: &age){print($0)}
0x00007ffeefbff430
0 elements
我们使用 x/8gx
格式化输出该地址的内存分布
(lldb) x/8gx 0x00007ffeefbff430
0x7ffeefbff430: 0x0000000000000012 0x0000000000000000
0x7ffeefbff440: 0x00007ffeefbff460 0x0000000100003c34
0x7ffeefbff450: 0x00007ffeefbff480 0x0000000100015025
0x7ffeefbff460: 0x00007ffeefbff470 0x00007fff20346621
0x0000000000000012
就是我们age的值18。
当 代码运行下一段的时候 var age2 = age
, 就将 age的值 拷贝到下一段内存空间
(lldb) po withUnsafePointer(to: &age){print($0)}
0x00007ffeefbff430
0 elements
(lldb) po withUnsafePointer(to: &age2){print($0)}
0x00007ffeefbff428
0 elements
我们可以看到 age
和 age2
的内存地址相差 8 字节,
(lldb) x/8gx 0x00007ffeefbff428
0x7ffeefbff428: 0x0000000000000012 0x0000000000000012 // age2 ,age
0x7ffeefbff438: 0x0000000000000000 0x00007ffeefbff460
0x7ffeefbff448: 0x0000000100003c34 0x00007ffeefbff480
0x7ffeefbff458: 0x0000000100015025 0x00007ffeefbff470
值类型数据在赋值时,会开辟一个新的内存空间,并将值拷贝至新的内存空间,地址存储的就是值。
结构体的值类型分布
struct Person {
var age :Int = 10
func run(){
print("person - run")
}
}
var p = Person()
var p1 = p
p1.age = 20
print(p1.age)
我们先找到 p
的内存地址
:
(lldb) po withUnsafePointer(to: &p) {print($0)}
0x0000000100008038
0 elements
然后在查看其内存分布情况
(lldb) x/4gx 0x0000000100008038
0x100008038: 0x000000000000000a 0x0000000000000000
0x100008048: 0x0000000000000000 0x0000000000000000
对于 p1 来言
(lldb) po withUnsafePointer(to: &p1) {print($0)}
0x0000000100008040
0 elements
(lldb) x/4gx 0x0000000100008040
0x100008040: 0x0000000000000014 0x0000000000000000
0x100008050: 0x0000000000000000 0x0000000000000000
(lldb) po withUnsafePointer(to: &p) {print($0)}
0x0000000100008038
0 elements
(lldb) x/4gx 0x0000000100008038
0x100008038: 0x000000000000000a 0x0000000000000014
0x100008048: 0x0000000000000000 0x0000000000000000
p1重新开劈了一段新的内存空间,修改p1的age属性时,并没有影响 p的age属性。俩个对象的内存空间是独立的。
通过SIL
分析 Person结构体
的初始化方法
// Person.init()
sil hidden @main.Person.init() -> main.Person : $@convention(method) (@thin Person.Type) -> Person {
// %0 "$metatype"
bb0(%0 : $@thin Person.Type):
%1 = alloc_stack $Person, let, name "self" // users: %4, %7
%2 = integer_literal $Builtin.Int64, 10 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // users: %5, %6
%4 = struct_element_addr %1 : $*Person, #Person.age // user: %5
store %3 to %4 : $*Int // id: %5
%6 = struct $Person (%3 : $Int) // user: %8
dealloc_stack %1 : $*Person // id: %7
return %6 : $Person // id: %8
} // en
它通过alloc_stack
在栈空间上申请了一段空间,并将值存放到栈上。
class
是引用类型
,它的地址保存的是一个指针,该指针指向堆中该对象值的内存空间。
mutating
在结构体中的方法中,修改变量的值,如果不加任何修饰,是不允许的,如下所示,编译器会报 Cannot assign to property: 'self' is immutable
的错误
此时若想修改 age 变量的值,需要添加
mutating
关键字才可以修改。为了方便对比查看,我们将以下代码转为SIL
文件,来探索 mutating
关键字
struct Person {
var age :Int = 10
mutating func run(){
age = 18
}
func walk(){
}
}
SIL代码为
// Person.run()
sil hidden @main.Person.run() -> () : $@convention(method) (@inout Person) -> () {
// %0 "self" // users: %4, %1
bb0(%0 : $*Person):
debug_value_addr %0 : $*Person, var, name "self", argno 1 // id: %1
%2 = integer_literal $Builtin.Int64, 18 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %6
%4 = begin_access [modify] [static] %0 : $*Person // users: %7, %5
%5 = struct_element_addr %4 : $*Person, #Person.age // user: %6
store %3 to %5 : $*Int // id: %6
end_access %4 : $*Person // id: %7
%8 = tuple () // user: %9
return %8 : $() // id: %9
} // end sil function 'main.Person.run() -> ()'
// Person.walk()
sil hidden @main.Person.walk() -> () : $@convention(method) (Person) -> () {
// %0 "self" // user: %1
bb0(%0 : $Person):
debug_value %0 : $Person, let, name "self", argno 1 // id: %1
%2 = tuple () // user: %3
return %2 : $() // id: %3
}
- 1,run()方法参数中多了
@inout
关键字,传入的Person
为inout
类型。 - 2,run()方法中的 self 为
指针类型($*Person, var, name "self",)
,是var
类型。walk()方法中 self 为值类型$Person, let, name "self"
, 是let
类型。
所以我们得出,mutating
本质上默认给self
加了一个 inout
参数,访问的是self的地址
,不是self的值
了。
method
接下来,我们来探索结构体的函数调用
struct Person {
var age :Int = 10
mutating func run(){
age = 18
}
func walk(){
print("person ------walk")
}
}
let p = Person()
p.walk()
通过断点,我们来看下其汇编代码
LYSwift`main:
0x100003bc0 <+0>: pushq %rbp
0x100003bc1 <+1>: movq %rsp, %rbp
0x100003bc4 <+4>: subq $0x40, %rsp
0x100003bc8 <+8>: movl %edi, -0x4(%rbp)
0x100003bcb <+11>: movq %rsi, -0x10(%rbp)
-> 0x100003bcf <+15>: callq 0x100003e20 ; LYSwift.Person.init() -> LYSwift.Person at main.swift:22
0x100003bd4 <+20>: movq %rax, 0x444d(%rip) ; LYSwift.p : LYSwift.Person
0x100003bdb <+27>: movq 0x4446(%rip), %rdi ; LYSwift.p : LYSwift.Person
0x100003be2 <+34>: callq 0x100003d00 ; LYSwift.Person.walk() -> () at main.swift:29
我们可以看到,walk()
方法的地址是一个常量,也就意味着,该方法在编译,链接完成时,它的地址就已经确定了,是静态方法调用,不需要存储结构体的func
。
总结
- 1,结构体会默认生成包含所有属性的构造方法,而类却不会。
- 2,结构体是值类型,分配在栈空间上,类是引用类型,分配在堆空间中。
- 3,mutating关键字,提供了结构体修改自身属性的能力,本质上,是把 self 变为了 inout 类型,传递的是内存地址,根据内存地址来修改属性值。
- 4,结构体的方法,是静态方法调度,在编译、链接完成时,该方法的内存地址就已经确定,不需要存储结构体的方法。
本文使用的lldb命令
// 输出 age 变量的内存地址值
po withUnsafePointer(to: &age){print($0)}
// 格式化输出 内存分布情况
// x: memory read 的缩写
// 8: 输出8个
// g: 字节大小为 8字节 b:byte 1字节,h:half word 2字节 w:word 4字节
// x: 是16进制,f是浮点,d是10进制
x/8gx 0x00007ffeefbff430