Encapsuleurs de propriétés Swift

Si vous avez utilisé SwiftUI, vous avez probablement fait attention aux mots clés tels que @ObservedObject, @EnvironmentObject, @FetchRequest et ainsi de suite. Les wrappers de propriété (ci-après dénommés «wrappers de propriété») est une nouvelle fonctionnalité de Swift 5.1. Cet article vous aidera à comprendre d'où viennent toutes les constructions de @, comment les utiliser dans SwiftUI et dans vos projets.



Traduit par: Evgeny Zavozhansky, développeur de FunCorp.


Remarque: Au moment où la traduction a été préparée, une partie du code source de l'article d'origine avait perdu sa pertinence en raison de changements dans la langue, donc certains exemples de code ont été intentionnellement remplacés.


Les wrappers de propriétés ont été introduits pour la première fois sur les forums Swift en mars 2019, quelques mois avant l'annonce de SwiftUI. Dans sa proposition originale, Douglas Gregor, membre de l'équipe Swift Core, décrivait cette construction (alors appelée délégués de propriété) comme «une généralisation accessible aux utilisateurs des fonctionnalités actuellement fournies par une construction de langage telle que lazy , par exemple».


Si une propriété est déclarée avec le mot-clé lazy , cela signifie qu'elle sera initialisée lors du premier accès. Par exemple, l'initialisation différée d'une propriété peut être implémentée à l'aide d'une propriété privée, accessible via une propriété calculée. Mais l'utilisation du mot-clé lazy rend cela beaucoup plus facile.


 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: Property Wrapper explique parfaitement la conception et la mise en œuvre des wrappers de propriété. Par conséquent, au lieu d'essayer d'améliorer la description dans la documentation officielle, considérez quelques exemples qui peuvent être implémentés à l'aide de wrappers de propriétés:


  • restriction de la valeur des propriétés;
  • conversion de valeurs lors du changement de propriétés;
  • changer la sémantique de l'égalité et comparer les propriétés;
  • enregistrement de l'accès à la propriété.

Limiter les valeurs de propriété


SE-0258: Property Wrapper fournit plusieurs exemples pratiques, notamment @Clamping , @Copying , @Atomic , @ThreadSpecific , @Box , @UserDefault . Considérez le wrapper @Clamping , qui vous permet de limiter la valeur maximale ou minimale d'une propriété.


 @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 peut être utilisé, par exemple, pour modéliser l'acidité d'une solution, dont la valeur peut prendre une valeur de 0 à 14.


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

Si vous tentez de définir la valeur du pH au-delà de la plage de (0...14) , la propriété prendra la valeur la plus proche de l'intervalle minimum ou maximum.


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

Les wrappers de propriété peuvent être utilisés pour implémenter d'autres wrappers de propriété. Par exemple, le wrapper @UnitInterval limite la valeur d'une propriété à l'intervalle (0...1) aide de @Clamping(0...1) :


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

Idées similaires


  • @Positive / @NonNegative indique que la valeur peut être un nombre positif ou négatif.
  • @NonZero indique que la valeur de la propriété ne peut pas être 0.
  • @Validated ou @Whitelisted / @Blacklisted restreint la valeur d'une propriété à certaines valeurs.

Conversion de valeurs lors de la modification des propriétés


La validation des valeurs des champs de texte est un casse-tête constant pour les développeurs d'applications. Il y a tellement de choses à garder à l'esprit: des platitudes telles que l'encodage aux tentatives malveillantes d'entrer un code via un champ de texte. Envisagez d'utiliser un wrapper de propriété pour supprimer les espaces qu'un utilisateur a saisis au début et à la fin d'une ligne.


 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 propose la méthode trimmingCharacters(in:) , avec laquelle vous pouvez supprimer des espaces au début et à la fin d'une ligne. Vous pouvez appeler cette méthode chaque fois que vous avez besoin de garantir l'exactitude de l'entrée, mais ce n'est pas très pratique. Vous pouvez utiliser le wrapper de propriété pour cela.


 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ées similaires


  • @Transformed applique la conversion ICU à la chaîne d' @Transformed .
  • @Rounded / @Truncated arrondit ou tronque une valeur de chaîne.

Changer la sémantique de l'égalité et la comparaison des propriétés


Dans Swift, deux chaînes sont égales si elles sont canoniquement équivalentes , c'est-à-dire contiennent les mêmes caractères. Mais supposons que nous voulons que les propriétés de chaîne soient égales, et non sensibles à la casse, qu'elles contiennent.


@CaseInsensitive implémente un wrapper pour les propriétés de type 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ées similaires


  • @Approximate pour une comparaison grossière des propriétés de type Double ou Float.
  • @Ranked pour les propriétés dont les valeurs sont en ordre (par exemple, le rang des cartes à jouer).

Journalisation de l'accès aux propriétés


@Versioned vous permettra d'intercepter les valeurs attribuées et de vous rappeler quand elles ont été définies.


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

La classe ExpenseReport permet d'enregistrer les horodatages des états de traitement du rapport de dépenses.


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

Mais l'exemple ci-dessus montre une sérieuse limitation dans l'implémentation actuelle des wrappers de propriétés, qui découle de la restriction Swift: les propriétés ne peuvent pas lever d'exceptions. Si nous voulions ajouter une restriction à @Versioned pour empêcher la valeur de .approved après avoir pris la valeur .denied , alors la meilleure option est fatalError() , qui ne convient pas aux applications réelles.


 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ées similaires


  • @Audited pour @Audited accès à la propriété.
  • @UserDefault pour encapsuler le mécanisme de lecture et d'enregistrement des données dans UserDefaults .

Limitations


Les propriétés ne peuvent pas lever d'exceptions


Comme déjà mentionné, les wrappers de propriété ne peuvent utiliser que quelques méthodes pour traiter les valeurs non valides:


  • les ignorer;
  • terminer l'application à l'aide de fatalError ().

Les propriétés encapsulées ne peuvent pas être marquées avec l'attribut `typealias`


L'exemple @UnitInterval ci-dessus, dont la propriété est limitée par l'intervalle (0...1) , ne peut pas être déclaré comme


 typealias UnitInterval = Clamping(0...1) 

Restriction à l'utilisation d'une composition de plusieurs wrappers de propriété


Composer des wrappers de propriétés n'est pas une opération commutative: l'ordre de la déclaration affectera le comportement. Prenons un exemple dans lequel la propriété slug, qui est l'url d'un article de blog, est normalisée. Dans ce cas, le résultat de la normalisation variera selon le moment où les espaces seront remplacés par des tirets, avant ou après la suppression des espaces. Par conséquent, pour le moment, une composition de plusieurs wrappers de propriété n'est pas prise en charge.


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

Cependant, cette limitation peut être contournée en utilisant des wrappers de propriété imbriqués.


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

Autres restrictions de wrapper de propriété


  • Ne peut pas être utilisé à l'intérieur du protocole.
  • Une instance de propriété wrapper ne peut pas être déclarée dans enum .
  • Une propriété encapsulée déclarée à l'intérieur d'une classe ne peut pas être remplacée par une autre propriété.
  • Une propriété @NSCopying ne peut pas être lazy , @NSCopying , @NSManaged , weak ou sans unowned .
  • Une propriété @Lazy var (x, y) = /* ... */ doit être la seule dans sa définition (c'est-à-dire que @Lazy var (x, y) = /* ... */ ).
  • Une propriété encapsulée ne peut pas avoir de getter et de setter définis.
  • Les types de la propriété wrappedValue et de la variable wrappedValue dans init(wrappedValue:) doivent avoir le même niveau d'accès que le type d'encapsuleur de la propriété.
  • La propriété type de projectedValue doit avoir le même niveau d'accès que le type wrapper de la propriété.
  • init() doit avoir le même niveau d'accès que le type d'encapsuleur de propriété.

Résumons. Les wrappers de propriétés dans Swift permettent aux auteurs de bibliothèques d'accéder au comportement de haut niveau précédemment réservé aux fonctions de langage. Leur potentiel pour améliorer la lisibilité et réduire la complexité du code est énorme, et nous n'avons examiné que superficiellement les capacités de cet outil.


Utilisez-vous des wrappers de propriétés dans vos projets? Écrivez dans les commentaires!

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


All Articles