Wrappers de propriedades Swift

Se você usou o SwiftUI, provavelmente prestou atenção a palavras-chave como @ObservedObject, @EnvironmentObject, @FetchRequest e assim por diante. Wrappers de propriedades (doravante referidos como "wrappers de propriedades") são um novo recurso do Swift 5.1. Este artigo o ajudará a entender de onde vêm todas as construções do @, como usá-las no SwiftUI e em seus projetos.



Traduzido por: Evgeny Zavozhansky, desenvolvedor da FunCorp.


Nota: Quando a tradução foi preparada, parte do código-fonte do artigo original havia perdido sua relevância devido a alterações no idioma; portanto, alguns exemplos de código foram intencionalmente substituídos.


Os invólucros de propriedades foram introduzidos nos fóruns da Swift em março de 2019, alguns meses antes do anúncio da SwiftUI. Em sua proposta original, Douglas Gregor, membro da equipe Swift Core, descreveu esse construto (então chamado de delegados de propriedades) como "uma generalização acessível ao usuário da funcionalidade atualmente fornecida por um construto de linguagem como o lazy , por exemplo".


Se uma propriedade for declarada com a palavra-chave lazy , isso significa que será inicializada na primeira vez que for acessada. Por exemplo, a inicialização da propriedade adiada pode ser implementada usando uma propriedade privada, acessada por meio de uma propriedade computada. Mas o uso da palavra-chave lazy torna isso muito mais fácil.


 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: O Wrapper de propriedades explica perfeitamente o design e a implementação dos wrappers de propriedades. Portanto, em vez de tentar melhorar a descrição na documentação oficial, considere alguns exemplos que podem ser implementados usando wrappers de propriedades:


  • restrição de valores de propriedade;
  • conversão de valores ao alterar propriedades;
  • mudança da semântica da igualdade e comparação de propriedades;
  • registro de acesso à propriedade.

Limitar valores de propriedade


SE-0258: O Wrapper de propriedades fornece vários exemplos práticos, incluindo @Clamping , @Copying , @Atomic , @ThreadSpecific , @Box , @UserDefault . Considere o wrapper @Clamping , que permite limitar o valor máximo ou mínimo de uma propriedade.


 @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 pode ser usado, por exemplo, para simular a acidez de uma solução, cujo valor pode levar de 0 a 14.


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

Tentar definir o valor do pH além da faixa de (0...14) fará com que a propriedade obtenha o valor mais próximo do intervalo mínimo ou máximo.


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

Os invólucros de propriedades podem ser usados ​​para implementar outros invólucros de propriedades. Por exemplo, o wrapper @UnitInterval limita o valor de uma propriedade ao intervalo (0...1) usando @Clamping(0...1) :


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

Idéias semelhantes


  • @Positive / @NonNegative indica que o valor pode ser um número positivo ou negativo.
  • @NonZero indica que o valor da propriedade não pode ser 0.
  • @Validated ou @Whitelisted / @Blacklisted restringe o valor de uma propriedade a determinados valores.

Convertendo valores ao alterar propriedades


A validação dos valores do campo de texto é uma dor de cabeça constante para desenvolvedores de aplicativos. Há muitas coisas a serem rastreadas: desde banalidades, como codificação, até tentativas maliciosas de inserir um código através de um campo de texto. Considere usar um wrapper de propriedade para remover os espaços que um usuário inseriu no início e no final de uma linha.


 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 oferece o método trimmingCharacters(in:) , com o qual você pode remover espaços no início e no final de uma linha. Você pode chamar esse método sempre que precisar garantir a correção da entrada, mas isso não é muito conveniente. Você pode usar o wrapper de propriedade para isso.


 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" 

Idéias semelhantes


  • @Transformed aplica a conversão ICU à string de @Transformed .
  • @Rounded / @Truncated arredonda ou trunca um valor de sequência.

Alterar semântica da igualdade e comparação de propriedades


No Swift, duas cadeias são iguais se forem canonicamente equivalentes , ou seja, contém os mesmos caracteres. Mas suponha que queremos que as propriedades da string sejam iguais, sem distinção entre maiúsculas e minúsculas, que elas contêm.


@CaseInsensitive implementa um wrapper para propriedades do tipo String ou SubString .


 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 

Idéias semelhantes


  • @Approximate para uma comparação aproximada das propriedades do tipo Double ou Float.
  • @Ranked propriedades cujos valores estão em ordem (por exemplo, a classificação das cartas de jogar).

Registro de acesso à propriedade


@Versioned permitirá que você intercepte os valores atribuídos e lembre-se de quando eles foram definidos.


 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    } } 

A classe ExpenseReport permite salvar registros de data e hora dos estados de processamento do relatório de despesas.


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

Mas o exemplo acima demonstra uma limitação séria na implementação atual dos wrappers de propriedades, que se segue da restrição Swift: as propriedades não podem lançar exceções. Se quisermos adicionar uma restrição a @Versioned para impedir que o valor seja alterado para .approved depois que o valor .denied , a melhor opção é fatalError() , que não é adequado para aplicativos reais.


 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: «»   . 

Idéias semelhantes


  • @Audited para @Audited acesso à propriedade.
  • @UserDefault para encapsular o mecanismo para ler e salvar dados em UserDefaults .

Limitações


Propriedades não podem lançar exceções


Como já mencionado, os wrappers de propriedades podem usar apenas alguns métodos para processar valores inválidos:


  • ignore-os;
  • finalize o aplicativo usando fatalError ().

Propriedades agrupadas não podem ser marcadas com o atributo `typealias`


O exemplo @UnitInterval acima, cuja propriedade é limitada pelo intervalo (0...1) , não pode ser declarado como


 typealias UnitInterval = Clamping(0...1) 

Restrição ao uso de uma composição de vários wrappers de propriedades


A composição de wrappers de propriedades não é uma operação comutativa: a ordem da declaração afetará o comportamento. Considere um exemplo em que a propriedade slug, que é o URL de uma postagem de blog, é normalizada. Nesse caso, o resultado da normalização variará dependendo de quando os espaços forem substituídos por hífens, antes ou depois da remoção dos espaços. Portanto, no momento, uma composição de vários wrappers de propriedades não é suportada.


 @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 } 

No entanto, essa limitação pode ser contornada usando wrappers de propriedades aninhados.


 @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 } 

Outras restrições de wrapper de propriedade


  • Não pode ser usado dentro do protocolo.
  • Uma instância de propriedade do wrapper não pode ser declarada em enum .
  • Uma propriedade quebrada declarada dentro de uma classe não pode ser substituída por outra propriedade.
  • Uma propriedade @NSCopying não pode ser lazy , @NSCopying , @NSManaged , weak ou unowned .
  • Uma propriedade agrupada deve ser a única dentro de sua definição (ou seja, @Lazy var (x, y) = /* ... */ ).
  • Uma propriedade getter não pode ter getter e setter definidos.
  • Os tipos da propriedade wrappedValue e a variável wrappedValue em init(wrappedValue:) devem ter o mesmo nível de acesso que o tipo de wrapper da propriedade.
  • A propriedade type de projectedValue deve ter o mesmo nível de acesso que o tipo de wrapper da propriedade.
  • init() deve ter o mesmo nível de acesso que o tipo de wrapper de propriedade.

Vamos resumir. Os wrappers de propriedades no Swift fornecem aos autores da biblioteca acesso ao comportamento de alto nível anteriormente reservado para funções de idioma. Seu potencial para melhorar a legibilidade e reduzir a complexidade do código é enorme, e apenas examinamos superficialmente os recursos dessa ferramenta.


Você usa wrappers de propriedade em seus projetos? Escreva nos comentários!

Source: https://habr.com/ru/post/pt485008/


All Articles