MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift结构体与类的区别及选择策略

2023-12-177.6k 阅读

一、Swift 结构体与类的基础概念

(一)结构体基础

在 Swift 中,结构体是一种复合数据类型,它将多个相关的变量和函数组合在一起。结构体可以定义属性(存储值的变量)和方法(函数),用于描述和操作数据。结构体的定义使用 struct 关键字。例如:

struct Point {
    var x: Int
    var y: Int
    func moveBy(xDelta: Int, yDelta: Int) {
        x += xDelta
        y += yDelta
    }
}
var myPoint = Point(x: 5, y: 10)
myPoint.moveBy(xDelta: 3, yDelta: 2)
print("New point: (\(myPoint.x), \(myPoint.y))")

在上述代码中,Point 结构体定义了两个存储属性 xy,以及一个实例方法 moveBy,用于移动点的位置。myPointPoint 结构体的一个实例,可以通过它来调用方法和访问属性。

(二)类的基础

类同样是一种复合数据类型,用于封装数据和行为。与结构体类似,类也可以定义属性和方法。类的定义使用 class 关键字。例如:

class Circle {
    var radius: Double
    init(radius: Double) {
        self.radius = radius
    }
    func area() -> Double {
        return Double.pi * radius * radius
    }
}
let myCircle = Circle(radius: 5.0)
let circleArea = myCircle.area()
print("Circle area: \(circleArea)")

在这个例子中,Circle 类有一个存储属性 radius,一个初始化方法 init 用于设置初始半径,以及一个计算面积的实例方法 areamyCircleCircle 类的一个实例,通过它可以调用 area 方法获取圆的面积。

二、值语义与引用语义

(一)结构体的值语义

结构体遵循值语义。这意味着当结构体实例被赋值给一个新的变量,或者作为函数参数传递时,会进行值的拷贝。例如:

struct Size {
    var width: Int
    var height: Int
}
var size1 = Size(width: 10, height: 20)
var size2 = size1
size2.width = 15
print("size1 width: \(size1.width), size2 width: \(size2.width)")

在上述代码中,size2size1 的一个拷贝。当修改 size2width 属性时,size1width 属性不受影响。这是因为结构体的赋值操作会创建一个新的实例,拥有独立的内存空间存储属性值。

(二)类的引用语义

类遵循引用语义。当类的实例被赋值给一个新的变量,或者作为函数参数传递时,传递的是引用(内存地址),而不是值的拷贝。例如:

class Person {
    var name: String
    init(name: String) {
        self.name = name
    }
}
var person1 = Person(name: "Alice")
var person2 = person1
person2.name = "Bob"
print("person1 name: \(person1.name), person2 name: \(person2.name)")

在这个例子中,person2person1 指向同一个 Person 实例。因此,当修改 person2name 属性时,person1name 属性也会改变,因为它们共享相同的内存地址。

三、内存管理与生命周期

(一)结构体的内存管理

结构体的内存管理相对简单。由于结构体遵循值语义,它们的实例通常存储在栈上(虽然在某些复杂情况下可能会存储在堆上)。当结构体实例的作用域结束时,它所占用的内存会自动从栈中移除。例如:

func createAndUseStruct() {
    struct TempStruct {
        var value: Int
    }
    var temp = TempStruct(value: 10)
    // 当函数结束时,temp 所占用的栈内存会被自动释放
}
createAndUseStruct()

在上述代码中,TempStruct 的实例 temp 在函数 createAndUseStruct 内部创建,当函数执行完毕,temp 所占用的栈内存会被自动回收。

(二)类的内存管理

类的实例存储在堆上。由于类遵循引用语义,多个变量可能引用同一个类的实例。Swift 使用自动引用计数(ARC)来管理类实例的内存。ARC 会自动跟踪每个类实例的引用数量,当一个实例的引用计数降为 0 时,ARC 会自动释放该实例所占用的内存。例如:

class DataObject {
    var data: String
    init(data: String) {
        self.data = data
        print("DataObject created: \(data)")
    }
    deinit {
        print("DataObject deallocated: \(data)")
    }
}
var obj1: DataObject? = DataObject(data: "Initial data")
var obj2 = obj1
obj1 = nil
// 此时 obj2 仍然引用 DataObject 实例,实例不会被释放
obj2 = nil
// 当 obj2 也被设置为 nil 时,引用计数降为 0,实例被释放

在上述代码中,DataObject 类有一个 deinit 方法,用于在实例被释放时打印一条消息。通过 obj1obj2DataObject 实例的引用操作,可以观察到 ARC 对内存的管理过程。

四、继承特性

(一)结构体不支持继承

结构体不支持继承。每个结构体都是独立的,不能从其他结构体派生。这有助于保持结构体的简单性和独立性。例如:

struct Shape {
    var color: String
}
// 以下代码会报错,结构体不能继承
// struct Rectangle: Shape {
//     var width: Int
//     var height: Int
// }

在上述代码中,如果尝试让 Rectangle 结构体继承自 Shape 结构体,编译器会报错,因为结构体不具备继承特性。

(二)类支持继承

类支持继承,一个类可以继承另一个类的属性、方法和其他特性。子类可以重写父类的方法和属性,以实现更具体的行为。例如:

class Animal {
    var name: String
    init(name: String) {
        self.name = name
    }
    func makeSound() {
        print("\(name) makes a sound.")
    }
}
class Dog: Animal {
    override func makeSound() {
        print("\(name) barks.")
    }
}
let myDog = Dog(name: "Buddy")
myDog.makeSound()

在这个例子中,Dog 类继承自 Animal 类。Dog 类重写了 Animal 类的 makeSound 方法,以实现狗叫的特定行为。myDog 作为 Dog 类的实例,调用 makeSound 方法时会执行 Dog 类中重写后的方法。

五、类型转换

(一)结构体的类型转换

由于结构体不支持继承,不存在类似类的向上和向下类型转换。结构体之间的转换通常是通过自定义方法或初始化器来实现。例如:

struct Square {
    var sideLength: Int
    func toRectangle() -> Rectangle {
        return Rectangle(width: sideLength, height: sideLength)
    }
}
struct Rectangle {
    var width: Int
    var height: Int
}
let mySquare = Square(sideLength: 5)
let myRectangle = mySquare.toRectangle()

在上述代码中,Square 结构体通过 toRectangle 方法将自身转换为 Rectangle 结构体,这种转换是通过自定义方法实现的,与类的基于继承的类型转换机制不同。

(二)类的类型转换

类支持基于继承的类型转换,包括向上转换和向下转换。向上转换是将子类实例转换为父类类型,这是安全的,因为子类是父类的一种特殊形式。向下转换需要使用 as?as! 操作符,并且需要进行类型检查以确保转换的安全性。例如:

class Vehicle {
    var brand: String
    init(brand: String) {
        self.brand = brand
    }
}
class Car: Vehicle {
    var model: String
    init(brand: String, model: String) {
        self.model = model
        super.init(brand: brand)
    }
}
let myCar = Car(brand: "Toyota", model: "Corolla")
let vehicle: Vehicle = myCar // 向上转换
if let car = vehicle as? Car {
    print("Car model: \(car.model)")
} else {
    print("Not a car.")
}

在上述代码中,myCarCar 类的实例,将其赋值给 Vehicle 类型的变量 vehicle 进行向上转换。然后通过 as? 操作符进行向下转换,并使用 if let 语句进行类型检查,以确保转换成功。

六、初始化与析构

(一)结构体的初始化

结构体有默认的成员逐一初始化器,即使没有显式定义初始化器,也可以通过成员逐一初始化器来创建实例。例如:

struct Book {
    var title: String
    var author: String
}
let myBook = Book(title: "Swift Programming", author: "John Doe")

在上述代码中,Book 结构体没有显式定义初始化器,但可以通过默认的成员逐一初始化器创建实例。如果结构体定义了自定义初始化器,默认的成员逐一初始化器将不再可用。此外,结构体也可以定义自定义的初始化器,以满足特定的初始化需求。例如:

struct Rectangle {
    var width: Int
    var height: Int
    init(sideLength: Int) {
        width = sideLength
        height = sideLength
    }
}
let square = Rectangle(sideLength: 10)

在这个例子中,Rectangle 结构体定义了一个自定义初始化器,通过传入一个边长来创建正方形的矩形实例。

(二)类的初始化

类的初始化相对复杂。类必须为所有存储属性提供初始值,可以在定义属性时提供默认值,也可以在初始化器中进行赋值。类有指定初始化器和便利初始化器,指定初始化器必须调用父类的指定初始化器,以确保父类的属性也被正确初始化。例如:

class Shape {
    var color: String
    init(color: String) {
        self.color = color
    }
}
class Circle: Shape {
    var radius: Double
    init(radius: Double, color: String) {
        self.radius = radius
        super.init(color: color)
    }
    convenience init(radius: Double) {
        self.init(radius: radius, color: "Black")
    }
}
let myCircle = Circle(radius: 5.0)

在上述代码中,Shape 类有一个指定初始化器,Circle 类继承自 Shape 类,并重写了指定初始化器,同时定义了一个便利初始化器。便利初始化器最终调用了指定初始化器。

(三)析构

结构体没有析构函数,因为它们的内存管理相对简单,当作用域结束时内存自动释放。而类可以定义析构函数 deinit,用于在实例被释放时执行清理操作,如关闭文件、释放资源等。例如:

class FileHandler {
    var filePath: String
    init(filePath: String) {
        self.filePath = filePath
        print("Opening file: \(filePath)")
    }
    deinit {
        print("Closing file: \(filePath)")
    }
}
var file: FileHandler? = FileHandler(filePath: "example.txt")
file = nil

在上述代码中,FileHandler 类的 deinit 方法在 file 实例被释放时打印关闭文件的消息,模拟了文件资源的清理操作。

七、性能考虑

(一)结构体的性能优势

  1. 栈存储:由于结构体通常存储在栈上,访问结构体实例的属性速度更快,因为栈的访问速度比堆快。这对于频繁访问的小型数据结构非常有利。例如,一个简单的 Point 结构体用于表示坐标,在图形渲染等性能敏感的场景中,使用结构体可以提高访问效率。
  2. 值语义:结构体的值语义使得在赋值和传递时的行为更可预测,并且在某些情况下可以避免复杂的引用计数和内存管理开销。例如,在一个函数内部频繁传递和修改一个表示尺寸的结构体,由于是值传递,不会影响其他地方对该结构体的使用,同时也不需要额外的引用计数操作。

(二)类的性能特点

  1. 堆存储:类实例存储在堆上,这使得它们更适合处理大型和复杂的数据结构,因为堆的内存空间更大。然而,堆的访问速度相对较慢,在性能敏感的操作中,频繁访问类实例的属性可能会带来一定的性能损耗。
  2. 引用语义:类的引用语义在某些场景下具有优势,例如在多个地方需要共享同一数据时,通过引用传递可以减少内存开销。但同时,引用计数和内存管理也会带来一定的性能成本,尤其是在频繁创建和销毁类实例的情况下。

八、选择策略

(一)选择结构体的场景

  1. 小型数据结构:当需要表示简单、小型的数据,如坐标、尺寸、颜色等,结构体是很好的选择。因为它们具有值语义和栈存储的优势,访问速度快且内存管理简单。例如,在游戏开发中表示一个物体的位置和大小,使用结构体可以提高性能。
  2. 独立性要求高:如果数据需要保持独立性,避免在传递和赋值时影响其他实例,结构体的值语义可以满足这一需求。例如,在一个金融计算的库中,用于表示货币金额的结构体,在不同的计算过程中传递时,不会相互影响。
  3. 不需要继承:当数据不需要从其他类型派生,也不需要进行复杂的类型转换时,结构体的简单性和不支持继承的特性可以使代码更清晰和易于维护。例如,在一个工具库中用于表示配置参数的结构体,不需要继承其他类型,直接定义属性和方法即可。

(二)选择类的场景

  1. 复杂数据结构和继承:当需要处理复杂的数据结构,并且需要使用继承来构建类型层次结构时,类是必要的选择。例如,在一个图形绘制库中,有 Shape 类作为基类,CircleRectangle 等子类继承自 Shape 类,通过继承实现不同形状的共性和特性。
  2. 共享数据:如果多个部分需要共享同一数据,类的引用语义可以方便地实现这一点。例如,在一个多线程的应用程序中,多个线程需要访问和修改同一个数据对象,使用类可以通过引用传递该对象,而不需要进行大量的值拷贝。
  3. 动态类型转换:当需要进行动态类型转换,根据对象的实际类型执行不同的操作时,类的继承和类型转换机制可以满足这一需求。例如,在一个游戏角色管理系统中,不同类型的角色(如战士、法师等)继承自 Character 类,可以通过类型转换根据角色类型执行不同的技能。

综上所述,在 Swift 编程中,结构体和类各有其特点和适用场景。了解它们的区别并根据具体需求选择合适的类型,有助于编写高效、清晰和可维护的代码。无论是小型数据的快速处理,还是复杂数据结构和继承体系的构建,都可以通过正确选择结构体或类来实现。