Swift 类和结构体

2021-04-01  本文已影响0人  小時間光

我的博客

girl052.jpg

1、值类型和引用类型

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

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
    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的默认初始化方法有两种:

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 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的共同点
3.2、structclass`的区别

4、structclass的选择

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

4.1、默认情况下使用struct

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

4.2、是否需要继承或Protocol
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 {
    
}
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进阶:结构体和类

上一篇下一篇

猜你喜欢

热点阅读