原文:Value and Reference Types | Swift by Sundell

一般来说,Swift 类型能够分为两类——值类型和引证类型——这决定了它们在不同函数和其他代码范围之间的处理方式。运用值类型时,每个实例都作为一个值独自处理和变异,而引证类型实例每个都充任对对象的引证。让咱们来看看这实践上意味着什么,以及一些实践影响。

让咱们从引证类型开始,它在 Swift 中本质上意味着定义为 class 的类型。假设咱们正在开发一个交际网络应用程序,并且咱们想要定义一个类型来表示用户能够发布的帖子。假如咱们挑选使它成为一个类,它可能看起来像这样:

class Post {
    var title: String
    var text: String
    var numberOfLikes = 0
    init(title: String, text: String) {
        self.title = title
        self.text = text
    }
}

接下来,假设咱们要编写一个函数,当用户按下某种方式的点赞按钮时能够调用该函数,这将增加帖子的 numberOfLikes 并显现承认 UI:

func like(_ post: Post) {
    post.numberOfLikes += 1
    showLikeConfirmation()
}

将上述两段代码放在一同,咱们现在能够创立一个 Post 实例,将其传递给咱们的 like 函数,并期望打印帖子的 numberOfLikes 将导致 1 显现在调试控制台中:

let post = Post(title: "Hello, world!", text: "...")
like(post)
print(post.numberOfLikes) // 1

到目前为止,一切都很好,并且总的来说,类(或引证类型)的行为方式通常非常直观——尤其是关于具有运用其他面向对象编程语言布景的开发人员而言。假如咱们将一个对象传递给一个函数,那么该函数内发生的任何突变也会反映在它之外——因为咱们总是引证原始实例,即便咱们将一个对象传递给咱们代码库的不同部分。

另一方面,值类型的行为完全不同。让咱们坚持咱们的 Post 类型和以前完全相同,只是把它从一个类变成一个结构:

struct Post {
    var title: String
    var text: String
    var numberOfLikes = 0
    init(title: String, text: String) {
        self.title = title
        self.text = text
    }
}

跟着上述改变的到位,编译器将迫使咱们略微修改咱们的 like 函数——因为传递给函数的值默许是常量,这意味着它们不能以任何方式改变。因而,为了让咱们能够增加传递的帖子的 numberOfLikes 特点,咱们需要创立它的可变副本,如下所示:

func like(_ post: Post) {
    // 简略地将帖子重新分配给一个新的、可变的变量
    // 实践上会创立它的一个新副本
    var post = post
    post.numberOfLikes += 1
    showLikeConfirmation()
}

但是,问题在于,因为咱们现在正在仿制该值,因而咱们在 like 函数范围内对其所做的任何更改都不会应用于咱们传入的原始 Post 值——使咱们之前的代码现在打印 0 而不是 1:

let post = Post(title: "Hello, world!", text: "...")
like(post)
print(post.numberOfLikes) // 0

解决上述问题的一种办法是运用 inout 关键字将 like 函数的 Post 参数转换为引证,即便它是值类型。这样,咱们能够自由地改变函数内部的值,并且更改将应用于传入的原始值 – 就像运用引证类型时相同:

func like(_ post: inout Post) {
    post.numberOfLikes += 1
    showLikeConfirmation()
}

唯一的区别是,在调用栈点,咱们现在需要运用 & 前缀传递 Post 值——这表明咱们传递了一个值类型作为引证,再次导致 1 被打印为喜爱的数量:

var post = Post(title: "Hello, world!", text: "...")
like(&post)
print(post.numberOfLikes) // 1

尽管 inout 的确有其用例,但能够说完全接受值类型的概念比将它们视为引证更好(假如咱们需要引证,为什么不坚持运用类呢?)。为此,让咱们让咱们的 like 函数回来传递的帖子的新的、更新的副本——而不是测验改变原始值:

func like(_ post: Post) -> Post {
    var post = post
    post.numberOfLikes += 1
    showLikeConfirmation()
    return post
}

完成上述更改后,咱们现在能够简略地将调用 like 的成果分配给咱们本来的 post 变量,以确保咱们的外部范围反映了咱们函数中所做的更改:

var post = Post(title: "Hello, world!", text: "...")
post = like(post)
print(post.numberOfLikes) // 1

咱们还能够更进一步,为 Post 增加一个mutating API 以增加喜爱的数量,使 post 值能够自行变异:

extension Post {
    mutating func like() {
        numberOfLikes += 1
    }
}

运用上述办法,咱们还能够创立另一个便当 API,它履行一次喜爱帖子所需的仿制和变异:

Mutating and non-mutating Swift contexts | Swift by Sundell

extension Post {
    func liked() -> Post {
        var post = self
        post.like()
        return post
    }
}

完成上述操作后,咱们现在能够回到咱们的 like 函数,并运用咱们新的便当 API 将其简化为仅充任显现承认 UI 和履行模型突变的包装器:

func like(_ post: Post) -> Post {
    showLikeConfirmation()
    return post.liked()
}

何时运用值类型与引证类型在很大程度上取决于咱们希望类型具有什么样的语义。将其视为一个简略值是否更有意义?只能在特定情况下进行部分变异,仍是每个实例具有实践身份并作为参阅传递更有意义?

不管咱们终究挑选什么,倾向于咱们挑选的语义——并相应地调整咱们的代码——而不是与类型体系对抗通常是一个更好的主见。

谢谢阅览!