现在位置: 首页 > Swift 教程 > 正文

Swift 类

类(class)是 Swift 中另一种自定义类型,和结构体最大的区别是它是引用类型。

同一个类实例可以被多个变量同时引用,通过任意一个变量修改,其他变量都会看到变化。

本文讲类的定义与实例化、引用语义、恒等运算符,以及 final 与 open 这两个修饰类的关键字。


定义与实例化

用 class 关键字定义类,属性写法与结构体一致。

和结构体不同,类不会自动获得成员逐一初始化器,所以每个存储属性都必须有一个初始值:要么在声明时给默认值,要么在 init 里赋值。

实例

// 属性没有默认值,必须自己写 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 构造过程》。


引用语义

把一个类实例赋给另一个变量,复制的是引用而不是对象本身,两个变量指向堆上的同一个实例。

实例

class RunoobCourse {
    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 自动引用计数》。


恒等运算符 === 与 !==

=== 判断两个变量是不是指向同一个实例,!== 则判断不是同一个实例。

它们比较的是「身份」,不是「内容」。

实例

class RunoobCourse {
    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 类不能被继承
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 class RunoobBase {
    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 很常见。