Contenedores de propiedad Swift

Si usó SwiftUI, probablemente prestó atención a palabras clave como @ObservedObject, @EnvironmentObject, @FetchRequest, etc. Los envoltorios de propiedades (en adelante denominados "envoltorios de propiedades") son una nueva característica de Swift 5.1. Este artículo lo ayudará a comprender de dónde provienen todas las construcciones de @, cómo usarlas en SwiftUI y en sus proyectos.



Traducido por: Evgeny Zavozhansky, desarrollador de FunCorp.


Nota: Cuando se preparó la traducción, parte del código fuente del artículo original había perdido su relevancia debido a cambios en el idioma, por lo que algunos ejemplos de código fueron reemplazados intencionalmente.


Los envoltorios de propiedades se introdujeron por primera vez en los foros de Swift en marzo de 2019, unos meses antes del anuncio de SwiftUI. En su propuesta original, Douglas Gregor, miembro del equipo de Swift Core, describió esta construcción (luego llamada delegados de propiedad) como "una generalización accesible para el usuario de la funcionalidad que actualmente proporciona una construcción de lenguaje como lazy , por ejemplo".


Si una propiedad se declara con la palabra clave lazy , esto significa que se inicializará la primera vez que se acceda a ella. Por ejemplo, la inicialización de propiedad diferida podría implementarse utilizando una propiedad privada, a la que se accede a través de una propiedad calculada. Pero usar la palabra clave lazy hace mucho más 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: Property Wrapper explica perfectamente el diseño y la implementación de los wrappers de propiedades. Por lo tanto, en lugar de tratar de mejorar la descripción en la documentación oficial, considere algunos ejemplos que se pueden implementar utilizando envoltorios de propiedades:


  • restricción de valores de propiedad;
  • conversión de valores al cambiar propiedades;
  • cambio de semántica de igualdad y comparación de propiedades;
  • Registro de acceso a la propiedad.

Límite de valores de propiedad


SE-0258: Property Wrapper proporciona varios ejemplos prácticos, incluidos @Clamping , @Copying , @Atomic , @ThreadSpecific , @Box , @UserDefault . Considere el contenedor @Clamping , que le permite limitar el valor máximo o mínimo de una propiedad.


 @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 se puede usar, por ejemplo, para simular la acidez de una solución, cuyo valor puede tomar un valor de 0 a 14.


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

Intentar establecer el valor de pH más allá del rango de (0...14) hará que la propiedad tome el valor más cercano al intervalo mínimo o máximo.


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

Los contenedores de propiedades se pueden usar para implementar otros contenedores de propiedades. Por ejemplo, el contenedor @UnitInterval limita el valor de una propiedad al 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    } } 

Ideas similares


  • @Positive / @NonNegative indica que el valor puede ser un número positivo o negativo.
  • @NonZero indica que el valor de la propiedad no puede ser 0.
  • @Validated o @Whitelisted / @Blacklisted restringe el valor de una propiedad a ciertos valores.

Convertir valores al cambiar propiedades


Validar los valores de los campos de texto es un dolor de cabeza constante para los desarrolladores de aplicaciones. Hay tantas cosas a tener en cuenta: desde lugares comunes como la codificación hasta intentos maliciosos de ingresar un código a través de un campo de texto. Considere usar un contenedor de propiedades para eliminar los espacios que un usuario ha ingresado al principio y al final de una línea.


 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 ofrece el método trimmingCharacters(in:) , con el que puede eliminar espacios al principio y al final de una línea. Puede llamar a este método siempre que necesite garantizar la exactitud de la entrada, pero no es muy conveniente. Puede usar el contenedor de propiedades para esto.


 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" 

Ideas similares


  • @Transformed aplica la conversión de ICU a la cadena de @Transformed .
  • @Rounded / @Truncated redondea o trunca un valor de cadena.

Cambiar la semántica de igualdad y comparación de propiedades


En Swift, dos cadenas son iguales si son canónicamente equivalentes , es decir Contienen los mismos caracteres. Pero supongamos que queremos que las propiedades de cadena sean iguales, no sensibles a mayúsculas y minúsculas, que contienen.


@CaseInsensitive implementa un contenedor para propiedades de tipo String o 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 

Ideas similares


  • @Approximate para una comparación aproximada de propiedades de tipo Double o Float.
  • @Ranked por propiedades cuyos valores están en orden (por ejemplo, el rango de las cartas).

Registro de acceso a la propiedad


@Versioned le permitirá interceptar los valores asignados y recordar cuándo se configuraron.


 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 clase ExpenseReport permite guardar marcas de tiempo de estados de procesamiento del informe de gastos.


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

Pero el ejemplo anterior demuestra una seria limitación en la implementación actual de envoltorios de propiedades, que se deriva de la restricción Swift: las propiedades no pueden arrojar excepciones. Si quisiéramos agregar una restricción a @Versioned para evitar que el valor cambie a .approved después de que tomó el valor .denied , entonces la mejor opción es fatalError() , que no es adecuada para aplicaciones reales.


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

Ideas similares


  • @Audited para @Audited acceso a la propiedad.
  • @UserDefault para encapsular el mecanismo para leer y guardar datos en UserDefaults .

Limitaciones


Las propiedades no pueden lanzar excepciones


Como ya se mencionó, los envoltorios de propiedades pueden usar solo unos pocos métodos para procesar valores no válidos:


  • ignóralos;
  • terminar la aplicación usando fatalError ().

Las propiedades envueltas no se pueden marcar con el atributo `typealias`


El ejemplo @UnitInterval anterior, cuya propiedad está limitada por el intervalo (0...1) , no se puede declarar como


 typealias UnitInterval = Clamping(0...1) 

Restricción en el uso de una composición de varios envoltorios de propiedades


La composición de envoltorios de propiedades no es una operación conmutativa: el orden de la declaración afectará el comportamiento. Considere un ejemplo en el que la propiedad slug, que es la url de una publicación de blog, está normalizada. En este caso, el resultado de la normalización variará dependiendo de cuándo se reemplacen los espacios por guiones, antes o después de la eliminación de espacios. Por lo tanto, por el momento, no se admite una composición de varios envoltorios de propiedades.


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

Sin embargo, esta limitación se puede eludir mediante el uso de envoltorios de propiedades anidadas.


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

Otras restricciones de envoltorio de propiedad


  • No se puede usar dentro del protocolo.
  • Una instancia de propiedad de contenedor no se puede declarar en enum .
  • Una propiedad envuelta declarada dentro de una clase no puede ser anulada por otra propiedad.
  • Una propiedad envuelta no puede ser lazy , @NSCopying , @NSManaged , weak o unowned propiedad.
  • Una propiedad envuelta debe ser la única dentro de su definición (es decir, @Lazy var (x, y) = /* ... */ ).
  • Una propiedad envuelta no puede tener getter y setter definidos.
  • Los tipos de la propiedad wrappedValue y la variable wrappedValue en init(wrappedValue:) deben tener el mismo nivel de acceso que el tipo de contenedor de la propiedad.
  • La propiedad type de projectedValue debe tener el mismo nivel de acceso que el tipo wrapper de la propiedad.
  • init() debe tener el mismo nivel de acceso que el tipo de contenedor de propiedades.

Resumamos Los contenedores de propiedades en Swift proporcionan a los autores de la biblioteca acceso al comportamiento de alto nivel previamente reservado para las funciones del lenguaje. Su potencial para mejorar la legibilidad y reducir la complejidad del código es enorme, y solo examinamos superficialmente las capacidades de esta herramienta.


¿Utiliza envoltorios de propiedades en sus proyectos? Escribe en los comentarios!

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


All Articles