Skip to content

第11天 访问控制、静态属性和方法

正如你所看到的,结构体让我们能够将单独的数据片段组合起来,创建出新的东西,然后附加方法以便我们操作这些数据。

今天你将学习结构体的一些更高级的特性,这些特性让它们更加强大,包括静态属性和访问控制 —— 这种技术可以阻止代码的其他部分去干预那些不该被干预的地方。

有一句很有名但不知作者的话,我觉得很适合这里:“隐私即力量 —— 人们不知道的事情,就无法破坏它。” 你会发现,在 Swift 中也是如此:隐藏对某些属性和方法的访问实际上能让我们的代码变得更好,因为能够访问它们的地方更少了。

提醒一下,这两个特性在 SwiftUI 中都被广泛使用,所以值得花时间现在就掌握它们,因为从我们的第一个项目开始就会用到。

今天你有两个教程要学习,在其中你会接触到多个级别的访问控制,以及创建静态属性和方法的能力。 观看每个视频并完成任何你想做的额外阅读后,会有一个简短的测试来帮助确保你理解了所教授的内容。

11.1 如何使用访问控制限制对内部数据的访问

作者:Paul Hudson 2022 年 5 月 12 日 已针对 Xcode 16.4 更新

默认情况下,Swift 的结构体允许我们自由访问它们的属性和方法,但通常这并非我们所愿 —— 有时我们希望对外部隐藏一些数据。例如,可能在操作属性之前需要应用一些逻辑,或者知道某些方法需要以特定的方式或顺序调用,因此不应被外部随意访问。

我们可以用一个示例结构体来演示这个问题:

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
        }
    }
}

它有存款和取款的方法,使用方式如下:

swift
var account = BankAccount()
account.deposit(amount: 100)
let success = account.withdraw(amount: 200)

if success {
    print("取款成功")
} else {
    print("取款失败")
}

但是 funds 属性对外暴露,那有什么能阻止我们直接操作它呢?答案是 “根本没有”—— 这样的代码是被允许的:

swift
account.funds -= 1000

这完全绕过了我们为防止人们取出超过账户余额的钱而设置的逻辑,现在我们的程序可能会出现奇怪的行为。

为了解决这个问题,我们可以告诉 Swift,funds 只能在结构体内部访问 —— 结构体自身的方法,以及任何计算属性、属性观察器等都可以访问。

只需添加一个关键字即可:

swift
private var funds = 0

现在,从结构体外部访问 funds 是不可能的,但在 deposit()withdraw() 内部是可以的。如果尝试从结构体外部读取或写入 funds,Swift 会拒绝编译代码。

这称为 “访问控制”,因为它控制着结构体的属性和方法如何从外部被访问。

Swift 提供了多种选项,但在学习阶段,你只需要掌握少数几个:

  • 使用 private 表示 “不允许结构体外部的任何东西使用它”。
  • 使用 fileprivate 表示 “不允许当前文件外部的任何东西使用它”。
  • 使用 public 表示 “允许任何地方的任何东西使用它”。

还有一个对学习者来说有时很有用的额外选项:private(set)。这意味着 “允许任何人读取这个属性,但只有我的方法可以修改它”。如果我们在 BankAccount 中使用它,就意味着我们可以在结构体外部打印 account.funds,但只有 deposit()withdraw() 才能实际更改它的值。

在这种情况下,我认为 private(set)funds 的最佳选择:你可以随时读取当前的银行账户余额,但必须通过我的逻辑才能更改它。

仔细想想,访问控制实际上是为了限制你和团队中的其他开发人员能做的事情 —— 这是很合理的!如果能让 Swift 本身阻止我们犯错,那总是一个明智的举措。

重要提示: 如果你对一个或多个属性使用 private 访问控制,很可能需要创建自己的初始化器。

【可选阅读】访问控制的意义是什么?

作者:Paul Hudson 2021 年 9 月 27 日 已针对 Xcode 16.4 更新

Swift 的访问控制关键字让我们可以限制代码不同部分的访问方式,但很多时候它只是在遵守我们设定的规则 —— 如果我们想的话可以移除这些规则并绕过限制,那它的意义何在呢?

答案有几个,但有一个特别简单,我先从它说起:有时访问控制用于你拥有的代码中,所以你无法移除这些限制。例如,当你使用 Apple 的 API 构建应用程序时,他们会对哪些可以做、哪些不可以做设置限制,而你需要遵守这些限制。

在你自己的代码中,当然可以移除你设置的任何访问控制限制,但这并不意味着它毫无意义。访问控制让我们可以确定一个值应该如何被使用,这样如果某些内容需要非常谨慎地访问,那么你就必须遵守规则。

之前我提到过我的 Swift 学习应用 Unwrap,我想再用它里面的另一个例子。当用户学习 Swift 的不同部分时,我将他们学到的内容名称存储在 User 结构体内部的一个私有 Set 中,声明如下:

swift
private var learnedSections = Set<String>()

它是私有的,这意味着没有人可以直接读取或写入它。相反,我提供了公共方法来读取或写入应该被使用的值。这是故意这样做的,因为学习一个部分不仅仅是将一个字符串插入到这个集合中 —— 还需要更新用户界面以反映变化,并且需要保存新信息,以便应用程序记住已经学习过的内容。

如果我没有将 learnedSections 属性设为私有,我可能会忘记,直接向其中写入内容。这会导致用户界面与其数据不一致,并且也不会保存更改 —— 总之都是不好的!

所以,通过在这里使用 private,我是在让 Swift 为我执行这些规则:不允许我从 User 结构体外部的任何地方读取或写入这个属性。

访问控制的另一个好处是,它让我们可以控制其他人如何看待我们的代码 —— 也就是所谓的 “表面区域”。想想看:如果我给你一个结构体使用,它有 30 个公共属性和方法,你可能不确定哪些是供你使用的,哪些实际上只是内部使用的。另一方面,如果我将其中 25 个标记为私有,那么很明显你不应该在外部使用它们。

访问控制可能是一个相当棘手的问题,特别是当考虑到外部代码时。因此,苹果自己的相关文档相当长也就不足为奇了 —— 你可以在这里找到它:https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html

【练习题】访问控制

问题 1/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct FacebookUser {
	private var privatePosts: [String]
	public var publicPosts: [String]
}
let user = FacebookUser()

问题 2/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Order {
	private var id: Int
	init(id: Int) {
		self.id = id
	}
}
let order = Order(id: "1")

问题 3/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Person {
	private var socialSecurityNumber: String
	init(ssn: String) {
		socialSecurityNumber = ssn
	}
}
let sarah = Person(ssn: "555-55-5555")

问题 4/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Contributor {
	private var name = "Anonymous"
}
let paul = Contributor()

问题 5/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct SecretAgent {
	private var actualName: String
	public var codeName: String
	init(name: String, codeName: String) {
		self.actualName = name
		self.codeName = codeName
	}
}
let bond = SecretAgent(name: "James Bond", codeName: 007)

问题 6/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Doctor {
	var name: String
	var location: String
	private var currentPatient = "No one"
}
let drJones = Doctor(name: "Esther Jones", location: "Bristol")

问题 7/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Office {
	private var passCode: String
	var address: String
	var employees: [String]
	init(address: String, employees: [String]) {
		self.address = address
		self.employees = employees
		self.passCode = "SEKRIT"
	}
}
let monmouthStreet = Office(address: "30 Monmouth St", employees: ["作者:Paul Hudson"])

问题 8/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct RebelBase {
	private var location: String
	private var peopleCount: Int
	init(location: String, people: Int) {
		self.location = location
		self.people = peopleCount
	}
}
let base = RebelBase(location: "Yavin", people: 1000)

问题 9/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct School {
	var staffNames: [String]
	private var studentNames: [String]
	init(staff: [String]) {
		self.staffNames = staff
		self.studentNames = [String]()
	}
}
let royalHigh = School(staff: ["Mrs Hughes"])

问题 10/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Customer {
	var name: String
	private var creditCardNumber: Int
	init(name: String, creditCard: Int) {
		self.name = name
		self.creditCardNumber = creditCard
	}
}
let lottie = Customer(name: "Lottie Knights", creditCard: 1234567890)

问题 11/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Toy {
	var customerPrice: Double
	private var actualPrice: Int
	init(price: Int) {
		actualPrice = price
		customerPrice = actualPrice * 2
	}
}
let buzz = Toy(price: 10)

问题 12/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct App {
	var name: String
	private var sales = 0
	init(name: String) {
		self.name = name
	}
}
let spotify = App(name: "Spotify")

11.2 静态属性和方法

作者:Paul Hudson 2022 年 5 月 12 日 针对 Xcode 16.4 更新

你已经了解了如何将属性和方法附加到结构体上,以及每个结构体如何拥有这些属性的独特副本,这样在结构体上调用方法时,不会读取来自同一类型的不同结构体的属性。

不过,有时候 —— 只是有时候 —— 你希望将属性或方法添加到结构体本身,而不是结构体的某个特定实例上,这样就可以直接使用它们。我在 SwiftUI 中经常使用这种技术来做两件事:创建示例数据,以及存储需要在各个地方访问的固定数据。

首先,让我们看看如何创建和使用静态属性及方法的简化示例:

swift
struct School {
    static var studentCount = 0

    static func add(student: String) {
        print("\(student) 加入了学校。")
        studentCount += 1
    }
}

注意其中的 static 关键字,这意味着 studentCount 属性和 add() 方法都属于 School 结构体本身,而不是该结构体的各个实例。

要使用这段代码,我们可以这样写:

swift
School.add(student: "泰勒·斯威夫特")
print(School.studentCount)

我没有创建 School 的实例 —— 我们确实可以直接在结构体上使用 add()studentCount。这是因为它们都是静态的,这意味着它们并不唯一存在于结构体的实例上。

这也解释了为什么我们能够修改 studentCount 属性而无需将方法标记为 mutating—— 这只在常规结构体函数中需要,当结构体的实例被创建为常量时才会用到,而调用 add() 时并没有实例。

如果你想混合使用静态和非静态的属性及方法,有两条规则:

  1. 要从静态代码中访问非静态代码…… 那你就运气不佳了:静态属性和方法不能引用非静态属性和方法,因为这毫无意义 —— 你指的是 School 的哪个实例呢?
  2. 要从非静态代码中访问静态代码,始终使用你的类型名称,例如 School.studentCount。你也可以使用 Self 来引用当前类型。

现在我们有了 selfSelf,它们的含义不同:self 指的是结构体的当前值,而 Self 指的是当前类型。

提示: 很容易忘记 selfSelf 之间的区别,但如果你仔细想想,这和 Swift 的其他命名规则是一致的 —— 我们所有的数据类型都以大写字母开头(IntDoubleBool 等),所以 Self 以大写字母开头也是合理的。

现在,你可能会听到很多学习者说 “这到底有什么用?”。我理解 —— 一开始这似乎是一个相当多余的功能。所以,我想向你展示我使用静态数据的两种主要方式。

首先,我使用静态属性来组织应用程序中的常见数据。例如,我可能有一个像 AppData 这样的结构体来存储许多在很多地方使用的共享值:

swift
struct AppData {
    static let version = "1.3 beta 2"
    static let saveFilename = "settings.json"
    static let homeURL = "https://www.hackingwithswift.com"
}

使用这种方法,无论在什么地方需要检查或显示诸如应用程序版本号之类的信息 —— 关于屏幕、调试输出、日志信息、支持邮件等 —— 我都可以读取 AppData.version

我通常使用静态数据的第二个原因是创建结构体的示例。正如你稍后将看到的,SwiftUI 在能够显示你开发的应用程序预览时效果最佳,而这些预览通常需要样本数据。例如,如果你要显示一个屏幕,上面显示一个员工的数据,你会希望能够在预览屏幕中显示一个示例员工,这样你就可以在工作时检查一切是否正常。

最好的做法是在结构体上使用静态的 example 属性,如下所示:

swift
struct Employee {
    let username: String
    let password: String

    static let example = Employee(username: "cfederighi", password: "hairforceone")
}

现在,每当你需要一个 Employee 实例在设计预览中使用时,你可以使用 Employee.example,这样就可以了。

就像我一开始说的,只有少数情况下静态属性或方法才有意义,但它们仍然是一个有用的工具。

【可选阅读】Swift 中静态属性和方法的意义是什么?

作者:Paul Hudson 2022 年 12 月 4 日 针对 Xcode 16.4 更新

大多数学习 Swift 的人立刻就能看到常规属性和方法的价值,但很难理解为什么静态属性和方法会有用。诚然,它们不如常规属性和方法有用,但在 Swift 代码中仍然相当常见。

静态属性和方法的一个常见用途是存储你在整个应用程序中使用的通用功能。例如,我制作了一个名为 Unwrap 的应用程序,这是一个面向学习 Swift 的人的免费 iOS 应用程序。在这个应用程序中,我想存储一些常见信息,例如应用程序在 App Store 上的 URL,这样我就可以在应用程序需要的任何地方引用它。所以,我有这样的代码来存储我的数据:

swift
struct Unwrap {
    static let appURL = "https://itunes.apple.com/app/id1440611372"
}

这样,当有人从应用程序中分享内容时,我可以编写 Unwrap.appURL,这有助于其他人发现这个应用程序。如果没有 static 关键字,我需要创建 Unwrap 结构体的一个新实例才能读取固定的应用程序 URL,这实际上是没有必要的。

我还在同一个结构体中使用静态属性和静态方法来存储一些随机熵,如下所示:

swift
static var entropy = Int.random(in: 1...1000)

static func getEntropy() -> Int {
    entropy += 1
    return entropy
}

随机熵是软件收集的一些随机性,用于提高随机数生成的有效性,但我在我的应用程序中稍微作弊了一下,因为我不想要真正的随机数据。这个应用程序旨在以随机顺序为你提供各种 Swift 测试,但如果它是真正随机的,那么你可能会有时连续看到同一个问题。我不希望这样,所以我的熵实际上降低了随机性,这样我们就能得到更公平的问题分布。所以,我的代码所做的是存储一个 entropy 整数,它开始时是随机的,但每次调用 getEntropy() 时就会增加 1。

这种 “公平随机” 的熵在整个应用程序中使用,这样就不会出现重复,所以它们再次由 Unwrap 结构体静态共享,以便所有地方都能访问它们。

在我继续之前,还有两件事我想提一下,可能会让你感兴趣。

首先,我的 Unwrap 结构体实际上根本不需要是一个结构体 —— 事实上,我应该将它声明为一个枚举而不是结构体。这是因为枚举没有任何情况,所以在这里它是比结构体更好的选择,因为我永远不想创建这种类型的实例 —— 没有理由这样做。创建一个枚举可以阻止这种情况的发生,这有助于阐明我的意图。

其次,因为我有一个专门的 getEntropy() 方法,我实际上要求 Swift 限制对 entropy 的访问,这样我就不能从任何地方访问它。这称为访问控制,在 Swift 中看起来像这样:

swift
private static var entropy = Int.random(in: 1...1000)

我们很快就会更深入地研究访问控制。

【练习题】静态属性和方法

问题 1/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Amplifier {
	static let maximumVolume = 11
	var currentVolume: Int
}

问题 2/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Question {
	static let answer = 42
	var questionText = "Unknown"
	init(questionText: String, answer: String) {
		self.questionText = questionText
		self.answer = answer
	}
}

问题 3/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct NewsStory {
	static var breakingNewsCount = 0
	static var regularNewsCount = 0
	var headline: String
	init(headline: String, isBreaking: Bool) {
		self.headline = headline
		if isBreaking {
			NewsStory.breakingNewsCount += 1
		} else {
			NewsStory.regularNewsCount += 1
		}
	}
}

问题 4/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Marathon {
	static distance = 42
	var name: String
	var location: String
}

问题 5/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct PlayingCards {
	static let deckSize
	var pictureStyle: String
}

问题 6/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Cat {
	static let allCats = [Cat]()
	init() {
		Cat.allCats.append(self)
	}
	static func chorus() {
		for _ in allCats {
			print("Meow!")
		}
	}
}

问题 7/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Person {
	static var population = 0
	var name: String
	init(personName: String) {
		name = personName
		population += 1
	}
}

问题 8/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct FootballTeam {
	static let teamSize = 11
	var players: [String]
}

问题 9/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Pokemon {
	static var numberCaught = 0
	var name: String
	static func catchPokemon() {
		print("Caught!")
		Pokemon.numberCaught += 1
	}
}

问题 10/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Order {
	static let orderFormat = "XXX-XXXX"
	var orderNumber: String
}

问题 11/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct Raffle {
	var ticketsUsed = 0
	var name: String
	var tickets: Int
	init(name: String, tickets: Int) {
		self.name = name
		self.tickets = tickets
		Raffle.ticketsUsed += tickets
	}
}

问题 12/12:这段代码是有效的 Swift 代码 —— 对还是错?

swift
struct LegoBrick {
	static var numberMade = 0
	var shape: String
	var color: String
	init(shape: String, color: String) {
		self.shape = shape
		self.color = color
		LegoBrick.numberMade += 1
	}
}

11.3 总结:结构体

作者:Paul Hudson 2021 年 11 月 8 日 针对 Xcode 16.4 更新

结构体在 Swift 中几乎无处不在:StringIntDoubleArray 甚至 Bool 都是作为结构体实现的,现在你可以认识到,像 isMultiple(of:) 这样的函数实际上是 Int 所属的方法。

让我们回顾一下我们学到的其他内容:

  • 你可以通过编写 struct、给它一个名称,然后将结构体的代码放在大括号内来创建自己的结构体。
  • 结构体可以有变量和常量(称为属性)以及函数(称为方法)。
  • 如果一个方法试图修改其结构体的属性,你必须将其标记为 mutating
  • 你可以在内存中存储属性,或者创建每次访问时计算值的计算属性。
  • 我们可以将 didSetwillSet 属性观察器附加到结构体内的属性上,当我们需要确保在属性更改时始终执行某些代码时,这很有帮助。
  • 初始化器有点像专门的函数,Swift 会使用所有结构体的属性名称为它们生成一个初始化器。
  • 如果你愿意,你可以创建自己的自定义初始化器,但你必须始终确保结构体中的所有属性在初始化器完成时,并且在调用任何其他方法之前都有一个值。
  • 我们可以根据需要使用访问控制来标记任何属性和方法在外部是否可用。
  • 可以将属性或方法直接附加到结构体上,这样你就可以在不创建结构体实例的情况下使用它们。

11.4 检查点 6

作者:Paul Hudson 2021 年 10 月 10 日 针对 Xcode 16.4 更新

结构体是每个 SwiftUI 应用程序的核心,所以你确实需要花一些额外的时间来确保你理解它们的作用和工作方式。

为了检查你的知识,这里有一个小任务给你:创建一个结构体来存储关于汽车的信息,包括它的型号、座位数和当前档位,然后添加一个方法来切换档位(升档或降档)。思考一下变量和访问控制:哪些数据应该是变量而不是常量,哪些数据应该公开?档位切换方法应该以某种方式验证其输入吗?

和往常一样,我会在下面写一些提示,但首先我会留出一些空间,这样你就不会意外看到提示。和往常一样,在查看提示之前自己尝试这个挑战真的是个好主意 —— 这是识别你不太有把握的部分的最快方法。

还在看吗?好吧,这里有一些提示:

  • 汽车的型号和座位数一旦生产出来就不会改变,所以它们可以是常量。但它的当前档位显然改变,所以把它设为变量。
  • 切换档位(升档或降档)应该确保这种改变是可能的 —— 例如,没有 0 档,而且可以合理地假设最多 10 档应该能涵盖大多数(如果不是全部)汽车。
  • 如果你使用 private 访问控制,你可能还需要创建自己的初始化器。(private 是这里的最佳选择吗?自己试试看,看看你的想法!)
  • 记住在更改属性的方法上使用 mutating 关键字!

第 11 天的内容就这些了,希望现在你知道该做什么了:去 Twitter、Facebook、Reddit 或任何你喜欢的社交媒体上分享你的进展吧。

本站使用 VitePress 制作