SwiftInDepth_03_属性整洁之道

前言

  1. 如何创建getter和setter计算属性
  2. 何时(不)使用计算属性
  3. 使用lazy property 提高性能
  4. lazy property在结构和易变性方面的表现
  5. 掌握存储属性使用

前言:简洁地使用属性可以简化结构、类和对象的接口、枚举,使您的代码更安全、更易于阅读和为他人(以及您未来的自己)使用。由于属性是Swift的核心部分,因此遵循本章中的指示可以帮助您直接提高代码的可读性。

1. 计算属性

  • 计算属性是伪装为属性的函数
  • 计算属性除了不存储任何数据之外和存储属性一样

1. 场景:实现Run

  1. 开始Run
  2. 获取Run 时长结束Run
  3. 判断是否完成Run

2. 使用函数实现Run功能


import Foundation

struct Run {
  let id: String
  let startTime: Date
  var endTime: Date?
  func elapsedTime() -> TimeInterval {
        return Date().timeIntervalSince(startTime)
  }

  func isFinished() -> Bool {
    return endTime != nil
  }
  
  mutating func setFinished() {
    endTime = Date()
  }
  
  init(id: String, startTime: Date) {
    self.id = id
    self.startTime = startTime
    self.endTime = nil
  }
}

/// 初始化
var run = Run(id: "10", startTime: Date())

/// 计算时间函数
print(run.elapsedTime())  3.6
sleep(4)
print(run.elapsedTime())  4.

/// 获取状态函数
print(run.isFinished())   false

/// 修改数据函数
run.setFinished()
print(run.elapsedTime())  4.00

  • 使用了三个函数获取和修改数据
  • 尝试用计算属性进行替换

3. 使用计算属性实现Run 功能


import Foundation

struct Run {
  let id: String
  let startTime: Date
  var endTime: Date?
    /// 计算属性
  var elapsedTime: TimeInterval {
       return Date().timeIntervalSince(startTime)
  }
  /// 计算属性
  var isFinished: Bool {
        return endTime != nil
  }
  
  mutating func setFinished() {
      endTime = Date()
  }

  init(id: String, startTime: Date) {
    self.id = id
    self.startTime = startTime
    self.endTime = nil
  }
}
  • 调用计算属性验证Run功能

var run = Run(id: "10", startTime: Date())
/// 调用计算属性
Log(message: run.elapsedTime) 

sleep(4)
Log(message: run.elapsedTime)

/// 调用计算属性
Log(message: run.isFinished)

/// 调用set方法
run.setFinished()

Log(message: run.isFinished)
Log(message: run.elapsedTime)
5.1021575927734375e-05
4.000301957130432
false
true
4.000576972961426

  • 这样Run 这个结构中只有一个计算函数,其他都是属性,两个函数被属性替换,完成同样的功能
  • 接下来我们可以尝试使用getter&setter 来继续替换函数

4. 使用getter&setter 来实现Run功能


import Foundation

struct Run {
  let id: String
  let startTime: Date
  var endTime: Date?
  var elapsedTime: TimeInterval {
        return Date().timeIntervalSince(startTime)
  }
  
  /// getter & setter replace function
  var isFinished: Bool {
    get {
      return endTime != nil
    } set {
      if newValue == false {
        endTime = nil
      } else if endTime == nil {
        endTime = Date()
      }
    }
  }
  
  init(id: String, startTime: Date) {
    self.id = id
    self.startTime = startTime
    self.endTime = nil
  }
}
  • 调用getter&setter属性验证Run功能

var run = Run(id: "10", startTime: Date())
Log(message: run.elapsedTime)
sleep(4)
Log(message: run.elapsedTime)
/// get properties
Log(message: run.isFinished)
/// set properties
run.isFinished = true
/// get properties
Log(message: run.isFinished)
Log(message: run.elapsedTime)
5.0902366638183594e-05
4.0003509521484375
false
true
4.000463962554932

  • 计算属性有时并不是最好的选择,比如,计算滞后或者频繁计算时计算属性的性能就会降低
  • 频发计算时使用Lazy properties是更有效的方式

5. 总结

  1. 将函数转换为属性,可以简化接口、提高代码可读性,隐藏实现细节,降低协作开发沟通成本
  2. 当出现频繁计算时,计算属性可能不是最佳选择,接下来让我们认识下懒加载属性如解决升频繁计算性能问题

2. 懒加载属性

  • 懒加载属性确保属性滞后(如果有)计算,并且只计算一次。
  • 当处理频繁的计算或当用户必须等待很长时间时,懒加载属性将大有可为。

1. 场景:实现LearningPlan

  1. 学习等级 level

  2. 学习描述 description

  3. 学习内容 contents 根据学习等级进行计算,一段时间之后得出不同学习结果

    struct LearningPlan {
        
        let level: Int
        
        var description: String
        
        //: contents is a computed property.
        var contents: String {
            // Smart algorithm calculation simulated here
            print("I'm taking my sweet time to calculate.")
            sleep(2)
            
            switch level {
            case ..<25: return "Watch an English documentary."
            case ..<50: return "Translate a newspaper article to English and transcribe one song."
            case 100...: return "Read two academic papers and translate them into your native language."
            default: return "Try to have 30 mins of English reading."
            }
        }
    }
    
    var plan = LearningPlan(level: 18, description: "A special plan for today!")
    print(Date())  /// 2021-09-02 05:44:57 +0000
    print(plan.contents)  /// I'm taking my sweet time to calculate. 先打印contents的第一行日志,等待2s 之后再打印出 level 18 对应的contents
    /// Watch an English documentary.
    print(Date()) /// 2021-09-02 05:44:59 +0000
    

2. 计算属性的困境

  • 刚才的需求,计算属性 耗时2s 实现了功能,但现在需求变为 输出5次contents

print(Date())
for _ in 0..<5 {
    plan.contents
}
print(Date())

2021-09-02 05:52:41 +0000
I'm taking my sweet time to calculate.
I'm taking my sweet time to calculate.
I'm taking my sweet time to calculate.
I'm taking my sweet time to calculate.
I'm taking my sweet time to calculate.
2021-09-02 05:52:51 +0000
  • 经过需求的变更之后,我们耗时51-41 = 10s 完成了5次的contents 输出
  • 如果输出的内容增加之后耗时将会递增,性能问题是软件质量的一个重要指标,虽然计算属性实现了功能但却不是优秀的程序
  • 怎么才能提高耗时程序的性能呢?让我们一起揭开懒加载属性神秘的面纱

3. 懒加载属性

1. 使用懒加载属性对计算属性进行重构

struct LearningPlan {
    
    let level: Int
    
    var description: String
    
    //: contents is now a lazy property.
    lazy var contents: String = {
        // Smart algorithm calculation simulated here
        print("I'm taking my sweet time to calculate.")
        sleep(2)
        
        switch level {
        case ..<25: return "Watch an English documentary."
        case ..<50: return "Translate a newspaper article to English and transcribe one song."
        case 100...: return "Read two academic papers and translate them into your native language."
        default: return "Try to have 30 mins of English reading."
        }
    }()
    
    init(level: Int, description: String) {
        self.level = level
        self.description = description
    }
}

var plan = LearningPlan(level: 18, description: "A special plan for today!")

print(Date())
for _ in 0..<5 {
    plan.contents
}
print(Date())

2021-09-02 06:09:51 +0000
I'm taking my sweet time to calculate.
2021-09-02 06:09:53 +0000

/// 使用懒加载属性替换计算属性之后 只打印了一次,并且耗时只有2s

2. 懒加载属性和计算属性差异

  • lazy var
  • 变量名和 “{}” 用 “=” 连接
  • “{}”结尾有()
  • 懒加载属性只会访问一次

3. 不够健壮的懒加载属性

  1. 懒加载属性解决了计算属性带来的性能问题,但也并不是无懈可击

  2. 懒加载属性容易被外部破坏

    var plan = LearningPlan(level: 18, description: "A special plan for today!")
    print(plan.contents)
    /// I'm taking my sweet time to calculate.
    /// Watch an English documentary.
    
    /// 修改懒加载属性值
    plan.contents = "Let's eat pizza and watch Netflix all day"
    print(plan.contents)
    // "Let's eat pizza and watch Netflix all day"
    
  3. 怎么才能避免懒加载属性被修改呢?让我们创建一个健壮的懒加载属性

4. 创建强健的懒加载

  • Swift 提供了关键字 private (set)来修饰属性,限制属性的访问级别,让懒加载属性更安全
        //: contents is now a lazy property.
    lazy private(set) var contents: String = {
        // Smart algorithm calculation simulated here
        print("I'm taking my sweet time to calculate.")
        sleep(2)
        
        switch level {
        case ..<25: return "Watch an English documentary."
        case ..<50: return "Translate a newspaper article to English and transcribe one song."
        case 100...: return "Read two academic papers and translate them into your native language."
        default: return "Try to have 30 mins of English reading."
        }
    }()

/// 修改被限制访问的懒加载属性
plan.contents = "Let's eat pizza and watch Netflix all day"

/// IDE error 提示不可赋值给contents
error: LearningPlan with a lazy private setter.xcplaygroundpage:38:6: error: cannot assign to property: 'contents' setter is inaccessible
plan.contents = "Let's eat pizza and watch Netflix all day"
~~~~~^~~~~~~~

5. 可变属性和懒加载

  • 初始化对象之后懒加载属性一旦被调用便无法被更改

    1. 调用懒加载属性之后的copy

    • copy之前对象调用懒加载属性,一旦调用就无法修改,因此copy之后属性值已经被确定并且无法被修改
    var intensePlan = LearningPlan(level: 138, description: "A special plan for today!")
    /// copy 之前调用lazy property
    intensePlan.contents
    /// copy 
    var easyPlan = intensePlan
    /// 修改level
    easyPlan.level = 0
    /// 输出copy的contents
    print(easyPlan.contents)
    
    /// 输出结果138 并非 copy之后重新赋值level=0 ,原因是copy之前调用懒加载属性,一旦调用就无法修改,因此copy之后属性值已经被确定并且无法被修改
    I'm taking my sweet time to calculate.
    Read two academic papers and translate them into your native language.
    
Copying after initializing a lazy description.png

2. 调用懒加载属性之前的copy

  • copy 对象之前并未调用lazy property ,原对象调用lazy property 对新对象的属性值不造成影响,新对象第一次调用lazy property 会输出level 对应的数据
var intensePlan = LearningPlan(level: 138, description: "A special plan for today!")
/// copy时未调用懒加载属性,因此属性值并未被确定
var easyPlan = intensePlan
/// 原对象调用懒加载属性,值被确定,但和copy的对象无关
intensePlan.contents
/// copy的对象设置level
easyPlan.level = 0
/// 输出已经被确定的lazy property
print(intensePlan.contents) // Read two academic papers and translate them into your native language.
/// copy的对象第一次调用lazy property,根据level = 0 输出
print(easyPlan.contents) // Watch an English documentary.
Copying before initializing a lazy description.png

6. 练习题

1. 请将以下代码中的函数替换为属性

struct Song: Codable {
    let duration: Int
    let track: String
    let year: Int
}

struct Artist {
    var name: String
    var birthDate: Date
    var songsFileName: String
    /// 函数
    func getAge() -> Int? {
        let years = Calendar.current
            .dateComponents([.year], from: birthDate, to: Date())
            .year
        return years
    }
    /// 函数
    func loadSongs() -> [Song] {
        guard
            let fileURL = Bundle.main.url(forResource: songsFileName, withExtension: ".plist"),
            let data = try? Data(contentsOf: fileURL),
            let songs = try? PropertyListDecoder().decode([Song].self, from: data) else {
                return []
            }
        return songs
    }
    /// 函数
    mutating func songsReleasedAfter(year: Int) -> [Song] {
        return loadSongs().filter { (song: Song) -> Bool in
            return song.year > year
        }
    }
}

2. 如果loadSongs 重构为lazy property songs ,请保证以下代码不可以被中断

// billWithers.songs = []
/// lazy property
    lazy var songs: [Song] = {
        guard
            let fileURL = Bundle.main.url(forResource: songsFileName, withExtension: ".plist"),
            let data = try? Data(contentsOf: fileURL),
            let songs = try? PropertyListDecoder().decode([Song].self, from: data) else {
                return []
            }
        return songs
    }()
    /// 函数
    mutating func songsReleasedAfter(year: Int) -> [Song] {
        return self.songs.filter { (song: Song) -> Bool in
            return song.year > year
        }
    }

3. 如果loadSongs 重构为lazy property songs ,请保证以下代码不可修改其属性值

billWithers.songs // load songs
billWithers.songsFileName = "oldsongs" // change file name
billWithers.songs.count // Should be 0 after renaming songsFileName, but is 2
    /// lazy private (set) property
    lazy private (set) var songs: [Song] = {
        guard
            let fileURL = Bundle.main.url(forResource: songsFileName, withExtension: ".plist"),
            let data = try? Data(contentsOf: fileURL),
            let songs = try? PropertyListDecoder().decode([Song].self, from: data) else {
                return []
            }
        return songs
    }()
    /// 函数
    mutating func songsReleasedAfter(year: Int) -> [Song] {
        return self.songs.filter { (song: Song) -> Bool in
            return song.year > year
        }
    }

3. 属性观察

  • property observers == store property & custom behavior
  • 属性监听可以在属性值发生变化时告知其他对象,也可以在属性值变更的同时做一些其他工作

1. 字符串去除空格

class Player {    
    let id: String    
    var name: String    
    init(id: String, name: String) {
        self.id = id
        self.name = name
    }
}

let jeff = Player(id: "1", name: "SuperJeff    ")
print(jeff.name)
print(jeff.name.count) // 13
  • 打印出jeff.name.count 13 空格也被计算在字符串长度之内
  • 如何做到去除空格呢?
var name: String {
        didSet {
                /// 当name 被修改时进行去除空格操作
            name = name.trimmingCharacters(in: .whitespaces)
        }
}

jeff.name = "SuperJeff    "
print(jeff.name)
print(jeff.name.count) // 9 当修改name 时 执行didSet 方法,在didSet 中对name 进行trimmingWhitespace
  • didSet 是在值更新时 可以获取到 oldValue

  • willSet 是在更新之前可以获取到 newValue

var name: String {
        didSet {
            print("start++oldValue:\(oldValue)++end")
            name = name.trimmingCharacters(in: .whitespaces)
        }
        willSet {
            print("start++newValue:\(newValue)++end")
        }
}

let jeff = Player(id: "1", name: "SuperJeff    ")
/// start++oldValue:SuperJeff    ++end
jeff.name = "SuperEric    "
/// start++newValue:SuperEric    ++end
print(jeff.name)/// SuperEric
print(jeff.name.count)/// 9

2. 初始化时监听属性

  • Swift 中对象初始化时无法触发 didSet进行属性监听
  • Swift 推荐在另一个方法中调用didSet, 然后在初始化时调用该方法进行属性监听
  • Swift 中的Defer 闭包是在初始化之后调用
  • Swift中如需在初始化时监听属性,推荐在对象初始化之后调用Defer 闭包,在Defer闭包中调用对象set property触发didSet,完成属性监听
init(id: String, name: String) {
        self.id = id
        self.name = name
}
 /// 初始化时无法触发didSet属性监听,无法进行trimming whitespace
let jeff = Player(id: "1", name: "SuperJeff    ")
print(jeff.name) /// SuperJeff    
print(jeff.name.count) /// 13

init(id: String, name: String) {
        defer { self.name = name }
        self.id = id
        self.name = name
}
/// 初始化对象之后调用defer 闭包,在defer中调用set property 触发didSet属性监听,执行didSet中的trimming whiteSpace
let jeff = Player(id: "1", name: "SuperJeff    ")
print(jeff.name) /// SuperJeff   
print(jeff.name.count) /// 9
  • Defer+didSet 既可以在初始化时进行属性监听,也可以进行存储数据,一举两得
  • Defer 并不是官方推荐的方法,以后可能会失效,谨慎使用

3. 练习题

1. 如果需要同时具有行为和存储的属性,使用哪种类型的属性?

  • 存储属性stored property + 属性监听property observer

2. 如果需要一个只有行为而没有存储的属性,使用什么类型的属性?

  • 计算属性 computed property 或者 懒加载属性 lazy property

3. 找出下面代码中的错误

struct Tweet {
    let date: Date
    let author: String
    var message: String {
        didSet {
            message = message.trimmingCharacters(in: .whitespaces)
        }
    }
    init(date: Date, author: String, message: String) {
        self.date = date
        self.author = author
        self.message = message
    }
}

let tweet = Tweet(date: Date(), author: "@tjeerdintveen", message: "This has a lot of unnecessary whitespace   ")
print(tweet.message.contains("   "))  /// true
  • 初始化对象无法监听属性触发didSet,因此无法触发trimmingWhiteSpace
  • 解决方案:在初始化之后调用Defer闭包,在闭包中调用set property 触发 didSet,执行trimmingWhiteSpace
init(date: Date, author: String, message: String) {
        defer {
            self.message = message
        }
        self.date = date
        self.author = author
        self.message = message
}
let tweet = Tweet(date: Date(), author: "@tjeerdintveen", message: "This has a lot of unnecessary whitespace   ")
print(tweet.message.contains("   "))  /// false

4. 结语

1. 计算属性用于执行一些除了存储之外的特定行为
2. 计算属性是函数伪装成的属性
3. 当一个属性在不同条件下拥有不同的值时,你可以使用计算属性
4. 只有轻量级的功能时建议使用计算属性
5. 懒加载属性在性能和效率上优于计算属性
6. 使用懒加载属性可以延迟计算或者在不需要是不初始化
7. 在类和结构体中懒加载属性滞后于其他属性
8. 在类或结构体中可以使用privete(set) 关键字标识属性对外只读
9. 当懒加载引用另一个属性时,请确保其他属性不可变以保持低复杂性。
10. 给存储属性添加其他行为,请使用属性监听 willSet、didSet
11. 在初始化对象时想要触发属性监听可以使用defer闭包

你可能感兴趣的:(SwiftInDepth_03_属性整洁之道)