Swift 类
类(class)是 Swift 中另一种自定义类型,和结构体最大的区别是它是引用类型。
同一个类实例可以被多个变量同时引用,通过任意一个变量修改,其他变量都会看到变化。
本文讲类的定义与实例化、引用语义、恒等运算符,以及 final 与 open 这两个修饰类的关键字。
定义与实例化
用 class 关键字定义类,属性写法与结构体一致。
和结构体不同,类不会自动获得成员逐一初始化器,所以每个存储属性都必须有一个初始值:要么在声明时给默认值,要么在 init 里赋值。
实例
class RunoobSite {
var name: String
var url: String
init(name: String, url: String) {
self.name = name
self.url = url
}
}
let site = RunoobSite(name: "Runoob", url: "www.runoob.com")
print(site.name)
// 所有属性都有默认值时,可以省略 init
class RunoobCourse {
var title = "Swift 教程"
var lessons = 43
}
let course = RunoobCourse()
print(course.title, course.lessons)
Runoob Swift 教程 43
如果既没给默认值也没写 init,编译会直接报错,属性处于未初始化状态是不允许的。
实例
//
// class RunoobBad {
// var name: String // 编译错误:class 'RunoobBad' has no initializers
// }
注意:结构体没写构造器时会自动得到成员逐一初始化器,类没有这个待遇。类的构造过程更复杂,涉及指定构造器、便利构造器与继承规则,可以看第 20 篇《Swift 构造过程》。
引用语义
把一个类实例赋给另一个变量,复制的是引用而不是对象本身,两个变量指向堆上的同一个实例。
实例
var title = "Swift 教程"
var lessons = 43
}
let a = RunoobCourse()
let b = a // b 和 a 指向同一个实例
b.lessons = 50 // 通过 b 修改
print(a.lessons, b.lessons) // a 也看到了变化
50 50
注意 a 是用 let 声明的,但依然能修改它指向实例的属性。
因为 let 限制的是「a 不能再指向别的实例」,而不是「实例内部不能变」。
注意:引用类型的实例存放在堆上,由 ARC 自动管理生命周期,具体规则可以看第 33 篇《Swift 自动引用计数》。
恒等运算符 === 与 !==
=== 判断两个变量是不是指向同一个实例,!== 则判断不是同一个实例。
它们比较的是「身份」,不是「内容」。
实例
var title = "Swift 教程"
var lessons = 43
}
let a = RunoobCourse()
let b = a
print(a === b) // true,同一个实例
let c = RunoobCourse()
print(a === c) // false,内容相同但是两个不同的实例
print(a !== c) // true
true false true
类默认没有 ==,想按内容比较要让类遵循 Equatable 协议并自己实现比较逻辑。
| 运算符 | 比较的是什么 | 适用类型 |
|---|---|---|
=== | 两个引用是否指向同一个实例 | 类实例(AnyObject) |
!== | 两个引用是否指向不同实例 | 类实例(AnyObject) |
== | 两个值的内容是否相等 | 遵循 Equatable 的类型 |
final 与 open
final 表示「到此为止,不能再被继承或重写」,open 则表示「欢迎其他模块来继承和重写」。
final 可以加在类上,也可以只加在某个方法或属性上。
实例
final class RunoobConfig {
var host = "www.runoob.com"
}
// 下面这行会编译报错:inheritance from a final class
//
// class RunoobSubConfig: RunoobConfig {}
let config = RunoobConfig()
print(config.host)
// final 也可以只修饰单个方法
class RunoobBase {
final func run() {
print("这个方法不能被子类重写")
}
func other() {
print("这个方法可以重写")
}
}
www.runoob.com
open 只能用于其他模块也能访问的类与成员,它比 public 多一层含义:允许跨模块继承与重写。
实例
open func runoob() { // 其他模块可以继承并重写
print("父类实现")
}
func helper() { // 只能在当前模块内被重写
print("模块内可见")
}
}
class RunoobSub: RunoobBase {
override func runoob() {
print("子类重写")
}
}
let sub = RunoobSub()
sub.runoob()
子类重写
| 修饰符 | 作用范围 | 说明 |
|---|---|---|
final | 当前类或当前成员 | 禁止继承与重写,编译器还可能做额外优化 |
open | 跨模块 | 允许其他模块继承类、重写成员,隐含 public |
注意:final 与 open 不能同时使用。继承与重写的一般写法看第 22 篇《Swift 继承》,完整的访问控制级别看第 39 篇《Swift 访问控制》。
类与结构体的选择准则
官方文档给出的默认建议是优先用结构体,只有在确实需要类的特性时才改用类。
结构体与类的完整对比表在第 15 篇《Swift 结构体》,这里只按实际场景给出选择建议。
| 场景 | 建议 | 原因 |
|---|---|---|
| 只是把几个属性打包在一起,比如坐标、配置、接口返回的数据 | 结构体 | 值语义让数据流向清晰,不会被意外改到 |
| 多个持有者需要看到同一份状态,比如共享的缓存、连接池 | 类 | 引用语义保证大家操作的是同一个对象 |
| 需要继承来实现多态,或者已有父类 | 类 | 结构体不支持继承 |
| 需要在实例销毁时释放资源、注销通知 | 类 | 只有类有 deinit,见第 21 篇《Swift 析构过程》 |
| 需要和 Objective-C 互操作,比如继承 NSObject | 类 | Objective-C 的类型系统基于类 |
| 需要自动生成的成员逐一初始化器 | 结构体 | 类不会自动生成 |
| 对象很大,复制开销明显,且本来就不需要副本 | 类 | 引用传递不复制内容 |
标准库里 String、Array、Dictionary、Set 都是结构体,而 URLSession 这类需要管理生命周期与共享状态的对象是类。
常见问题
下面是类在使用中最容易被问到的问题。
为什么类必须有 init 或默认值
类的存储属性在 init 返回之前必须全部处于已初始化状态,这是编译器的硬性检查。
给属性写默认值也是一种初始化,所以所有属性都有默认值时,Swift 会提供一个无参数的默认 init。
let 声明的类实例为什么还能改属性
let 限制的是引用本身,让它不能再指向另一个实例;实例内部的 var 属性能不能改,由属性自己的声明决定。
想禁止外部修改某个属性,可以把它声明成 let,或者用 private(set) 限制写入权限。
什么时候该加 final
没有继承需求时都可以加。final 让编译器知道不会有子类重写,省去动态派发的开销,也明确表达了这个类的设计意图。
代价是将来真的需要继承时要回头改,所以在设计框架的公开类型时要谨慎,普通业务代码里加 final 很常见。
