Swift 中变量的使用细节

变量(variable)是任何一门语言的构成基础, 对 Swift 也不例外. 大家可能会想, 这东西还有什么"细节"可言, 正常使用不就行了.
其实, 在 Swift 中, 变量使用起来还是有一些细节问题需要注意以及提防的, 作者就根据自己的实际经验总结了如下一些东西, 仅供大家参考.
由于这里探讨的是使用细节问题, 因此基本的变量声明、操作以及相关概念等此文便不再赘述.
本文将分成如下 3 部分进行说明.

1. stored variable 和 computed variable

2. Observer(观察器)

3. 懒加载(重点说明!!!)


1. stored variable 和 computed variable

Swift 中的变量分为 stored variable 和 computed variable.
stored variable 顾名思义, 就是我们一般意义上理解的可以进行赋值和取值的变量.

var number: Int = 10
上面的 number 就是一个 stored variable, 对其进行正常的读(取值)写(赋值)操作即可, 因为 stored variable 是可以存储值的.

computed variable, 字面意思为"计算型的变量", 虽然也叫"变量", 但要注意, 这个家伙是根本没法存储值的.
来看下面的例子, 这里定义了一个名为 number 的 computed variable:

var number: Int {
    get {
        return 10
    }
    set {
        print("number 被设置为了 \(newValue)")
    }
}

下面对 number 进行我们"想当然"的读写操作:

number = 100        // 输出结果为 " number 被设置为了 100 "
print(number)      // 输出结果为 " 10 "

What? 明明给 number 赋了新值 100, 并且输出结果也明确指出 number 被设置为了 100, 为什么再次打印 number 的值时, 却还是 10 ???

切记, computed variable 根本不会存储我们主观上认为的"值"!!! 它实质上是一些待执行的代码!!!
number 被赋值时, set 中的代码被执行. 同理, 当执行读取 number 的操作时, get 中的代码被执行.

再次重申, computed variable 只是一些待执行的代码!!!

在对 computer variable 赋值时, 我们能够取到赋给其的新值, 那就是 set 中的 newValue, 至于这个值要怎么用, 那就看实际的需要了. 但要注意, newValue 不会存储至 computed variable中! 毕竟, computed variable 只是一些待执行的代码! 重要的事情说三遍!!!

可能有人会想, 毕竟 computed variable 叫做"变量", 如果不能像 stored variable 那样进行很直观的读写操作, 总觉得很奇怪, 我下面给出了一种用 computed variable 实现 stored variable 效果的思路.

这里首先定义一个 PositiveNumber 类, 用来表示正数:

class PositiveNumber {
    private var _value: Double = 0.0
    var value: Double {
        get {
            return _value
        }
        set {
            _value = newValue
        }
    }
}

我为 PositiveNumber 类定义了两个属性, 分别为私有的store property _value 和可供外部访问的接口: 一个 computed property value.
是不是很面熟? 没错, 这跟 OC 中定义一个 @property 是完全相同的!

let number = PositiveNumber()
number.value = 10       // 利用 computed property 的 set 方法为 _value 赋值
print(number.value)   // 利用 computed property 的 get 方法读取 _value 的值

看到这里, 你可能会想, 如此大费周折, 只做了一件 stored property 很容易就能做到的事儿, 这不是闲的吗?

我们仔细思考一下上例, number 是一个 PositiveNumber 的实例, 在上述实现方式下, 我们可以为 number.value 赋任意值, 如: number.value = -10
这在逻辑上明显是不合适的, 因为 number 是一个"正数", 所以我们要对赋给 number.value 的值进行一定的处理. 这种逻辑交给 computed property 就再合适不过了.
修改上述代码如下:

class PositiveNumber {
    private var _value: Double = 0.0
    var value: Double {
        get {
            return _value
        }
        set {
            if (newValue <= 0) {
                print("赋值不合理!")
            } else {
                _value = newValue
            }
        }
    }
}

这样, 若再执行 number.value = -10 则会执行判断的逻辑, 从而使对 _value 的赋值操作更加严谨.

当然, computed property 的作用不仅如此, 下面说到懒加载时还会提到.


2. Observer(观察器)

Observer(即 willSet 和 didSet 方法) 是用在 stored variable 上的, 毕竟只有能存储值的东西才有被观察的价值!
基本的使用方法这里就不赘述了, 只对使用时的一些细节做一下说明.

  • 当 stored variable 被初始化时, 观察器方法不会被调用
  • 如果在 didSet 方法中改变了 stored variable 本身的值, 观察器方法也不会被调用. 这个逻辑很容易理通, 因为若在观察器方法中修改 stored variable 的值还会调用观察器方法的话, 那么便会导致观察器方法的无限循环调用!
  • 在 willSet 方法中去操作 stored variable, 其实是对旧值进行操作, 新值为 newValue
  • 在 didSet 方法中去操作 stored variable, 其实是对新值进行操作, 旧值为 oldValue

参考下面的代码

class PositiveNumber {
    var value: Double {
        willSet {
            print("willSet 方法被调用")
            print("在 willSet 中, value = \(value), newValue = \(newValue)")
        }
        didSet {
            print("didSet 方法被调用")
            print("在 didSet 中, value = \(value), oldValue = \(oldValue)")
        }
    }
    
    init(value: Double) {
        self.value = value
    }
}

此处测试一下
let number = PositiveNumber(value: 10.0)
此时, 控制台未输出任何信息! 可见这个初始化的操作并没有调用观察器方法.

再来执行 number.value = 20

控制台输出信息
willSet 方法被调用
在 willSet 中, value = 10.0, newValue = 20.0
didSet 方法被调用
在 didSet 中, value = 20.0, oldValue = 10.0

由上例可以看到观察器调用时的一些细节, 大家在使用时注意一下就好.


3. 懒加载

对于开发者来说,懒加载最被人熟知的优点就在于只在需要某个 variable 时, 才去进行加载.其实, 懒加载还能处理一些普通的初始化方法处理不了的情况.
以初始化一个类的实例为例, 在普通的初始化方法中, 若该实例的初始化过程还未结束, 那么开发者是无法在该初始化方法中引用该实例的属性和方法的. 而懒加载则可以解决这个问题, 因为当需要懒加载某个属性时, 该实例变量已经初始化完毕, 因此开发者可以在懒加载的流程中去任意使用该实例的属性以及方法!

下面说明一下懒加载的使用细节.

  • 全局变量默认都是懒加载的! 其实仔细想想, 苹果的这种设计还是非常合理的. 对于一个全局变量来说, 何时加载它最合适呢? 显然程序一启动就加载的方式不太合理, 于是乎, 还是在需要它时再加载吧!
  • static 的属性默认都是懒加载的! static 的属性某种程度上同全局变量很类似, 例如一个 struct 中含有 static 的属性, 那么这个属性是属于这个 struct 类型的, 而不是属于某个具体的 struct 变量! 于是问题又来了, 何时加载这个属性最合适? 依然还是需要它时再加载吧!
  • 实例属性默认都不是懒加载的! 注意! 都不是懒加载的! 若要令其变为懒加载, 那就在声明时加 lazy, 并且这个实例属性必须是 var, 而不能是 let. 同时, 懒加载的实例属性也无法实现观察器方法, 即没有 willSet 和 didSet 方法.

虽然 Swift 已经提供给了我们一种超级给力的实现懒加载的方式, 即只要一句 lazy 就搞定了, 但上述最后一条细节还是留给了我们很多遗憾!

考虑这样一种需求: 某个实例属性一定要采用懒加载的方式, 而且要对其实现观察器的功能, 即可观测其值的变化并进行一定的逻辑处理.
再考虑另一种需求: 某个实例属性一定要采用懒加载的方式, 并且加载完成后是只读的, 不能对其进行修改.

乍一看, 这些需求明显是在给 Swift 原生的懒加载方式找茬嘛! 上述需求, 原生的 lazy var 一条都实现不了!!! 首先, 懒加载的实例属性本身就无法实现观察器方法, 同时 lazy var 这种形式的声明又导致该实例变量可以被修改, 此时会想, 如果有个 lazy let 就好了...

但是, 上述需求并不过分, 开发中也会碰到, 怎么办? 只能手写一个满足上述需求的懒加载了. 下面只提供了一种思路, 仅供大家参考

class PositiveNumber {
    private var token: dispatch_once_t = 0
    private var _value: Double = 0.0
    var value: Double {
        get {
            dispatch_once(&token) {
                // 一些非常耗时的初始化 _value 的流程
            }
            return _value
        }
        set {
            _value = value
        }
    }
}

其实仔细想想, 懒加载在某种程度上同 computed variable 是相同的, 即只有需要时才调用! 那么就可以利用 computed variable 的这些特点来人造一个懒加载方式.

上段代码采用了本文中第 1 点提到的方式, 实现了通过一个 computed variable 来操作一个 stored variable 的功能, 然后利用 GCD 的一次性代码实现了懒加载的"只加载一次"操作. 至此, 我们手写了一个与使用 lazy 来实现懒加载操作相同的方法.

但是, 要注意! 上段代码的灵活性更强一些, 包括:

  • 若想实现观察器方法, 只要在 set 方法中构建相应的逻辑即可. 例如, 在 _value = value 前添加 willSet 中希望具有的逻辑, 在 _value = value 后添加 didSet 中希望具有的逻辑即可
  • 若想实现 lazy let 形式的懒加载, 只需要把 set 方法去掉即可, 那么属性初始化后就变成只读的了, 外界无法对其进行任何修改
  • 采用了 GCD 的一次性代码, 可以保证线程安全. 根据 Apple 官方的《The Swift Programming Language》, 普通的采用 lazy var 形式的懒加载, 无法保证线程安全. 一旦某个线程对某一实例属性的懒加载过程未结束, 而另一个线程同时又操作了该实例属性, 那么会导致又一次加载该属性, 此时该属性便被初始化了多次, 已不再具有"懒"的特点了
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 天花板上的想象, 存在真理与逃亡。 为何海涅和阿格瑞斯刀剑相向, 老鼠却提着花篮? 哒哒的马蹄, 是结束的序曲, ...
    晚秋暮月阅读 287评论 0 3
  • 发自iPhone客户端。 用于测试 Markdown支持。
    ted005阅读 561评论 0 51
  • 家庭理财是对家庭收入和支出进行分析、预测并加以合理安排的一种财务规划活动。那么,搞好家庭理财到底可以带来哪些好处呢...
    财管评测师阅读 542评论 0 0