Swift 类和结构体

我的博客

girl052.jpg

1、值类型和引用类型

在iOS中虚拟内存分为五大内存分区:堆区、栈区、全局区、常量区、代码区。在Swift中根据对象在内存的存储位置不同分为值类型和引用类型。

  • 值类型: Swift中的值类型主要有:enum,struct、Array、Dictionary、Tuple

  • 引用类型:Swift中的引用类型主要有:methods,class,clousre

1.1、值类型和引用类型的内存分配和管理方式

Swift 中的值类型,一般存储于栈内存中(也不一定),由于栈的特性这部分内存是由CPU直接管理和优化的,对于值的copy也是深拷贝(deep copy),当使用完一个值后系统会立即释放这部分内存。所以存储于栈上的内存在创建、使用、释放都非常效率。

一般情况下,当创建一个struct默认被存储于栈区。当编译器侦测到结构体变量被一个函数闭合的时候,此时这个结构体将存储在堆上。此处可参考《Swift 进阶》一书中的结构体和类章节 闭包和可变性小节的内容。

Swift中的引用类型,一般存储于堆区,苹果采用ARC的方式来管理这部分内存,对于这部分内存的追踪就是对引用计数的追踪,当类对象被引用时引用计数+1,当引用计数为0时,ARC会释放这部分内存。

2、复杂的值类型struct

Swift提供了多种可以结构化存储数据的方式,struct、enum、class等。这里先说在开发中经常用作Model来使用的struct

2.1、struct的定义和初始化

从定义一个struct开始,这里我定义一个用来表示颜色的struct

struct Color {
    var red: Double
    var green: Double
    var blue: Double
    // 透明度
    var alpha: Double? = 1
}

我定义了四个属性,red、double、blue表示红绿蓝三色值,alpha表示透明度。这些属性所占用的内存空间决定了Color的大小。当我定义完四个属性后直接build,没有任何问题,这里首先来了解struct的初始化方法。

2.1.1、Memberwise initializer

当我定义一个struct而不为其创建任何init方法,也可以正常运行,这是由于Swift编译器自动为这个struct创建了一个初始化方法,这种init方法叫做Memberwise initializer.当我需要使用这个struct时可以这样初始化一个color对象:

var color = Color(red: 200, green: 200, blue: 200)
2.1.2 、Default initializer

如果我想在创建color对象的时候不指定参数,自动的给属性设置默认值,这时有两种方法可供选择:

  • 在定义每个属性时都为其设置默认值

    struct Color {
        var red: Double = 0
        var green: Double = 0
        var blue: Double = 0
        
        var alpha: Double? = 1
    }
    

    这样就可以在初始化color对象时,不用指定参数来初始化color对象了。

    var color = Color()
    

    这么做的要求就是在设置属性时必须为每个属性都设置默认值,因为Swift要求init方法必须初始化自定义类型的每一个属性。

  • init方法中为每个属性设置默认值

struct Color {
    var red: Double
    var green: Double 
    var blue: Double
    
    var alpha: Double?
    
    init(red: Double = 0, green: Double = 0, blue: Double = 0, alpha: Double? = 1) {
        self.red = red
        self.green = green
        self.blue = blue
        self.alpha = alpha
    }
}

这和之前直接给属性设置默认值的方法效果是一样的。当为某个struct创建了init方法后一定要保证其正确性,因为当重写了init方法后,系统就不会在创建默认的init方法了。

2.1.3、Failable init

在使用struct作为model时,如果要将其显示在界面上最终都需要转换成字符串String,这里可以使用系统提供的Codable协议来把服务器返回的进行转换,当然也可以使用其他开源库来完成这些操作。由于在初始化时可能会解析Data数据失败,这里采用init?的方式来初始化,当解析失败时,直接返回nil

struct Color {
    var red: Double
    var green: Double 
    var blue: Double
    
    var alpha: Double? = 1
  
    private enum CodingKeys: String,CodingKey {
        case red
        case green
        case blue
        case alpha
    }
}

extension Color: Codable {
    init?(data: Data) {
        guard let model = try? JSONDecoder().decode(Color.self, from: data) else { return nil }
        self = model
    }
}
2.1.4、Type property设置一个常用的值

对于一个struct经常会使用的值,我们可以采用在struct中定义成Type property。如App的主题颜色是我在代码中要经常使用的,可以采用Type roperty的初始化方式。

extension Color {
    static let themeColor = Color(red: 200, green: 200, blue: 200, alpha: 1)
}
// 当我要使用App主题颜色时
let color = Color.themeColor

当创建了默认主题颜色时,它不是struct对象的一部分,因此不会增加color对象的大小,还可以使代码看起来更简洁明了。

2.2、为struct添加方法

在Swift中不仅可以为struct添加属性还可以添加方法,只不过struct的方法,默认都是只读的,例如我要为Color添加一个修改透明度的方法:

func modifyWith(alpha: Double) {

}
2.3、mutating关键字

当我要在struct的方法中修改struct中的某个属性值时,要在这个方法前面加上mutating关键字。当添加mutating之后,Swift会隐式的把self标记为inout,这样就可以在方法中修改struct中的属性值。

mutating func modifyWith(alpha: Double) {
    self.alpha = alpha
}
2.4、修改struct

我定义了一个color对象,为了更好观察这个变量被修改时发生了什么,给他添加一个didSet clousre,只要color的值发生变化,就可以看到打印的内容:

var color = Color(red: 200, green: 200, blue: 200) {
    didSet {
        print("color============\(color)")
    }
}

现在我修改color的值,再看打印的结果。

let colorB = Color(red: 100, green: 100, blue: 100)
color = colorB
// 打印结果
color============Color(red: 100.0, green: 100.0, blue: 100.0, alpha: Optional(1.0))

这里修改了color的值所以触发了didSet方法。

如果只修改color其中某个属性值,如我要修改red属性值为110,

color.red = 110
// 打印结果
color============Color(red: 110.0, green: 100.0, blue: 100.0, alpha: Optional(1.0))

可以看到依旧会打印,也就是说只要修改color的任何一个属性值,其实整个color变量都被修改了

3、引用类型class

3.1、class的定义和初始化

当我定义一个表示颜色的类MyColor,为其设置四个属性:

class MyColor {
    var red: Double
    var green: Double
    var blue: Double

    var alpha: Double? = 1
}

这里我让这个类没有父类,也可以根据情况设置其父类。如果没有没有fu此时编译器会提示Class 'MyColor' has no initializers,这是由于类是引用类型必须有一个完整的生命周期,类必须被明确的初始化、使用、最后被明确的释放。所以当我定义了一个类时必须明确的构建init方法。这也是classstruct的一个区别之一。

3.1.1、默认init

一般的最简单的初始化方法可以直接调用init方法

let color = MyColor()

如果我想像这样初始化一个color对象,可以使用class默认的初始化方法,class的默认初始化方法有两种:

  • 为每个属性设置默认值

    class MyColor {
        var red: Double = 0.0
        var green: Double = 0.0
        var blue: Double = 0.0
    
        var alpha: Double? = 1
    }
    

    再次运行代码,就可以编译成功。这种方式的确解决了编译报错问题,如果我想在初始化时为类的每个属性设置默认值就会报错,所以为每个属性都设置默认值的方式只适合表意简单的并且初始值固定或者在其内部赋值的class。如果我想类的外部为其设置属性值,可以采用其他的初始化方式:

    let color = MyColor(red: 100, green: 100, blue: 100) // Argument passed to call that takes no arguments
    
  • init方法中为每个属性设置默认值

init(_ red: Double = 0, _ green: Double = 0, _ blue: Double = 0, _ alpha: Double? = 1) {
     self.red = red
     self.green = green
     self.blue = blue
     self.alpha = alpha
}
// 初始化
let mycolorA = MyColor()
let mycolorB = MyColor(100, 100, 100)

这样就可以根据实际的需求来初始化MyColor类了。在Swift中,初始化类的init方法必须定义在class内部,而不能定义在extension,否则会导致编译错误。structinit方法是可以定义在extension,这也是classstruct的区别之一。

3.2、 Convenience init

如果构造方法前面没有convenience关键字称作便利构造方法。如果没有称作指定构造方法

  • 便利构造方法: 初始化方法前有convenience关键字,不用对所有的属性进行初始化,因为便利构造方法依赖于指定构造方法。如果想给系统提供的类提供一个快捷创建的方法,就可以自定义一个便利构造方法
  • 指定构造方法:必须对所有的属性初始化
convenience init(at: (Double, Double, Double, Double?)) {
    self.init(at.0, at.1, at.2, at.3)
}
3.3、Failable init

在大多数时候和服务器交互数据为了统一和方便,会把所有的数据都采用字符串(String)格式, 这就需要在初始化的时候做一些处理:

convenience init?(at: (String, String, String, String?)) {
     guard let red = Double(at.0), let green = Double(at.1), let blue = Double(at.2) else {
          return nil
     }
     self.init(red, green, blue)
}

由于Stringinit可能会失败,这里采用可选的形式来定义。在其实现中,如果StringDouble失败,就返回nil,表示初始化失败。

3、比较structclass

前面分别简单介绍了structclass,这里对这两者做一个比较:

3.1 structclass的共同点
  • 都可以定义属性并用来保存值
  • 都可以构建方法
  • 都可以设置其每个属性的初始值以设置其初始状态
  • 都可以采用下标的方式来访问他的值
  • 都可以对其做extension操作,用来扩展其超出默认实现的功能
  • 都可以遵循某个协议用来提供某些标准的功能
3.2、structclass`的区别
  • struct会默认生成init方法,class必须明确指定init方法

  • struct不能继承(但是可以遵循协议),class是可以继承的

  • struct更多的是关注其值,当我修改struct其中任意属性值时整个struct都会被重新修改一次,class更多的是关注的是对象本身

4、structclass的选择

Swift中structclass有这么多共同点,那在实际开发中要如何选用structclass呢,作为开发者需要根据当前的使用时机来选择使用哪种类型:

4.1、默认情况下使用struct

一般创建一个struct其会被存储于栈区,因为struct一般不涉及到堆内存分配,无论是创建、追踪还是销毁都非常快,所以默认情况下优先选择struct

4.2、是否需要继承或Protocol
  • 如果对继承和协议没有要求,优先使用struct
  • 如果需要继承,那么只能使用class
4.3、需要和Objective-C时,使用class

SwiftObjective-C交互时,可以在class前面加@objcMembers,或者要调用的方法和变量前加@objc,在要调用的Objecrtive-C文件中导入#import "工程名-Swift.h",即可使用调用Swift类。

// Swift 文件中
@objcMembers class Model {
    func coverModel() {
     
    }
}
// Objective-C 文件中
#import "工程名-Swift.h"

Model *model = [[Model alloc] init];
[model coverModel];
4.4、需要控制身份时使用class
  • 当需要使用===比较两个实例一致性时。===会自动检查两个对象是否完全一致,包括存储数据的内存地址
let mycolorA = MyColor()
let mycolorB = mycolorA
if mycolorA === mycolorB {
    
}
  • 当需要创建用于共享、可改变的数据时
4.5、不控制身份时使用struct
  • 当需要使用==比较实例数据,用于比较两个值是否相等
var colorA = Color(red: 200, green: 200, blue: 200)
let colorB = Color(red: 100, green: 100, blue: 100)
if colorA.alpha == colorB.alpha {
    
}
  • 当需要在多线程中修改值时,优先使用struct,因为struct是线程安全的
var colorArray = [colorA, colorB, colorC, colorD, colorE]
let queue = DispatchQueue.global()
let count = colorArray.count
queue.async { [colorArray] in
    for index in 0..<colorArray.count {
        print("index=========\(colorArray[index])")
        Thread.sleep(forTimeInterval: 1)
    }
}
queue.async {
    Thread.sleep(forTimeInterval: 0.5)
    colorArray.remove(at: 2)
    print("-------\(colorArray.count)")
}

以上代码可以正常运行。

当然所谓的线程安全也是相对而言的,修改如下代码:

var colorArray = [colorA, colorB, colorC, colorD, colorE]
let queue = DispatchQueue.global()
let count = colorArray.count
queue.async {
    for index in 0..<count {
        print("index=========\(colorArray[index])")
        Thread.sleep(forTimeInterval: 1)
    }
}
queue.async {
    Thread.sleep(forTimeInterval: 0.5)
    colorArray.removeLast()
    print("-------\(colorArray.count)")
}

再次运行代码就会Crash并打印错误:

Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444

这是由于我移除了数组的最后一个元素,当需要打印最后一个(也就是第五个)元素时,数组中其实已经没有这个元素了,所以会数组越界Crash。而第一段代码之所以不会Crash,是因为在新的线程中会copy一份数组内容。所以当新建一个线程操作数据时copy值类型到新线程操作是线程安全的。

本文主要介绍了structclass及其异同点,同时简单分析了在开发中应该如何选择struct还是class。如果我的理解有不对地方欢迎指出。


本文参考:

Apple Developer: Choosing Between Structures and Classes

The Swift Programming Language: Structures and Classes

Swift进阶:结构体和类

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 在面向过程的语言中,要想实现类似类的功能只能借助结构体,其实从OC源码也能看出来,类的组成本就是复杂的结构体实现的...
    如风如花不如你阅读 8,203评论 2 6
  • Swift 中类和结构体有很多共同点。共同处在于: 定义属性用于存储值 定义方法用于提供功能 定义下标操作使得可以...
    赵哥窟阅读 949评论 0 1
  • 类和结构体是通用的,灵活的结构,成为程序代码的基础。 您可以通过使用与常量,变量和函数完全相同的语法来定义属性和方...
    Joker_King阅读 319评论 0 0
  • 类和结构体是人们构建代码所用的一种通用且灵活的构造体。我们可以使用完全相同的语法规则来为类和结构体定义属性(常量、...
    CDLOG阅读 154评论 0 0
  • 最近项目使用的是OC,后头看之前用Swift开发的一个项目时,发现很多细节都忘记了😭😭。为了回忆和以后方便查看,现...
    wg刚阅读 519评论 0 0