浅谈 Swift 中的属性(Property)

由于种种原因,简书等第三方平台博客不再保证能够同步更新,欢迎移步 GitHub:https://github.com/kingcos/Perspective/。谢谢!

Properties in Swift

Date Notes Swift Xcode
2017-04-27 扩充 #延迟存储属性# 部分并新增 #devxoul/Then# 3.1 8.3.2
2016-10-26 首次提交 3.0 8.1 Beta 3

前言

Swift 中的属性分为存储属性与计算属性,存储属性即为我们平时常用的属性,可以直接赋值使用,而计算属性不直接存储值,而是根据其他(存储)属性计算得来的值。

在其他面向对象的编程语言中,例如 Java 和 Objective-C 中,get 和 set 方法提供了统一、规范的接口,可以使得外部访问或设置对象的私有属性,而不破坏封装性,也可以很好的控制权限(选择性实现 get 或 set 方法)。而 Swift 中似乎并没有见到类似的 get 和 set 方法,而 Swift 使用了一种名为属性观察器的概念来解决该问题。

本文简单介绍下 Swift 中的这两种属性,以及属性观察器。

延迟存储属性

  • 存储属性使用广泛,即是类或结构体中的变量或常量,可以直接赋初始值,也可以修改其初始值(仅指变量)。
  • 延迟存储属性是指第一次使用到该变量再进行运算(这里的运算不能依赖其他成员属性,@匿了不怕 评论提示,延迟存储属性可以使用成员属性,只是需要在闭包中明确指出其持有对象本身,即加上 self;也可以直接使用静态/类属性)。
  • 延迟存储属性必须声明为 var 变量,因为其属性值在对象实例化前可能无法得到,而常量必须在初始化完成前拥有初始值。
  • 在 Swift 中,可以将消耗性能才能得到的值作为延迟存储属性,即懒加载。
  • 全局的常量和变量也是延迟存储属性,但不需要显式声明为 lazy(不支持 Playground)。

Demo

  • 这里假定在 ViewController.swift 有一个属性,需要从 plist 文件读取内容,将其中的字典转为模型。如果 plist 文件中内容很多,那么就十分消耗性能。如果用户不触发相应事件,也没有必要加载这些数据。那么这里就很适合使用懒加载,即延迟存储属性。

ViewController.swift

class ViewController: UIViewController {

    lazy var goods: NSArray? = {
        var goodsArray: NSMutableArray = []

        if let path = Bundle.main.path(forResource: "Goods", ofType: "plist") {
            if let array = NSArray(contentsOfFile: path) {
                for goodsDict in array {
                    goodsArray.add(Goods(goodsDict as! NSDictionary))
                }
                return goodsArray
            }
        }

        return nil
    }()

    // 这样也是允许的,可以把初始化的代码直接放在构造方法中
    lazy var testLazy = Person()
}

class Person {}

可以在延迟存储属性运算的代码中加入打印语句,即可验证其何时初始化。

Lazy 初始化的「演变」过程

struct Person {
    var name = ""

    init() {
        print(#function)
    }
}

// 直接初始化
let p1 = Person()

// 利用闭包
let getOnePerson = { () -> Person in
    let p = Person()
    return p
}

let p2 = getOnePerson()

// 闭包执行
let p3 = { () -> Person in
    let p = Person()
    return p
}()

// 简化
let p4: Person = {
    let p = Person()
    return p
}()

Lazy 方法

  • @Onevcat 的 Swifter Tips 中也提到,在 Swift 标准库中,也有一些 Lazy 方法,就可以在不需要运行时,避免消耗太多的性能。
let data = 0...3
let result = data.lazy.map { (i: Int) -> Int in
    print("Handling...")
    return i * 2
}

print("Begin:")
for i in result {
    print(i)
}
// OUTPUT:
// Begin:
// Handling...
// 0
// Handling...
// 2
// Handling...
// 4
// Handling...
// 6

devxoul/Then

Demo

ViewController.swift

import UIKit
import Then

class ViewController: UIViewController {
    lazy var myLabel = UILabel().then {
        $0.text = "My Label"
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        myLabel.frame = CGRect(x: 0.0, y: 0.0,
                               width: 100.0, height: 100.0)
        view.addSubview(myLabel)
    }
}

Source Code

  • Then 的核心源码部分总共只有不到 20 行,非常简单易懂。
  • Then 库中定义了一个名为 Then 的空协议,之后通过协议扩展(Protocol Extension),来为协议添加默认的方法实现。

then()

  • 因为 then() 内部将 self 返回,即可在初始化完成后,调用该方法,并在闭包中设置属性,而且不需要再将自身返回。
  • 支持 NSObject 子类,也可以将自定义类型(仅支持 AnyObject 类型,即 class)声明实现该协议即可(协议扩展已经拥有默认实现,所以仅声明实现协议即可)。
extension Then where Self: AnyObject {

  /// Makes it available to set properties with closures just after initializing.
  public func then(_ block: (Self) -> Void) -> Self {
    block(self)
    return self
  }

}

with()

  • then() 适用于引用类型,而 with() 适用于值类型。
  • 使用了 inout 确保方法内外共用一个值类型变量。
extension Then where Self: Any {

  /// Makes it available to set properties with closures just after initializing and copying the value types.
  public func with(_ block: (inout Self) -> Void) -> Self {
    var copy = self
    block(&copy)
    return copy
  }

}

do()

  • do() 使得可以直接在闭包中简单地执行一些操作。
extension Then where Self: Any {
  /// Makes it available to execute something with closures.
  public func `do`(_ block: (Self) -> Void) {
    block(self)
  }

}

计算属性

  • 举个例子,一个矩形结构体(类同理),拥有宽度高度两个存储属性,以及一个只读面积的计算属性,因为通过设置矩形的宽度和高度即可计算出矩形的面积,而无需直接设置其值。当宽度或高度改变,面积也应当可以跟随其变化(反之不能推算,因此为只读)。为说明 setter 以及便捷 setter 说明,另外添加了原点(矩形左下角)存储属性,以及中心计算属性。

Demo

struct Point {
    var x = 0.0
    var y = 0.0
}

struct Rectangle {
    var width = 0.0
    var height = 0.0
    var origin = Point()

    // 只读计算属性
    var size: Double {
        get {
            return width * height
        }
    }

    // 只读计算属性简写为
//    var size: Double {
//        return width * height
//    }

    var center: Point {
        get {
            return Point(x: origin.x + width / 2,
                         y: origin.y + height / 2)
        }

        set(newCenter) {
            origin.x = newCenter.x - width / 2
            origin.y = newCenter.y - height / 2
        }

        // 便捷 setter 声明
//        set {
//            origin.x = newValue.x - width / 2
//            origin.y = newValue.y - height / 2
//        }
    }

}

var rect = Rectangle()
rect.width = 100
rect.height = 50
print(rect.size)

rect.origin = Point(x: 0, y: 0)
print(rect.center)

rect.center = Point(x: 100, y: 100)
print(rect.origin)

// 5000.0
// Point(x: 50.0, y: 25.0)
// Point(x: 50.0, y: 75.0)

综上,getter 可以根据存储属性推算计算属性的值,setter 可以在被赋值时根据新值倒推存储属性,但它们与我们在其他语言中的 get/set 方法却不一样。

属性观察器

  • 属性观察器算是 Swift 中的一个 feature,变量在设值会先进入 willSet,这时默认 newValue 等于即将要赋值的值,而变量本身尚未改变。变量在设值会先进入 didSet,这时默认 oldValue 等于赋值前变量的值,而变量变为新值。
  • 这样,开发者即可在 willSetdidSet 中进行相应的操作,如果只是取值和设值而不进行额外操作,那么直接使用点语法即可。但是有时候一个变量只需要被访问,而不能在外界赋值,那么可以使用访问控制修饰符加上 (set) 即可私有化 set 方法。例如 fileprivate(set)private(set),以及 internal(set)。值得注意的是,这里的访问控制修饰符修饰的是 set 方法,访问权限(即 get)是另外设置的。例如 public fileprivate(set) var prop = 0,该变量全局可以访问,但只有同文件内可以使用 set 方法。

Demo

struct Animal {
    // internal 为默认权限,可不加
    internal private(set) var privateSetProp = 0

    var hungryValue = 0 {
        // 设置前调用
        willSet {
            print("willSet \(hungryValue) newValue: \(newValue)")
        }

        // 设置后调用
        didSet {
            print("didSet \(hungryValue) oldValue: \(oldValue)")
        }

        // 也可以自己命名默认的 newValue/oldValue
        // willSet(new) {}
        // didSet(old) {}
    }
}

var cat = Animal()

// private(set) 即只读
// cat.privateSetProp = 10
print(cat.privateSetProp)

cat.hungryValue += 10
print(cat.hungryValue)

// 0
// willSet 0 newValue: 10
// didSet 10 oldValue: 0
// 10

总结

Swift 的这几个 feature 我未曾在其他语言中见过,对于初学者确实容易造成凌乱。特别是 getter/setter 以及属性观察器中均没有代码提示,容易造成手误,代码似乎也变得臃肿。但是熟悉之后,这些也都能完成之前的功能,甚至更加细分。保持每一部分可控,也使得整个程序更加严谨,更加安全。

参考

也欢迎您关注我的微博 @萌面大道V & 简书

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容