一个Swift属性包装器能否引用其封装的属性的所有者?

29
在Swift的属性包装器内部,你是否可以引用拥有被包装属性的类或结构体的实例?使用self显然不起作用,super也不行。我尝试将self传递给属性包装器的init(),但这也不起作用,因为当评估@propertywrapper时,Configuration上的self还未定义。我的使用场景是在管理大量设置或配置的类中。如果更改任何属性,我只想通知感兴趣的方面某些事情已经改变。他们并不需要知道哪个值刚刚发生了变化,所以不需要为每个属性使用像KVOPublisher这样的东西。属性包装器看起来很理想,但我无法弄清楚如何传递某种引用到包装器可以回调到的拥有实例。 参考:SE-0258
enum PropertyIdentifier {
  case backgroundColor
  case textColor
}

@propertyWrapper
struct Recorded<T> {
  let identifier:PropertyIdentifier
  var _value: T

  init(_ identifier:PropertyIdentifier, defaultValue: T) {
    self.identifier = identifier
    self._value = defaultValue
  }

  var value: T {
    get {  _value }
    set {
      _value = newValue

      // How to callback to Configuration.propertyWasSet()?
      //
      // [self/super/...].propertyWasSet(identifier)
    }
  }
}

struct Configuration {

  @Recorded(.backgroundColor, defaultValue:NSColor.white)
  var backgroundColor:NSColor

  @Recorded(.textColor, defaultValue:NSColor.black)
  var textColor:NSColor

  func propertyWasSet(_ identifier:PropertyIdentifier) {
    // Do something...
  }
}

对于您描述的用例,我会觉得使用didSet属性观察器更简单。如果您需要使用Recorded包装器注释1000个属性并且必须调整,则可以剪切和粘贴didSet { self.propertyWasSet(.textColor) } - 如果适合您,甚至可以考虑放弃PropertyIdentifier并改用KeyPath - ctietze
我希望避免复制/粘贴,因为最终的属性包装器将包含其他逻辑,例如如果newValue与oldValue相同,则不通知观察者,以及对属性执行一些清理和验证。现有的Objective-C实现使用构建脚本自动生成.m实现,但我希望有一个更Swift的解决方案。 - kennyc
那么我仍然会使用didSet属性观察器:将差分添加到您的辅助函数中,并使用propertyWasSet(.textColor,oldValue,textColor)调用它来执行其操作。这是一种有点有状态的操作。有些人已经将差分部分称为视图模型;而Configuration订阅自己的更改事实使其成为一种反应绑定情况。您可以将此知识提升到包装该属性的类型中,例如Binding<NSColor,Configuration>并将self传递给其中。 - ctietze
1
看一下2014年的一个简单的Swift方法:http://rasic.info/bindings-generics-swift-and-mvvm/ -- 另外,也许Sourcery或SwiftGen可以帮助实际代码生成 :) 我个人偏好将状态与事件中心分开,例如在所有属性上使用KVO或类似的东西,但然后不将任何细节转发给实际的订阅者。 - ctietze
1
我可以理解,在上面的非常基本的例子中,可能有更好的设计模式适用,但这并没有真正解决核心问题,即属性包装器是否可以访问封装属性的实例。在同一模型内,有许多情况下一个属性的设置可能取决于其他属性的值。如果该模式在代码库中足够频繁,则应将其分解为某种可重用组件。属性包装器可能非常适合此操作,这就是我试图弄清楚的内容。 - kennyc
好的。至少我确保你不仅仅是在寻找潜在问题的解决方案,而是真正在寻找这个具体的实现问题 :) - ctietze
4个回答

13

答案是否定的,目前的规范无法实现。

我曾希望做类似的事情。我能想到的最好方法是在init(...)的结尾处使用反射。至少这样,您可以为您的类型添加注释,并且只需要在init()中添加一个函数调用。


fileprivate protocol BindableObjectPropertySettable {
    var didSet: () -> Void { get set }
}

@propertyDelegate
class BindableObjectProperty<T>: BindableObjectPropertySettable {
    var value: T {
        didSet {
            self.didSet()
        }
    }
    var didSet: () -> Void = { }
    init(initialValue: T) {
        self.value = initialValue
    }
}

extension BindableObject {
    // Call this at the end of init() after calling super
    func bindProperties(_ didSet: @escaping () -> Void) {
        let mirror = Mirror(reflecting: self)
        for child in mirror.children {
            if var child = child.value as? BindableObjectPropertySettable {
                child.didSet = didSet
            }
        }
    }
}

7

目前无法直接实现这个功能。

不过,您参考的提案在最新版本中探讨了这个方向: https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type

现在,您可以使用 projectedValueself 分配给一个变量。 然后,在设置 wrappedValue 后,您可以使用它来触发某些操作。

例如:

import Foundation

@propertyWrapper
class Wrapper {
    let name : String
    var value = 0
    weak var owner : Owner?

    init(_ name: String) {
        self.name = name
    }

    var wrappedValue : Int {
        get { value }
        set {
            value = 0
            owner?.wrapperDidSet(name: name)
        }
    }

    var projectedValue : Wrapper {
        self
    }
}


class Owner {
    @Wrapper("a") var a : Int
    @Wrapper("b") var b : Int

    init() {
        $a.owner = self
        $b.owner = self
    }

    func wrapperDidSet(name: String) {
        print("WrapperDidSet(\(name))")
    }
}

var owner = Owner()
owner.a = 4 // Prints: WrapperDidSet(a)

4
答案是肯定的!请参考此答案
使用UserDefaults包装器调用ObservableObject发布者的示例代码:
import Combine
import Foundation

class LocalSettings: ObservableObject {
  static var shared = LocalSettings()

  @Setting(key: "TabSelection")
  var tabSelection: Int = 0
}

@propertyWrapper
struct Setting<T> {
  private let key: String
  private let defaultValue: T

  init(wrappedValue value: T, key: String) {
    self.key = key
    self.defaultValue = value
  }

  var wrappedValue: T {
    get {
      UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
    }
    set {
      UserDefaults.standard.set(newValue, forKey: key)
    }
  }

  public static subscript<EnclosingSelf: ObservableObject>(
    _enclosingInstance object: EnclosingSelf,
    wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, T>,
    storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Setting<T>>
  ) -> T {
    get {
      return object[keyPath: storageKeyPath].wrappedValue
    }
    set {
      (object.objectWillChange as? ObservableObjectPublisher)?.send()
      UserDefaults.standard.set(newValue, forKey: object[keyPath: storageKeyPath].key)
    }
  }
}

4

基于我的实验:https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type

该链接讨论了在Swift语言中引用包装类型中的封闭self。
protocol Observer: AnyObject {
    func observableValueDidChange<T>(newValue: T)
}

@propertyWrapper
public struct Observable<T: Equatable> {
    public var stored: T
    weak var observer: Observer?

    init(wrappedValue: T, observer: Observer?) {
        self.stored = wrappedValue
    }

    public var wrappedValue: T {
        get { return stored }
        set {
            if newValue != stored {
                observer?.observableValueDidChange(newValue: newValue)
            }
            stored = newValue
        }
    }
}

class testClass: Observer {
    @Observable(observer: nil) var some: Int = 2

    func observableValueDidChange<T>(newValue: T) {
        print("lol")
    }

    init(){
        _some.observer = self
    }
}

let a = testClass()

a.some = 4
a.some = 6

Robert,这真是太棒了!在我的情况下,我需要同时使用委托来进行获取和设置,因此如果委托为nil,我将使用fatalError()来捕获添加新属性但init未设置委托的时间。太棒了,真的太棒了!!! - David H

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接