7

抽象属性的行为 —— Property Delegates

 3 years ago
source link: https://kemchenj.github.io/2019-04-14/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

抽象属性的行为 —— Property Delegates

2019-04-142020-12-14 3.6k

之前 Joe 跟 Doug 提了一个草案,提议加入一个叫做 Property Delegate 的提案,昨天作为一份预备提案发了出来,我看了之后觉得特别兴奋,所以写了这篇文章跟大家分享一下。

我们在 Swift 定义属性的时候,有时候会需要它表现出更复杂的行为,例如线程安全(例子来源于 Objc.io 的这篇文章):

private let queue: DispatchQueue = ...
private var _count: Int = 3
    
var count: Int {
  get { return queue.sync { _count } }
  set { queue.sync { self._count = newValue } }
}

但每次都需要写这么长的代码显得很多余,所以这次提案提议增加一个名为 Property Delegates 的功能来简化这些属性的声明,这个名字翻译过来就是属性代理

有了这个功能之后,想要一个线程安全的属性时只要加上一个注解 @Atomic 即可:

@Atomic var count: Int = 3

编译器会自动将属性的声明展开成下面这样:

var $count = Atomic<Int>(initialValue: 3)
var count: Int {
  get { return $count.value }
  set { $count.value = newValue }
}

$count 其实就是 count 的属性代理,一切 get / set 行为都会通过 $count 去实现,$ 是属性代理特有的前缀,我们只要在属性名前面加上就可以访问到实际的属性代理:

counter.count  // 类型是 Int
counter.$count // 类型是 Atomic<Int>

Atomic 其实是一个具体的类型,声明属性代理的方式很简单,只要满足下面的要求即可:

  1. 声明类型时必须用 @propertyDelegate 标注
  2. 必须有一个可读写 value 属性

这里我简单实现了一个 Atomic 类型:

@propertyDelegate
class Atomic<Value> {
  private let queue = DispatchQueue(label: "Atomic serial queue")
  private var _value: Value
    
  init(_ value: Value) {
    self._value = value
  }

  var value: Value {
    get { return queue.sync { self._value } }
    set { queue.sync { self._value = newValue } }
  }
}

BTW,这个功能是借鉴 Kotlin 的,语法有微妙的区别。

除了 Atomic 之外,还有很多玩法,例如通过 UserDefaults 存取的值:

@propertyDelegate
struct UserDefaultValue<T> {
  let key: String
  
  init(key: String {
    self.key = key
  }
  
  var value: T {
    get { return UserDefaults.standard.object(forKey: key) as! T }
    set { UserDefaults.standard.set(newValue, forKey: key) }
  }
}

@UserDefaultValue(key: "count") var count: Int

甚至是 Swift 值类型使用的优化技巧 —— 写时复制:

protocol Copyable: AnyObject {
  func copy() -> Self
}

@propertyDelegate
struct CopyOnWrite<Value: Copyable> {
  init(initialValue: Value) {
    value = initialValue
  }
  
  private(set) var value: Value
  
  var storageValue: Value {
    mutating get {
      if !isKnownUniquelyReferenced(&value) {
        value = value.copy()
      }
      return value
    }
    set {
      value = newValue
    }
  }
}

@CopyOnWrite var array = NSMutableArray<Int>()

目前 Swift 对于这些属性的高级行为支持是硬编码在语言里的,例如说 lazy@NSCopying,更好的方式是将通过某种统一的功能形式来完成它们的功能,Property Delegate 就是这么一个角色。

将属性的行为使用属性代理来处理,并且让它们的声明糅合到一起能够简化代码,但也会带来很多问题,在这里我列举其中一部分出来。

原有的 lazy 没办法被完全替代

Property Delegate 只是单纯的语法糖,我们可以把它看成是一种特殊的宏,让编译器把属性的声明进行了简单的展开,但它本质上并没有改变语言运作的方式,现有代码能做到的它也能做到,现有代码做不到的它也做不到

最典型的就是前面提到的 Lazy,Property Delegate 目前的设计让它没办法完全替代现有的 lazy 声明,原因很简单 —— 它访问不到 self

lazy var count = self.previousCount
@Lazy var count = self.previousCount // 编译错误

语法如何融入到当前的设计里也是一个难点首先是访问级别,当我们声明一个延迟加载的属性时 @Lazy var count: Int = 3,我们希望 countopen 的,并且让属性代理 $count 隐藏起来,语法该怎么设计会更好?

目前提案给出的解决方案是让它们的访问级别保持一致,之后的版本可能会仿照 set 的访问级别的声明这样去处理:

@Lazy
public private(storage) var count: Int = 330

但现阶段如果有需要的话,就还是用回之前的写法(不使用 Property Delegate)。

组合属性代理

我们可能会需要将属性代理组合起来使用:

@Lazy
@Atomic
var object: NSObject = ...

直觉上我们会期望 object 既是线程安全的,又是写时复制的,当问题在于当前设计里的属性代理,代理处理的不只是 setter 和 getter,还有属性的存储,这也就导致了属性代理无法组合到一起,但前面提到 Property Delegate 只是单纯的语法糖,如果把这段代码展开,那它可能会是这样的:

var object: NSObject {
  get { return $object.value }
  set { $object.value = newValue }
}

var $object: Atomic<NSObject> {
  get { return $$object.value }
  set { $$object.value = value }
}

var $$object: Lazy<Atomic<NSObject>>= ... 

官方的解释是,这样的写法虽然在大部分情况下能够表现出我们期望的行为,但实际的语句含义与我们表达的并不相符,并且部分情况下可能会产生预期之外的行为,所以目前只支持单个属性代理。

BTW,这一部分官方其实写的比较含糊,我自己也没太理解什么情况下会出问题。

其它一些细枝末节的东西也挺多的,例如是否应该使用 $ 作为标示等,有人提议使用下划线,这样会更加符合现有代码库的做法,把 $ 这个标示留下来给以后的功能使用。

还有什么命名一致性的问题,目前的注解有大写驼峰,也有小写驼峰,并且也没有一个统一的规则,虽然这不在这个提案的讨论范围内,但还是需要给之后的规则保留足够的制定空间。

增强属性的声明其实早在 SE-0030 提案里就有了,但因为底层设计没有稳定,而且优先级不高,所以这个提案虽然通过了,但一直没有实现,现在里面的设计已经不太符合现在的 Swift 了。

目前 Property Delegate 的提案还没有正式提交,正在讨论阶段,但其实功能本身的思路和优势是很清晰的,我个人认为这种功能肯定会被引入,讨论的重点主要在于语法的形式,如何与现有的语法结合的更好,如何适应之后自定义注解功能的加入。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK