价值和参考类型
通常,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按钮时可以调用该函数,这将增加帖子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
我们还可以更进一步,并添加一个mutatingAPI来Post增加喜欢的数量,使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()
}
何时使用值与引用类型在很大程度上取决于我们希望类型具有何种语义。是否最有意义的是将其视为一个简单的值,只能在特定情况下进行局部变更,或者每个实例具有实际身份更有意义,并作为参考传递?
无论我们最终选择什么,通常更好的方法是依靠我们选择的语义 - 并相应地调整我们的代码 - 而不是与类型系统作斗争。
https://www.swiftbysundell.com/basics/value-and-reference-types