关于Swift的Property Wrappers

由donnywals于2020年6月8日发布
属性包装器是Swift 5.1中引入的一项功能,它们在SwiftUI和Combine中发挥了巨大作用,这是iOS 13中与Swift 5.1一起提供的两个框架。社区很快创建了一些有用的示例,这些示例很快就被人们所接受。 

作为属性包装器的用户,您不必担心它们的确切含义或工作方式。 您只需要知道如何使用它们即可。 但是,如果您好奇属性包装器如何在内部工作,这就是适合您的文章。

我想深入研究Property Wrappers,并带您探索它们的工作原理。Property Wrappers的应用示例,可以参考《SwiftUI从入门到实战》第10到17节:

10. 如何使用@Binding绑定包装关闭模态窗口  11. 如何使用@ObservedObject监听实例对象一  12. 如何使用@ObservedObject监听实例对象二  13. 如何使用@StateObject实现简单的购物车功能  14. 使用@EnvironmentObject进行页面间的数据传递  15. 使用@Environment访问环境中的指定key的值 16. 使用@AppStorage将属性的值同步到UserDefaults 17. 使用@SceneStorage存储各个场景的状态

 

Swift的发展建议

至少可以说,Swift中关于属性包装器的提案是一个有争议的提案。 此功能最初是由“property delegates”创造的,但在最初的审核中未能实现。 它对最初的提案进行了三处修订,以使属性包装器得到社区的接受。

最终,社区同意了第四版属性包装器提案。

在提出属性包装器之前,乔·格罗夫(Joe Groff)提出了一项名为``Property Behaviors''的功能。 这发生在2016年,即引入属性包装器的三年之前。

我们可以从中得出的结论是,属性包装器是一个已经存在很长时间的功能,并且基于最初建议的长度和透彻性,我认为可以肯定地说,属性包装器的设计已经花费了很多年。 在最终提案被Swift 5.1接受并实施之前,它逐渐成熟。

为什么我们需要属性包装器?

如果您还没有阅读Swift进化建议书,就去看属性包装器,您可能会想知道为什么我们甚至需要它们。属性包装器只是使Swift看起来像Java,而这显然是不可取的(我在开玩笑,Java是一种很好的语言)。

Swift团队想要向Swift语言添加属性包装器的原因,是为了帮助促进始终应用于属性的通用模式。如果您曾经将某个属性标记为惰性,则可以使用这种模式。 Swift编译器知道如何处理延迟,并且将延迟关键字扩展,使您的属性变为惰性的代码所需的所有代码,都被硬编码到编译器中。

由于可以将更多这些模式应用于属性,因此对所有这些模式进行硬编码是没有意义的。特别是因为属性包装器的目标之一,是允许开发人员以属性包装器的形式提供自己的模式。

让我们聚焦在lazy的关键字。我刚刚提到过,编译器会将您的代码扩展为使您的属性变得lazy的代码。让我们看一下如果没有lazy关键字,而我们不得不自己编写一个lazy属性,那会是什么样子。

这个例子直接取自Swift的开发建议,并为可读性做了一些修改:

struct MyObject {
  private var _myProperty: Int?

  var myProperty: Int {
    get {
      if let value = _myProperty { return value }
      let initialValue = 1738
      _myProperty = initialValue
      return initialValue
    }
    set {
      _myProperty = newValue
    }
  }
}

请注意,此代码比仅编写lazy var myProperty:Int?更冗长。

在编译器中捕获大量这些模式是不理想的,并且它也不是很可扩展。 Swift团队希望允许开发人员可以使用关键字来定义自己的类似于lazy的模式,以帮助他们清理代码并使其代码更具表现力。

请注意,属性包装器不允许开发人员执行其他不可能的事情。 它们仅允许开发人员使用更具表现力的语法来表达模式和意图。 让我们继续看一个例子。

分拆属性包装器

我经常使用的属性包装器是Combine的@Published属性包装器。 此属性包装器将应用到的属性转换为发布者,该发布者将在您更改该属性的值时通知订阅者。 此属性包装器的用法如下:

class MyObject {
  @Published var myProperty = 10
}

很简单,对不对?

要访问创建的发布者,我需要在myProperty上使用$前缀:$ myProperty。 在这种情况下,myProperty属性指向基础值,该基础值是默认值为10的Int类型。 还有一个第二个前缀可以应用于属性包装器,即_,因此是_myProperty。 这是一个私有属性,因此在这种情况下只能从MyObject内部访问,但它告诉我们很多有关属性包装器如何工作的信息。 在上面的MyObject示例中,_myProperty是发布的<Int>。 $ myProperty是已发布的发布者,而myProperty是一个Int。 因此,单行代码会导致我们可以访问三种不同的属性。 让我们定义一个自定义属性包装器,找出这三个属性分别是什么,以及它的作用。

@propertyWrapper
struct ExampleWrapper<Value> {
  var wrappedValue: Value
}

此属性包装器非常小,根本没有用。 但是,对于我们来说探查属性包装器的结构就足够了。

首先,请注意,ExampleWrapper结构在其定义之前的行上有一个注释:@propertyWrapper。 此注释意味着在它之后定义的结构是属性包装器。 还要注意,ExampleWrapper是通用的。 此值是包装值属性的类型。

属性包装器不必是通用的。 您可以根据需要对wrappedValue的类型进行硬编码。 如果您希望属性包装仅适用于特定类型,则可以对wrappedValue进行硬编码。 或者,如果需要,您可以限制Value的类型。

属性包装器需要wrappedValue属性。 所有属性包装器都必须具有称为wrappedValue的非静态属性。

让我们将此ExampleWrapper发挥作用:

class MyObject {
  @ExampleWrapper var myProperty = 10

  func allVariations() {
    print(myProperty)
    //print($myProperty)
    print(_myProperty)
  }
}

let object = MyObject()
object.allVariations()

请注意,我已注释掉$ myProperty。 一会儿我将解释原因。

运行此代码时,您会在Xcode的控制台中看到以下内容:

10
ExampleWrapper<Int>(wrappedValue: 10)

myProperty仍显示为10.直接访问用属性包装器标记的属性,将打印该属性包装器的wrappedValue属性。 当打印_myProperty时,您将访问属性包装器对象本身。 请注意,_myProperty是MyObject的成员。 您可以键入self._myProperty,即使您从未明确定义_myProperty,Swift也会知道该怎么做。 我之前提到过_myProperty是私有的,因此您不能从MyObject外部访问它,但是它在那里。

原因是Swift编译器将@ExampleWrapper var myProperty = 10在幕后进行了转换:

private var _myProperty: ExampleWrapper<Int> = ExampleWrapper<Int>(wrappedValue: 10)

var myProperty: Int {
  get { return _myProperty.wrappedValue }
  set { _myProperty.wrappedValue = newValue }
}

我们可以从此示例中学到两件事。首先,您可以看到属性包装器确实不是魔术。它们实际上相对简单。这并没有使它们变得简单或容易,但是一旦您知道将来自单个定义的转换分解为两个单独的定义,则突然变得很容易推论。

_myProperty不是某种神奇的价值。它是由编译器创建的MyObject的真正成员。并且myProperty返回wrappedValue的值,因为它是以这种方式进行硬编码的。不是我们的,而是编译器的。

_myProperty属性称为综合存储属性。这是为包装的值提供存储的属性包装器所在的位置。

那么$ myProperty在哪里?

并非所有的属性包装器都带有$前缀。属性包装的属性的$前缀版本称为预计值。映射值可用于为特定的属性包装器提供特殊的或不同的接口,例如@Published。要将映射值添加到属性包装器,必须在属性包装器定义上实现一个projectedValue属性。

在MyExampleWrapper中,如下所示:

@propertyWrapper
struct ExampleWrapper<Value> {
  var wrappedValue: Value

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

这个例子根本没有用,我将在下一节中向您展示一个更有用的例子。 现在,我想向您展示一个属性包装器的剖析。

如果像以前一样使用此属性包装器,Swift将为您生成以下代码:

private var _myProperty: ExampleWrapper<Int> = ExampleWrapper<Int>(wrappedValue: 10)

var myProperty: Int {
  get { return _myProperty.wrappedValue }
  set { _myProperty.wrappedValue = newValue }
}

var $myProperty: Int {
  get { return _myProperty.projectedValue }
  set { _myProperty.projectedValue = newValue }
}

创建了一个额外的属性,该属性使用私有_myProperty的projectedValue进行获取和设置实现。

由于_myProperty是私有的,因此您的预测值可能会提供对属性包装器的直接访问权限,这是原始属性包装器建议中显示的示例之一。 或者,您可以将完全不同的对象作为属性包装器的投影值公开。@Published属性包装器使用其projectedValue公开发布者。

实施属性包装器

我已经向您展示了如何定义一个简单的属性包装器,但是老实说。 这个例子很无聊,有点不好。 在本节中,我们将研究实现一个自定义属性包装器,该包装器将模仿Combine的@Published属性包装器的行为。

让我们先定义一个基础:

@propertyWrapper
struct DWPublished<Value> {
  var wrappedValue: Value
}

这定义了包装的属性,该属性包装了任何类型的值。 那挺好的。 此处的目标是实现公开某种发布者的预计价值。 我将为此使用CurrentValueSubject。 每当wrappedValue获得一个新值时,CurrentValueSubject应该向其订阅者发出一个新值。 一个基本的实现可能看起来像这样:

@propertyWrapper
class DWPublished<Value> {
  var wrappedValue: Value {
    get { subject.value }
    set { subject.value = newValue }
  }

  private let subject: CurrentValueSubject<Value, Never>

  var projectedValue: CurrentValueSubject<Value, Never> {
    get { subject }
  }

  init(wrappedValue: Value) {
    self.subject = CurrentValueSubject(wrappedValue)
  }
}

警告:
此实现是非常基本的,不应用作实际实现@Published的参考。 我确定此代码可能存在错误。 我的目标是帮助您了解属性包装器的工作方式。 并不是要向您展示一个完美的自定义@Published属性包装器。

该代码与您之前看到的代码有很大的不同。 包装的值使用私有对象来实现其获取和设置。 这意味着包装的值始终与对象的当前值同步。

仅指定了projectedValue。 我们不希望该属性包装器的用户将任何东西分配给projectedValue; 它是只读的。

当属性包装器以其最简单的形式初始化时,它会接收其包装的值。 传递给DWPublished的包装值用于设置主题,并将我们应该包装的值作为其初始值。

使用此属性包装器将如下所示:

class MyObject {
  @DWPublished var myValue = 1
}

let obj = MyObject()
obj.$myValue.sink(receiveValue: { int in
  print("received int: \(int)")
})

obj.myValue = 2

The printed output for this example would be:

received int: 1
received int: 2

很整洁吧?

由于属性包装器的预计值是CurrentValueSubject,因此它具有我们可以为其分配值的值属性。 如果这样做,属性包装器的wrappedValue也会被更新,因为CurrentValueSubject用于驱动属性包装器的wrappedValue。

obj.$myValue.value = 3
print(obj.myValue) // 3

包裹@Published属性是不可能做到这一点的,因为Apple公开了@Published的projectedValue作为自定义类型Publisher.Publisher而不是CurrentValueSubject。

更复杂的属性包装器可能需要某种配置,例如最大值或最小值。 假设我想扩展我的@DWPublished属性包装器以通过反跳来限制其输出。 我想在MyObject中编写以下代码进行配置:

class MyObject {
  @DWPublished(debounce: 0.3) var myValue = 1
}

这将以300毫秒的速度消除我发布的值。 我们可以将DWPublished的初始化程序更新为接受此参数,然后重构一下代码:

@propertyWrapper
class DWPublished<Value> {
  var wrappedValue: Value {
    get { subject.value }
    set { subject.value = newValue }
  }

  private let subject: CurrentValueSubject<Value, Never>
  private let publisher: AnyPublisher<Value, Never>

  var projectedValue: AnyPublisher<Value, Never> {
    get { publisher }
  }

  init(wrappedValue: Value, debounce: DispatchQueue.SchedulerTimeType.Stride) {
    self.subject = CurrentValueSubject(wrappedValue)
    self.publisher = self.subject
      .debounce(for: debounce, scheduler: DispatchQueue.global())
      .eraseToAnyPublisher()
  }
}

我的属性包装器的初始化程序现在接受去抖动间隔,并使用该间隔创建一个全新的发布者来对我的CurrentValueSubject进行去抖动。 我将此发布者擦除为AnyPublisher,所以我有一种适合我的发布者的类型,而不是Publishers.Debounce <CurrentValueSubject <Value,Never>,S>,其中S:调度程序,如果我不擦除它,则为发布者的类型。

我的属性包装器的wrappedValue仍然是shadowssubject.value。 现在,projectedValue使用去抖动的发布者而不是CurrentValueSubject来获取它。

现在使用此属性包装器如下所示:

class MyObject {
  @DWPublished(debounce: 0.3) var myValue = 1
}

var cancellables = Set<AnyCancellable>()

let obj = MyObject()
obj.$myValue
  .sink(receiveValue: { int in
    print("received int: \(int)")
  })
  .store(in: &cancellables)

obj.myValue = 2
obj.myValue = 3
obj.myValue = 4

如果要在playground上运行,则只能打印int:4。

请注意,由于属性包装器的预计值现在是AnyPublisher,因此不再像以前那样可以使用$ myValue.value来分配新值。

总结

在本周的帖子中,您了解了属性包装器如何在内部工作,以及使用属性包装器时会发生什么。 我向您展示了Swift编译器代表您生成代码,并且属性包装器离magic还很远。 Swift会生成一个_前缀的私有属性,它是您的属性包装器的一个实例,一个$前缀的属性掩盖了私有属性的projectedValue属性,而原始属性则掩盖了_前缀的私有属性的wrappedValue属性。

了解这些内容后,您可以快速查看属性包装器的工作方式以及如何实现它们。 我通过实现自己的Combine的@Published属性包装器版本来证明了这一点。 之后,我向您展示了如何通过扩展属性包装器的初始化程序来创建可以使用额外参数配置的属性包装器。

译自:https://www.donnywals.com/category/combine/page/3/

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值