• Swift 5.9 新特性揭秘:非复制类型的安全与高效
  • 发布于 1周前
  • 49 热度
    0 评论
前言
在 Swift 中,类型默认是可复制的。这种设计简化了开发过程,因为它允许值在赋值给新变量或传递给函数时轻松复制。然而,这种便利有时会导致意想不到的问题。例如,复制单次使用的票据或重复连接数据库可能会导致无效状态或资源冲突。为了解决这些问题,在 Swift 5.9 中引入了非复制类型。通过将类型标记为~Copyable 来实现,我们可以显式地阻止 Swift 复制它。这保证了值的唯一所有权,并施加了更严格的约束,从而降低了出错的风险。接下来让我们详细了解一下非复制类型。

非复制类型的示例
以下是一个非复制类型的简单示例:
struct SingleUseTicket: ~Copyable {
    let ticketID: String
}
与常规值类型的行为不同,当我们将非复制类型的实例分配给新变量时,值会被移动而不是复制。如果我们尝试在稍后使用原始变量,会得到编译时错误:
let originalTicket = SingleUseTicket(ticketID: "S645")
let newTicket = originalTicket
print(originalTicket.ticketID) // 报错 'originalTicket' used after consume
需要注意的是,类不能被声明为非复制类型。所有类类型仍然是可复制的,通过保留和释放对对象的引用来实现。

非复制类型中的方法
在非复制类型中,方法可以读取、修改或消费self。

借用方法
非复制类型中的方法默认是借用borrowing 的。这意味着它们只能读取实例,允许安全地检查实例而不影响其有效性。
struct SingleUseTicket: ~Copyable {
    let ticketID: String
    
    func describe() {
        print("This ticket is \(ticketID).")
    }
}

let ticket = SingleUseTicket(ticketID: "A123")
// 堆代码 duidaima.com
// 打印 `This ticket is A123.`
ticket.describe()
可变方法
可变方法mutating 提供了对self 的临时写访问,允许在不使实例无效的情况下进行修改。
struct SingleUseTicket: ~Copyable {
    var ticketID: String
    mutating func updateID(newID: String) {
        ticketID = newID
        print("Ticket ID updated to \(ticketID).")
    }
}
var ticket = SingleUseTicket(ticketID: "A123")
// 打印 `Ticket ID updated to B456.`
ticket.updateID(newID: "B456")
消费方法
消费方法consuming 接管self 的所有权,一旦方法完成就使实例无效。这对于完成或处置资源的任务非常有用。在调用方法后,任何尝试访问实例的操作都会导致编译错误。
struct SingleUseTicket: ~Copyable {
    let ticketID: String
    
    consuming func use() {
        print("Ticket \(ticketID) used.")
    }
}

func useTicket() {
    let ticket = SingleUseTicket(ticketID: "A123")
    ticket.use()
    
    ticket.use() // 报错 'ticket' consumed more than once
}
useTicket()
需要注意的是,我们不能消费存储在全局变量中的非复制类型,因此在我们的示例中我们将代码包装在useTicket() 函数中。

非复制类型在函数参数中的应用
当将非复制类型作为参数传递给函数时,Swift 要求我们为该函数指定所有权模型。我们可以将参数标记为借用borrowing、输入输出inout 或消费consuming,每种标记提供不同级别的访问权限,类似于类型内部的方法。

借用参数
借用所有权允许函数临时读取值,而不消耗或修改它。
func inspectTicket(_ ticket: borrowing SingleUseTicket) {
    print("Inspecting ticket \(ticket.ticketID).")
}
输入输出参数
输入输出参数inout 提供了对值的临时写访问,允许函数修改它,同时将所有权返回给调用者。
func updateTicketID(_ ticket: inout SingleUseTicket, to newID: String) {
    ticket.ticketID = newID
    print("Ticket ID updated to \(ticket.ticketID).")
}
消费参数
当一个参数被标记为消费时,函数完全接管该值的所有权,使其对于调用者无效。例如,如果我们有一个消费方法,我们可以在函数中使用它,而无需担心在函数外部使用该值。
func processTicket(_ ticket: consuming SingleUseTicket) {
    ticket.use()
}

析构函数和丢弃操作符
非复制结构体和枚举可以像类一样拥有析构函数deinit,它们会在实例生命周期结束时自动运行。
struct SingleUseTicket: ~Copyable {
    let ticketID: Int
    deinit {
        print("Ticket deinitialized.")
        
        // 清理逻辑
    }
}
然而,当一个消费方法和一个析构函数都执行清理时,可能会有冗余操作的风险。为了解决这个问题,Swift 引入了丢弃操作符discard。通过在消费方法中使用discard self,我们可以显式阻止调用析构函数,从而避免重复逻辑:
struct SingleUseTicket: ~Copyable {
    let ticketID: Int
    
    consuming func invalidate() {
        print("Ticket \(ticketID) invalidated.")
        
        // 清理逻辑
        
        discard self
    }
    
    deinit {
        print("Ticket deinitialized.")
        
        // 清理逻辑
    }
}
另外需要注意的是,只有当我们的类型包含可轻松销毁的存储属性时,才能使用discard。不能包含引用计数、泛型。

总结
最近几年,swift 出了很多新特性,非复制类型是其中之一,实际开发中,非复制类型很少用到,但是了解这些特性,可以让我们在开发中更加得心应手。随着 Swift 的不断发展,这些类型代表了语言在性能和正确性方面的重大进步。

但是这些越来越复杂的特性也让 swift 初学者望而却步,希望这篇文章能帮助大家了解非复制类型,在实际开发中,如果需要使用非复制类型,可以参考这篇文章。
用户评论