一、前言

在 Swift 编程里,结构体和类是两种很重要的数据类型。很多开发者在使用的时候,常常纠结该选结构体还是类。这篇博客就来聊聊选择它们的标准,还有它们对性能的影响,让大家在写代码的时候能做出更合适的选择。

二、Swift 中结构体和类的基本概念

2.1 结构体

结构体是值类型,就好像是一个装东西的盒子,当你把这个盒子复制一份给别人的时候,其实是复制了一份全新的内容,而不是共享同一个盒子。下面是一个简单的结构体示例:

// Swift 技术栈
// 定义一个表示人的结构体
struct Person {
    var name: String
    var age: Int
}

// 创建一个 Person 结构体实例
var person1 = Person(name: "张三", age: 25)
// 复制 person1 到 person2
var person2 = person1
// 修改 person2 的属性
person2.name = "李四"

print(person1.name) // 输出: 张三
print(person2.name) // 输出: 李四

从这个例子可以看出,修改 person2name 属性,并不会影响 person1name 属性,因为它们是两个独立的副本。

2.2 类

类是引用类型,就像一个共享的文件,很多人可以同时打开这个文件进行操作。下面是一个简单的类示例:

// Swift 技术栈
// 定义一个表示动物的类
class Animal {
    var species: String
    init(species: String) {
        self.species = species
    }
}

// 创建一个 Animal 类的实例
var animal1 = Animal(species: "猫")
// 让 animal2 引用 animal1
var animal2 = animal1
// 修改 animal2 的属性
animal2.species = "狗"

print(animal1.species) // 输出: 狗
print(animal2.species) // 输出: 狗

在这个例子中,修改 animal2species 属性,animal1species 属性也会跟着改变,因为它们引用的是同一个对象。

三、选择标准

3.1 数据独立性

如果你希望数据是独立的,不希望被其他地方修改,那么结构体是更好的选择。比如,在一个游戏中,每个玩家的分数都是独立的,不会相互影响,这时候就可以用结构体来表示玩家的分数。

// Swift 技术栈
// 定义一个表示玩家分数的结构体
struct PlayerScore {
    var score: Int
}

// 创建两个玩家的分数实例
var player1Score = PlayerScore(score: 100)
var player2Score = PlayerScore(score: 200)

// 修改 player1 的分数
player1Score.score = 150

print(player1Score.score) // 输出: 150
print(player2Score.score) // 输出: 200

3.2 数据共享

如果需要多个地方共享同一个数据,并且对数据的修改会影响到所有引用它的地方,那么类是更好的选择。比如,在一个多人协作的文档编辑系统中,所有用户看到的都是同一个文档内容,这时候就可以用类来表示文档。

// Swift 技术栈
// 定义一个表示文档的类
class Document {
    var content: String
    init(content: String) {
        self.content = content
    }
}

// 创建一个文档实例
var doc1 = Document(content: "这是一份文档")
// 让 doc2 引用 doc1
var doc2 = doc1
// 修改 doc2 的内容
doc2.content = "这是修改后的文档"

print(doc1.content) // 输出: 这是修改后的文档
print(doc2.content) // 输出: 这是修改后的文档

3.3 继承需求

如果需要使用继承来实现代码的复用和扩展,那么只能选择类。结构体不支持继承。比如,在一个图形绘制系统中,有圆形、矩形等不同的图形,它们都有一些共同的属性和方法,这时候就可以创建一个基类 Shape,然后让 CircleRectangle 类继承自 Shape

// Swift 技术栈
// 定义一个基类 Shape
class Shape {
    var color: String
    init(color: String) {
        self.color = color
    }
    func draw() {
        print("绘制一个 \(color) 的图形")
    }
}

// 定义一个 Circle 类,继承自 Shape
class Circle: Shape {
    var radius: Double
    init(color: String, radius: Double) {
        self.radius = radius
        super.init(color: color)
    }
    override func draw() {
        print("绘制一个 \(color) 的半径为 \(radius) 的圆形")
    }
}

// 创建一个 Circle 实例
let circle = Circle(color: "红色", radius: 5.0)
circle.draw() // 输出: 绘制一个 红色 的半径为 5.0 的圆形

四、性能影响

4.1 内存管理

结构体是值类型,它的内存管理比较简单。当结构体实例被创建时,会在栈上分配内存,当实例超出作用域时,栈上的内存会自动释放。而类是引用类型,它的实例会在堆上分配内存,需要通过引用计数来管理内存,当引用计数为 0 时,堆上的内存才会被释放。下面是一个简单的示例:

// Swift 技术栈
// 定义一个结构体
struct Point {
    var x: Int
    var y: Int
}

// 定义一个类
class PointClass {
    var x: Int
    var y: Int
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

func testMemory() {
    // 创建结构体实例
    let pointStruct = Point(x: 1, y: 2)
    // 创建类实例
    let pointClass = PointClass(x: 1, y: 2)
    // 当函数结束时,pointStruct 的内存会自动释放
    // pointClass 的内存需要等引用计数为 0 时才会释放
}

testMemory()

4.2 复制操作

结构体的复制操作是深复制,会创建一个全新的副本,这可能会消耗一定的性能,尤其是当结构体包含大量数据时。而类的复制操作是浅复制,只是复制了引用,不会复制对象本身,性能开销相对较小。下面是一个示例:

// Swift 技术栈
// 定义一个包含大量数据的结构体
struct BigStruct {
    var data: [Int] = Array(repeating: 0, count: 1000000)
}

// 定义一个包含大量数据的类
class BigClass {
    var data: [Int] = Array(repeating: 0, count: 1000000)
}

// 测试结构体的复制性能
let startStruct = Date()
var bigStruct1 = BigStruct()
var bigStruct2 = bigStruct1
let endStruct = Date()
let structTime = endStruct.timeIntervalSince(startStruct)

// 测试类的复制性能
let startClass = Date()
let bigClass1 = BigClass()
let bigClass2 = bigClass1
let endClass = Date()
let classTime = endClass.timeIntervalSince(startClass)

print("结构体复制时间: \(structTime) 秒")
print("类复制时间: \(classTime) 秒")

从这个示例可以看出,结构体的复制时间通常会比类的复制时间长。

五、应用场景

5.1 结构体的应用场景

  • 简单数据存储:比如表示坐标、颜色等简单的数据,使用结构体可以保证数据的独立性。
// Swift 技术栈
// 定义一个表示坐标的结构体
struct Coordinate {
    var x: Double
    var y: Double
}

let coordinate = Coordinate(x: 10.0, y: 20.0)
  • 函数参数传递:当函数只需要使用数据的副本时,使用结构体可以避免不必要的内存开销。
// Swift 技术栈
// 定义一个结构体
struct Rectangle {
    var width: Double
    var height: Double
}

// 计算矩形面积的函数
func calculateArea(rect: Rectangle) -> Double {
    return rect.width * rect.height
}

let rect = Rectangle(width: 5.0, height: 10.0)
let area = calculateArea(rect: rect)
print("矩形面积: \(area)")

5.2 类的应用场景

  • 复杂对象管理:比如管理用户信息、数据库连接等复杂对象,使用类可以方便地共享和修改数据。
// Swift 技术栈
// 定义一个表示用户的类
class User {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// 创建一个用户实例
let user = User(name: "张三", age: 25)
  • 多态和继承:当需要实现多态和继承时,类是必不可少的。比如在游戏开发中,不同类型的角色可以继承自一个基类,然后实现各自的行为。
// Swift 技术栈
// 定义一个基类 Character
class Character {
    var name: String
    init(name: String) {
        self.name = name
    }
    func attack() {
        print("\(name) 进行攻击")
    }
}

// 定义一个 Warrior 类,继承自 Character
class Warrior: Character {
    override func attack() {
        print("\(name) 挥舞宝剑进行攻击")
    }
}

// 定义一个 Mage 类,继承自 Character
class Mage: Character {
    override func attack() {
        print("\(name) 释放魔法进行攻击")
    }
}

// 创建不同类型的角色实例
let warrior = Warrior(name: "战士")
let mage = Mage(name: "法师")

warrior.attack() // 输出: 战士 挥舞宝剑进行攻击
mage.attack() // 输出: 法师 释放魔法进行攻击

六、技术优缺点

6.1 结构体的优缺点

优点

  • 数据独立性:保证数据的独立性,避免数据被意外修改。
  • 内存管理简单:栈上分配内存,自动释放,减少内存泄漏的风险。
  • 线程安全:由于结构体是值类型,在多线程环境下使用更安全。

缺点

  • 复制开销大:深复制会消耗一定的性能,尤其是数据量较大时。
  • 不支持继承:无法通过继承来实现代码的复用和扩展。

6.2 类的优缺点

优点

  • 数据共享:多个地方可以共享同一个对象,方便数据的修改和管理。
  • 支持继承:可以通过继承来实现代码的复用和扩展,提高代码的可维护性。
  • 复制开销小:浅复制只复制引用,性能开销相对较小。

缺点

  • 内存管理复杂:需要通过引用计数来管理内存,容易出现内存泄漏的问题。
  • 线程安全问题:在多线程环境下,需要注意线程安全问题,避免数据竞争。

七、注意事项

7.1 结构体的注意事项

  • 避免频繁复制:如果结构体包含大量数据,频繁复制会影响性能,尽量减少不必要的复制操作。
  • 合理设计结构体:结构体的设计应该尽量简单,避免包含过多的属性和方法。

7.2 类的注意事项

  • 避免循环引用:循环引用会导致内存泄漏,需要使用弱引用或无主引用等方式来避免。
// Swift 技术栈
// 定义一个类 A
class A {
    weak var b: B?
    init() {}
    deinit {
        print("A 被释放")
    }
}

// 定义一个类 B
class B {
    var a: A
    init(a: A) {
        self.a = a
    }
    deinit {
        print("B 被释放")
    }
}

var a: A? = A()
var b: B? = B(a: a!)
a?.b = b

a = nil
b = nil
  • 注意线程安全:在多线程环境下,对类的属性进行修改时,需要使用锁等机制来保证线程安全。

八、文章总结

在 Swift 中,结构体和类各有优缺点,选择哪种类型需要根据具体的应用场景来决定。如果需要数据的独立性、简单的内存管理和线程安全,那么结构体是更好的选择;如果需要数据共享、继承和多态,那么类是更好的选择。同时,我们也需要注意它们的性能影响和使用注意事项,这样才能写出高效、稳定的代码。