Swift 你真的会用属性吗? | 七日打卡

3,220 阅读2分钟

点赞评论,感觉有用的朋友可以关注笔者公众号 iOS 成长指北,持续更新

本文是 《Swift 100 Days》系列的的第 15 天, Swift 100 Days 是笔者记录自己 Swift 学习的记录,欢迎各位指正。

结构和类(统称为“类型”)可以有自己的变量和常量,这些统称为属性。

允许将值附加到类型上并以唯一地表示。

结构体与对象 三个小节中,我们提及了属性的使用。

编程离不开对象,对象离不开属性!

Swift 中有两种属性:存储属性,它们将状态和对象相关联;计算属性,则根据该状态执行计算。

属性观察器

当你声明一个存储属性,你可以使用闭包定义一个 属性观察器,该闭包中的代码会在属性被设值的时候执行。Swift 允许你观察属性即将更改或已更改。

有两种类型的属性观察器:willSetdidSet,它们在属性更改之前或之后被调用。在 willSet 中 Swift 为你的代码提供了一个叫做 newValue 的特殊值,表明属性即将变成的新值,而在 didSet 中你会得到 oldValue 来表示更新之前的值。

struct Product {
    var price: Double {
        willSet {
            print("changing from \(price) to \(newValue)")
        }
        
        didSet {
            print("changed from \(oldValue) to \(price)")
        }
    }
}

var product = Product(price: 30.0)
product.price = 28.0
product.price = 35.0

//changing from 30.0 to 28.0
//changed from 30.0 to 28.0
//changing from 28.0 to 35.0
//changed from 28.0 to 35.0

任何功能都离不开实践,没有实践意义的功能,可能不是迫切的等待你学习。

增加值约束

使用属性观察器来对类型接受的值增加额外的约束。

我们为我们的产品添加一个最低/高价。当值小于/大于最低/高价时,将其重置为最低/高价。

struct Product {
    var price: Double {
        willSet {
            print("changing from \(price) to \(newValue)")
        }
        
        didSet {
            print("changed from \(oldValue) to \(price)")
            if self.price < 30.0 {
                self.price = 30.0
            }
        }
    }
}

var product = Product(price: 30.0)
product.price = 28.0
product.price = 35.0
//changing from 30.0 to 28.0
//changed from 30.0 to 28.0
//changing from 30.0 to 35.0
//changed from 30.0 to 35.0

在观察器内部设置属性不会触发额外的回调,上面的代码不会产生无限循环。

我们之所以不使用 willSet 观察器是因为即使我们在其回调中进行任何赋值,都会在属性被赋予 newValue 时覆盖。

当然,这次赋值最终还是执行了的。只不过赋值成为我们想要的值。如果我们想要指定一个如果设置错误就报错的机制的话。使用 didSet 观察器达不到这个效果。

避免不需要的刷新

使用属性观察器来对避免不需要的刷新。或者说我们总在需要的时候才需要更新数据。

如果我们的 UI 会因为某个值而改变。那么我们应该尽量当且仅当值改变的时候才去更新这个页面,而不是约束在变量的赋值操作。

what new

在 Swift 5.3 中,属性观察器可以附加到惰性属性上,这使得我们可以自动地观察何时给给定属性分配了新值,即使它的值在第一次访问时是惰性加载的。

总结

使用 willSetdidSet 进行属性观察提供了一种非常强大的方法,可以轻松观察值的变化,而无需任何其他类型的抽象。它们可用于确保我们基于单一的事实来源来驱动逻辑,并允许我们在给定属性更改时以反应方式更新其他值和状态。

计算属性

计算属性是一个计算并返回值的属性,而不仅仅只是存储它。

计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。

计算属性的 getter 和 setter

简要介绍一下我们使用的语法:

var property:type {
    get {
        code
    }
    set(value) {
        code
    }
}

计算属性首先是一个变量,如果一个常量,并没有必要当做一个计算属性。

其次,set 方法中的 value 既可以显示声明,也可以隐式声明

set {
    // Use `newValue` in here (隐式)
}
set(newString) {
    // Use `newString` in here (显示)
}

只读计算属性

只有 getter 没有 setter 的计算属性叫只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。

var sellPrice: Double {
  get {
    return Double(count) * price
  }
}

var description: String {
  return "iOS 成长指北"
}

var name: String {
  "iOS 成长指北"
}

自 Swift 5.1 以来,为了清晰、简洁和表达,单行表达式可以省略显式返回。

使用函数返回值,同样也能达到相同的效果。在 Swift 中,在计算属性和方法之间进行选择时,我会在以下情况下选择使用只读计算属性:

  • 我所定义的 API 返回某种形式的关于对象或值状态的信息。
  • 其计算属性的时间复杂度为 O(1)——不需要进行过于冗杂的逻辑计算
let sellPrice = product.sellPrice()

//or

let sellPrice = product.sellPrice

惰性/延迟加载计算属性

惰性属性可以使你的 iOS 开发更高效,更容易阅读。

Day9 结构体和对象 中 我们介绍了如何在 Swift 中使用 lazy 处理存储属性。但是如果简单使用 lazy 关键字来修饰计算属性,毫无作用

class Circle {
    var radius:Double = 0

    lazy var circumference:Double {
        Double.pi * 2 * radius
    }
}

当我们兴致昂昂的写下上述代码时,编译器报错

// Output: 'lazy' may not be used on a computed property
// Output: Lazy properties must have an initializer

那我们怎么实现一个惰性/延迟加载的计算属性呢

我们可以这么操作。

class Circle {
    var radius:Double = 0

    lazy var circumference:Double = {
        Double.pi * 2 * self.radius
    }()
}

惰性/延迟加载的计算属性现在使用闭包来进行延迟计算。

惰性/延迟加载计算的属性仅计算一次!

你需要时刻注意什么情况下才需要使用这种方式。

一般我们推荐将其作为语法糖来实现代码级别的 UI 绘制

lazy var label:UILabel = {
  UILabel.init()
}()

延迟加载延迟初始化属性,直到它第一次被访问。计算属性不允许使用 lazy 关键字,因此可以使用隐式调用的闭包来获得相同的效果。

属性包装器(Property Wrappers)

在 WWDC 2019 上,随着对 SwiftUI 的介绍,我们第一次在 Swift 中看到了属性包装器。

从 Swift 5.1 以后,Swift 家族有了一系列 @s

属性包装器除了在 SwiftUI 中十分有用,在实际的 Swift 编程中也很有作用。

在我们学习 SwiftUI 之前,我们先对属性包装器有一个具象的认知。

什么是属性包装器?

Swift 中的属性包装器允许你在不同的包装器对象中提取公共逻辑。

属性包装器可以看作是一个额外的层,它定义了如何在读取时存储或计算属性。它对于替换属性的 getter 和 setter 中的重复代码特别有用。

透明包装(Transparently Wrapping)

我们定义一个用来实现名称首字母大写的属性包装器

@propertyWrapper struct Capitalized {
    var wrappedValue: String {
        didSet {
            wrappedValue = wrappedValue.capitalized
        }
    }
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue.capitalized
    }
}

注意:我们需要显式地将传入初始化器的任何字符串大写,因为属性观察者只有在值或对象完全初始化之后才会触发。

我们定义两个结构体,一个是 User,一个是Book。并且这两种结构 name 属性的首字母都需要大写。

如果不使用属性包装器,我们可以使用属性观察器,在 didSet 中重新赋值。

但我们可以这么使用

struct User {
    @Capitalized var name: String
}
let user = User(name: "iOS 成长指北")
struct Book {
    @Capitalized var name: String = "iOS 成长指北"
}
let book = Book()

print(user.name)
print(book.name)

//Ios 成长指北
//Ios 成长指北

这样,当我们需要我们的属性具备这个效果时,我们只需要在定义属性时加一个 @Capitalized 前缀就行了。

属性包装器和 UserDefaults

属性包装器也可以拥有自己的属性,从而可以进行进一步的自定义,甚至可以将依赖项注入到我们的包装器类型中。

Swift 中有一种存储功能 UserDefaults,一个用户默认数据库的接口,你可以在应用程序的启动过程中持久化存储键值对

let defaults = UserDefaults.standard
defaults.set(25, forKey: "Age")
defaults.set("iOS 成长指北", forKey: "name")
let age = defaults.integer(forKey: "Age") // 25
let name = defaults.string(forKey: "name") //iOS 成长指北

这样做通常需要编写某种形式的映射代码,以便将每个值与其底层的 UserDefaults 存储进行同步——通常这需要为我们要存储的每个数据段进行复制。并且根据类型的不同需要使用不同的获取方法。

但是,通过属性包装器,可以使我们的方法更加的优雅

@propertyWrapper struct UserDefaultsBacked<Value> {
    let key: String
    var storage: UserDefaults = .standard

    var wrappedValue: Value? {
        get { storage.value(forKey: key) as? Value }
        set { storage.setValue(newValue, forKey: key) }
    }
}

当我们需要使用时,我们可以为需要存储的数据

@UserDefaultsBacked<String>(key: "name") var name
    
@UserDefaultsBacked<Int>(key: "age") var age

这样,当我们赋值时,会存储当前的对象,读取值时,也很方便,并且使用是,可以直接点语法获取。

现在,这些知识已经足够我们编写足够简洁的代码了。

感谢你阅读本文! 🚀