Saltar al contenido

Envoltorio de propiedad en Swift

Envoltorio de propiedad en Swift

Alexander, el desarrollador sénior de iOS de tecnologiapc, continúa compartiendo los trucos para trabajar con Swift. En esta publicación, echamos un vistazo más de cerca a las envolturas de propiedades.

No diría que los envoltorios de propiedades son muy difíciles de entender, pero vale la pena entenderlos mejor, porque también hay matices. Entonces, ¿qué es un sobre de propiedad? Por el nombre en sí, puede adivinar que se trata de un envoltorio de una propiedad que agrega lógica a esta propiedad.

La capacidad de agregar envoltorios de propiedades en Swift se ha agregado como parte de la propuesta de envoltorio de propiedades SE-0258. Esto se hizo principalmente para SwiftUI. Para facilitar el trabajo con datos, hemos agregado @State, @Binding, @ObservedObject etc.

Antes de sumergirnos en ejemplos más complejos, creemos un contenedor ficticio simple que no hace nada, solo almacena un valor. Establecido SE-0258, para crear su envoltorio, se deben cumplir 2 condiciones:

  1. El tipo está precedido por un atributo @propertyWrapper,
  2. El tipo debe contener una variable wrapValue con un nivel de acceso no inferior al del propio tipo.

Entonces, el ejemplo más simple se verá así:

@propertyWrapper
struct Simplest {
    var wrappedValue: T
}

Intentemos aplicar nuestro envoltorio:

struct TestSimplest {
    @Simplest var value: String
}

let simplest = TestSimplest(value: "test")
print(simplest.value)

La consola mostrará: prueba.

Pero si estudiamos la propuesta detenidamente, descubriremos cómo se revelan realmente los envoltorios de propiedad dentro del objeto:

struct TestSimplest {
    @Simplest var value: String

    // будет развернуто в 
    private var _value: Simplest
    var value: String { /* доступ через _value.wrappedValue */ }
}

Debido a la privacidad externa, no podemos acceder al contenedor: imprimir (semplicest._value) dará un error.

Pero desde dentro del tipo, podemos acceder fácilmente al contenedor directamente:

extension TestSimplest {
    func describe() {
        print("value: (value) type: (type(of: value))")
        print("_value: (_value) type: (type(of: _value))")
        print("_value.wrappedValue: (_value.wrappedValue) type: (type(of: _value.wrappedValue))")
    }
}

let simplest = TestSimplest(value: "test")
simplest.describe()

Esto se producirá

value: test type: String
_value: Simplest(wrappedValue: "test") type: Simplest
_value.wrappedValue: test type: String

lo que confirma que _value es un envoltorio verdadero y value == _value.wrappedValue == String.

Después de tratar con el ejemplo más simple, intentemos crear algo un poco más útil, por ejemplo, un contenedor para enteros con la siguiente lógica: si se asigna un número negativo, lo hacemos positivo, de hecho, un contenedor en una función. abdominales:

@propertyWrapper
struct Abs {
    private var value: Int = 0

    var wrappedValue: Int {
        get { value }
        set {
            value = abs(newValue)
        }
    }

    init(wrappedValue: Int) {
        self.wrappedValue = wrappedValue
    }
}

struct TestAbs {
    @Abs var value: Int = 0
}

var testAbs = TestAbs(value: -10)
print(testAbs.value)
testAbs.value = 20
print(testAbs.value)
testAbs.value = -30
print(testAbs.value)

La consola lo hará

10
20
30

Pongamos lógica en el set para wrapValue; junto con un inicializador donde asignamos el valor inicial a la propiedad wrapValue, esto nos permite lograr el comportamiento deseado tanto al inicializar una variable con un envoltorio, como al modificarla más, ya que un número negativo no puede estar en valor en principio. Les llamo la atención: es importante que en el inicializador el primer parámetro sea un parámetro con el nombre wrapValue, esto permite que Swift permita tales asignaciones bajo el capó, cuando podemos asignar un valor del tipo que contiene a una variable marcada con un contenedor:

@Abs var value: Int = 0

Si cambiamos, por ejemplo, a

init(custom: Int) {
    self.wrappedValue = custom
}

ya no funcionará.

Cabe señalar que desde entonces efectivamente se implementan @propertyWrapper los tipos más comunes, podemos parametrizar los envoltorios.

Por ejemplo, creemos un contenedor de mayúsculas que también acepte como entrada el número de caracteres que deben convertirse a mayúsculas desde el principio de la línea.

@propertyWrapper
struct Uppercased {
    private var count: Int
    private var value: String = ""

    var wrappedValue: String {
        get { value }
        set {
            let uppercased = String(newValue.prefix(count)).uppercased()
            value = uppercased
            guard let from = newValue.index(newValue.startIndex, offsetBy: count, limitedBy: newValue.endIndex) else { return }
            value += newValue.suffix(from: from)
        }
    }

    init(wrappedValue: String, count: Int) {
        self.count = count
        self.wrappedValue = wrappedValue
    }

}

struct TestUppercased {
    @Uppercased(count: 5) var value: String = ""
}

var testAbs = TestUppercased(value: "hello world")
print(testAbs.value)
testAbs.value = "another example"
print(testAbs.value)
testAbs.value = "abc"
print(testAbs.value)

La consola lo hará

HELLO world
ANOTHer example
ABC

También me gustaría llamar su atención sobre la «magia»: este ejemplo no se compilará si en PruebaUppercased eliminaremos la asignación de cadena, p. ej. bajo el capó

@Uppercased(count: 5) var value: String = "" 

llama a init (wrapValue: String, count: Int), como wrapValue el valor que asignamos al valor recién pasado.

Para sortear esta limitación, deberá inicializar en el constructor:

struct TestUppercased2 {
    @Uppercased var value: String

    init(count: Int, example: String) {
        _value = Uppercased(wrappedValue: example, count: count)
    }
}

var testAbs2 = TestUppercased2(count: 3, example: "super puper")
print(testAbs2.value)

Si tiene éxito trabajando con SwiftUI, luego, creo, prestó atención a las variables precedidas por el signo de dólar $ valor: solemos transferirlos al niño Vista, que variable se define como @Vinculante. La propuesta explica para qué sirve. Recuerde lo que sucede cuando declara una variable como PropertyWrapper, – no será posible acceder a él desde el exterior del tipo:

struct TestSimplest {
    @Simplest var value: String

    // будет развернуто в 
    private var _value: Simplest
    var value: String { /* доступ через _value.wrappedValue */ }
}

Y si queremos que los usuarios de la estructura PruebaSimplest ¿Tiene acceso a la lógica contenedora de sus propiedades? Para hacer esto, necesita definir la propiedad en el contenedor de propiedades projectedValue.

@propertyWrapper
struct VarWithMemory {
    private var _current: T
    private (set) var previousValues: [T] = []

    var wrappedValue: T {
        get { _current }
        set {
            previousValues.append(_current)
            _current = newValue
        }
    }

    var projectedValue: VarWithMemory {
        get { self }
        set { self = newValue }
    }

    init(wrappedValue: T) {
        self._current = wrappedValue
    }

    mutating func clear() {
        previousValues.removeAll()
    }

}

struct TestVarWithMemory {
    @VarWithMemory var value: String = ""
}

var test = TestVarWithMemory(value: "initial")
print("1. current value: (test.value)")
test.value = "second"
print("2. current value: (test.value)")
test.value = "third"
print("3. current value: (test.value)")

// value: String, won't work
// print(test.value.previousValues)

print("4. history: (test.$value.previousValues)")
print("5. clear")
test.$value.clear()
print("6. current value: (test.value)")
print("7. history: (test.$value.previousValues)")

Salida de registro:

1. current value: initial
2. current value: second
3. current value: third
4. history: ["initial", "second"]
5. clear
6. current value: third
7. history: []

Entonces,

@VarWithMemory var value: String = ""

se convertirá en algo similar

private var _value: VarWithMemory = VarWithMemory(wrappedValue: "")

public var value: String {
  get { _value.wrappedValue }
  set { _value.wrappedValue = newValue }
}

public var $value: VarWithMemory {
  get { _value.projectedValue }
  set { _value.projectedValue = newValue }
}

Es importante señalar que el tipo projectedValue puede ser cualquier cosa y no coincidir con el tipo en el que se define la variable. Esto permitió @Estado al recibir projectedValue mediante PS – recibir en la salida no Expresar, es Vinculante.

¿Cuáles son los principales casos de uso obvios en los que puede pensar?

  • Cuando trabaja con un valor, en realidad ejecuta el proxy y, de hecho, la variable se almacena en la base de datos / Valores predeterminados del usuario.
  • Cuando queremos convertir un valor en una asignación de alguna manera; un ejemplo de esto sería el anterior Abdominales superiores, bien, o de la propuesta de Sujeción para recortar el valor de los límites mínimo / máximo.
  • Copia en la implementación de escritura, etc.

Tenga en cuenta que existen algunas restricciones sobre el uso de envoltorios de propiedades:

  • el protocolo no puede indicar que esta propiedad deba declararse con tal o cual Envoltorio de propiedad‘ohm;
  • una propiedad con un envoltorio de propiedad no se puede utilizar en extensión es enum;
  • una propiedad con un envoltorio de propiedad no se puede invalidar en un heredero de clase;
  • una propiedad con un envoltorio de propiedad no puede ser perezoso, @NSCopying, @NSManaged, débil o sin propiedad;
  • una propiedad con un envoltorio de propiedad no puede tener un valor personalizado Prepárate;
  • nivel de acceso wrapValue y los niveles de acceso para todos los siguientes (si los hay) deben ser idénticos al nivel de acceso del tipo en el que están definidos: projectedValue, init (wrapValue 🙂, dentro ()

Por cierto, aunque las envolturas se pueden combinar, hay una advertencia. La combinación se lleva a cabo según el principio de una muñeca de anidación y, por ejemplo, este código:

struct TestCombined {
    @VarWithMemory @Abs var value: Int = 0
}

var test = TestCombined()
print(test.value)
test.value = -1
test.value = -2
test.value = -3
print(test.value)
print(test.$value.previousValues)

grabará

0
3
[__lldb_expr_173.Abs(_value: 0), __lldb_expr_173.Abs(_value: 1), __lldb_expr_173.Abs(_value: 2)]

inesperado

0
3
[0, 1, 2]

A la entrada de VarWithMemory llega la variable no tipo En t, pero cómo Abdominales ¿Y si las envolturas no fueran Genérico, pero si aceptaran, digamos, solo cadenas, ni siquiera se compilaría. No existe una buena solución; Como opción, puede crear versiones especializadas de envoltorios para que un tipo sea aceptado en el constructor del segundo y dentro de él ya funcione con el tipo interno del segundo.

Resumiendo

¿Cuáles son los beneficios de los envoltorios patentados? Le permiten ocultar la lógica personalizada detrás de una definición de variable simple agregando @<Тип>

¿Cuáles son las desventajas? Desde el punto de vista de la aplicación práctica, se derivan de su principal ventaja: la complejidad de la envoltura está oculta a la vista, incluso el hecho de que esté trabajando con una envoltura no se da por sentado hasta que se mira la definición de un envoltorio. variable. Por lo tanto, recomendaría usarlos con cuidado en su proyecto.

Cuales son las alternativas?

  • Para crear lógica como Observador – usar willSet / didSet propiedad.
  • Para agregar cambio de lógica / ubicación de almacenamiento: use Prepárate propiedad.

Patio de recreo con las fuentes del artículo está disponible aquí.

Si encuentra un error, seleccione un fragmento de texto y presione Ctrl + Entrar