Swfit结构体

344 阅读3分钟

本文主要来介绍 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

我们可以看到 ageage2的内存地址相差 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关键字,传入的 Personinout类型。
  • 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