到了20世纪30年代,鲁伯·戈德堡已经成为家喻户晓的名字,是**“自制餐巾等连环画中描绘的极其复杂和异想天开的发明的同义词。”大约在同一时间,阿尔伯特·爱因斯坦在批评尼尔斯·玻尔对**量子力学的普遍解释时,推广了“远距离幽灵般的行动这个短语”。
近一个世纪后,现代软件开发已经成为戈德堡装置的精髓—通过量子计算机越来越接近那个怪异的领域。
作为软件开发人员,我们’鼓励尽可能减少代码中的远程操作。这被编入了听起来令人印象深刻的指导方针,如**单一责任原则、最小惊讶原则和德米特里定律**。然而,尽管他们对产生副作用的代码有所担忧,但有时这种技术可能会澄清而不是混淆。
这就是本周关于Swift中属性观察者的文章的重点,它提供了一个内置的、轻量级的替代方案,来替代模型-视图-视图模型(MVVM)功能反应性编程(FRP)等更形式化的解决方案。
Swift中有两种属性:存储属性,它将状态与对象相关联;计算属性,它基于该状态执行计算。例如,
struct S { // Stored Property var stored: String = "stored" // Computed Property var computed: String { return "computed" }}
当您声明存储属性时,您可以选择定义属性观察员,并在设置属性时执行代码块。will Set观察员在存储新值之前运行,didSet观察员在存储新值之后运行。无论旧值是否等于新值,它们都将运行。
struct S { var stored: String { willSet { print("willSet was called") print("stored is now equal to \(self.stored)") print("stored will be set to \(newValue)") } didSet { print("didSet was called") print("stored is now equal to \(self.stored)") print("stored was previously set to \(oldValue)") } }}
例如,运行以下代码将生成的文本打印到控制台:
var s = S(stored: "first")s.stored = "second"
-
Will Set被称为
-
存储现在等于first
-
存储将被设置为秒
-
didSet被称为
-
存储现在等于秒
-
存储之前设置为first
Swift属性观察者从一开始就是语言的一部分。为了更好地理解原因,让我们快速了解一下目标C中的工作方式:
目标C中的属性
在目标C中,所有属性在某种意义上都是计算的。每次通过点表示法访问属性时,调用都被转换成等价的getter或setter方法调用。这反过来又被编译成执行读取或写入实例变量的函数的消息发送。
// Dot accessorperson.name = @"Johnny";// ...is equivalent to[person setName:@"Johnny"];// ...which gets compiled toobjc_msgSend(person, @selector(setName:), @"Johnny");// ...whose synthesized implementation yieldsperson->_name = @"Johnny";
在编程中,你通常希望避免副作用,因为它们会让你很难对程序行为进行推理。但是许多目标C开发人员已经开始依赖于根据需要向getter或setter方法注入额外行为的能力。
Swift对属性的设计将这些模式形式化,并区分了装饰状态访问(存储属性)和重定向状态访问(计算属性)的副作用。对于存储属性,will Set和didSet观察者替换了您’在ivar访问旁边包含的代码。对于计算属性,get和set访问器替换了您可能在目标C中为@动态属性实现的代码。
因此,我们得到了更一致的语义学和更好的保证机制,如键值观察(KVO)和键值编码(KVC)与属性交互。
那么,你能对斯威夫特的房地产观察员做些什么呢?这里有几个想法供你考虑:
验证/标准化值
有时,您希望对类型可接受的值施加额外的约束。
例如,如果你正在开发一个与政府官僚机构接口的应用程序,你’需要确保用户’无法提交缺少所需字段或包含无效值的表单。
比方说,如果一个表单要求名称使用不带重音的大写字母,您可以使用didSet属性观察者来自动去掉变音符号和大写的新值:
var name: String? { didSet { self.name = self.name? .applyingTransform(.stripDiacritics, reverse: false)? .uppercased() }}
幸运的是,在观察者的主体中设置一个属性’触发额外的回调,所以我们’在这里创建一个无限循环。这也是为什么这’不能作为will Set观察者工作的原因;当属性设置为新值时,回调中设置的任何值都会立即被覆盖。
虽然这种方法可以用于一次性问题,但像这样的重复使用是业务逻辑的一个强有力的指标,可以在类型中形式化。
一个更好的设计是创建一个标准化文本类型,它封装了以这样一种形式输入的文本的要求:
struct NormalizedText { enum Error: Swift.Error { case empty case excessiveLength case unsupportedCharacters } static let maximumLength = 32 private(set) var value: String init(_ string: String) throws { if string.isEmpty { throw Error.empty } guard let value = string.applyingTransform(.stripDiacritics, reverse: false)? .uppercased(), value.canBeConverted(to: .ascii) else { throw Error.unsupportedCharacters } guard value.count < NormalizedText.maximumLength else { throw Error.excessiveLength } self.value = value }}
一个失败的或抛出的初始化器可以以一种didSet观察者无法’的方式向调用者显示错误。现在,当像***Llanfair pwllgwyngyllgo gery ch wyrn drobwllantysilio gogoch***这样的麻烦制造者’敲门时,我们可以给他什么!(也就是说,以合理的方式向他传达错误,而不是默默地失败或允许无效数据)
传播从属国
属性观察者的另一个潜在用例是将状态传播到视图控制器中的依赖组件。
考虑以下Track模型和呈现它的Track View Controller的示例:
struct Track { var title: String var audioURL: URL}class TrackViewController: UIViewController { var player: AVPlayer? var track: Track? { willSet { self.player?.pause() } didSet { guard let track = self.track else { return } self.title = track.title let item = AVPlayerItem(url: track.audioURL) self.player = AVPlayer(playerItem: item) self.player?.play() } }}
设置视图控制器的跟踪属性时,会自动发生以下情况:
-
任何先前的音轨的音频暂停
-
视图控制器的标题设置为新的音轨标题
-
新曲目的音频已加载并播放
很酷吧?
你甚至可以将这种行为级联到多个观察到的属性上,比如***Mouse Hunt***的一个场景。
一般来说,在编程时要避免副作用,因为它们会使复杂行为的推理变得困难。下次使用这个新工具时,请记住这一点。
然而,从这座摇摇欲坠的抽象之塔的顶端,拥抱系统的混乱可能是诱人的—,有时可能是值得的。总是遵循规则就是这样一个玻尔。