正如你所看到的,结构体让我们可以将单个数据片段组合成新的东西,然后附加方法以便我们可以操作这些数据。
今天,我们将学习结构体的一些更高级的特性,包括静态属性和访问控制,它们使结构体变得更加强大。
在 Swift 中隐藏对某些属性和方法的访问实际上可以让我们的代码变得更好,因为能够访问它的地方更少了。
需要提醒大家的是,这两样东西在 SwiftUI 中都会被广泛使用,所以现在就花时间掌握它们是值得的,因为从你的第一个项目开始就会用到它们。
今天我们将学习两部分内容,在这两部分内容中,你将了解多级访问控制以及创建静态属性和方法的能力。现在让我们开始吧!
如何使用访问控制限制对内部数据的访问
默认情况下,Swift 的结构体允许我们自由访问其属性和方法,但这往往不是你想要的——有时你想要隐藏某些数据,防止外部访问。例如,你可能需要在访问属性之前应用某些逻辑,或者你知道某些方法需要以特定的方式或顺序调用,因此不应该被外部访问。
我们可以用一个结构示例来说明这个问题:
struct BankAccount {
var funds = 0
mutating func deposit(amount: Int) {
funds += amount
}
mutating func withdraw(amount: Int) -> Bool {
if funds >= amount {
funds -= amount
return true
} else {
return false
}
}
}
它有从银行账户存款和取款的方法,应该这样使用:
var account = BankAccount()
account.deposit(amount: 100)
let success = account.withdraw(amount: 200)
if success {
print("Withdrew money successfully")
} else {
print("Failed to get the money")
}
但属性funds只是从外部暴露给我们的,那么是什么阻止了我们直接接触它呢?答案是完全没有——这种代码是允许的:
account.funds -= 1000
这就完全绕过了我们为阻止人们取出比他们拥有的更多资金而设置的逻辑,现在我们的程序可能会以奇怪的方式运行。
为了解决这个问题,我们可以告诉 Swift,资金只能在结构体内部访问——通过属于结构体的方法以及任何计算属性、属性观察器等。
这只需要一个额外的单词 private:
private var funds = 0
现在,从结构体外部访问资金是不可能的,但在 deposit() 和 withdraw() 内却可以。如果你尝试从结构体外部读取或写入资金,Swift 将拒绝编译您的代码。
这就是所谓的访问控制,因为它控制了如何从结构体外部访问结构体的属性和方法。
Swift 为我们提供了多个选项,但在学习过程中,你只需要几个选项:
- 使用 private 表示 “不要让结构体之外的任何东西使用它”。
- 使用 fileprivate 表示 “不允许当前文件以外的任何内容使用此结构”。
- 使用 public 表示 “允许任何人在任何地方使用它”。
还有一个额外的选项有时对学习者很有用,那就是:private(set)。这意味着 “允许任何人读取此属性,但只允许我的方法写入它”。如果我们在 BankAccount 中使用了这种方法,就意味着我们可以在结构体之外打印 account.funds,但只有 deposit() 和 withdraw() 可以实际更改该值。
在这种情况下,我认为 private(set) 是资金的最佳选择:你可以随时读取当前的银行账户余额,但如果不通过我的逻辑,你就无法改变它。
仔细想想,访问控制其实就是限制你和你团队中其他开发人员的行为,这是明智之举!如果我们能让 Swift 本身阻止我们犯错,这总是明智之举。
重要提示:如果你对一个或多个属性使用私有访问控制,你很可能需要创建自己的初始化器。
访问控制的意义何在?
Swift 的访问控制关键字让我们可以限制如何访问代码的不同部分,但很多时候它只是在遵守我们制定的规则——如果我们愿意,我们可以移除这些规则,绕过限制,那又有什么意义呢?
答案有几个,但有一个特别简单,所以我就从这里说起:有时访问控制会用到你不拥有的代码中,所以你无法取消限制。例如,当你使用苹果公司的 API 构建应用程序时,这种情况就很常见:他们会限制你能做什么,不能做什么,你需要遵守这些限制。
当然,在你自己的代码中,你可以删除任何你设置的访问控制限制,但这并不意味着它毫无意义。访问控制可以让我们确定如何使用某个值,因此,如果需要非常小心地访问某些东西,那么你就必须遵守规则。
这里再举个例子,如下:
private var learnedSections = Set<String>()
它是私有的,这意味着没有人可以直接读取或写入它。于是需要提供用于读取或写入值的公共方法。这是有意为之,因为这个例子中学习章节需要做的不仅仅是向该集合中插入一个字符串,它还需要更新用户界面以反映变化,并需要保存新信息以便应用程序记住它已被学习。
如果没有将 learnedSections 属性设置为私有,就有可能忘记并直接向其中写入内容。这将导致用户界面与其数据不一致,而且也无法保存更改——总之是很糟糕的!
因此,通过使用私有属性,这里需要要求 Swift 为用户执行规则:不要从 User 结构之外的任何地方读取或写入该属性。
访问控制的另一个好处是,它可以让我们控制其他人如何查看我们的代码,也就是所谓的 “表面区域(surface area)”。试想一下:如果我给你一个结构体,其中有 30 个公共属性和方法,你可能无法确定哪些是你可以使用的,哪些是内部使用的。另一方面,如果我将其中的 25 个属性和方法标记为私有,那么你就会立刻明白,你不应该在外部使用它们。
访问控制可能是一个相当棘手的问题,尤其是当你考虑到外部代码时。因此,苹果公司自己的相关文档相当长也就不足为奇了——你可以在这里找到:https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html。
静态属性和方法
你已经看到了我们如何将属性和方法附加到结构体上,以及每个结构体如何拥有自己唯一的属性副本,这样调用结构体上的方法就不会读取同一类型中不同结构体的属性。
有时——只是有时——你想将属性或方法添加到结构体本身,而不是结构体的某个特定实例,这样就可以直接使用它们。我们会在 SwiftUI 中经常使用这种技术来做两件事:创建示例数据和存储需要在不同地方访问的固定数据。
首先,我们来看一个如何创建和使用静态属性和方法的简化示例:
struct School {
static var studentCount = 0
static func add(student: String) {
print("\(student) joined the school.")
studentCount += 1
}
}
注意其中的关键字 static,这意味着 studentCount 属性和 add() 方法都属于 School 结构本身,而不是结构的单个实例。
要使用这段代码,我们可以写如下代码:
School.add(student: "Taylor Swift")
print(School.studentCount)
我没有创建学校的实例——我们可以直接在结构体上使用 add() 和 studentCount。这是因为它们都是静态的,这意味着它们并不唯一地存在于结构体的实例中。
这也解释了为什么我们可以修改 studentCount 属性而不将方法标记为mutating——只有在结构体的实例作为常量创建时才需要使用普通的结构体函数,而调用 add() 时并不存在实例。
如果要混合使用静态和非静态属性和方法,有两条规则:
- 要从静态代码访问非静态代码……你就不走运了:静态属性和方法不能引用非静态属性和方法,因为这根本说不通——你要引用的是school的哪个实例?
- 要从非静态代码访问静态代码,请始终使用你的类型名称,如 School.studentCount。你也可以使用 Self 来引用当前类型。
现在我们有了 self 和 Self,它们的含义不同:self 指的是结构体的当前值,而 Self 指的是当前类型。
提示:我们很容易忘记 self 和 Self 之间的区别,但仔细想想,这就像 Swift 的其他命名方式一样——我们用大写字母(Int、Double、Bool 等)开始所有数据类型,所以 Self 也用大写字母开始也是合理的。
现在,你可以听到无数其他学习者在说:“这到底是为什么?”我明白–起初,这似乎是一个相当多余的功能。因此,我想向你展示使用静态数据的两种主要方式。
首先,使用静态属性来组织应用程序中的常用数据。例如,可能会使用 AppData 这样的结构来存储在许多地方使用的大量共享值:
struct AppData {
static let version = "1.3 beta 2"
static let saveFilename = "settings.json"
static let homeURL = "https://beautylife-studio.top"
}
使用这种方法,我可以在任何需要检查或显示应用程序版本号的地方(关于屏幕、调试输出、日志信息、支持邮件等)读取 AppData.version。
常用静态数据的第二个原因是为了创建结构体的示例。正如你稍后将看到的,SwiftUI 的最佳工作方式是在开发过程中显示应用程序的预览,而这些预览通常需要示例数据。例如,如果你要在屏幕上显示一名员工的数据,你就需要在预览屏幕上显示一名员工的示例,这样你就可以在工作时检查所有数据是否正确。
最好使用结构体上的静态示例属性来实现这一点,如下所示:
struct Employee {
let username: String
let password: String
static let example = Employee(username: "cfederighi", password: "hairforceone")
}
现在,当你需要在设计预览中使用 Employee 实例时,只需使用 Employee.example 就可以了。
正如我在开头所说,静态属性或静态方法只有在极少数情况下才有意义,但它们仍然是非常有用的工具。
Swift 中的静态属性和方法有什么用?
大多数学习 Swift 的人一眼就能看出常规属性和方法的价值,但却很难理解为什么静态属性和方法会有用。当然,它们确实不如常规属性和方法有用,但它们在 Swift 代码中仍然相当常见。
静态属性和方法的一个常见用途是存储你在整个应用程序中使用的常用功能。例如,我制作了一款名为 UI HandBook 的应用程序,这是一款供学习 SwiftUI 的人使用的 iOS 应用程序,用户可以通过该应用程序自定义参数来获取预览效果和生成代码的内容。我想在应用程序中存储一些常用信息,例如应用程序在 App Store 上的 URL,这样我就可以在应用程序需要的地方引用这些信息。因此,我用这样的代码来存储数据:
struct UIHandBook {
static let appURL = "https://apps.apple.com/app/ui-handbook/id6463360470"
}
这样,当有人分享应用中的内容时,我就可以写 UIHandBook.appURL,帮助其他人发现应用。如果没有 static 关键字,我就需要创建一个新的 UIHandBook 结构实例来读取固定的应用程序 URL,而这其实是没有必要的。
另一个例子是使用静态属性和静态方法在同一个结构体中存储一些随机熵,就像这样:
struct MathSimply {
static var entropy = Int.random(in: 1...1000)
static func getEntropy() -> Int {
entropy += 1
return entropy
}
}
随机熵是软件为使随机数生成更有效而收集的一些随机性,但我在应用程序中做了一点手脚,因为我不想要真正随机的数据。该应用程序的设计是按照随机顺序为你提供各种 Swift 测试,但如果真的是随机的,那么你很可能有时会连续看到相同的问题。我不希望出现这种情况,所以我的熵实际上是让随机性变得不那么随机,这样我们就能得到更公平的问题分布。因此,这里的代码所做的就是存储一个熵整数,它一开始是随机的,但每次调用 getEntropy() 时都会递增 1。
这个 “公平随机 “的熵在整个应用程序中都会使用,这样就不会出现重复的问题,所以它们也是由 MathSimply 结构静态共享的,因此任何地方都可以访问它们。
在继续之前,我还想提两件大家可能感兴趣的事情。
首先,我的 MathSimply 结构其实根本不需要是结构体——我可以也应该将它声明为枚举而不是结构体。这是因为枚举没有任何实例,所以在这里它比结构体更好,因为我不想创建这个类型的实例——没有理由这样做。使用枚举可以避免这种情况的发生,从而有助于明确我的意图。
其次,因为我有一个专用的 getEntropy() 方法,所以我实际上要求 Swift 限制对entrogy的访问,这样我就不能从任何地方访问它。这就是所谓的访问控制,在 Swift 中看起来是这样的:
private static var entropy = Int.random(in: 1...1000)
我们很快就会详细学习访问控制。
总结
结构体在 Swift 中几乎随处可见: 字符串、Int、Double、数组甚至 Bool 都是以结构体的形式实现的,现在你可以识别出 isMultiple(of:) 这样的函数实际上是属于 Int 的方法。
让我们回顾一下我们还学到了什么:
- 你可以通过编写 struct,为其命名,然后将结构体的代码放在大括号内来创建自己的结构体。
- 结构体可以有变量、常量(称为属性)和函数(称为方法)
- 如果方法试图修改结构体的属性,必须将其标记为mutating。
- 你可以将属性存储在内存中,也可以创建计算属性,在每次访问时计算一个值。
- 我们可以为结构体内部的属性附加 didSet 和 willSet 属性观察器,当我们需要确保某些代码总是在属性发生变化时被执行时,这就很有用了。
- 初始化器有点像专用函数,Swift 会使用所有结构体的属性名称为其生成一个初始化器。
- 你可以根据需要创建自己的自定义初始化器,但必须确保在初始化器结束时,结构体中的所有属性都有一个值,然后再调用其他方法。
- 我们可以根据需要,使用访问权限从外部将任何属性和方法标记为可用或不可用。
- 我们可以将属性或方法直接附加到结构体上,这样就可以在不创建结构体实例的情况下使用它们。
思考
结构体是SwiftUI应用程序的核心,所以我们必须花费大量的时间来确保了解它的作用和工作方式。
所以这里设置一个思考题,我们一起来想一下:创建一个结构体来存储汽车的信息,包括汽车的品牌、型号、座位数、当前档位,然后添加一个方法来升档或降档。考虑一下变量和访问控制:哪些数据应该是变量而不是常量,哪些数据应该公开?换挡方法是否应该以某种方式验证其输入?
这里有一些提示:
- 汽车的品牌和型号以及座位数,一旦生成就不会改变了,因此可以保持不变(常量)。
- 但当前档位显然可以发生变化,因此可以将其作为变量。增减档位应确保这种变化是可能的——例如:汽车没有0档,但是有N档和R档,且最大档位一般也不会超过10。
- 如果使用私有访问控制,可能还需要创建自己的初始化程序。
- 记住在改变属性的方法中使用mutating关键字。
此问题没有标准答案,所以大家只要是可行的代码就是正确的。大家试试看吧!