欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

06-结构体和类

程序员文章站 2024-03-23 09:57:28
...
  • 结构体

  • 在 Swift 标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分
  • 比如 Bool、Int、Double、 String、Array、Dictionary 等常见类型都是结构体
struct Date {
    var year: Int
    var month: Int
    var day: Int
}
var date = Date(year: 2020, month: 6, day: 5)
  • 所有的结构体都有一个编译器自动生成的初始化器(initializer,初始化方法、构造器、构造方法)
  • 在第6行调用的,可以传入所有成员值,用以初始化所有成员(存储属性,Stored Property)
  • 结构体的初始化器

  • 编译器根据情况,可能会为结构体生成多个初始化器。前提是保证 所有成员都有初始值
    06-结构体和类

06-结构体和类
06-结构体和类
06-结构体和类

  • 思考:下面的代码能编译通过吗

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

var p1 = Point(x: 10, y: 10)
var p2 = Point(x: 10)
var p3 = Point(y: 10)
var p4 = Point()
  • 因为可选项有个默认值 nil, 所以可以编译通过

  • 自定义初始化器

  • 一但在定义结构体时自定义初始化器,编译器就不会帮自动生成其他初始化器。
    06-结构体和类

  • 窥探初始化器的本质

  • 以下2段代码完全等效

struct Point {
    var x: Int = 0
    var y: Int = 0
}
var p = Point()
struct Point {
    var x: Int
    var y: Int
    
    init() {
        x = 0
        y = 0
    }
}
var p = Point()
  • 结构体内存结构

struct Point {
    var x: Int = 0
    var y: Int = 0
    var origin: Bool = false
}
print(MemoryLayout<Point>.size)         // 17
print(MemoryLayout<Point>.stride)       // 24
print(MemoryLayout<Point>.alignment)    // 8

64位系统下,结构体中 x 占 8 个字节,y 占 8 个字节,origin 占 1 个字节,所以 Point 用到的内存一共占 17 个字节,但是因为要遵守内存对齐原则(8个字节),所以系统会分配 24 个字节来存储 Point。

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

  • 类的初始化器

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

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

class Point {
    var x: Int = 10
    var y: Int = 20
}
let p1 = Point()
class Point {
    var x: Int
    var y: Int
    init() {
        x = 10
        y = 20
    }
}
let p1 = Point()

上面2段代码是完全等效的

  • 结构体与类的本质区别

  • 结构体是值类型(枚举也是值类型),类是引用类型(指针类型)
class Size {
    var width = 1
    var height = 2
}

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

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

06-结构体和类
上图都是针对 64 bit 环境。

  • 值类型

  • 值类型赋值给 var、let或者给函数传参,是直接将所有内容拷贝一份
  • 类似于对文件进行 copy、paste操作,产生了全新的文件副本。属于深拷贝(deep copy)
struct Point {
    var x: Int
    var y: Int
}

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

06-结构体和类

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

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

06-结构体和类
请问 p1.x 和 p1.y 是多少?

p2.x = 11
p2.y = 22

因为 p2 是拷贝了 p1 的内容并且产生全新副本(深拷贝), 所以 p1 的值还是原来的值,与 p2 是否有变动无关。

  • 值类型的赋值操作

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]
  • 在Swift标准库中,为了提升性能,String、Array、Dictionary、Set 采取了Copy On Write的技术, 比如仅当有“写”操作时,才会真正执行拷贝操作
  • 对于标准库值类型的赋值操作,Swift 能确保最佳性能,所有没必要为了保证最佳性能来避免赋值
  • 建议:不需要修改的,尽量定义成 let
struct Point {
    var x: Int
    var y: Int
}

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

06-结构体和类

  • 引用类型

  • 引用赋值给var、let或者给函数传参,是将内存地址拷贝一份
  • 类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件。属于浅拷贝(shallow copy)
class Size {
    var width: Int
    var height: Int
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

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

06-结构体和类

请问s1.width和s1.height是多少?

s2.width = 11
s2.height = 22

是 11 和 22 ,因为 s1 和 s2 保存的内存地址相同,所以当去修改 s2 的值时,s1 的值也会改变,他们都指向堆空间中的同一块内存。

  • 对象的堆空间申请过程

在Swift中,创建类的实例对象,要向堆空间申请内存,大概流程如下

  • Class.__allocating_init()
  • libswiftCore.dylib: swift_allocObject
  • libswiftCore.dylib: swift_slowAlloc
  • libsystem_malloc.dylib: malloc
  • 在Mac、iOS中的 malloc 函数分配的内存大小总是16 的倍数
  • 通过 class_getInstanceSize 可以得知:类的对象至少需要占用多少内存
class Point  {
    var x = 11
    var test = true
    var y = 22
}
var p = Point()
class_getInstanceSize(type(of: p)) // 40
class_getInstanceSize(Point.self) // 40

可以看到,打印的结果都是 40 个字节。
在类中,x 占 8 个字节,y 占 8 个字节,test 占 1 个字节,所以目前我们看到的有 17 个字节。
但是因为在内存中,类存储在堆空间中,它前面会有 8 个字节存放类型信息,8个字节存引用计数,再加上面的,加起来一共是 33 个字节
根据内存对齐原则(8 个字节),系统会分配 40 字节来存储 Point。

  • 引用类型的赋值操作

06-结构体和类

  • 值类型,引用类型的 let

06-结构体和类

  • 可以看到,当结构体为 let 时,无论是重新赋值还是去修改结构体中变量的值,编译器都会报错,而类只有重新赋值的时候编译器才会报错,修改类中变量的值不会报错。

  • 原因是结构体存在栈空间中,假如去修改了它的值或者它的变量,都会改变结构体的内存数据,使用 let 的时候是不允许的。

  • 而实例化一个类时,它的指针常量是存在栈空间,所以不能被重新赋值,但是,指针常量所指向堆空间的内存地址中,width 这个变量是可以被修改的。原因有两点:1.修改类的中的变量,不会影响他在栈空间中定义的常量。2.因为是变量,所以可以被修改。

  • 嵌套类型

struct Poker {
    enum Suit: Character {
        case spades = "♠️", hearts = "❤️", diamornds = "♦️", clubs = "♣️"
    }
    
    enum Rank: Int {
        case tow = 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 = .diamornds

var rank = Poker.Rank.five
rank = .king
  • 枚举,结构体,类都可以定义方法

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

enum PokerFace: Character {
    case spades = "♠️", hearts = "❤️", diamornds = "♦️", clubs = "♣️"
    func show() {
        print("face is \(rawValue)")
    }
}

let pf = PokerFace.diamornds
pf.show() // face is ♦️
struct Point {
    var x = 10
    var y = 10
    func show() {
        print("x = \(x), y = \(y)")
    }
}
let p = Point()
p.show()    // x = 10, y = 10
class Size {
    var width = 10
    var height = 10
    func show() {
        print("width = \(width), height = \(height)")
    }
}
let s = Size()
s.show()  // width = 10, height = 10

本文章只是本人的学习笔记!

上一篇: JavaScript(3)

下一篇: 12-初始化