Swift 进阶(四)结构体和类

结构体

基本概念

在Swift标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分

比如Bool、Int、String、Double、Array、Dictionary等常见类型都是结构体

struct Date {
    var year: Int
    var month: Int
    var day: Int
}

所有的结构体都有一个编译器自动生成的孵化器(initializer,初始化方法、构造器、构造方法)

可以传入所有成员值,用以初始化所有成员(存储属性,Stored Property)

var date = Date(year: 2019, month: 6, day: 23)

结构体的初始化器

编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值

如果结构体的成员定义的时候都有默认值了,那么生成的初始化器不会报错

-w569

如果是下面这几种情况就会报错

-w642
-w645
-w640

如果是可选类型的初始化器也不会报错,因为可选类型默认的值就是nil

-w457

自定义初始化器

我们也可以自定义初始化器

struct Point {
    var x: Int = 0
    var y: Int = 0
    
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

var p1 = Point(x: 10, y: 10)

下面这几个初始化器报错的原因是因为我们已经自定义初始化器了,编译器就不会再帮我们生成其他的初始化器了

-w643

初始化器的本质

下面这两种写法是完全等效的

struct Point {
    var x: Int = 0
    var y: Int = 0
}

等效于

struct Point {
    var x: Int
    var y: Int
    
    init() {
        x = 0
        y = 0
    }
}

var p4 = Point()

我们通过反汇编分别对比一下两种写法的实现,发现也是一样的

-w713
-w715

1.然后我们分别打印结构体的占用内存大小和内存布局

struct Point {
    var x: Int = 10
    var y: Int = 20
}

var p4 = Point()

debugPrint(MemoryLayout<Point>.stride) // 16
debugPrint(MemoryLayout<Point>.size) // 16
debugPrint(MemoryLayout<Point>.alignment) // 8

debugPrint(Mems.memStr(ofVal: &p4)) // 0x000000000000000a 0x0000000000000014

可以看到系统一共分配了16个字节的内存空间

前8个字节存储的是10,后8个字节存储的是20

2.我们再看下面这个结构体

struct Point {
    var x: Int = 10
    var y: Int = 20
    var origin: Bool = false
}

var p4 = Point()

debugPrint(MemoryLayout<Point>.stride) // 24
debugPrint(MemoryLayout<Point>.size) // 17
debugPrint(MemoryLayout<Point>.alignment) // 8

debugPrint(Mems.memStr(ofVal: &p4)) // 0x000000000000000a 0x0000000000000014 0x0000000000000000

可以看到结构体实际只用了17个字节,而系统因为内存对齐分配了24个字节

前8个字节存储的是10,中间8个字节存储的是20,最后1个字节存储的是false,也就是0

类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器

-w646

如果成员没有初始值,所有的初始化器都会报错

-w648

类的初始化器

如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器

成员的初始化是在这个初始化器中完成的

class Point {
    var x: Int = 0
    var y: Int = 0
}

let p1 = Point()

下面这两种写法是完全等效的

class Point {
    var x: Int = 0
    var y: Int = 0
}

等效于

class Point {
    var x: Int
    var y: Int
    
    init() {
        x = 0
        y = 0
    }
}

let p1 = Point()

结构体与类的本质区别

结构体是值类型(枚举也是值类型),类是引用类型(指针类型)

下面我们分析函数内的局部变量分别都在内存的什么位置

class Size {
    var width = 1
    var height = 2
}

struct Point {
    var x: Int = 3
    var y: Int = 4
}

func test() {
    var size = Size()
    var point = Point()
}

变量size和point都是在栈空间

不同的是point的值是一个结构体类型,结构体因为也是值类型,所以也会在栈空间中,它里面的两个成员x、y按顺序的排布

而size的值是类,类是引用类型,所以size是个指针,指向的类是在堆中的,size里存储的是类的地址

-w1020

下面我们分析类的详细内存布局

1.我们先来看一下类的占用内存大小是多少

class Size {
    var width = 1
    var height = 2
}

debugPrint(MemoryLayout<Size>.stride) // 8

通过打印我们可以发现MemoryLayout获取的8个字节实际上是指针变量占用多少存储空间,并不是对象在堆中的占用大小

2.然后我们再看类的内存布局是怎样的

var size = Size()

debugPrint(Mems.ptr(ofVal: &size)) // 0x000000010000c388
debugPrint(Mems.memStr(ofVal: &size)) // 0x000000010072dba0

通过打印我们可以看到变量里面存储的值也是一个地址

3.我们再打印该变量所指向的对象的内存布局是什么

debugPrint(Mems.size(ofRef: size)) // 32
debugPrint(Mems.ptr(ofRef: size)) // 0x000000010072dba0
debugPrint(Mems.memStr(ofRef: size)) // 0x000000010000c278 0x0000000200000003 0x0000000000000001 0x0000000000000002

通过打印可以看到在堆中存储的对象的地址和上面的指针变量里存储的值是一样的

内存布局里一共占用32个字节,前16个字节分别用来存储一些类信息和引用计数,后面16个字节存储着类的成员变量的值

下面我们再从反汇编的角度来分析

我们要想确定类是否在堆空间中分配空间,通过反汇编来查看是否有调用malloc函数

-w708
-w716

然后就一直跟进直到这里最好调用了swift_slowAlloc

-w714

发现函数内部调用了系统的malloc在堆空间分配内存

-w709

注意:结构体和枚举存储在哪里取决于它们是在哪里分配的,如果是在函数中分配的那就是在栈里,如果是在全局中分配的那就是在数据段

而类无论是在哪里分配的,对象都是在堆空间中,指向对象内存的指针的存储位置是不确定的,可能在栈中也可能在数据段

我们再看下面的类型占用内存大小是多少

class Size {
    var width: Int = 0
    var height: Int = 0
    var test = true
}

let s = Size()

print(Mems.size(ofRef: s)) // 48

Mac、iOS中的malloc函数分配的内存大小总是16的倍数

类最前面会有16个字节用来存储类的信息和引用计数,所以实际占用内存是33个字节,但由于malloc函数分配的内存都是116的倍数1,所以分配48个字节

我们还可以通过class_getInstanceSize函数来获取类对象的内存大小

// 获取的是经过内存对齐后的内存大小,不是malloc函数分配的内存大小
print(class_getInstanceSize(type(of: s))) // 40
print(class_getInstanceSize(Size.self)) // 40

值类型和引用类型

值类型

值类型赋值给var、let或者给函数传参,是直接将所有内容拷贝一份

类似于对文件进行copy、paste操作,产生了全新的文件副本,属于深拷贝(deep copy)

值类型进行拷贝的内存布局如下所示

struct Point {
    var x: Int = 3
    var y: Int = 4
}

func test() {
    var p1 = Point(x: 10, y: 20)
    var p2 = p1

    p2.x = 4
    p2.y = 5

    print(p1.x, p1.y)
}

test()

-w536

我们通过反汇编来进行分析

-w712
-w947
-w713
-w1048

通过上述分析可以发现,值类型的赋值内部会先将p1的成员值保存起来,再给p2进行赋值,所以不会影响到p1

值类型的赋值操作

在Swift标准库中,为了提升性能,Array、String、Dictionary、Set采用了Copy On Write的技术

如果只是进行赋值操作,那么只会进行浅拷贝,两个变量使用的还是同一块存储空间

只有当进行了”写“的操作时,才会进行深拷贝操作

对于标准库值类型的赋值操作,Swift能确保最佳性能,所以没必要为了保证最佳性能来避免赋值

var s1 = "Jack"
var s2 = s1
s2.append("_Rose")

print(s1) // Jack
print(s2) // Jack_Rose

var a1 = [1, 2, 3]
var a2 = a1
a2.append(4)
a1[0] = 2

print(a1) // [2, 2, 3]
print(a2) // [1, 2, 3, 4]
var d1 = ["max" : 10, "min" : 2]
var d2 = d1
d1["other"] = 7
d2["max"] = 12

print(d1) // ["other" : 7, "max" : 10, "min" : 2]
print(d2) // ["max" : 12, "min" : 2]

注意:不需要修改的尽量改成let

我们再看下面这段代码

对于p1来说,再次赋值也只是覆盖了成员x、y的值而已,都是同一个结构体变量

struct Point {
    var x: Int
    var y: Int
}

var p1 = Point(x: 10, y: 20)
p1 = Point(x: 11, y: 22)

用let定义的赋值操作

如果用let定义的常量赋值结构体类型会报错,并且修改结构体里的成员值也会报错

let定义就意味着常量里存储的值不可更改,而结构体是由x和y这16个字节组成的,所以更改x和y就意味着结构体的值要被覆盖,所以报错

-w645

引用类型

引用赋值给var、let或者给函数传参,是将内存地址拷贝一份

类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件,属于浅拷贝(shallow copy)

class Size {
    var width = 0
    var height = 0
    
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

func test() {
    var s1 = Size(width: 10, height: 20)
    var s2 = s1
    
    s2.width = 11
    s2.height = 22
    
    print(s1.height, s1.width)
}

test()

由于s1和s2都指向的同一块存储空间,所以s2修改了成员变量,s1再调用成员变量也已经是改变后的了

-w1124

我们通过反汇编来进行分析

-w1049
-w1052
-w1052

堆空间分配完内存之后,我们拿到rax的值查看内存布局,发现rax里和对象的结构一样,证明rax里存储的就是对象的地址

-w1051
-w1187

将新的值11和22分别覆盖掉堆空间对象的成员值
-w1223
-w1224
-w1220
-w1225

通过上面的分析可以发现,修改的成员值都是改的同一个地址的对象,所以修改了p2的成员值,会影响到p1

引用类型的赋值操作

将引用类型对象赋值给同一个变量,变量会指向另一块存储空间

class Size {
    var width: Int
    var height: Int
    
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

var s1 = Size(width: 10, height: 20)
s1 = Size(width: 11, height: 22)

用let定义的赋值操作

如果用let定义的常量赋值引用类型会报错,因为会改变指针常量里存储的8个字节的地址值

但修改类里的属性值不会报错,因为修改属性值并不是修改的指针常量的内存,只是通过指针常量找到类所存储的堆空间的内存地址去修改类的属性

-w643

嵌套类型

struct Poker {
    enum Suit: Character {
        case spades = "♠️",
             hearts = "♥️",
             diamonds = "♦️",
             clubs = "♣️"
    }
    
    enum Rank: Int {
        case two = 2, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king, ace
    }
}

print(Poker.Suit.hearts.rawValue)

var suit = Poker.Suit.spades
suit = .diamonds

var rank = Poker.Rank.five
rank = .king

枚举、结构体、类都可以定义方法

一般把定义在枚举、结构体、类内部的函数,叫做方法

struct Point {
    var x: Int = 10
    var y: Int = 10
    
    func show() {
        print("show")
    }
}

let p = Point()
p.show()
class Size {
    var width: Int = 10
    var height: Int = 10
    
    func show() {
        print("show")
    }
}

let s = Size()
s.show()
enum PokerFace: Character {
    case spades = "♠️",
         hearts = "♥️",
         diamonds = "♦️",
         clubs = "♣️"
    
    func show() {
        print("show")
    }
}

let pf = PokerFace.hearts
pf.show()

方法不管放在哪里,其内存都是放在代码段中

枚举、结构体、类里的方法其实会有隐式参数

class Size {
    var width: Int = 10
    var height: Int = 10
    
    // 默认会有隐式参数,该参数类型为当前枚举、结构体、类
    func show(self: Size) {
        print(self.width, self.height)
    }
}
posted on 2021-03-14 17:43  FunkyRay  阅读(320)  评论(0编辑  收藏  举报