Swift属性包装器

如果使用SwiftUI,您可能会注意诸如@ ObservedObject,@ EnvironmentObject,@ FetchRequest等关键字。 属性包装程序(以下称为“属性包装程序”)是Swift 5.1的新功能。 本文将帮助您了解@的所有构造的来源,以及如何在SwiftUI和项目中使用它们。



翻译:FunCorp的开发商Evgeny Zavozhansky。


注意:在准备翻译时,由于语言的更改,原始文章的部分源代码已失去其相关性,因此特意替换了一些代码示例。


属性包装器最早在2019年3月在Swift论坛上推出,也就是SwiftUI宣布发布的前几个月。 Swift Core团队的成员Douglas Gregor在他的原始建议中将这种结构(当时称为属性委托)描述为“例如,用户可访问的对诸如lazy类的语言结构当前提供的功能的概括。”


如果使用lazy关键字声明属性,则意味着将在首次访问该属性时对其进行初始化。 例如,可以使用可通过计算属性访问的私有属性来实现延迟属性初始化。 但是,使用lazy关键字会使此操作变得更加容易。


 struct Structure {    //      lazy    lazy var deferred = …    //            private var _deferred: Type?    var deferred: Type {        get {            if let value = _deferred { return value }            let initialValue = …            _deferred = initialValue            return initialValue        }        set {            _deferred = newValue        }    } } 

SE-0258:属性包装程序完美地解释了属性包装程序的设计和实现。 因此,与其尝试改善官方文档中的描述,不如考虑一些可以使用属性包装器实现的示例:


  • 财产价值的限制;
  • 更改属性时的值转换;
  • 改变平等的语义并比较属性;
  • 属性访问日志记录。

限制属性值


SE-0258:Property Wrapper提供了几个实际示例,包括@Clamping@Copying@Atomic@ThreadSpecific @Copying@Atomic@ThreadSpecific @UserDefault 。 考虑@Clamping包装器,它允许您限制属性的最大值或最小值。


 @propertyWrapper struct Clamping<Value: Comparable> {    var value: Value    let range: ClosedRange<Value>    init(initialValue value: Value, _ range: ClosedRange<Value>) {        precondition(range.contains(value))        self.value = value        self.range = range    }    var wrappedValue: Value {        get { value }        set { value = min(max(range.lowerBound, newValue), range.upperBound) }    } } 

例如,可以使用@Clamping来模拟溶液的酸度,其值可以取0到14的值。


 struct Solution {    @Clamping(0...14) var pH: Double = 7.0 } let carbonicAcid = Solution(pH: 4.68) 

尝试将pH值设置为超出(0...14)将导致该属性采用最接近最小或最大间隔的值。


 let superDuperAcid = Solution(pH: -1) superDuperAcid.pH // 0 

属性包装器可用于实现其他属性包装器。 例如, @UnitInterval包装器使用@Clamping(0...1)将属性的值限制为间隔@Clamping(0...1)


 @propertyWrapper struct UnitInterval<Value: FloatingPoint> {    @Clamping(0...1)    var wrappedValue: Value = .zero    init(initialValue value: Value) {        self.wrappedValue = value    } } 

类似的想法


  • @Positive / @NonNegative表示该值可以是正数或负数。
  • @NonZero表示该属性的值不能为0。
  • @Validated@Whitelisted / @Blacklisted将属性的值限制为某些值。

更改属性时转换值


对于应用程序开发人员而言,验证文本字段值一直是头疼的问题。 有很多事情需要跟踪:从诸如编码之类的陈词滥调到通过文本字段输入代码的恶意尝试。 考虑使用属性包装器删除用户在行的开头和结尾输入的空格。


 import Foundation let url = URL(string: " https://habrahabr.ru") // nil let date = ISO8601DateFormatter().date(from: " 2019-06-24") // nil let words = " Hello, world!".components(separatedBy: .whitespaces) words.count // 3 

Foundation提供了trimmingCharacters(in:)方法,您可以使用该方法删除行首和结尾的空格。 您可以在需要保证输入正确性时调用此方法,但这不是很方便。 您可以为此使用属性包装器。


 import Foundation @propertyWrapper struct Trimmed {    private(set) var value: String = ""    var wrappedValue: String {        get { return value }        set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }    }    init(initialValue: String) {        self.wrappedValue = initialValue    } } 

 struct Post {    @Trimmed var title: String    @Trimmed var body: String } let quine = Post(title: " Swift Property Wrappers ", body: "…") quine.title // "Swift Property Wrappers" —        quine.title = "   @propertyWrapper " // "@propertyWrapper" 

类似的想法


  • @TransformedICU转换应用于@Transformed字符串。
  • @Rounded / @Truncated或截断字符串值。

更改相等和属性比较的语义


在Swift中,如果两个字符串在规范上相等 ,则它们相等 ,即 包含相同的字符。 但是假设我们希望它们包含的字符串属性相等,不区分大小写。


@CaseInsensitive为类型StringSubString属性实现包装器。


 import Foundation @propertyWrapper struct CaseInsensitive<Value: StringProtocol> {    var wrappedValue: Value } extension CaseInsensitive: Comparable {    private func compare(_ other: CaseInsensitive) -> ComparisonResult {        wrappedValue.caseInsensitiveCompare(other.wrappedValue)    }    static func == (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {        lhs.compare(rhs) == .orderedSame    }    static func < (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {        lhs.compare(rhs) == .orderedAscending    }    static func > (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {        lhs.compare(rhs) == .orderedDescending    } } 

 let hello: String = "hello" let HELLO: String = "HELLO" hello == HELLO // false CaseInsensitive(wrappedValue: hello) == CaseInsensitive(wrappedValue: HELLO) // true 

类似的想法


  • @Approximate用于对Double或Float类型的属性进行粗略比较。
  • @Ranked其值顺序@Ranked的属性(例如,扑克牌等级)。

物业访问记录


@Versioned允许您拦截分配的值并记住设置它们的时间。


 import Foundation @propertyWrapper struct Versioned<Value> {    private var value: Value    private(set) var timestampedValues: [(Date, Value)] = []    var wrappedValue: Value {        get { value }        set {            defer { timestampedValues.append((Date(), value)) }            value = newValue        }    }    init(initialValue value: Value) {        self.wrappedValue = value    } } 

ExpenseReport类使ExpenseReport可以保存费用报表处理状态的时间戳。


 class ExpenseReport {    enum State { case submitted, received, approved, denied }    @Versioned var state: State = .submitted } 

但是上面的示例说明了在当前属性包装器实现中的一个严重限制,这是从Swift限制中得出的:属性不能引发异常。 如果我们想在@Versioned上添加一个限制,以防止该值在使用.approved值后更改为.denied ,则最好的选择是fatalError() ,它不适合实际应用程序。


 class ExpenseReport {    @Versioned var state: State = .submitted {        willSet {            if newValue == .approved,                $state.timestampedValues.map { $0.1 }.contains(.denied)            {                fatalError("")            }        }    } } var tripExpenses = ExpenseReport() tripExpenses.state = .denied tripExpenses.state = .approved // Fatal error: «»   . 

类似的想法


  • @Audited@Audited属性访问。
  • @UserDefault封装了在UserDefaults读取和保存数据的UserDefaults

局限性


属性不能引发异常


如前所述,属性包装器只能使用几种方法来处理无效值:


  • 忽略他们;
  • 使用fatalError()终止应用程序。

包装的属性不能用`typealias`属性标记


上面的@UnitInterval示例的属性受时间间隔(0...1) ,不能声明为


 typealias UnitInterval = Clamping(0...1) 

限制使用多个属性包装器的组合


组成属性包装器不是可交换的操作:声明的顺序将影响行为。 考虑一个示例,其中将slug属性(即博客文章的网址)标准化。 在这种情况下,归一化的结果将取决于在删除空格之前或之后,何时用破折号替换空格。 因此,目前不支持多个属性包装器的组合。


 @propertyWrapper struct Dasherized {    private(set) var value: String = ""    var wrappedValue: String {        get { value }        set { value = newValue.replacingOccurrences(of: " ", with: "-") }    }    init(initialValue: String) {        self.wrappedValue = initialValue    } } struct Post {    …    @Dasherized @Trimmed var slug: String // error: multiple property wrappers are not supported } 

但是,可以通过使用嵌套属性包装程序来规避此限制。


 @propertyWrapper struct TrimmedAndDasherized {    @Dasherized    private(set) var value: String = ""    var wrappedValue: String {        get { value }        set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }    }    init(initialValue: String) {        self.wrappedValue = initialValue    } } struct Post {    …    @TrimmedAndDasherized var slug: String } 

其他属性包装器限制


  • 不能在协议内部使用。
  • 包装器属性实例不能在enum声明。
  • 在类内部声明的包装属性不能被另一个属性覆盖。
  • 包装的属性不能是lazy@NSCopying@NSManagedweakunowned
  • 包装属性应该是其定义中唯一的属性(即@Lazy var (x, y) = /* ... */ )。
  • 包装的属性不能定义gettersetter
  • wrappedValue属性和init(wrappedValue:)wrappedValue变量init(wrappedValue:)必须与属性的包装器类型具有相同的访问级别。
  • projectedValue的type属性必须与该属性的包装器类型具有相同的访问级别。
  • init()必须具有与属性包装器类型相同的访问级别。

让我们总结一下。 Swift中的属性包装器为库作者提供了访问以前为语言功能保留的高级行为的权限。 它们在提高可读性和减少代码复杂性方面的潜力是巨大的,我们只是从表面上检查了该工具的功能。


您在项目中使用属性包装器吗? 写评论!

Source: https://habr.com/ru/post/zh-CN485008/


All Articles