写点什么

【Swift 专题】聊聊 Swift 中的属性

作者:珲少
  • 2024-01-31
    上海
  • 本文字数:3852 字

    阅读完需:约 13 分钟

引言

属性是面向对象语言中非常基础的语法特性,我们讲属性,实际上就是讲与类本身或类实例关联的数据。在面向对象的语言中,类作为重要的数据结构会封装数据与函数,类中的函数我们通常称其为方法,而数据则就是属性。


Swift 语言是一门比较现代化的语言,并且直到今日,其还在不断进行语法特性与编程模式的更新。学习 Swift 语言不仅能够进行实用的编程,从其设计思想和许多语法定义细节上我们也可以受益匪浅。就好比读一本内容深厚的文学作品,它会启发你的思考,对编程的设计和应用有更深的理解。


本文将以”属性“为专题介绍 Swift 语言中相关功能的设计与应用。如果你正在寻找这部分的内容与知识,希望本文可以带给你帮助。

进入正题

和大多数编程语言一样,Swift 语言中的属性也分为存储属性(stored)与计算属性(computed)。属性可以关联在类本身上,也可以关联在类的实例上,当然,这里说”类“并不准确,属性也适用于结构体和枚举。存储属性顾名思义会存储数据,通常大多数属性也都是以存储属性的方式定义。计算属性则更像是一个方法,其定义的是一个计算过程,计算属性本身并不存储任何数据,通常计算属性会用于二次处理其他存储属性的值。在 Swift 中,计算属性可以在_结构体枚举中定义,而存储属性只允许在****和结构体_中定义。

存储属性

存储属性定义在类或结构体中,可以将存储属性定义为常量也可以定义为变量。在 Swift 语言中,类是引用类型和结构体是值类型,因此如果结构体实例被定义成了常量,则无论其中的存储属性是否是变量,都将不可修改,类则不同。


class ClassDemo {    var value:Int = 0}struct StructDemo {    var value:Int}
let c = ClassDemo()let s = StructDemo(value: 1)
c.value = 2// 结构体常量不允许任何修改// s.value = 3
复制代码


上面代码中,虽然 c 类定义成了常量,但由于引用类型的性质,我们依然可以对其中定义为变量的存储属性进行修改。

关于懒加载

在 Objective-C 语言中,如果我们想让某个属性在使用时再创建,可以手动为其实现 Setter 方法。Swift 语言则方便很多,只需要使用 Lazy 关键字来修饰存储属性即可,懒加载是一种很实用的编程技巧,我们再设计某个类型时,如果其中某个属性并不是必须的,就可以将其设置为懒加载属性,这样只有当真正使用到此属性时,才会进行属性的创建,这会减轻实例初始化时的负担。


class ClassDemo {    var value:Int = 0        lazy var description: String = {        print("懒加载属性创建")        return "ClassDemo description"    }()}
复制代码


上面的代码,只有当实例调用到 description 属性时,才会打印出创建信息。直观上看,懒加载属性的定义更像是定义了一个属性的值的构造方法,第一次用到时才会构造。上面的例子其实并不明显,如果我们某个属性的值是需要读文件来获取的,则使用懒加载可以大大提高实例创建的性能。


另外,Lazy 只能修饰定义为变量的属性,不能修饰常量属性,这是因为懒加载的本身逻辑是与 Swift 常量属性的性质相悖的,Swift 中的常量属性必须在实例构造好前完成初始化,而懒加载的属性是允许实例构造完成后属性并未初始化的。Lazy 关键字虽然好用,但是其并不是线程安全的,如果在多个线程中访问懒加载属性,则其有可能会被初始化多次,造成难以预料的异常问题。

计算属性

与存储属性对应,计算属性并不真正的存储数据,而是提供一种计算算法,直接将计算出的结果作为计算属性的值。


struct StructDemo {    var value:Int        var exp: Int {        set(newValue) {            value = newValue / 2        }        get {            value * 2        }    }}let s = StructDemo(value: 2)print(s.exp) // 4
复制代码


上面示例代码中,exp 是一个计算属性,用来对 value 的值乘以 2,从使用上看,计算属性和存储属性并没有太大差别,当对计算属性进行赋值时,会调用其中的 set 代码块,当读取计算属性的值时,会调用其中的 get 代码块。一个计算属性也可以只提供 get 而不提供 set,这样的计算属性与只读存储属性类似,就只能读取不能赋值了。

计算属性的简化写法

Swift 语言的设计理念是极简的,简单层面的简化可以更聚焦逻辑,但同时也会带来一些弊端,极致的简化需要靠大量的语法静态约定来支持,这就需要开发者额外记忆一些约定,因此 Swift 为开发者提供了简写与非简写两种编码方式。我们可以根据自己的编程习惯以及业务的特性来选择。对于计算属性,set 部分的形参是可以省略的,如果省略形参,则约定形参的名字就是 newValue,例如如下的简写方式:


var exp: Int {    set {        value = newValue / 2    }    get {        value * 2    }}
复制代码


另外,如果只提供 get 块,则可以直接省略任何声明,直接做 get 计算即可:


var exp: Int {    value * 2}
复制代码


需要注意,上面的示例是一种比较极端的情况,当只有一句计算语句时可以对 return 关键字进行省略。

类属性

前面提到的存储属性和计算属性都是实例属性,实例属性通过类实例进行访问,我们也可以直接将属性关联到类型上,这时定义的属性为类属性。类属性使用关键字 static 或 class 进行定义。static 定义的类属性不能被子类覆写,如果需要定义子类和覆写的类计算属性,则需要使用 class 关键字。类属性直接使用类名来访问,其性质上和实例属性并没太大差别。

属性监听器

属性监听器提供了一种监听属性变化的方法,每当属性被赋值时,都会调用监听器回调,另外,如果赋值前后属性的值并没有变化,监听器依然是生效的,回调依然会正常执行。


class ClassDemo {    var value:Int = 0        lazy var description: String = {        print("懒加载属性创建")        return "ClassDemo description"    }(){        didSet {            print("属性监听:didSet")        }        willSet {            print("属性监听:willSet - \(newValue)")        }    }        var exp: Int {        get {value * 2}        set {value = newValue / 2}    }}
复制代码


其中,didSet 会在属性赋值完成后回调,这是再取属性的值已经是赋值后的结果,willSet 会在属性赋值前调用,willSet 中也会自动传入一个 newValue 参数,它就是将要被赋值的数据值。


并非所有的场景都支持定义属性监听器,能够定义属性监听器的场景有:


1. 类中定义的存储属性。


2. 子类继承的存储属性。


3. 子类继承的计算属性。


需要注意的是当前类中定义的计算属性并不能定义属性监听器,这很好理解,因为即使支持在这种场景定义属性监听器也没有任何意义,因为 set 块在调用时我们已经可以处理任何需要监听器处理的逻辑。上面的 ClassDemo,子类继承示例如下:


class SubClassDemo: ClassDemo {    override var value:Int {        didSet {}        willSet {}    }        override var exp: Int {        didSet {}        willSet{}    }}
复制代码

属性包装器

属性包装器是 Swift 语言中有关属性部分非常强大的功能。我们知道,通过定义计算属性可以定义内部属性的存储方式,如果我们想让这一部分计算逻辑能够复用,例如前面示例代码中的对数据乘 2 的操作,使用属性包装器就非常方便。例如:


@propertyWrapperstruct MultipleTwo {    private var number = 0        var wrappedValue: Int {        get { number * 2}        set {number = newValue}    }}
复制代码


使用**@propertyWrapper**可以将某个结构定义成属性包装器,属性包装器中通常会定义一个私有的存储属性存储本质的数据,wrappedValue 计算属性用来提供外界访问的数据。在定义普通的存储属性时,可以使用包装器对其进行包装,其使用起来就会和包装器中 wrappedValue 逻辑一致,例如:


struct StructDemo {    @MultipleTwo var exp: Int}var s = StructDemo()// 赋值为2s.exp = 2// 实际上访问到了包装器的get,返回4print(s.exp) // 4
复制代码


属性包装器中存储的属性也支持通过初始化方法来设定初值,例如:


@propertyWrapperstruct MultipleTwo {    private var number: Int        var wrappedValue: Int {        get { number * 2}        set {number = newValue}    }        init(number: Int) {        self.number = number    }}struct StructDemo {    @MultipleTwo(number: 2) var exp: Int}var s = StructDemo()// 赋值为2s.exp = 2// 实际上访问到了包装器的get,返回4print(s.exp) // 4
复制代码


属性包装器在实际项目开发中是非常有用的,例如我们可以编写一个持久化存储的包装器,当属性被赋值时,自动的将数据同步到文件。


还有一点需要注意,一般情况下,我们无需访问属性包装器中真实存储数据的存储属性,但 Swift 语言也提供了一种方式来访问此属性的值,仍然是通过语法规范约定的方式,只需要将属性包装器中存储属性的属性名定义为 projectedValue,之后即可使用属性名加 $前缀的方式来访问,如下:


@propertyWrapperstruct MultipleTwo {    private(set) var projectedValue: Int        var wrappedValue: Int {        get { projectedValue * 2}        set {projectedValue = newValue}    }        init(number: Int) {        self.projectedValue = number    }}struct StructDemo {    @MultipleTwo(number: 2) var exp: Int}var s = StructDemo()// 赋值为2s.exp = 2// 实际上访问到了包装器的get,返回4print(s.exp) // 4// 访问到真实存储的数据,返回2print(s.$exp) // 2
复制代码


另外,上述的属性监听器和包装器其实也适用于变量中,本篇文章不再过多介绍。

发布于: 刚刚阅读数: 5
用户头像

珲少

关注

还未添加个人签名 2022-07-26 加入

还未添加个人简介

评论

发布
暂无评论
【Swift专题】聊聊Swift中的属性_珲少_InfoQ写作社区