Swift笔记7:数据类型8(Struct、Class)

Swift 标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分
比如Bool、Int、Double、 String、Array、Dictionary等常见类型都是结构体

结构体
struct PersonInfo {
    static let title = "成绩单"  //存储属性
    var name = "wang" //存储属性
    var age: Int? //存储属性
    var math: Int?
    var chinese: Int?
    var total: Int?
    mutating func getTotal() { // 更改属性需要加上mutating
        total = (chinese ?? 0) + (math ?? 0)
    }
}

所有的结构体都有一个编译器自动生成的初始化器(initializer,初始化方法、构造器、构造方法)
调用的,可以传入所有成员值,用以初始化所有成员(存储属性,Stored Property)
编译器会根据情况,可能会为结构体生成多个初始化器,
宗旨是:保证所有成员都有初始值。可选项都有个默认值nil,或者初始赋值都可以再构造器中不再初始化
比如
var tom = PersonInfo(name: "tom", age: 15, math: 70, chinese: 70)
var jim1 = PersonInfo(name: "jim21", age: 17, math: 50)
var jim2 = PersonInfo(name: "jim2", age: 18)

func execStruct() {
    var jim = PersonInfo()
    jim.name = "jim wang"
    jim.age = 18
    jim.total = 99
    print(PersonInfo.title) // 类属性调用
    print(jim) // PersonInfo(name: "jim wang", age: Optional(18), math: nil, total: Optional(99))
    var tom = PersonInfo(name: "tom", age: 15, math: 70, chinese: 70)
    tom.getTotal()
    print(tom) // PersonInfo(name: "tom", age: Optional(15), math: Optional(70), chinese: Optional(70), total: Optional(140))
    var jim1 = PersonInfo(name: "jim21", age: 17, math: 50)
    var jim2 = PersonInfo(name: "jim2", age: 18)
}

一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器
格式很简单,不需要写func,只需要init。同时也没有返回值。

    init(name: String = "wang", age: Int? = nil, math: Int? = nil, chinese: Int? = nil, total: Int? = nil) {
        self.name = name
        self.age = age
        self.math = math
        self.chinese = chinese
        self.total = total
    }

结构体内存结构

结构体内存中结构成员是连续的,字节对齐是8
Point中x8个字节,y8个字节,origin布尔类型1个字节。需要占用17个字节,分配24个字节存储

struct Station {
    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  字节对齐是8
}

类对象主要分为2部分:8字节存放metadata地址,后面存放属性的堆空间。
SWIFT_RUNTIME_EXPORT @interface SwiftObject<NSObject> {
@private
Class isa;
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
}
类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器。
如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器。
不管是类还是结构体,初始化器在汇编层面上都是必须存在的,并且在初始化器中完成的。

class Point {
    var x: Int = 0
    var y: Int?
}
let p1 = Point()

结构体与类的本质区别

let s1 = Station()
let p1 = Point()
1 结构体和类中都可以定义方法。C和OC语言中结构体不允许定义函数方法,C++和Swift中可以。
2 结构体和类在底层也是调用alloc,和malloc中创建的。__allocating_init()向对空间申请内存。malloc向对空间分配内存。在Mac、iOS中的malloc函数分配的内存大小总是16的倍数。也就是堆空间16内存对齐。

    class InstanceSize {
        // 类信息8+引用计数8 = 16字节
        var x = 11 // 8字节
        var isTest = true // 1字节
        var y = 22 // 8字节
    }
    // 33 实际使用内存.
    // 40 按8内存对齐,分配能工作的最少需要的内存
    // malloc堆内存16对齐,实际分配的内存
    let p = InstanceSize() // malloc
    let t = class_getInstanceSize(InstanceSize.self) // 总计40字节  [InstanceSize class]
    let m = class_getInstanceSize(type(of: p)) // 40 .这里实际上不是全部占用的内存.malloc_size()方法才是获取全部内存.
    print(t, m) // 40,40
    print(MemoryLayout<InstanceSize>.size) // size真正有利用的内存 8,这里是指针
    print(MemoryLayout<InstanceSize>.stride) // stride真正给它分配的内存大小 8这里是指针

3 结构体是值类型(枚举也是值类型),类是引用类型(指针类型)
4 函数调用内存在栈空间。无论函数在哪里没区别。
5 结构体的内存要看哪里定义的,如果是函数里面定义的就在栈空间。如果定义为全局变量,就存在数据段也就是全局区。如果这个结构体再类里面,那么它的存储空间在堆中。
类不论在哪里创建,内存都在堆空间。它的指针变量的内存,可能在栈,全局区,堆中。
如果在类的方法里定义结构体,结构体的内存还在栈中。
6 我们在在64位环境变量,函数中创建结构体和类有下面的区别。
值类型的特点是如果值类型是在函数中创建的,这个结构体变量s1,s1的内存在栈空间。它的内部存储变量是连续的。
如果是类的实例变量p1,它是个指针变量,指针在64位环境变量中占8个字节。p1指针变量的内存地址存储在栈空间。p1在栈空间存放的内存地址,是类的实例在堆空间的地址。p1指向堆空间存放着实例变量的首地址。在堆空间占32个字节,其中指向类型信息8字节,这8字节的地址存放着和类相关的信息的内存地址。引用计数8。两个成员变量8x2.我们要注意我们创建的类对象指针,仅仅是个地址,在栈空间。这个指向的堆空间地址才是存放类对象内存的地方。

注意我们使用
print(MemoryLayout<Point>.stride) //8 并不是堆空间的大小,是指针变量的大小。
当我们结构体中存储变量是optional时内存也+1

struct Station {
    var x: Int = 0
    var y: Int?
    var origin: Bool = false
}
print(MemoryLayout<Station>.size)  //18
print(MemoryLayout<Station>.stride) //24  对齐

// malloc(<#T##__size: Int##Int#>)
// C语言库函数,传一个UnsafeRawPointer指针
// malloc(16)申请16个字节内存.pointAddress是堆空间16字节的地址
var pointAddress = malloc(1) //向对空间申请 一个字节
// 在Mac、iOS中的malloc函数分配的内存大小总是16的倍数。
print(malloc_size(pointAddress)) //16
内存

内存地址格式为:0x4bdc(%rip),一般是全局变量,全局区(数据段)
内存地址格式为:-0x78(%rbp),一般是局部变量,栈空间
内存地址格式为:0x10(%rax),一般是堆空间

值类型和引用类型

1 值类型。当它被赋值给一个变量、常量或者被传递给一个函数的时候,其值会被拷贝。
(1.1) 实际上,Swift 中所有的基本类型:整数(integer)、浮点数(floating-point number)、布尔值(boolean)、字符串(string)、数组(array)和字典(dictionary),都是值类型,其底层也是使用结构体实现的。
(1.2) Swift 中所有的结构体和枚举类型都是值类型。这意味着它们的实例,以及实例中所包含的任何值类型的属性,在代码中传递的时候都会被复制。
(1.3) 当值类型作为一个参数传给函数时,也是深拷贝。

注意
在Swift标准库中,为了提升性能,String、Array、Dictionary、Set采取了Copy On Write的技术。特别比如我们在常操作的字符串、数组、字典在修改内存时,就是深拷贝。否则就是浅拷贝。
比如仅当有“写”操作时,才会真正执行拷贝操作
对于标准库值类型的赋值操作,Swift 能确保最佳性能,所有没必要为了保证最佳性能来避免赋值
建议:不需要修改的,尽量定义成let
标准库定义的集合,例如数组,字典和字符串,都对复制进行了优化以降低性能成本。新集合不会立即复制,而是跟原集合共享同一份内存,共享同样的元素。在集合的某个副本要被修改前,才会复制它的元素。而你在代码中看起来就像是立即发生了复制。
同时更要注意是Swift标准库才会有以上操作,我们自定义的结构体是不支持的。
比如我们自定义的Station,值类型赋值给var、let或者给函数传参,是直接将所有内容拷贝一份
进行copy、paste操作,产生了全新的文件副本。深拷贝(deep copy)
var s1 = Station()
s2 = s1 //深拷贝(deep copy)s2是另外开辟了一段空间深拷贝了s1的内存。修改存储属性,互不影响

2 引用类型。
引用赋值给var、let或者给函数传参,是将内存地址拷贝一份
类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件。属于浅拷贝(shallow copy)
比如Point类,p1和p2的指针变量地址虽然不同,但是它们指向的堆内存是同一块空间。通过引用计数类来管理这份内存。我们修改p1,p2修改的是同一块内存。
如果我们再用对p1重新初始化,相当于重新给指针变量赋值,相当于开辟了块新的内存空间。老的p1的地址如果引用计数为0,即没有任何指针指向这个对象的话,会被arc销毁。

    let p1 = Point()
    let p2 = p1
    p1.x = 77
    print(p2.x)  //77 

(2.1) 当引用类型作为一个参数传给函数时,只是将存储的地址值传递给了函数。

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

一般把定义在枚举、结构体、类内部的函数,叫做方法
方法是不占用实例对象内存的。公共的方法,所有的实例对象访问的都是代码段中相同的内存。
函数和方法存在代码段。不论定义在什么作用域中都在代码段。
为什么放在类中可以,函数就可以访问类的属性呢?
实际方法的格式是:

func show(self: Point){  //把类对象传进来了
  print(self.x, self.y)
}
值类型、引用类型的let

如果结构体初始化定义为let,首先结构体整体是不能覆盖的,其次它的成员也是不可以改的。
如果是类初始化定义为let,类对象的指针是不可以修改的,但是内部的成员变量可以更改。

struct Station {
    var x: Int = 0
    var y: Int?
    var origin: Bool = false
}

class Point {
    var x: Int = 0
    var y: Int?
    init(x: Int, y: Int? = nil) {
        self.x = x
        self.y = y
    }
}
let s = Station()
s.x = 20  //会报错提示let常量不能修改
let p1 = Point(x: 10, y: 20)
p1.x = 77 //可以修改
p1 = Point(x: 11, y: 22) //报错 Cannot assign to value: 'p1' is a 'let' constant

s和p1的内存是不可以改的,s的整体24个字节都不可以改。p1指针变量的8个字节不可以改,指向的堆空间是可以改的。同理let 的字符串数组等等也是结构体,不可改。

嵌套类型
    enum 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
恒等运算符

因为类是引用类型,所以多个常量和变量可能在幕后同时引用同一个类实例。(对于结构体和枚举来说,这并不成立。因为它们作为值类型,在被赋予到常量、变量或者传递到函数时,其值总是会被拷贝。)
判定两个常量或者变量是否引用同一个类实例有时很有用。为了达到这个目的,Swift 提供了两个恒等运算符:
相同(===)
不相同(!==)
使用这两个运算符检测两个常量或者变量是否引用了同一个实例:

if tenEighty === alsoTenEighty {
    print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
}
// 打印 "tenEighty and alsoTenEighty refer to the same VideoMode instance."

请注意,“相同”(用三个等号表示,===)与“等于”(用两个等号表示,==)的不同。“相同”表示两个类类型(class type)的常量或者变量引用同一个类实例。“等于”表示两个实例的值“相等”或“等价”,判定时要遵照设计者定义的评判标准。
当在定义你的自定义结构体和类的时候,你有义务来决定判定两个实例“相等”的标准

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容