原文:Value and Reference Types | Swift by Sundell
一般来说,Swift 类型可以分为两类------值类型和引用类型------这决定了它们在不同函数和其他代码范围之间的处理方式。使用值类型时,每个实例都作为一个值单独处理和变异,而引用类型实例每个都充当对对象的引用。让我们来看看这实际上意味着什么,以及一些实际影响。
让我们从引用类型开始,它在 Swift 中本质上意味着定义为 class
的类型。假设我们正在开发一个社交网络应用程序,并且我们想要定义一个类型来表示用户可以发布的帖子。如果我们选择使它成为一个类,它可能看起来像这样:
swift
class Post {
var title: String
var text: String
var numberOfLikes = 0
init(title: String, text: String) {
self.title = title
self.text = text
}
}
接下来,假设我们要编写一个函数,当用户按下某种形式的点赞按钮时可以调用该函数,这将增加帖子的 numberOfLikes
并显示确认 UI:
swift
func like(_ post: Post) {
post.numberOfLikes += 1
showLikeConfirmation()
}
将上述两段代码放在一起,我们现在可以创建一个 Post
实例,将其传递给我们的 like
函数,并期望打印帖子的 numberOfLikes
将导致 1 显示在调试控制台中:
swift
let post = Post(title: "Hello, world!", text: "...")
like(post)
print(post.numberOfLikes) // 1
到目前为止,一切都很好,而且总的来说,类(或引用类型)的行为方式通常非常直观------尤其是对于具有使用其他面向对象编程语言背景的开发人员而言。如果我们将一个对象传递给一个函数,那么该函数内发生的任何突变也会反映在它之外------因为我们总是引用原始实例,即使我们将一个对象传递给我们代码库的不同部分。
另一方面,值类型的行为完全不同。让我们保持我们的 Post
类型和以前完全一样,只是把它从一个类变成一个结构:
swift
struct Post {
var title: String
var text: String
var numberOfLikes = 0
init(title: String, text: String) {
self.title = title
self.text = text
}
}
随着上述变化的到位,编译器将迫使我们稍微修改我们的 like
函数------因为传递给函数的值默认是常量,这意味着它们不能以任何方式改变。因此,为了让我们能够增加传递的帖子的 numberOfLikes
属性,我们需要创建它的可变副本,如下所示:
swift
func like(_ post: Post) {
// 简单地将帖子重新分配给一个新的、可变的变量
// 实际上会创建它的一个新副本
var post = post
post.numberOfLikes += 1
showLikeConfirmation()
}
但是,问题在于,由于我们现在正在复制该值,因此我们在 like
函数范围内对其所做的任何更改都不会应用于我们传入的原始 Post
值------使我们之前的代码现在打印 0 而不是 1:
swift
let post = Post(title: "Hello, world!", text: "...")
like(post)
print(post.numberOfLikes) // 0
解决上述问题的一种方法是使用 inout
关键字将 like
函数的 Post
参数转换为引用,即使它是值类型。这样,我们可以自由地改变函数内部的值,并且更改将应用于传入的原始值 - 就像使用引用类型时一样:
swift
func like(_ post: inout Post) {
post.numberOfLikes += 1
showLikeConfirmation()
}
唯一的区别是,在调用栈点,我们现在需要使用 &
前缀传递 Post
值------这表明我们传递了一个值类型作为引用,再次导致 1 被打印为喜欢的数量:
swift
var post = Post(title: "Hello, world!", text: "...")
like(&post)
print(post.numberOfLikes) // 1
虽然 inout
确实有其用例,但可以说完全接受值类型的概念比将它们视为引用更好(如果我们需要引用,为什么不坚持使用类呢?)。为此,让我们让我们的 like
函数返回传递的帖子的新的、更新的副本------而不是尝试改变原始值:
swift
func like(_ post: Post) -> Post {
var post = post
post.numberOfLikes += 1
showLikeConfirmation()
return post
}
完成上述更改后,我们现在可以简单地将调用 like
的结果分配给我们原来的 post
变量,以确保我们的外部范围反映了我们函数中所做的更改:
swift
var post = Post(title: "Hello, world!", text: "...")
post = like(post)
print(post.numberOfLikes) // 1
我们还可以更进一步,为 Post
添加一个mutating
API 以增加喜欢的数量,使 post
值能够自行变异:
swift
extension Post {
mutating func like() {
numberOfLikes += 1
}
}
使用上述方法,我们还可以创建另一个便利 API,它执行一次喜欢帖子所需的复制和变异:
Mutating and non-mutating Swift contexts | Swift by Sundell
swift
extension Post {
func liked() -> Post {
var post = self
post.like()
return post
}
}
完成上述操作后,我们现在可以回到我们的 like
函数,并使用我们新的便利 API 将其简化为仅充当显示确认 UI 和执行模型突变的包装器:
swift
func like(_ post: Post) -> Post {
showLikeConfirmation()
return post.liked()
}
何时使用值类型与引用类型在很大程度上取决于我们希望类型具有什么样的语义。将其视为一个简单值是否更有意义?只能在特定情况下进行局部变异,还是每个实例具有实际身份并作为参考传递更有意义?
不管我们最终选择什么,倾向于我们选择的语义------并相应地调整我们的代码------而不是与类型系统对抗通常是一个更好的主意。
谢谢阅读!🚀