Swift值类型和引用类型

204 阅读4分钟

Swift值类型和引用类型

hudson 译 原文

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

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

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

接下来,假设我们想编写一个函数,当用户按下某种形式的like按钮时可以调用,该函数将增加帖子的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) {
    // 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被打印为喜欢的数量:

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,以增加点赞数量,使帖子值能够自行修改:

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

使用上述方法,还可以创建另一个方便的API,一次性执行喜欢帖子(操作)所需的复制和修改:

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

有了上述修改,现在可以简化like函数,仅作为新的便利API的包装器, 显示确认UI和执行模型修改:

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

何时使用值类型和引用类型在很大程度上取决于我们希望类型具有哪种语义。是将其视为一个简单的值,只能在特定情况下进行局部修改有意义,还是每个实例具有实际身份并作为引用传递更有意义?

无论最终选择什么,都应该倾向于选择的语义——并相应地调整代码——通常是一个更好的主意,而不是与Swift类型系统作斗争。

感谢您的阅读! 🚀