Swift类型通常可以分为两类——值类型和引用类型——这决定了它们将如何在不同的函数和其他代码范围之间处理。当使用值类型时,每个实例都被单独处理并作为值改变,而引用类型实例每一个都作为对象的引用。让我们看看这到底意味着什么,以及其中的一些实际含义是什么。

让我们从引用类型开始,它在Swift中本质上是指定义为类的类型。假设我们正在开发一个社交网络应用程序,并且我们想定义一个类型来表示用户可以发布的帖子。 如果我们选择让它成为一个类,它可能看起来像这样:

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

接下来,假设我们想要编写一个函数,当用户按下某种形式的like按钮时可以调用这个函数,它将增加帖子的like数量,并显示一个确认UI:

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

将上述两段代码放在一起,我们现在可以创建一个Post实例,将它传递给like函数,并期望打印Post的likes数将导致调试控制台显示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函数 -因为传入函数的值默认是常量,这意味着它们不能以任何方式改变。 -因为传入函数的值默认是常量,这意味着它们不能以任何方式改变。

func like(_ post: Post) {
    // Simply re-assigning the post to a new, mutable, variable
    // will actually create a new copy of it.
    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被打印为likes的数量:

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

虽然inout有它自己的用例,但是完全接受值类型的概念比把它们当作引用(如果我们需要引用,为什么不坚持使用类来代替呢?)为了做到这一点,让like函数返回一个新的、更新过的post副本,而不是试图改变原来的值:

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添加一个突变API,以增加点赞数,使Post值能够自身突变:

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

使用上述方法,我们还可以创建另一个方便的API,它可以一次完成喜欢一个帖子所需的复制和修改:

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

有了上面的这些,我们现在可以回到我们的like函数,并简化它,只作为一个包装器来显示确认UI和执行我们的模型修改,使用我们新的便利API:

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

什么时候使用值,什么时候使用引用类型,这在很大程度上取决于我们希望一个类型具有哪种语义。 把它作为一个简单的值来处理(只能在特定的环境下局部变异)最有意义吗?还是让每个实例都有一个实际的标识并作为引用传递更有意义?

不管我们最终选择了什么,通常更好的做法是采用我们选择的语义——并相应地调整我们的代码——而不是与类型系统作对。

原文链接