Swift 5.1 @propertyWrapper-在初始化所有存储的属性之前在属性访问中使用“自我”

时间:2019-10-06 12:36:08

标签: swift swift5.1

我试图使用Swift 5.1属性包装器,但是每次我觉得有一个很酷的用例时,我最终都会遇到无法在View Model的初始化器中使用它们的问题。

以这个非常简单的示例为例。

class NoProblem {
  var foo = "ABC"
  let upperCased: String

  init(dependencies: AppDependencies) {
    self.upperCased = foo.uppercased()
  }
}
@propertyWrapper
struct Box<Value> {
  private var box: Value

  init(wrappedValue: Value) {
    box = wrappedValue
  }

  var wrappedValue: Value {
    get { box }
    set { box = newValue }
  }
}

class OhNoes {
  @Box var foo = "ABC"
  let upperCased: String

  init(dependencies: AppDependencies) {
    self.upperCased = foo.uppercased()
  }
}

NoProblem中,一切正常。但是在OhNoes中,我得到了这个错误:'self' used in property access 'foo' before all stored properties are initialized

当然,这是一个极为简化的示例,但是当对可观察属性进行@Property包装或像this article中的@Injected包装等时,我遇到相同的问题。< / p>

不,可悲的是,将其设置为普通属性也不会起作用:Property 'foo' with a wrapper cannot also be lazy


如果上面的示例过于简单,这是我碰到的一个更真实的示例:

final class ContactUsVMWorks {
  let subject = Observable<String?>("")
  let replyToEmail = Observable<String?>("")
  let message = Observable<String?>("")
  let formValid: Signal<Bool, Never>

  init() {
    let subjectValid: Signal<Bool, Never> = subject.map { $0.nilIfEmpty != nil }
    let emailValid: Signal<Bool, Never> = replyToEmail.map { $0?.isEmail() ?? false }
    let messageValid: Signal<Bool, Never> = message.map { $0.nilIfEmpty != nil }

    formValid = combineLatest(subjectValid, emailValid, messageValid) { subjectValid, emailValid, messageValid in
      subjectValid && emailValid && messageValid
    }
  }
}

@propertyWrapper
struct Property<Value> {
  private let property: ReactiveKit.Property<Value>

  init(wrappedValue: Value) {
    self.property = ReactiveKit.Property(wrappedValue)
  }

  var wrappedValue: Value {
    get { property.value }
    set { property.value = newValue }
  }

  public var projectedValue: ReactiveKit.Property<Value> {
    return property
  }
}

final class ContactUsVMBroken {
  @Property var subject: String? = ""
  @Property var replyToEmail: String? = ""
  @Property var message: String? = ""
  let formValid: Signal<Bool, Never>

  init() {
    let subjectValid: Signal<Bool, Never> = $subject.map { $0.nilIfEmpty != nil }
    let emailValid: Signal<Bool, Never> = $replyToEmail.map { $0?.isEmail() ?? false }
    let messageValid: Signal<Bool, Never> = $message.map { $0.nilIfEmpty != nil }

    formValid = combineLatest(subjectValid, emailValid, messageValid) { subjectValid, emailValid, messageValid in
      subjectValid && emailValid && messageValid
    }
  }
}

再举一个例子:

final class DependencyViewModelWorks {
  private let dependencies: AppDependencies
  let isLoggedIn: Signal<Bool, Never>

  init(dependencies: AppDependencies) {
    self.dependencies = dependencies
    isLoggedIn = dependencies.userManager.observableUser.map { $0 != nil }
  }
}

@propertyWrapper
struct Injected<Service> {
  private var service: Service?
  public var container: Resolver?
  public var name: String?
  public var wrappedValue: Service {
    mutating get {
      if service == nil {
        service = (container ?? Resolver.root).resolve(
          Service.self,
          name: name
        )
      }
      return service! // swiftlint:disable:this force_unwrapping
    }
    mutating set {
      service = newValue
    }
  }

  public var projectedValue: Injected<Service> {
    get {
      return self
    }
    mutating set {
      self = newValue
    }
  }
}

final class DependencyViewModelBroken {
  @Injected var dependencies: AppDependencies
  let isLoggedIn: Signal<Bool, Never>

  init() {
    isLoggedIn = dependencies.userManager.observableUser.map { $0 != nil }
  }
}

2 个答案:

答案 0 :(得分:1)

编辑:

实际上,更好的解决方法是直接使用_foo.wrappedValue.uppercased()而不是foo.uppercased()

这也解决了双重初始化的另一个问题。

对此进行更深入的思考,这绝对是预期的行为。

如果我理解正确,在OhNoes中,foo就是以下简称:

var foo: String {
  get {
    return self._foo.wrappedValue
  }
  set {
    self._foo.wrappedValue = newValue
  }
}

所以这不可能以任何其他方式起作用。


我正面临着与您相同的问题,我实际上认为这是某种错误/有害行为。

无论如何,我能带出去的最好的东西是:

@propertyWrapper
struct Box<Value> {
  private var box: Value

  init(wrappedValue: Value) {
    box = wrappedValue
  }

  var wrappedValue: Value {
    get { box }
    set { box = newValue }
  }
}

class OhNoes {
  @Box var foo : String
  let upperCased: String

  init() {
    let box = Box(wrappedValue: "ABC")
    _foo = box
    self.upperCased = box.wrappedValue.uppercased()
  }
}

那是相当不错的(我的意思是,它没有副作用,但是很丑)。

此解决方案的问题在于,如果您的属性包装器有一个空的初始化器init()或被包装的值为Optional,它实际上是不会起作用的(没有副作用)。

例如,如果您尝试下面的代码,您将意识到Box被初始化了两次:一次是在成员变量的定义上,一次是在OhNoes的init中,然后将替换前者。


@propertyWrapper
struct Box<Value> {
  private var box: Value?

  init(wrappedValue: Value?) { // Actually called twice in this case
    box = wrappedValue 
  }

  var wrappedValue: Value? {
    get { box }
    set { box = newValue }
  }
}

class OhNoes {
  @Box var foo : String?
  let upperCased: String?

  init() {
    let box = Box(wrappedValue: "ABC")
    _foo = box
    self.upperCased = box.wrappedValue?.uppercased()
  }
}

我认为这绝对是我们不应该拥有的东西(或者至少我们应该能够退出这种行为)。无论如何,我认为这与他们在this pitch中所说的有关:

当属性包装器类型具有无参数init()时,属性 使用该包装类型的容器将通过init()隐式初始化。

PS:您找到其他方法了吗?

答案 1 :(得分:0)

最省力的解决方法是使upperCased成为var而不是let。好的,这可能并不理想,但至少意味着您可以保留所有代码并立即使用生成的OhNoes实例:

struct AppDependencies {}
@propertyWrapper struct Box<T> {
    private var boxed: T
    init(wrappedValue: T) {
        boxed = wrappedValue
    }
    var wrappedValue: T {
        get { boxed }
        set { boxed = newValue }
    }
}
class OhNoes {
    @Box var foo = "abc"
    var upperCased: String = "" // this is the only real change
    init(dependencies: AppDependencies) {
        self.upperCased = foo.uppercased()
    }
}

如果您真的不喜欢,请按照其他答案的建议直接参考_foo.wrappedValue