Swift 5.1 (10) - 属性

级别: ★☆☆☆☆
标签:「iOS」「Swift 5.1」「存储属性」「计算属性」「属性观察」
作者: 沐灵洛
审校: QiShare团队


属性

属性将值与特定类,结构或枚举相关联。
存储属性:存储值,将常量和变量值存储为实例的一部分。存储属性仅由类和结构体支持。
计算属性:计算值,计算属性由类,结构体和枚举支持。

存储属性

存储属性可以是变量存储属性,通过var关键字来声明,也可以是常量存储属性,通过let关键字来声明。
结构体存储属性定义示例:

//定义一个结构体
struct StructEnumType {
    //! 定义一个常量存储属性
    let constantPro : Int
    //! 定义一个变量存储属性
    var varPro : Int //!< `var`关键字可以这般声明属性,但是必须在初始化方法中赋值。
}
//使用`var`声明StructEnumType的变量实例
var enumType = StructEnumType(constantPro: 0, varPro: 3)
//尝试修改变量属性
//enumType.constantPro = 0 //!< 编译器报错:Cannot assign to property: 'constantPro' is a 'let' constant
//尝试修改变量属性
enumType.varPro = 6
print(enumType)//!< 改变成功。输出:StructEnumType(constantPro: 0, varPro: 6)

常量结构体实例的存储属性

//使用`let`声明StructEnumType的常量实例
let enumType = StructEnumType(constantPro: 0, varPro: 3)
//尝试修改变量属性
//enumType.constantPro = 0 //!< 编译器报错:Cannot assign to property: 'constantPro' is a 'let' constant
//尝试修改变量属性
enumType.varPro = 6 //!< 编译器报错:Cannot assign to property: 'enumType' is a 'let' constant

综上述:
结构体StructEnumType分别定义了变量和常量属性。
•使用var定义StructEnumType的变量实例,该变量实例无法修改结构体中的常量属性,但是可以修改其变量属性的值。
•使用let定义StructEnumType的常量实例,该常量实例无法修改结构体中的常量属性和变量属性。
总结:
结构体是值类型,当标记值类型的实例为常量时其所有属性便都会标记为常量。
类的存储属性定义示例:

//类的存储属性与变量属性
var classVarPro : Int
let classConstantPro : Int = 8
override init() {
    classVarPro = 7
    super.init()
}
//使用`var`和`let`是一样的
var classObj = PropertiesClass()
//    classObj.classConstantPro = 7 //!< 编译器报错:Cannot assign to property: 'constantPro' is a 'let' constant
classObj.classVarPro = 10 //!< 正确输出

综上述:
• 类是引用类型,不管是使用类的变量实例还是常量实例,都可以修改变量属性的值,但是不能修改常量属性的值。
懒存储属性
懒存储属性:其初始值在初次使用的时候才会被调用。类似懒加载。
声明懒存储属性:使用关键字lazy
注意:懒属性必须使用var关键字声明为变量属性。
存储属性和实例变量
Objective-C提供了两种方法来存储值和引用作为类实例的一部分。除了属性之外,还可以使用实例变量。而Swift中只有属性并且Swift中的属性没有相应的实例变量。

计算属性

类,结构体和枚举类型中可以定义计算属性,计算属性会提供一个getter和一个可选的setter来间接获取和设置其他属性和值。计算属性的声明只能使用var关键字

//! 定义一个坐标点的结构体
struct Point {
    var x = 0,y = 0
}
//! 定义一个大小的结构体
struct Size {
    var width = 0,height = 0
}
/* 定义一个rect的结构体:一个rect会有原点,会有大小 根据这两个存储属性,可以计算得出 rect的center。
因此我们会在`rect`中定义一个计算属性。利用其提供的`getter`和`setter`方法进行值得获取,和值被设置时进行相应处理*/
struct rect {
    var origin = Point()
    var size = Size()
    //! get 和 set 都需要出现,
    var center : Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set(customValue){
            //`customValue`便是所赋新值的点
            origin.x = customValue.x - (size.width / 2)
            origin.y = customValue.y - (size.height / 2)
        }
    }
}

定义一个rect的结构体:一个rect会有原点,会有大小,根据这两个存储属性,可以计算得出中心。因此我们在rect中定义一个计算属性center。利用其提供的gettersetter方法进行center计算属性值的获取,属性值的关联处理。调用如下:

var frame = rect(origin: Point(x: 2, y: 2), size: Size.init(width: 10, height: 10)) //!< 不能使用`let`..
frame.center = Point.init(x: 10, y: 10) //!<不重新赋值便会得到Point(x: 7, y: 7)
print("应该调用了set方法后:\(frame.center)")//Point(x: 10, y: 10)

使用frame.center时会调用结构体rect中的center计算属性的getter方法。使用frame.center = Point.init(x: 10, y: 10)会调用setter方法,设置与获取center属性值关联的属性值。若外部对于center计算属性只是获取,则只需要getter方法即可,若不仅会获取,还会重新设置,则setter也是必要的。
简写的setter方法声明
如果计算属性的setter方法没有为要设置的新值定义名称,则使用默认名称newValue

struct rect {
    var origin = Point()
    var size = Size()
    
    var center : Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set{
            //`newValue`便是所赋新值的点,系统的默认值
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

只读计算属性
只读计算属性:具有getter但没有setter方法的计算属性。只读计算属性始终返回一个值,可以通过点语法访问,但不能设置为其他值。
注意:计算属性,包括只读计算属性,必须使用var关键字将其声明为变量属性,因为计算属性的值不固定。let关键字仅用于常量属性,一旦将它们设置为实例初始化的一部分,就不能更改它们的值。

//! 定义面积的结构体
struct areaStruct {
    var width = 0.0,height = 0.0
    var area:Double {
        get {
            return width * height
        }
    }
}

只读计算属性的声明,我们也可以通过删除get关键字及其{}来简化

struct areaStruct {
    var width = 0.0,height = 0.0
    var area:Double {
        return width * height
    }
}
/*使用`let`关键字修饰的结构体实例,其内部的变量属性,都将变为常量属性。
即`setter`方法不可用,但是`getter`方法不收影响。*/
let area = areaStruct.init(width: 20.0, height: 3.0)
print("\(area.area)")

属性观察者

属性观察者:观察和响应属性值的变化。每次属性的值被设置时,属性观察者都会被调用,即使新值和属性当前的值是一样的。
我们可以为我们定义的任何存储属性添加属性观察者,除了懒储存属性,即使用lazy修饰的变量属性。也可以通过在子类中重写继承自父类的属性(存储属性和计算属性均可),为该属性添加属性观察者。
属性观察者的定义:
willSet存储新值之前调用
didSet存储新值之后调用
可以在属性中定义willSetdidSet中的任一个,或者两者都定义。
willSet此观察者方法,会将新属性值作为常量参数传递。可以在该方法实现中指定该常量参数的名称。若未在实现中指定参数名称,将会使用默认参数名称newValue
didSet此观察者方法,会将旧属性值作为常量参数传递。可以在该方法实现中指定该常量参数的名称。若未在实现中指定参数名称,将会使用默认参数名称oldValue。另:
如果在didSet观察者方法内,为该属性分配一个新值,则这个新的值将会替换刚刚设置的值。
当调用了父类的初始化方法后,并在子类的初始化方法中设置父类的属性时,父类属性的willSetdidSet的观察者方法会被调用。在父类的初始化方法被调用之前,类在设置自己的属性时,willSetdidSet的观察者方法不会被调用。

//属性观察者
class propertyObeserver: NSObject {
    var progress = 0 {
        willSet(newprogress) {
          print("将要设置的新值\(newprogress)")
        }
//            willSet {
//                print("将要设置的新值\(newValue)")
//            }
        didSet(oldProgress){
            print("设置的新值与旧值得差值\(progress - oldProgress)")
        }
//            didSet {
//               print("设置的新值与旧值得差值\(progress - oldValue)")
//            }
    }
}
//使用
//属性观察者
let propertyObj = propertyObeserver.init()
propertyObj.progress = 200 //!< 将要设置的新值200; 设置的新值与旧值得差值200
propertyObj.progress = 100 //!< 将要设置的新值

注意:如果将具有观察者的属性作为输入输出参数传递给函数,则始终调用willSetdidSet观察者方法。因为in-out参数的copy-in copy-out内存模型:在函数结束的时候作为参数的属性的值总是会重新写入原始的属性中。
关于in-out参数传递时的copy-in copy-out
copy-in copy-out的行为也称为按值调用结果,具体的行为有:
•调用该函数时,将复制inout参数的值。
•在函数体中,参数值的副本被修改
•当函数返回的时候,参数值的副本将会赋值给原始的参数。
例如:计算属性或者具有观察者的属性作为函数的输入输出参数传递时,它们的getter方法会作为函数调用的一部分被调用。它们的setter作为函数返回的一部分被调用。
作为优化,当参数值是存储在内存中的物理地址时,在函数的内部和外部会使用相同的内存区域。这个优化被称为引用调用。它满足了copy-in copy-out模型的所有要求,同时消除了复制的开销。使用copy-in copy-out模型编写代码,而不依赖于按引用调用优化,以便在有或没有优化的情况下copy-in copy-out的行为是正确的。
在函数内,不要访问作为输入输出参数传递的值,即使原始值在当前范围内可用。因为违反了Swift的内存独占性,同时也无法将相同的值传递给多个输入输出参数。

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

increment(&stepSize)//!< Error: 访问 stepSize冲突了

一个捕获输入输出参数的闭包或者嵌套函数一定是非逃逸的。如果需要捕获输入输出参数而不改变它或者要观察其他代码所做的更改,需要使用捕获列表以不可变的方式显式捕获参数。

static func someFunction1(a: inout Int) -> () -> Int {
    return { [a] in return a + 1 }
}
//方法的调用
var b = 7
let c =  someFunction1(a: &b)()
print("\(b)...\(c)")//7...8

上述示例:函数someFunction1具有输入输出参数a,函数的返回值是() -> Int函数类型。在someFunction1函数的返回值,中需要捕获输入输出参数a,并在someFunction1函数外部进行调用,因此使用捕获列表的形式[a]捕获该输入输出参数。
如果需要捕获并改变输入输出参数,则需要使用显式的本地副本。

//可变的输入输出参数
static func mutateFuncation(x:inout Int) ->Void{
    
    //输出输出参数`x`会被改变,所以定义一个本地的副本
    var localX = x
    //在函数结束之前需要调用defer关键字修饰的代码,一遍将副本的改变操作,赋值给原始参数`x`
    defer {/*< 在结束范围之前的'defer'语句总是立即执行。
作用:修饰一段函数内任一段代码,使其必须在函数中的其余代码都执行完毕,函数即将结束前调用*/
        x = localX
    }
    changeValue(b: &localX)
}
static func changeValue(b : inout Int) {
    b += 3
}
//调用
var b = 7
mutateFuncation(x: &b)
print("可变的调用结果\(b)")//!< 可变的调用结果10

上述示例:函数mutateFuncation具有输入输出参数x,且该参数会在函数内部被changeValue函数改变,因此输入输出参数x在函数mutateFuncation内部是被改变的。所以需要使用显式的本地副本。并定义defer{}代码块,以便副本修改完成后可以赋值给原始的值。

全局和局部变量

计算属性和具有观察者的属性可用于全局变量和局部变量。也可以在全局或局部范围定义计算变量,也可以为存储变量定义观察者。计算变量的定义方式与计算属性相同。
全局变量:是在任何函数,方法,闭包或类型的上下文之外定义的变量。
局部变量:是在函数,方法或闭包上下文中定义的变量。

类型属性

实例属性:特定类型的实例的属性。每次创建的特定类型的新实例,它都会有属于自己的一组属性值,并且和其他的实例是区分开的。
类型属性:属于该类型本身的属性,不管我们创建多少该类型的实例,类型属性只会有一个副本。
类型属性可以定义对特定类型的所有实例通用的值。例如所有实例都可以使用的常量属性(如C中的静态常量),或者所有实例都可以使用的存储全局值的变量属性(如C中的静态变量)。存储的类型属性可以是变量或常量。计算类型属性始终声明为变量属性,与实例计算属性的方式相同。
类型属性语法
C和Objective-C中,类型属性是将与类型关联的静态常量和变量定义为全局静态变量。参考
Swift中,类型属性是作为类型定义的一部分进行定义的,在类型的外部{}中,并且每个类型属性都显式限定为它支持的类型。
使用static关键字定义类型属性。对于类类型的计算类型属性,可以使用class关键字来代替static定义类型属性,从而允许子类重写父类的实现。

struct SomeStructure {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 1
    }
}
enum SomeEnumeration {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 6
    }
}
class SomeClass {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 27
    }
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}
class subSomeClass: SomeClass {
   //! 当父类实用static时 此处的重写会报错`Cannot override static property1
   override class var overrideableComputedTypeProperty: Int {
        return 227
    }
}

查询和设置类型属性
使用点语法进行查询类型属性和设置类型属性,就像实例属性一样。但是类型属性是在类型上查询和设置类型属性,而不是在该类型的实例上。

print("获取结构体的存储类型属性\(SomeStructure.storedTypeProperty)")//!< 获取结构体的存储类型属性Some value.
SomeStructure.storedTypeProperty = "Another value."
print("设置结构体的存储类型属性后\(SomeStructure.storedTypeProperty)")//!< 设置结构体的存储类型属性后Another value.
print("获取结构体的计算类型属性\(SomeStructure.computedTypeProperty)")//!< 获取结构体的计算类型属性1
print("获取枚举类型的计算类型属性\(SomeEnumeration.computedTypeProperty)")//!< 获取枚举类型的计算类型属性6
print("获取类类型的计算类型属性\(SomeClass.computedTypeProperty)")//!< 获取类类型的计算类型属性27
print("获取子类的类类型的计算类型属性\(subSomeClass.overrideableComputedTypeProperty)")//!< 获取类类型的计算类型属性227
//实用场景
struct volume {//!< 定义音量结构体
    static let maxLevel = 20 //!< 最大的音量
    static var isNorse : Bool = false //!< 是否是噪音
    var currentLevel: Int = 0 {//!< 当前音量
        didSet {
            if currentLevel > volume.maxLevel {
                currentLevel = volume.maxLevel
            }
            
            if currentLevel > volume.maxLevel/2 {
                volume.isNorse = true
            } else {
                volume.isNorse = false
            }
        }
    }
}

参考资料:
swift 5.1官方编程指南


小编微信:可加并拉入《QiShare技术交流群》。

关注我们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)

推荐文章:
iOS App后台保活
Swift 中使用 CGAffineTransform
iOS 性能监控(一)—— CPU功耗监控
iOS 性能监控(二)—— 主线程卡顿监控
iOS 性能监控(三)—— 方法耗时监控
初识Flutter web
用SwiftUI给视图添加动画
用SwiftUI写一个简单页面
iOS App启动优化(三)—— 自己做一个工具监控App的启动耗时
iOS App启动优化(二)—— 使用“Time Profiler”工具监控App的启动耗时
iOS App启动优化(一)—— 了解App的启动流程
奇舞周刊

推荐阅读更多精彩内容