iOS应用开发从零基础到上架商店 - 第3节

面向对象编程:类、结构体、枚举、属性、方法、继承、协议、扩展与泛型

在iOS开发中,Swift语言的核心魅力之一在于其强大的面向对象编程(OOP)与面向协议编程(POP)能力。无论是构建一个简单的工具类,还是设计一套复杂的业务模块,理解类、结构体、枚举、属性、方法、继承、协议、扩展和泛型这九大核心概念,都是你从“会写代码”迈向“会设计代码”的关键一步。本章将带你逐一击破这些概念,并通过一个完整的“订单管理系统”实战案例,将这些知识点串联起来,帮助你建立真正的工程化思维。

Swift 面向对象编程核心概念关系图 面向对象编程 (OOP) 类 (Class) 结构体 (Struct) 枚举 (Enum) 属性 (Properties) 方法 (Methods) 下标 (Subscripts) 继承 (Inheritance) 协议 (Protocols) 扩展 (Extensions) 泛型 (Generics) 类与结构体是数据载体,枚举定义状态,协议与扩展提供行为抽象,泛型保证类型安全复用

上图清晰地展示了本章九大概念之间的逻辑关系。类、结构体和枚举是Swift中定义数据模型的基础类型;属性、方法和下标是这些类型的内部成员;继承、协议和扩展则是赋予类型行为与抽象能力的高级特性;而泛型贯穿始终,为所有类型提供复用与抽象支持。接下来,我们将逐一深入探讨。

一、类、结构体与枚举:数据模型的三种形态

1.1 核心原理:值类型与引用类型的本质区别

在Swift中,结构体(Struct)和枚举(Enum)是值类型,而类(Class)是引用类型。这是理解三者差异的基石。值类型在赋值或传递时,会复制一份全新的实例,彼此独立;引用类型在赋值或传递时,只是增加了一个指向同一内存地址的引用,修改任何一个引用都会影响原对象。

对比维度类 (Class)结构体 (Struct)枚举 (Enum)
类型本质引用类型值类型值类型
继承支持支持不支持(但可遵守协议)不支持(但可遵守协议)
可变性var/let均可修改属性let实例不可修改属性let实例不可修改关联值
内存管理ARC自动引用计数栈上分配,自动释放栈上分配,自动释放
典型应用场景单例、视图控制器、网络管理器坐标、尺寸、配置模型、数据模型状态机、方向、类型分类

1.2 判断方法:如何选择类还是结构体?

Apple官方给出了明确的指导原则:默认使用结构体,仅在以下情况使用类:

  • 需要继承时(例如自定义一个继承自UIView的组件)。
  • 需要比较实例身份时(使用===判断两个变量是否指向同一对象)。
  • 实例需要被多个地方共享并修改时(例如应用全局配置)。

对于枚举,它最适合用来表示一组有限且互斥的状态。例如网络请求的状态(加载中、成功、失败)、用户角色(管理员、普通用户、访客)等。

1.3 案例拆解:订单状态机设计

假设我们正在开发一个电商App,需要管理订单的完整生命周期。使用枚举来定义订单状态是最佳实践:

enum OrderStatus {
    case pending          // 待支付
    case paid             // 已支付
    case shipping         // 配送中
    case delivered        // 已送达
    case completed        // 已完成
    case cancelled        // 已取消
    
    var description: String {
        switch self {
        case .pending: return "等待买家付款"
        case .paid: return "卖家正在处理"
        case .shipping: return "商品运输中"
        case .delivered: return "已签收"
        case .completed: return "交易完成"
        case .cancelled: return "订单已关闭"
        }
    }
}

在这个设计中,枚举的关联值还可以携带额外信息。例如,对于.shipping状态,我们可以关联快递公司和运单号:

case shipping(company: String, trackingNumber: String)

这样,状态机不仅清晰,还能携带业务数据,避免了使用字符串或整数表示状态时容易出现的拼写错误和魔法数字问题。

经验提醒:在iOS开发中,使用枚举配合switch语句时,编译器会强制检查所有case,确保穷尽性。这比使用if-else链更安全,也更易维护。

二、属性与方法:封装数据与行为

2.1 核心原理:存储属性与计算属性

存储属性直接保存值,是实例的一部分;计算属性不存储值,而是通过getter和setter来间接获取和设置其他属性的值。计算属性特别适合用于派生数据的场景,例如根据订单金额和折扣计算最终支付金额。

struct Order {
    var items: [CartItem]      // 存储属性
    var discount: Double = 0   // 存储属性
    
    // 计算属性:总金额
    var totalAmount: Double {
        let subtotal = items.reduce(0) { $0 + $1.price * Double($1.quantity) }
        return subtotal * (1 - discount)
    }
    
    // 计算属性:订单状态描述(只读)
    var statusDescription: String {
        return status.description
    }
}

2.2 方法的设计原则:实例方法与类型方法

实例方法操作实例自身的属性,而类型方法(使用static或class关键字)则与具体实例无关。在iOS开发中,类型方法常用于创建工具类或工厂方法。例如,创建一个日期格式化工具:

struct DateFormatterUtil {
    static func formatTimestamp(_ timestamp: TimeInterval) -> String {
        let date = Date(timeIntervalSince1970: timestamp)
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm"
        return formatter.string(from: date)
    }
}

2.3 实战场景:订单金额计算与展示

在订单详情页,我们需要展示商品小计、折扣金额和最终应付金额。通过计算属性,我们可以确保这些数据始终同步,避免手动更新导致的逻辑错误。同时,使用didSet属性观察器可以在折扣变化时自动刷新UI:

var discount: Double = 0 {
    didSet {
        // 折扣变化时,通知UI刷新
        NotificationCenter.default.post(name: .orderDiscountDidChange, object: nil)
    }
}
风险提示:在计算属性中,避免执行耗时操作(如网络请求或数据库查询),因为计算属性可能被频繁调用,影响性能。如果确实需要,请使用懒加载(lazy)或缓存机制。

三、继承、协议与扩展:构建可扩展的架构

3.1 继承:类独有的复用机制

继承允许子类复用父类的属性和方法,并可以重写(override)父类的方法来改变行为。在iOS中,继承常用于UI组件的定制。例如,创建一个带圆角和阴影的按钮:

class RoundedButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }
    
    private func setup() {
        layer.cornerRadius = 8
        layer.shadowOpacity = 0.3
        layer.shadowRadius = 4
    }
}

3.2 协议:面向协议编程的核心

协议定义了一套蓝图(属性、方法、初始化器的要求),任何类型(类、结构体、枚举)都可以遵守协议。在iOS开发中,UITableViewDataSource和UITableViewDelegate是最经典的协议应用。协议让代码解耦,使得组件可以独立测试和替换。

protocol OrderCalculable {
    var totalAmount: Double { get }
    func calculateShippingCost() -> Double
}

extension Order: OrderCalculable {
    func calculateShippingCost() -> Double {
        return totalAmount > 100 ? 0 : 10
    }
}

3.3 扩展:为现有类型添加新能力

扩展可以在不修改原始代码的情况下,为类、结构体、枚举或协议添加新功能。这是Swift语言的一大亮点。例如,为Int添加一个判断是否为偶数的扩展:

extension Int {
    var isEven: Bool {
        return self % 2 == 0
    }
}

扩展还可以为协议提供默认实现,这使得所有遵守该协议的类型自动获得该功能,而无需重复代码。

3.4 案例拆解:订单列表的协议化设计

假设我们需要支持多种订单类型(普通订单、团购订单、秒杀订单),它们有不同的金额计算和展示逻辑。通过协议+扩展,我们可以优雅地实现多态:

protocol OrderDisplayable {
    var title: String { get }
    var subtitle: String { get }
    func displayPrice() -> String
}

struct NormalOrder: OrderDisplayable {
    var title: String { return "普通订单" }
    var subtitle: String { return "标准配送" }
    func displayPrice() -> String { return "¥\(totalAmount)" }
}

struct GroupOrder: OrderDisplayable {
    var title: String { return "团购订单" }
    var subtitle: String { return "满3人成团" }
    func displayPrice() -> String { return "团购价 ¥\(totalAmount * 0.8)" }
}

在列表页,我们只需要处理OrderDisplayable协议,而不需要关心具体类型,这大大降低了耦合度。

核心要点:在Swift中,优先使用协议和扩展来设计抽象,而不是依赖继承。这符合苹果倡导的“面向协议编程”理念,能让代码更灵活、更易测试。

四、泛型:编写灵活且类型安全的代码

4.1 核心原理:类型参数化

泛型允许你编写不指定具体类型的函数或类型,在使用时才确定类型。这既保证了类型安全,又避免了代码重复。Swift标准库中的Array、Dictionary都是泛型类型。

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

4.2 泛型约束:限定类型范围

有时我们需要限制泛型参数必须遵守某个协议或继承自某个类。例如,要求泛型类型必须可以进行相等比较:

func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

4.3 实战场景:通用的网络请求管理器

在iOS开发中,网络请求是最常见的场景。使用泛型可以构建一个类型安全的API客户端:

struct APIResponse<T: Decodable> {
    let data: T
    let statusCode: Int
}

class APIClient {
    func request<T: Decodable>(_ endpoint: String, 
                                method: String = "GET",
                                completion: @escaping (Result<APIResponse<T>, Error>) -> Void) {
        // 实际网络请求代码...
        // 成功时: completion(.success(APIResponse(data: decodedData, statusCode: 200)))
        // 失败时: completion(.failure(error))
    }
}

使用时,只需指定返回类型,编译器会自动推断:

let client = APIClient()
client.request<UserProfile>("/user/profile") { result in
    switch result {
    case .success(let response):
        print("用户昵称: \(response.data.nickname)")
    case .failure(let error):
        print("请求失败: \(error.localizedDescription)")
    }
}

4.4 常见误区与总结复盘

常见误区正确做法
滥用类,所有模型都用Class定义默认使用Struct,仅在需要继承或引用共享时使用Class
用字符串拼接表示状态使用枚举定义状态,配合switch保证穷尽性
协议只用于代理模式将协议作为核心设计工具,用于依赖注入和模块解耦
泛型只用于集合类型在自定义函数、类、结构体中广泛使用泛型,提升复用性

通过本章的学习,你应该已经掌握了Swift面向对象编程的核心骨架。在实际开发中,请时刻思考:这个数据模型应该用值类型还是引用类型?这个行为应该通过继承还是协议来抽象?这段逻辑是否可以用泛型来消除重复?带着这些问题去实践,你的代码质量将会有质的飞跃。