Skip to content

第10天 结构体、计算属性和属性观察者

我知道你们中的一些人可能急于开始今天新的 Swift 学习内容,但先稍等一下:你们刚刚学完了闭包,这是一个颇有难度的主题。而且你们还回来继续学习。说真的,这非常值得肯定。

还有个好消息要告诉大家。首先,接下来的几天里你们完全可以不用去想闭包的事,而且等过段时间,我们就会开始在实际的 iOS 项目中运用闭包。所以,即便你们现在对闭包的工作原理以及使用原因不是百分之百确定,之后一切都会豁然开朗的 —— 坚持下去就好!

言归正传,今天的主题是结构体。结构体是 Swift 允许我们将几个小型类型组合起来创建自定义数据类型的方式之一。例如,你可以把三个字符串和一个布尔值组合在一起,说它们代表了你应用中的一个用户。实际上,Swift 自身的大多数类型都是用结构体实现的,包括 StringIntBoolArray 等等。

这些自定义类型 —— 用户、游戏、文档等等 —— 构成了我们所构建软件的真正核心。如果能把这些类型设计好,通常代码也就顺理成章了。

正如极具影响力的著作《人月神话》的作者弗雷德・布鲁克斯曾经说过的:“陷入困境的程序员,往往最好的做法是从代码中抽离出来,退后一步,仔细思考他们的数据。数据的表示是编程的本质。”

此外,结构体在 SwiftUI 中极为常见,因为我们设计的每一个 UI 元素都是建立在结构体之上的,其中还包含了许多内部结构体。结构体并不难学,说实话,在学完闭包之后,几乎所有东西看起来都更容易了!

今天你们要学习四个教程,会接触到自定义结构体、计算属性、属性观察器等等内容。 看完每个视频后,也可以选读额外的阅读材料,之后还有一些小测试,帮助你们确认自己是否理解了所学内容。

10.1 如何创建自己的结构体

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

Swift 的结构体允许我们创建自己的自定义复杂数据类型,这些类型可以包含自己的变量和函数。

一个简单的结构体如下所示:

swift
struct Album {
    let title: String
    let artist: String
    let year: Int

    func printSummary() {
        print("\(title)\(year))by \(artist)")
    }
}

这创建了一个名为Album的新类型,包含两个字符串常量titleartist,以及一个整数常量year。我还添加了一个简单的函数,用于汇总这三个常量的值。

注意到Album是以大写字母 A 开头的吗?这是 Swift 中的标准规范,我们一直都在遵循 —— 想想StringIntBoolSet等等。当你指代一种数据类型时,我们使用首字母大写的驼峰式命名法;而当你指代类型内部的东西(如变量或函数)时,我们使用首字母小写的驼峰式命名法。记住,这在很大程度上只是一种约定而非规则,但遵循它会很有帮助。

在这一点上,Album就像StringInt一样 —— 我们可以创建它们、赋值、复制等等。例如,我们可以创建几张专辑,然后打印它们的一些值并调用它们的函数:

swift
let red = Album(title: "Red", artist: "Taylor Swift", year: 2012)
let wings = Album(title: "Wings", artist: "BTS", year: 2016)

print(red.title)
print(wings.artist)

red.printSummary()
wings.printSummary()

注意,我们可以像调用函数一样创建一个新的Album—— 只需要按照定义时的顺序为每个常量提供值即可。

如你所见,redwings都来自同一个Album结构体,但一旦创建,它们就像两个字符串一样是相互独立的。

当我们在每个结构体上调用printSummary()时,你可以看到这一点的实际效果,因为该函数引用了titleartistyear。在这两个实例中,每个结构体的正确值都会被打印出来:red打印出 “Red(2012)by Taylor Swift”,wings打印出 “Wings(2016)by BTS”——Swift 知道,当在red上调用printSummary()时,应该使用同样属于redtitleartistyear常量。

当你希望拥有可以更改的值时,事情会变得更有趣。例如,我们可以创建一个Employee结构体,它可以根据需要休假:

swift
struct Employee {
    let name: String
    var vacationRemaining: Int

    func takeVacation(days: Int) {
        if vacationRemaining > days {
            vacationRemaining -= days
            print("我要去度假啦!")
            print("剩余天数:\(vacationRemaining)")
        } else {
            print("哎呀!剩余天数不够了。")
        }
    }
}

然而,这实际上行不通 ——Swift 会拒绝编译这段代码。

你看,如果我们使用let将一个员工创建为常量,Swift 会使该员工及其所有数据都成为常量 —— 我们可以正常调用函数,但这些函数不应该被允许更改结构体的数据,因为我们已将其设为常量。

因此,Swift 要求我们多做一步:任何只读取数据的函数都没问题,但任何更改结构体所属数据的函数都必须用一个特殊的mutating关键字标记,如下所示:

swift
mutating func takeVacation(days: Int) {

现在我们的代码可以正常编译了,但 Swift 会阻止我们在常量结构体上调用takeVacation()

在代码中,这样是允许的:

swift
var archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.takeVacation(days: 5)
print(archer.vacationRemaining)

但如果你将var archer改为let archer,你会发现 Swift 再次拒绝编译你的代码 —— 我们试图在一个常量结构体上调用一个变异函数,这是不被允许的。

在接下来的几章中,我们将详细探讨结构体,但首先我想给一些东西起个名字。

  • 属于结构体的变量和常量称为属性
  • 属于结构体的函数称为方法
  • 当我们用结构体创建一个常量或变量时,我们称之为实例—— 例如,你可以创建Album结构体的十几个独特实例。
  • 当我们创建结构体的实例时,我们使用像这样的初始化器Album(title: "Wings", artist: "BTS", year: 2016)

最后一点起初可能看起来有点奇怪,因为我们把结构体当作函数来对待并传入参数。这有点像所谓的 “语法糖”——Swift 在结构体内部默默地创建了一个名为init()的特殊函数,使用结构体的所有属性作为其参数。然后它会自动将以下两段代码视为相同的:

swift
var archer1 = Employee(name: "Sterling Archer", vacationRemaining: 14)
var archer2 = Employee.init(name: "Sterling Archer", vacationRemaining: 14)

我们之前实际上依赖过这种行为。在我第一次介绍Double的时候,我解释过不能将IntDouble相加,而是需要编写这样的代码:

swift
let a = 1
let b = 2.0
let c = Double(a) + b

现在你可以明白真正发生了什么:Swift 自己的Double类型是作为结构体实现的,并且有一个接受整数的初始化器函数。

Swift 在生成初始化器方面很智能,即使我们为属性分配了默认值,它也能处理。

例如,如果我们的结构体有这两个属性:

swift
let name: String
var vacationRemaining = 14

那么 Swift 会默默地生成一个初始化器,为vacationRemaining提供默认值 14,使得以下两种方式都是有效的:

swift
let kane = Employee(name: "Lana Kane")
let poovey = Employee(name: "Pam Poovey", vacationRemaining: 35)

提示: 如果你给一个常量属性分配了默认值,那么它将被从初始化器中完全移除。要分配一个默认值但又保留在需要时覆盖它的可能性,请使用变量属性。

【可选阅读】结构体和元组有什么区别?

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

Swift 的元组让我们可以在一个变量中存储多个不同的命名值,而结构体的功能也大致相同 —— 那么它们的区别是什么,什么时候应该选择其中一个而不是另一个呢?

刚开始学习的时候,区别很简单:元组实际上就是一个没有名字的结构体,就像一个匿名结构体。这意味着你可以将它定义为(name: String, age: Int, city: String),它的作用和下面的结构体是一样的:

swift
struct User {
    var name: String
    var age: Int
    var city: String
}

然而,元组有一个问题:虽然它们非常适合一次性使用,特别是当你想从一个函数中返回多个数据时,但反复使用它们会很麻烦。

想想看:如果你有几个处理用户信息的函数,你更愿意这样写:

swift
func authenticate(_ user: User) { ... }
func showProfile(for user: User) { ... }
func signOut(_ user: User) { ... }

还是这样写:

swift
func authenticate(_ user: (name: String, age: Int, city: String)) { ... }
func showProfile(for user: (name: String, age: Int, city: String)) { ... }
func signOut(_ user: (name: String, age: Int, city: String)) { ... }

想想看,给你的User结构体添加一个新属性是多么容易(确实非常容易),而在使用元组的所有地方给元组添加另一个值又会有多难呢?(非常难,而且容易出错!)

所以,当你想从一个函数中返回两个或多个任意值时,使用元组;但当你有一些固定的数据需要多次发送或接收时,优先使用结构体。

【可选阅读】函数和方法有什么区别?

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

Swift 的函数让我们可以为一段功能命名并重复运行它,而 Swift 的方法也做着非常类似的事情,那么它们的区别是什么呢?

说实话,唯一真正的区别是方法属于一种类型,比如结构体、枚举和类,而函数不属于任何类型。就是这样 —— 这是唯一的区别。两者都可以接受任意数量的参数,包括可变参数,并且都可以返回值。事实上,它们非常相似,以至于 Swift 仍然使用func关键字来定义方法。

当然,与特定的类型(如结构体)相关联意味着方法获得了一个重要的超级能力:它们可以引用该类型内部的其他属性和方法,这意味着你可以为User类型编写一个describe()方法,用于打印用户的姓名、年龄和城市。

方法还有一个优势,但这个优势相当微妙:方法可以避免命名空间污染。每当我们创建一个函数时,该函数的名称就开始在我们的代码中有了意义 —— 我们可以编写wakeUp()并让它做一些事情。所以,如果你编写 100 个函数,最终会有 100 个保留名称;如果你编写 1000 个函数,就会有 1000 个保留名称。

这很快就会变得混乱,但通过将功能放入方法中,我们限制了这些名称的可用范围 ——wakeUp()不再是一个保留名称,除非我们特意编写someUser.wakeUp()。这减少了所谓的污染,因为如果我们的大部分代码都在方法中,那么我们就不会意外地发生名称冲突。

【可选阅读】为什么有些方法需要标记为 mutating?

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

修改结构体的属性是可能的,但前提是该结构体是作为变量创建的。当然,在你的结构体内部,无法判断你将要处理的是变量结构体还是常量结构体,所以 Swift 有一个简单的解决方案:每当结构体的方法试图更改任何属性时,你必须将其标记为mutating

你只需要将方法标记为mutating,不需要做其他任何事情,但这样做可以让 Swift 获得足够的信息,阻止该方法在常量结构体实例上使用。

有两个重要的细节你会觉得有用:

  • 将方法标记为mutating会阻止该方法在常量结构体上被调用,即使该方法本身实际上并没有更改任何属性。如果你说它会更改内容,Swift 就会相信你!
  • 未标记为mutating的方法不能调用标记为mutating的函数 —— 你必须将它们都标记为mutating

【练习题】结构体

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

swift
struct Order {
	var customerID: Int
	var itemID: Int
}
let order = Order(customer: 143, item: 556)

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

swift
struct ChessPiece {
	var name: String
	var value: Int
}

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

swift
struct Dog (
	var name: String
	var breed: String
)

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

swift
struct User {
	var name = "Anonymous"
	var age: Int
}
let twostraws = User(name: "Paul", age: 38)

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

swift
struct Phone {
	var manufacturer: String
	var screenSize: Double
}

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

swift
struct LeviJeans {
	var fitNumber: Int
	var waist: Int
	var leg: Int
}
let jeans = LeviJeans(fitNumber: "501", waist: "34", leg: "32")

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

swift
struct WeatherForecast {
	var dayNumber: Int
	var temperature: Int
}
let monday = WeatherForecast(dayNumber: 1, temperature: 25)

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

swift
struct RubiksCube {
	var size = 3
}
let large = RubiksCube(5)

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

swift
struct Book {
	var title: String
	var author = "Unknown"
	var pageCount = 0
}

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

swift
struct Boat {
	name: String
	homePort: String
	maxSpeed: Int
}

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

swift
struct Player {
	var name: String
	var position: String
}
let harry = Player(name: "Harry Kane", position: "Forward")

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

swift
struct Cup {
	var size: Int
	var color = White
}

【练习题】可变方法

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

swift
struct Diary {
	var entries: String
	mutating func add(entry: String) {
		entries += "\(entry)"
	}
}

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

swift
struct Surgeon {
	var operationsPerformed = 0
	mutating func operate(on patient: String) {
		print("Nurse, hand me the scalpel!")
		operationsPerformed += 1
	}
}

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

swift
struct Stapler {
	var stapleCount: Int
	func staple() {
		if stapleCount > 0 {
			stapleCount -= 1
			print("It's stapled!")
		} else {
			print("Please refill me.")
		}
	}
}

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

swift
struct Tree {
	var height: Int
	mutating func grow() {
		height *= 1.001
	}
}

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

swift
struct Car {
	let mileage: Int
	mutating func drive(distance: Int) {
		mileage += distance
	}
}

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

swift
struct Book {
	var totalPages: Int
	var pagesLeftToRead = 0
	mutating func read(pages: Int) {
		if pages < pagesLeftToRead {
			pagesLeftToRead -= pages
		} else {
			pagesLeftToRead = 0
			print("I'm done!")
		}
	}
}

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

swift
struct BankAccount {
	var balance: Int
	mutating func donateToCharity(amount: Int) {
		balance -= amount
	}
}

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

swift
struct Switch {
	var isOn: Bool
	mutating func toggle {
		if isOn {
			isOn = false
		} else {
			isOn = true
		}
	}
}

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

swift
struct MeetingRoom {
	var isBooked = true
	mutating book(for name: String) {
		if isBooked {
			print("Sorry, the meeting room is already taken.")
		} else {
			isBooked = true
			print("It's reserved for \(name).")
		}
	}
}

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

swift
struct Delorean {
	var speed = 0
	mutating func accelerate() {
		speed += 1
		if speed == 88 {
			travelThroughTime()
		}
	}
	func travelThroughTime() {
		print("Where we're going we don't need roads.")
	}
}

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

swift
struct Singer {
	var name: String
	var bankBalance: Double
	mutating func goOnTour(venues: Int) {
		print("Come and see \(name) live on stage!")
		bankBalance += venues * 100_000
	}
}

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

swift
struct Bicycle {
	var currentGear: Int
	mutating func changeGear(to newGear: Int) {
		currentGear = newGear
		print("I'm now in gear \(currentGear).")
	}
}

10.2 如何动态计算属性值

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

结构体可以有两种类型的属性:存储型属性是在结构体实例中保存数据的变量或常量,而计算型属性则在每次访问时动态计算属性的值。这意味着计算型属性是存储型属性和函数的混合体:它们像存储型属性一样被访问,但像函数一样工作。

例如,之前我们有一个 Employee 结构体,它可以跟踪员工剩余的假期天数。以下是简化版本:

swift
struct Employee {
    let name: String
    var vacationRemaining: Int
}

var archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.vacationRemaining -= 5
print(archer.vacationRemaining)
archer.vacationRemaining -= 3
print(archer.vacationRemaining)

作为一个简单的结构体,这是可行的,但我们丢失了有价值的信息 —— 我们给这个员工分配了 14 天假期,然后随着假期的使用进行减法运算,但这样做我们已经不知道最初给了多少天假期。

我们可以调整为使用计算型属性,如下所示:

swift
struct Employee {
    let name: String
    var vacationAllocated = 14
    var vacationTaken = 0

    var vacationRemaining: Int {
        vacationAllocated - vacationTaken
    }
}

现在,vacationRemaining 不再是我们可以直接赋值的属性,而是通过从分配的假期天数中减去已使用的假期天数来计算。

当我们读取 vacationRemaining 时,它看起来就像一个常规的存储型属性:

swift
var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
print(archer.vacationRemaining)
archer.vacationTaken += 4
print(archer.vacationRemaining)

这非常强大:我们读取的内容看起来像一个属性,但在幕后,Swift 每次都会运行一些代码来计算它的值。

不过,我们不能对它进行写入操作,因为我们还没有告诉 Swift 应该如何处理写入。要解决这个问题,我们需要同时提供 * getter* 和 * setter*—— 分别是 “读取代码” 和 “写入代码” 的专业名称。

在这种情况下,getter 很简单,就是我们现有的代码。但setter 更有趣 —— 如果你为员工设置 vacationRemaining,你的意思是希望他们的 vacationAllocated 值增加或减少,还是 vacationAllocated 保持不变,而我们改变 vacationTaken

我假设第一种情况是正确的,在这种情况下,属性看起来是这样的:

swift
var vacationRemaining: Int {
    get {
        vacationAllocated - vacationTaken
    }

    set {
        vacationAllocated = vacationTaken + newValue
    }
}

注意 getset 如何标记读取或写入值时要运行的各个代码段。更重要的是,注意 newValue—— 这是 Swift 自动提供给我们的,它存储了用户试图分配给属性的值。

有了 getter 和 setter 之后,我们现在可以修改 vacationRemaining 了:

swift
var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
archer.vacationRemaining = 5
print(archer.vacationAllocated)

SwiftUI 广泛使用计算型属性 —— 你会在你创建的第一个项目中看到它们!

【可选阅读】何时应该使用计算型属性或存储型属性?

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

属性让我们可以将信息附加到结构体上,Swift 给了我们两种变体:存储型属性,其值被存储在某个内存中供以后使用;计算型属性,其值每次被调用时都会重新计算。在幕后,计算型属性实际上只是一个函数调用,恰好属于你的结构体。

决定使用哪种属性,部分取决于你的属性值是否依赖于其他数据,部分也取决于性能。性能方面很简单:如果你在属性值没有变化的情况下经常读取它,那么使用存储型属性会比使用计算型属性快得多。另一方面,如果你的属性很少被读取,甚至可能根本不被读取,那么使用计算型属性可以避免你必须计算其值并将其存储在某个地方。

当涉及到依赖关系 —— 你的属性值是否依赖于其他属性的值时,情况就不同了:这是计算型属性有用的地方,因为你可以确保它们返回的值总是考虑到最新的程序状态。

延迟属性有助于减轻很少读取的存储型属性的性能问题,而属性观察器则减轻了存储型属性的依赖问题 —— 我们很快就会看到它们。

【练习题】计算型属性

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

swift
struct Code {
	var language: String
	var containsErrors = false
	var report {
		if containsErrors {
			return "This \(language) code has bugs!"
		} else {
			return "This looks good to me."
		}
	}
}

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

swift
struct City {
	var population: String
	var description: String {
		if population < 100_000 {
			return "This is a small city."
		} else if population < 1_000_000 {
			return "This is a medium-sized city."
		} else {
			return "This is a large city."
		}
	}
}
let tokyo = City(population: 12_000_000)

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

swift
struct Candle {
	var burnLength: Int
	var alreadyBurned = 0
	let burnRemaining: Int {
		return burnLength - alreadyBurned
	}
}

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

swift
struct Wine {
	var age: Int
	var isVintage: Bool
	var price: Int {
		if isVintage {
			return age + 20
		} else {
			return age + 5
		}
	}
}
let malbec = Wine(age: 2, isVintage: true)
print(malbec.price)

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

swift
struct Medicine {
	var amount: Int
	var frequency: Int
	var dosage: String {
		return "Take \(amount) pills \(frequency) times a day."
	}
}

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

swift
struct Dog {
	var breed: String
	var cuteness: Int
	var rating: String {
		if cuteness < 3 {
			print("That's a cute dog!")
		} else if cuteness < 7 {
			print("That's a really cute dog!")
		} else {
			print("That a super cute dog!")
		}
	}
}
let luna = Dog(breed: "Samoyed", cuteness: 11)
print(luna.rating)

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

swift
struct Sunglasses {
	var protectionLevel: Int
	var visionTest: String {
		if protectionLevel < 3 {
			return "These aren't very dark"
		} else if protectionLevel < 6 {
			return "These are just right"
		} else if protectionLevel < 10 {
			return "Who turned the lights out?"
		}
	}
}

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

swift
struct Swordfighter {
	var name: String
	var introduction: String {
		return "Hello, my name is \(name)."
	}
}
let inigo = Swordfighter(name: "Inigo Montoya")

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

swift
struct Race {
	var distance: Int
	var runners = 0
	var description = String {
		return "This is a \(distance)km race with \(runners) runners."
	}
}
let londonMarathon = Race(distance: 42, runners: 40_000)

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

swift
struct Keyboard {
	var isMechanical = false
	var noiseLevel: Int {
		if isMechanical {
			return 11
		} else {
			return 3
		}
	}
}
let majestouch = Keyboard(isMechanical: true)
print(majestouch.noiseLevel)

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

swift
struct Investor {
	var age: Int
	var investmentPlan: String {
		if age < 30 {
			return "Shares"
		} else if age < 60 {
			return "Equities"
		} else {
			return "Bonds"
		}
	}
}
let investor = Investor(age: 38)
print(investor.investmentPlan)

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

swift
struct Toy {
	var color: String
	var isForGirls: Bool {
		if color == "Pink" {
			return true
		} else {
			return true
		}
	}
}

10.3 如何在属性发生变化时执行操作

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

Swift 允许我们创建属性观察器,它们是在属性发生变化时运行的特殊代码片段。属性观察器有两种形式:didSet 观察器在属性刚刚发生变化时运行,willSet 观察器在属性将要发生变化时运行。

要理解为什么可能需要属性观察器,不妨看看这样的代码:

swift
struct Game {
    var score = 0
}

var game = Game()
game.score += 10
print("现在的分数是 \(game.score)")
game.score -= 3
print("现在的分数是 \(game.score)")
game.score += 1

这段代码创建了一个 Game 结构体,并对其分数进行了几次修改。每次分数变化后,都会跟着一行 print() 语句,以便我们跟踪变化。但这里有个漏洞:最后一次分数变化没有被打印出来,这是个错误。

有了属性观察器,我们就可以通过 didSetprint() 调用直接附加到属性上,这样无论在何处修改该属性,只要它发生变化,我们的代码就总会运行。

下面是同一个示例,现在添加了属性观察器:

swift
struct Game {
    var score = 0 {
        didSet {
            print("现在的分数是 \(score)")
        }
    }
}

var game = Game()
game.score += 10
game.score -= 3
game.score += 1

Swift 会在 didSet 内部自动提供常量 oldValue,以便你需要根据原来的值来实现自定义功能时使用。还有一个 willSet 变体,它会在属性发生变化之前运行一些代码,相应地,它会提供将要被赋值的新值,方便你根据新值采取不同的操作。

我们可以通过一个代码示例来展示所有这些功能,运行代码时会打印消息,这样你就能看到代码的执行流程:

swift
struct App {
    var contacts = [String]() {
        willSet {
            print("当前值是:\(contacts)")
            print("新值将会是:\(newValue)")
        }

        didSet {
            print("现在有 \(contacts.count) 个联系人。")
            print("旧值是 \(oldValue)")
        }
    }
}

var app = App()
app.contacts.append("Adrian E")
app.contacts.append("Allen W")
app.contacts.append("Ish S")

是的,向数组追加元素会触发 willSetdidSet,所以运行这段代码会打印很多文本。

实际上,willSet 的使用频率远低于 didSet,但你还是可能会时不时遇到它,所以了解它的存在很重要。无论选择哪种观察器,都请尽量避免在属性观察器中放入过多的工作 —— 如果像 game.score += 1 这样看似简单的操作会触发大量耗时的工作,那你肯定会经常遇到麻烦,还会导致各种性能问题。

【可选阅读】什么时候应该使用属性观察器?

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

Swift 的属性观察器让我们可以附加在属性更改之前或之后运行的功能,分别使用 willSetdidSet。大多数时候,属性观察器并不是必需的,只是用起来方便 —— 我们也可以正常更新属性,然后自己在代码中调用函数。那为什么还要用属性观察器呢?什么时候才真正需要用到它?

最重要的原因是便捷性:使用属性观察器意味着只要属性发生变化,你的功能就会执行。当然,你也可以用函数来实现,但你能保证每次都记得调用吗?在所有修改属性的地方都记得吗?

这就是函数方法的问题所在:必须由你自己记住在每次属性变化时调用那个函数,如果忘了,代码中就会出现莫名其妙的错误。另一方面,如果你用 didSet 把功能直接附加到属性上,它就总会执行,你把确保这一点的工作交给了 Swift,这样你的大脑就可以专注于更有趣的问题了。

一种情况不适合使用属性观察器,那就是在里面放入耗时的操作。如果有一个 User 结构体,里面有一个 age 整数,你会期望修改 age 几乎是瞬间完成的 —— 毕竟它只是一个数字。但如果你给它附加了一个 didSet 属性观察器,里面做了各种耗时的工作,那么突然之间,修改一个整数可能会比你预期的慢得多,还可能给你带来各种问题。

【可选阅读】什么时候应该用 willSet 而不是 didSet?

作者:Paul Hudson 2020 年 7 月 29 日 已针对 Xcode 16.4 更新

willSetdidSet 都能让我们给属性附加观察器,这意味着当属性变化时,Swift 会运行一些代码,让我们有机会对变化做出响应。问题是:你想在属性变化之前知道,还是之后知道?

简单来说:大多数时候你会用 didSet,因为我们想在变化发生之后采取行动,比如更新用户界面、保存更改等等。这并不是说 willSet 没用,只是在实际中它的使用频率远低于 didSet

willSet 最常见的使用场景是需要知道程序在变化发生前的状态时。例如,SwiftUI 会在一些地方使用 willSet 来处理动画,以便在变化发生前对用户界面进行快照。有了 “之前” 和 “之后” 的快照,它就可以对比两者,找出用户界面中所有需要更新的部分。

【练习题】属性观察器

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

swift
struct BankAccount {
	var name: String
	var isMillionnaire = false
	var balance: Int {
		didSet {
			if balance > 1_000_000 {
				isMillionnaire = true
			} else {
				isMillionnaire = false
			}
		}
	}
}

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

swift
struct App {
	var name: String
	var isOnSale: Bool {
		didSet {
			if isOnSale {
				print("快去下载我的应用吧!")
			} else {
				print("或许之后再下载吧。")
			}
		}
	}
}

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

swift
struct Child {
	var name: String
	var age: Int {
		didSet {
			print("生日快乐,\(name)!")
		}
	}
}

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

swift
struct StepCounter {
	var steps: Int {
		hasSet {
			print("你已经走了 \(steps) 步——真棒!")
		}
	}
}

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

swift
struct Person {
	var clothes: String {
		didSet {
			print("我要换成 \(clothes)")
		}
	}
}

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

swift
struct FuzzyClock {
	var hour: Int {
		dset {
			if hour < 12 {
				print("现在是早上")
			} else if hour < 18 {
				print("现在是下午")
			} else {
				print("现在是晚上。")
			}
		}
	}
}

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

swift
struct FishTank {
	var capacity: Int
	var fishCount: Int {
		didSet {
			if fishCount > capacity {
				print("你的鱼太多了!")
			}
		}
	}
}

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

swift
struct Dog {
	var age {
		didSet {
			let dogAge = age * 7
			print("按狗的年龄算,我现在 \(dogAge) 岁了。")
		}
	}
}

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

swift
struct FootballMatch {
	let homeTeamScore: Int {
		didSet {
			print("太棒了——我们进球了!")
		}
	}
	let awayTeamScore: Int {
		didSet {
			print("讨厌——他们进球了!")
		}
	}
}

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

swift
struct Game {
	var score: Int {
		didSet {
			print("你的分数现在是 \(score) 分。")
		}
	}
}

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

swift
struct House {
	var numberOfOccupants: Int {
		didSet:
			print("\(numberOfOccupants) 人现在住在这里。")
	}
}

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

swift
enum Student {
	var name: String
	var debt: Int {
		didSet {
			if debt < 5_000 {
				print("太好了!")
			} else if debt < 20_000 {
				print("还可以。")
			} else {
				print("我能假装自己死了吗?")
			}
		}
	}
}

10.4 如何创建自定义初始化器

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

初始化器是专门用于准备新结构体实例以供使用的方法。你已经了解到,Swift 会根据我们在结构体中放置的属性自动为我们生成一个初始化器,但你也可以创建自己的初始化器,只要遵循一个黄金法则:在初始化器结束时,所有属性都必须有一个值。

让我们先回顾一下 Swift 为结构体提供的默认初始化器:

swift
struct Player {
    let name: String
    let number: Int
}

let player = Player(name: "梅根·R", number: 15)

这通过为两个属性提供值来创建一个新的Player实例。Swift 称此为成员初始化器,这是一种花哨的说法,指的是按属性定义顺序接受每个属性的初始化器。

就像我说的,这种代码之所以可行,是因为 Swift 会自动生成一个接受这两个值的初始化器,但我们也可以自己编写一个来实现同样的功能。这里唯一需要注意的是,你必须小心区分传入的参数名称和要赋值的属性名称。

下面是具体的写法:

swift
struct Player {
    let name: String
    let number: Int

    init(name: String, number: Int) {
        self.name = name
        self.number = number
    }
}

这与我们之前的代码功能相同,只是现在这个初始化器由我们自己掌控,因此如果需要,我们可以在其中添加额外的功能。

不过,有几件事我想让你注意:

  1. 没有func关键字。是的,从语法上看它像一个函数,但 Swift 会特殊对待初始化器。
  2. 尽管初始化器会创建一个新的Player实例,但它从不显式地有返回类型 —— 它们总是返回它们所属的数据类型。
  3. 我使用self来将参数赋值给属性,以明确表示 “将name参数赋值给我的name属性”。

最后一点尤为重要,因为如果没有self,我们会写成name = name,这毫无意义 —— 我们是将属性赋值给参数,将参数赋值给自身,还是其他什么?通过编写self.name,我们明确表示 “属于当前实例的name属性”,而不是其他任何东西。

当然,我们的自定义初始化器不必像 Swift 提供的默认成员初始化器那样工作。例如,我们可以规定必须提供球员姓名,但球衣号码是随机的:

swift
struct Player {
    let name: String
    let number: Int

    init(name: String) {
        self.name = name
        number = Int.random(in: 1...99)
    }
}

let player = Player(name: "梅根·R")
print(player.number)

只需记住那条黄金法则:在初始化器结束时,所有属性都必须有一个值。如果我们没有在初始化器中为number提供值,Swift 会拒绝编译我们的代码。

重要提示: 虽然你可以在初始化器内部调用结构体的其他方法,但在为所有属性赋值之前不能这样做 ——Swift 需要确保在进行其他任何操作之前,一切都是安全的。

你可以根据需要为结构体添加多个初始化器,也可以利用外部参数名和默认值等特性。但是,一旦你实现了自己的自定义初始化器,你将无法再使用 Swift 生成的成员初始化器,除非你采取额外的步骤来保留它。这并非偶然:如果你有一个自定义初始化器,Swift 实际上会认为这是因为你有一些特殊的方式来初始化属性,这意味着默认的初始化器应该不再可用。

【可选阅读】Swift 的成员初始化器是如何工作的?

作者:Paul Hudson 2024 年 4 月 30 日 已针对 Xcode 16.4 更新

默认情况下,所有 Swift 结构体都会自动获得一个合成的成员初始化器,这意味着我们会自动获得一个接受结构体每个属性值的初始化器。这个初始化器让结构体使用起来很方便,而且 Swift 还做了另外两件特别巧妙的事情。

首先,如果你的任何属性有默认值,那么它们会作为默认参数值融入到初始化器中。所以,如果我创建这样一个结构体:

swift
struct Employee {
    var name: String
    var yearsActive = 0
}

那么我可以用以下两种方式创建它:

swift
let roslin = Employee(name: "Laura Roslin")
let adama = Employee(name: "William Adama", yearsActive: 45)

这使得创建实例更加容易,因为你只需要填写你需要的部分。

Swift 做的第二件巧妙的事情是,如果你创建了自己的初始化器,它会移除成员初始化器。

例如,如果我有一个创建匿名员工的自定义初始化器,它会是这样的:

swift
struct Employee {
    var name: String
    var yearsActive = 0

    init() {
        self.name = "Anonymous"
        print("Creating an anonymous employee…")
    }
}

有了这个之后,我就不能再依赖成员初始化器了,所以下面这种写法就不再被允许:

swift
let roslin = Employee(name: "Laura Roslin")

这并非偶然,而是一个刻意设计的特性:我们创建了自己的初始化器,如果 Swift 保留它的成员初始化器,那么它可能会漏掉我们在自己的初始化器中所做的重要工作。

所以,一旦你为结构体添加了自定义初始化器,默认的成员初始化器就会消失。如果你想让它保留,可以把你的自定义初始化器移到扩展中,就像这样:

swift
struct Employee {
    var name: String
    var yearsActive = 0
}

extension Employee {
    init() {
        self.name = "Anonymous"
        print("Creating an anonymous employee…")
    }
}

// 现在可以创建命名员工了
let roslin = Employee(name: "Laura Roslin")

// 也可以创建匿名员工
let anon = Employee()

【可选阅读】什么时候会在方法中使用 self?

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

在方法内部,Swift 允许我们使用 self 来引用结构体的当前实例,但总的来说,除非你特别需要区分所指的内容,否则不建议这样做。

到目前为止,使用 self 最常见的原因是在初始化器内部,你可能希望参数名与你的类型的属性名相匹配,就像这样:

swift
struct Student {
    var name: String
    var bestFriend: String

    init(name: String, bestFriend: String) {
        print("Enrolling \(name) in class…")
        self.name = name
        self.bestFriend = bestFriend
    }
}

当然,你不一定非得这样做,但给参数名添加某种前缀会显得有点笨拙:

swift
struct Student {
    var name: String
    var bestFriend: String

    init(name studentName: String, bestFriend studentBestFriend: String) {
        print("Enrolling \(studentName) in class…")
        name = studentName
        bestFriend = studentBestFriend
    }
}

在初始化器之外,使用 self 的主要原因是我们处于一个闭包中,而 Swift 要求我们这样做,以明确我们明白正在发生的事情。这只在从属于类的闭包内部访问 self 时才需要,而且 Swift 会拒绝编译你的代码,除非你添加它。

【练习题】初始化器

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

swift
struct Book {
	var title: String
	var author: String
	init(bookTitle: String) {
		title = bookTitle
	}
}
let book = Book(bookTitle: "Beowulf")

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

swift
struct Media {
	var type: String
	var users: Int
	init() {
		
	}
}
let tv = Media(type: "Television", users: 10_000_000)

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

swift
struct Experiment {
	var cost = 0
}
let lhc = Experiment(cost: 13_250_000_000)

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

swift
struct Wine {
	var grape: String
	var region: String
}
let malbec = Wine(grapes: "Malbec", region: "Cahors")

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

swift
struct Dictionary {
	var words = Set<String>()
}
let dictionary = Dictionary()

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

swift
struct Sport {
	var name: String
	var isOlympicSport: Bool
}
let chessBoxing = Sport(name: "Chessboxing", isOlympicSport: "false")

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

swift
struct Country {
	var name: String
	var usesImperialMeasurements: Bool
	init(countryName: String) {
		name = countryName
		let imperialCountries = ["Liberia", "Myanmar", "USA"]
		if imperialCountries.contains(name) {
			usesImperialMeasurements = true
		} else {
			usesImperialMeasurements = false
		}
	}
}

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

swift
struct Tree {
	var type: String
	var hasFruit: Bool
	func init() {
		type = "Cherry"
		hasFruit = true
	}
}
let cherryTree = Tree()

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

swift
struct Starship {
	var name: String
	var maxWarp: Double
	init(starshipName: String) {
		name = starshipName
	}
}
let voyager = Starship(starshipName: "Voyager")

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

swift
struct Message {
	var from: String
	var to: String
	var content: String
	init() {
		from = "Unknown"
		to = "Unknown"
		content = "Yo"
	}
}
let message = Message()

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

swift
struct PowerTool {
	var name: String
	var cost: Int
}
let drill = PowerTool(name: "Hammer Drill", cost: 80)

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

swift
struct Cabinet {
	var height: Double
	var width: Double
	var area: Double
	init (itemHeight: Double, itemWidth: Double) {
		height = itemHeight
		width = itemWidth
		area = height * width
	}
}
let drawers = Cabinet(itemHeight: 1.4, itemWidth: 1.0)

【练习题】引用当前实例

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

swift
struct Conference {
	var name: String
	var location: String
	init(name: String, location: String) {
		self.name = name
		self.location = location
	}
}
let wwdc = Conference(name: "WWDC", location: "San Jose")

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

swift
struct SuperHero {
	var nickname: String
	var powers: [String]
	init(nickname: String, superPowers: [String]) {
		self.nickname = nickname
		self.powers = superPowers
	}
}
let batman = SuperHero(nickname: "The Caped Crusader", superPowers: ["He's really rich"])

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

swift
struct Kitchen {
	var utensils: [String]
	init(utensils: [String]) {
		self.utensils = utensils
	}
}

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

swift
struct Parent {
	var numberOfKids: Int
	var tirednessPercent: Int
	init (kids: Int) {
		self.numberOfKids = kids
	}
}
let james = Parent(kids: 2)

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

swift
struct Language {
	var nameEnglish: String
	var nameLocal: String
	var speakerCount: Int
	init(english: String, local: String, speakerCount: Int) {
		self.nameEnglish = english
		self.nameLocal = local
		self.speakerCount = speakerCount
	}
}
let french = Language(nameEnglish: "French", nameLocal: "français", speakerCount: 220_000_000)

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

swift
struct Bus {
	var routeNumber: String
	init(route: Int) {
		self.routeNumber = route
	}
}

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

swift
struct Cat {
	var name: String
	var breed: String
	var meowVolume: Int
	init(name: String, breed: String) {
		self.name = name
		self.breed = breed
	}
}
let toby = Cat(name: "Toby", breed: "Burmese")

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

swift
struct Character {
	var name: String
	var actor: String
	var probablyGoingToDie: Bool
	init(name: String, actor: String) {
		self.name = name
		self.actor = actor
		if self.actor == "Sean Bean" {
			probablyGoingToDie = true
		} else {
			probablyGoingToDie = false
		}
	}
}

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

swift
struct Cottage {
	var rooms: Int
	var rating = 5
	init(rooms: Int) {
		self.rooms = rooms
	}
}
let bailbrookHouse = Cottage(rooms: 4)

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

swift
struct Framework {
	var name: String
	var language: String
	func init(name: String, language: String) {
		self.name = name
		self.language = language
	}
}
let vapor = Framework(name: "Vapor", language: "Swift")

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

swift
struct Computer {
	var cpus: Int
	var ramGB: Int
	init(cpus: Int, ram: Int) {
		self.cpus = cpus
		self.ram = ram
	}
}

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

swift
struct District {
	var number: Int
	var supervisor: String
	init(number: Int, supervisor: String) {
		self.number = number
		self.supervisor = supervisor
	}
}
let district = District(number: 9, supervisor: "Unknown")

本站使用 VitePress 制作