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

Swift 属性

属性是类型和实例携带的数据,Swift 把它们分成存储属性和计算属性两大类。

存储属性真正占用内存保存一个值,计算属性则每次访问时现算,不占存储空间。

本文讲存储与计算属性、只读计算属性、属性观察器、延迟属性 lazy,以及容易被写错的类型属性 static 与 class 的区别。


存储属性与计算属性

存储属性用 var 或 let 声明,直接保存一个值。

计算属性必须用 var 声明,通过 get 和 set 两个代码块来读取和写入,本身不存储任何数据。

实例

struct RunoobRect {
    var originX: Double = 0.0    // 存储属性:真实保存一个值
    var width: Double = 0.0      // 存储属性

    var centerX: Double {        // 计算属性:每次访问时计算
        get { originX + width / 2 }
        set { originX = newValue - width / 2 }
    }
}

var rect = RunoobRect(originX: 10, width: 20)
print(rect.centerX)     // 读取时执行 get
rect.centerX = 50       // 写入时执行 set,newValue 就是 50
print(rect.originX)
20.0
40.0

set 代码块里可以直接用 newValue 这个默认名字,也可以写成 set(newCenter) { ... } 自己指定参数名。

注意:计算属性不能写成 let。let 表示值一旦确定就不再变化,而计算属性本身没有值,只有每次访问时才产生的计算结果。


只读计算属性

只提供 get、不提供 set 的计算属性就是只读计算属性,此时 get 关键字可以省略。

实例

struct RunoobSquare {
    var side: Double

    // 只有 get,可以省略 get 关键字
    var area: Double { side * side }

    // 显式写出 get 也完全等价
    var perimeter: Double {
        get { side * 4 }
    }
}

let square = RunoobSquare(side: 3)
print(square.area, square.perimeter)
9.0 12.0

只读计算属性可以像普通属性一样用点语法读取,但尝试给它赋值会编译报错。

用 let 声明的结构体实例也能读取只读计算属性,因为它只读不写。


属性观察器 willSet 与 didSet

属性观察器用来监听存储属性的变化,willSet 在赋值之前调用,didSet 在赋值之后调用。

Swift 属性分类树:属性分为存储属性与计算属性,存储属性含普通存储、lazy 延迟存储和属性观察器,计算属性含只读与读写;右侧标注类型属性 static 与 class,底部标注计算属性不能有观察器、观察器在初始化时不触发

最容易被忽略的一点是:初始化阶段给属性赋值不会触发观察器。

实例

class RunoobCounter {
    var total: Int {
        willSet { print("willSet: \(total) -> \(newValue)") }
        didSet  { print("didSet: \(oldValue) -> \(total)") }
    }

    init(total: Int) {
        self.total = total   // 初始化时不触发观察器
    }
}

let counter = RunoobCounter(total: 5)
print("初始化完成,total = \(counter.total)")
counter.total = 10       // 这才是赋值,观察器会被触发
初始化完成,total = 5
willSet: 5 -> 10
didSet: 5 -> 10

观察器的参数名也可以自己指定,不写就用默认的 newValue 和 oldValue。

实例

class RunoobScore {
    var value: Int = 0 {
        willSet(newValue) { print("即将写入 \(newValue)") }
        didSet(oldValue)  { print("旧值 \(oldValue)") }
    }
}

let score = RunoobScore()
score.value = 90
即将写入 90
旧值 0

属性观察器只能加在存储属性上;在子类里重写继承来的属性时,也可以给它补上观察器,这部分看《Swift 继承》。

注意:在 init 里给属性赋初值不会触发 willSet 和 didSet,只有初始化完成之后的赋值才会。如果初始化时也需要执行某些逻辑,应该直接写在 init 里。


延迟存储属性 lazy

用 lazy 修饰的存储属性在第一次被访问时才计算初始值,之后一直复用这个结果。

它适合初始化开销大、或者依赖其他属性的场景。

实例

class RunoobLoader {
    lazy var content: String = {
        print("开始加载数据……")
        return "www.runoob.com 的数据"
    }()
}

let loader = RunoobLoader()
print("实例已创建")
print(loader.content)   // 第一次访问,闭包执行
print(loader.content)   // 第二次访问,直接返回缓存结果
实例已创建
开始加载数据……
www.runoob.com 的数据
www.runoob.com 的数据

lazy 属性必须用 var 声明,因为它第一次被访问时值才会被写入,而 let 属性在初始化后就不能再赋值。

注意:lazy 属性不是线程安全的。多个线程同时第一次访问同一个 lazy 属性,可能导致闭包执行多次,并发场景要自己加锁或改用其他方案。


类型属性:static 与 class 的区别

类型属性属于类型本身,不属于任何一个实例,用类型名访问。

存储型类型属性必须用 static;class 只能用来修饰可被重写的计算型类型属性,不能修饰存储属性。

实例

struct RunoobConfig {
    static var siteName = "RUNOOB"   // 存储型类型属性:必须用 static
    static let version = "1.0"       // 常量类型属性也用 static
}

print(RunoobConfig.siteName, RunoobConfig.version)

class RunoobBase {
    static var stored = "存储型,子类不可重写"
    static var staticComputed: String { "静态计算型" }

    // class 只能修饰可重写的计算型类型属性
    class var overridable: String { "父类的计算型类型属性" }
}

class RunoobSub: RunoobBase {
    override class var overridable: String { "子类重写后的计算型类型属性" }
}

print(RunoobBase.overridable)
print(RunoobSub.overridable)
print(RunoobSub.stored)   // 存储型类型属性可以被子类继承访问
RUNOOB 1.0
父类的计算型类型属性
子类重写后的计算型类型属性
存储型,子类不可重写
关键字能否修饰存储属性能否被子类重写可用位置
static能不能结构体、枚举、类都可以
class不能能只能用在类的计算属性、方法、下标上

常见的写法是把共享常量声明成 static let,把需要子类定制的行为声明成 class var 或 class func。

注意:不要写 class var name = "...",存储型类型属性用 class 会直接编译报错,必须改成 static。


全局变量与局部变量

在类型之外、文件作用域声明的变量叫全局变量,在函数内部声明的叫局部变量。

全局变量和类型属性默认都是延迟初始化的,第一次访问时才创建,这一点和 lazy 一致;局部变量则是在执行到声明语句时初始化。

实例

// 全局变量,首次访问时才初始化
var runoobGlobal = "www.runoob.com"

func showLocal() {
    var local = "局部变量"
    local += ",只在函数内可见"   // 局部变量可以修改
    print(local)
}

showLocal()
print(runoobGlobal)
局部变量,只在函数内可见
www.runoob.com

全局变量同样可以加属性观察器,写法与普通存储属性一致。

实例

var runoobCount = 0 {
    didSet { print("全局变量被修改为 \(runoobCount)") }
}

runoobCount = 5
全局变量被修改为 5

常见问题

下面是属性相关的几个高频疑问。

计算属性能不能加属性观察器

不能。观察器只能加在存储属性上,因为计算属性每次访问都会执行代码,再包一层观察器没有意义。

需要在计算属性里做额外处理,直接把逻辑写进 get 和 set 即可。

lazy 和计算属性有什么区别

lazy 是真正的存储属性,值算出来之后会保存下来,第二次访问不再重复计算。

计算属性每次访问都会重新执行 get,不会缓存结果,两者要按「结果是否需要复用」来选择。

static 和 class 到底怎么选

先看要写的是存储属性还是计算属性:存储属性只能用 static。

如果是计算属性,再看需不需要子类重写:需要重写用 class,不需要就用 static。

为什么属性观察器在 init 里没触发

初始化阶段的任务是给属性建立初始值,此时属性还没有「旧值」,观察器自然不适用。

官方明确规定了这一点,所以依赖 didSet 做校验或同步的逻辑,要额外考虑初始化路径。