SwiftUI 和 Swift 5.1 新特性(2) 属性代理Property Delegates

5,226 阅读2分钟

SwiftUI 带来的 Swift 5.1 的新特性比框架本身更重要。我们可以预见到,这些新的语言特性很快会被各个库作者所使用。在上一篇中,我们解释了 SwiftUI 代码中 some Viewsome 是什么以及它为何很重要 SwiftUI 和 Swift 5.1 新特性(1) 不透明返回类型 Opaque Result Type。在这篇中,我们需要一起学习下 Swift UI 中 @State@Binding 的准备知识, 这种标记的本质是属性代理(Property Delegates),也叫属性包装器(Property Wrappers)。代码如下:

struct OrderForm : View {
  @State private var order: Order
  
  var body: some View {
    Stepper(value: $order.quantity, in: 1...10) {
      Text("Quantity: \(order.quantity)")
    }
  }
}

这个语言特性非常通用,任何对于属性的存取有“套路”的访问,都可以用它来包装这种“套路”。我们先来学习一下几个套路。

1. 包装懒初始化逻辑

为了实现属性 text 为懒初始化的属性,我们可以写成如下代码:

public struct MyType {
  var textStorage: String? = nil
  
  public var text: String {
    get {
      guard let value = textStorage else {
        fatalError("text has not yet been set!")
      }
      return value
    }
    
    set {
      textStorage = newValue
    }
  }
}

然而如果有很多属性都是这样的逻辑,这样的写法是很冗余的。所以属性代理就是解决这个问题的:

@propertyDelegate
public struct LateInitialized<Value> {
  private var storage: Value?
  
  public init() {
    storage = nil
  }
  
  public var value: Value {
    get{
      guard let value = storage else {
        fatalError("value has not yet been set!")
      }
      return value
    }
    set {
      storage = newValue
    }
  }
}

// 应用属性代理 LateInitialized
public struct MyType {
  @LateInitialized public var text: String?
}

属性代理 LateInitialized 是一个泛型类型,它本身用 @propertyDelegate 修饰,它必须有一个叫 value 的属性类型为 Value,有了这些约定后,编译器可以为 MyTypetext 生成以下代码:

public struct MyType {
  var $text: LateInitialized<String> = LateInitialized<String>()

  public var text: String {
      get { $text.value }
      set { $text.value = newValue}
  }
}

可以看到,经过属性代理包装过后的 text,编译器帮助生成了一个存储属性为 $text,类型就是这个属性代理,而 text本身变成了一个计算属性。大家可能觉得 $text属性是编译器生成的,所以不可以访问,事实恰恰相反,text$text 都可以用。

2. 包装防御性拷贝

我们再来看一下一个防御性拷贝的例子,它基于 NSCopying

@propertyDelegate
public struct DefensiveCopying<Value: NSCopying> {
  private var storage: Value
  
  public init(initialValue value: Value) {
    storage = value.copy() as! Value
  }
  
  public var value: Value {
    get { storage }
    set {
      storage = newValue.copy() as! Value
    }
  }
}

// 应用属性代理 DefensiveCopying
public struct MyType {
  @DefensiveCopying public var path: UIBezierPath = UIBezierPath()
}

属性代理 DefensiveCopying 的不同点在于它的初始化函数 init(initialValue:),这个函数由于编译器的约定,所以一定得叫这个名字。与上个例子一样,编译器会生成存储属性 $path,并用初始值初始化。

这里我们吹毛求疵一下,UIBezierPath 被强制拷贝了一次,所以我们再提供一个属性代理的初始化函数,并应用它:

// DefensiveCopying 中增加
  public init(withoutCopying value: Value) {
    storage = value
  }
  
// 应用不拷贝的初始化函数
public struct MyType {
  @DefensiveCopying public var path: UIBezierPath
  
  init() {
    $path = DefensiveCopying(withoutCopying: UIBezierPath())
  }
}

在应用的部分我们看到可以像初始化一个一般变量一样初始化$path,这也印证了我们之前说的$pathpath的本质。但是这样的语法毕竟有点难看,在不需要$path 出现的时候应该尽可能隐藏它:

public struct MyType {
  @DefensiveCopying(withoutCopying: UIBezierPath())
  public var path: UIBezierPath
}

3. 包装 UserDefaults 的存取

我们经常需要将属性写成针对UserDefaults存取的计算属性,而这个通用访问策略也能用属性代理实现:

@propertyDelegate
struct UserDefault<T> {
  let key: String
  let defaultValue: T
  
  var value: T {
    get {
      return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
    }
    set {
      UserDefaults.standard.set(newValue, forKey: key)
    }
  }
}

// 应用属性代理 UserDefault
enum GlobalSettings {
  @UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
  static var isFooFeatureEnabled: Bool
  
  @UserDefault(key: "BAR_FEATURE_ENABLED", defaultValue: false)
  static var isBarFeatureEnabled: Bool
}

结语

所有对于属性访问策略的抽象,都可以使用属性代理来实现,我们还可以想到 Thread-local storage(线程本地存储)属性存取、原子属性存取、Copy-on-write 属性存取、引用包装类型属性的存取都可以使用属性代理来实现。当然 SwiftUI 的 @State@Binding 也是属性代理,要详细解释它们,还需要一些 Swift 的知识,我们在下一篇中,给大家详细说一说。

相关文章:

SwiftUI 和 Swift 5.1 新特性(1) 不透明返回类型 Opaque Result Type

扫描下方二维码,关注“面试官小健”