Swift - 如何创建类约束的属性包装器?



我正在尝试编写一个类约束的属性包装器,就像Combine提供@Published一样。 例如:

struct Person {
// error: 'wrappedValue' is unavailable: @Published is only available on properties of classes
@Published var age = 0 
}

我无法弄清楚如何自己实现这种行为。 查看接口的Published,没有语法表明属性包装器是类约束的:

@propertyWrapper public struct Published<Value> {
public init(wrappedValue: Value)
public init(initialValue: Value)
public struct Publisher : Publisher {
// ...
}
public var projectedValue: Published<Value>.Publisher { mutating get set }
}

不知何故,只有在AnyObject中定义属性包装器时,wrappedValue才可用。 由于错误消息,看起来这以某种方式利用@available来推断定义它的上下文,然后wrappedValue有条件地不可用(如果是这样)。 请注意错误消息的措辞:

@available(*, unavailable, message: "This is a test")
func foo() {}
foo() // error: 'foo()' is unavailable: This is a test

是否可以自己实现此行为?

// How to constrain to only use in classes?
@propertyWrapper
struct MyClassWrapper<Value> {
var state: Value
var wrappedValue: Value {
get {
state
}
set {
state = newValue
}
}
}

从我所看到的行为来看,我认为它要么简单地标记为这样unavailable

@available(*, unavailable, message: "@Published is only available on properties of classes")
var wrappedValue: Value

这是因为您无法从任何地方访问Published.wrappedValue。例如,即使这样也会给出相同的"不可用"错误:

class Bar: ObservableObject {
@Published var x = ""

func f() {
// 'wrappedValue' is unavailable: @Published is only available on properties of classes
print(_x.wrappedValue) 
}
}

显然,该错误在这里没有多大意义,因此可能是wrappedValue本身被标记为不可用。

用属性包装器标记的属性通常降低为包装类型的计算属性和包装类型的存储属性,例如

@Published var x = "foo"

降低到:

var x: String {
get { _x.wrappedValue }
set { _x.wrappedValue = newValue }
}
private var _x = Published(wrappedValue: "foo")

(来源:SE-0258)

当您尝试在结构中使用@Published时,就会发生这种情况,正如您所看到的,它使用wrappedValue,这会产生错误。但是,当您在类中使用@Published时,我怀疑会发生一些编译器魔术,并以不同的方式降低它,这不涉及wrappedValue

编译器需要为类做这个特殊的事情,因为类应该继承ObservableObject,并且需要一些编译器的魔法,以便objectWillChange的默认实现可以找到所有@Published属性。

在任何情况下,也可以编写一个不受类约束的Published实现,但当然,如果您选择这样做,则不会获得自动生成的objectWillChange实现。这是根据此处改编的示例。

@propertyWrapper
struct Published<T> {
private var innerSubject = PassthroughSubject<T, Never>()

init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}

var wrappedValue: T {
didSet {
innerSubject.send(wrappedValue)
}
}
var projectedValue: AnyPublisher<T, Never> {
innerSubject.eraseToAnyPublisher()
}
}

这是对原始问题的完整答案,并且使用的实现与Apple在其原始@Published中使用的实现相同。

@propertyWrapper
public final class Published<Value> {

/// This is here only to allow to have an init() not requiring any parameters
/// It's not necessary if you don't need that functionality. In that case the publisher below can be created in the init...
private var initialValue: Value?

private lazy var publisher = CurrentValueSubject<Value, Never>(initialValue!)

/// Published Value
@available(*, unavailable, message: "@Published is only available on properties of classes")
public var wrappedValue: Value {
get {
publisher.value
}
set {
if initialValue == nil {
initialValue = newValue
}

publisher.send(newValue)
}
}

/// Subscript to allow classes to access the wrappedValue
public static subscript<EnclosingContainer: AnyObject>(
_enclosingInstance object: EnclosingContainer,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingContainer, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingContainer, Published<Value>>
) -> Value {
get {
object[keyPath: storageKeyPath].publisher.value
}
set {
let object = object[keyPath: storageKeyPath]

if object.initialValue == nil {
object.initialValue = newValue
}

object.publisher.send(newValue)
}
}

/// This is use when declaring like this: @Published var blabla: Bool
/// in this case, the default value of blabla needs to be set in the init of the classe
/// init() {
///     blabla = true
/// }
public init() { }

/// This is use when declaring like this: @Published var blabla = true
public init(wrappedValue: Value) {
initialValue = wrappedValue
}
}
// MARK: How not to use...
/// Failling as expected because it's not a class
struct Failling {
/// 'wrappedValue' is unavailable: @Published is only available on properties of classes
@Published var errorHere: Bool
}
// MARK: How to use...
class Working {
@Published var workingHere: Bool

init(_ workingHere: Bool) {
self.workingHere = workingHere
}
}
class WorkingToo {
@Published var workingHere = true

/// only here to show this is empty...
init() {}
}

@Pbulished使用了下划线前缀的语言功能,您可以从此处了解更多信息。

您可以创建一个类约束的属性包装器,而不使用下划线前缀的语言功能,如下所示:

@propertyWrapper
struct ClassWrapper<T, Value> where T: AnyObject {
private var state: Value

init(wrappedValue: Value) {
state = wrappedValue
}

var wrappedValue: Value {
get { state }
set { state = newValue }
}
}
protocol ClassWrappedProtocol: AnyObject {
typealias ClassWrapped<T> = ClassWrapper<Self, T>
}
class YourClass: ClassWrappedProtocol {
@ClassWrapped var value = 0
}

最新更新