Desarrollo móvil. Swift: el misterio de los protocolos

Hoy continuamos con la serie de publicaciones sobre el tema del desarrollo móvil para iOS. Y si la última vez fue sobre lo que necesita y no necesita preguntar en las entrevistas, en este material abordaremos el tema de los protocolos, que es importante en Swift. Se tratará de cómo se organizan los protocolos, cómo difieren entre sí y cómo se combinan con las interfaces Objective-C.



Como dijimos anteriormente, el nuevo lenguaje de Apple continúa evolucionando, y la mayoría de sus parámetros y características están claramente indicados en la documentación. Pero, ¿quién lee la documentación cuando el código debe escribirse aquí y ahora? Así que repasemos las características principales de los protocolos Swift en nuestra publicación.

Para empezar, debe tenerse en cuenta que los protocolos de Apple son un término alternativo para el concepto de "Interfaz", que se utiliza en otros lenguajes de programación. En Swift, los protocolos se usan para indicar patrones de ciertas estructuras (el llamado plano) que se pueden trabajar en un nivel abstracto. En palabras simples, el protocolo define una serie de métodos y variables que un determinado tipo debe heredar sin falta.

Más adelante en el artículo, los momentos se revelarán gradualmente de la siguiente manera: desde simples y de uso frecuente hasta otros más complejos. En principio, en las entrevistas, puede hacer preguntas en este orden, ya que determinan el nivel de competencia del solicitante, desde el nivel de junio hasta el nivel de las personas mayores.

¿Qué protocolos se necesitan en Swift?


Los desarrolladores móviles a menudo no usan protocolos en absoluto, pero pierden la capacidad de trabajar con algunas entidades de manera abstracta. Si destacamos las características principales de los protocolos en Swift, obtenemos los siguientes 7 puntos:

  • Los protocolos proporcionan herencia múltiple
  • Los protocolos no pueden almacenar estado
  • Los protocolos pueden ser heredados por otros protocolos.
  • Los protocolos se pueden aplicar a estructuras (estructura), clases (clase) y enumeraciones (enumeración), definiendo la funcionalidad del tipo
  • Los protocolos genéricos le permiten especificar dependencias complejas entre tipos y protocolos durante su herencia
  • Los protocolos no definen referencias variables "fuertes" o "débiles"
  • En las extensiones de los protocolos, se pueden describir implementaciones específicas de métodos y valores calculados.
  • Los protocolos de clase permiten que solo las clases hereden

Como sabes, todos los tipos simples (string, int) en Swift son estructuras. En la biblioteca estándar de Swift, esto, por ejemplo, se ve así:

public struct Int: FixedWidthInteger, SignedInteger { 

Al mismo tiempo, los tipos de colección (colección), a saber, matriz, conjunto, diccionario, también se pueden empaquetar en el protocolo, porque también son estructuras. Por ejemplo, un diccionario se define de la siguiente manera

 public struct Dictionary<Key, Value> where Key: Hashable { 

Por lo general, en la programación orientada a objetos, se utiliza el concepto de clases, y todos conocen el mecanismo para heredar métodos y variables de la clase principal de la clase descendiente. Al mismo tiempo, nadie le prohíbe contener métodos y variables adicionales.

En el caso de los protocolos, puede crear una jerarquía de relaciones mucho más interesante. Para describir la siguiente clase, puede usar varios protocolos al mismo tiempo, lo que le permite crear diseños bastante complejos que satisfarán muchas condiciones al mismo tiempo. Por otro lado, las limitaciones de los diferentes protocolos hacen posible formar algún objeto destinado únicamente a una aplicación limitada y que contiene un cierto número de funciones.

La implementación del protocolo en Swift es bastante simple. La sintaxis implica un nombre, varios métodos y parámetros (variables) que contendrá.

 protocol Employee { func work() var hours: Int { get } } 

Además, en Swift, los protocolos pueden contener no solo los nombres de los métodos, sino también su implementación. El código del método en el protocolo se agrega a través de extensiones. En la documentación puede encontrar muchas menciones de extensiones, pero con respecto a los protocolos en extensiones, puede colocar el nombre de la función y el cuerpo de la función.

 extension Employee { func work() { print ("do my job") } } 

Puedes hacer lo mismo con las variables.

 extension Employee { var hours: Int { return 8 } } 

Si usamos el objeto asociado con el protocolo en algún lugar, podemos establecer una variable con un valor fijo o transmitido. De hecho, una variable es una pequeña función sin parámetros de entrada ... o con la posibilidad de asignar directamente un parámetro.

Ampliar el protocolo en Swift le permite implementar el cuerpo de la variable y, de hecho, será un valor calculado, un parámetro calculado con las funciones get y set. Es decir, dicha variable no almacenará ningún valor, pero desempeñará el papel de una función o funciones, o desempeñará el papel de un proxy para alguna otra variable.

O si tomamos alguna clase o estructura e implementamos el protocolo, entonces podemos usar la variable habitual en él:

 class Programmer { var hours: Int = 24 } extension Programmer: Employee { } 

Vale la pena señalar que las variables en la definición del protocolo no pueden ser débiles. (débil es una implementación de variación).

Hay ejemplos más interesantes: puede implementar la extensión de la matriz y agregar allí una función relacionada con el tipo de datos de la matriz. Por ejemplo, si la matriz contiene valores enteros o tiene el formato equitativo (adecuado para la comparación), la función puede, por ejemplo, comparar todos los valores de las celdas de la matriz.

 extension Array where Element: Equatable {   var areAllElementsEqualToEachOther: Bool {       if isEmpty {           return false       }       var previousElement = self[0]       for (index, element) in self.enumerated() where index > 0 {           if element != previousElement {               return false           }           previousElement = element       }       return true   } } [1,1,1,1].areAllElementsEqualToEachOther 

Un pequeño comentario. Las variables y funciones en los protocolos pueden ser estáticas.

Usando @ objc


Lo principal que necesita saber en este asunto es que los protocolos @ objc Swift son visibles en el código Objective-C. Estrictamente hablando, para esta "palabra mágica" @ objc existe. Pero todo lo demás permanece sin cambios.

 @objc protocol Typable {   @objc optional func test()   func type() } extension Typable {   func test() {       print("Extension test")   }   func type() {       print("Extension type")   } } class Typewriter: Typable {   func test() {       print("test")   }   func type() {       print("type")   } } 


Los protocolos de este tipo solo pueden ser heredados por clases. Para listados y estructuras esto no se puede hacer.

Esa es la única manera.
 @objc protocol Dummy { } class DummyClass: Dummy { } 


Vale la pena señalar que en este caso es posible definir funciones opcionales (función opcional @obj), que, si se desea, pueden no implementarse, como para la función test () en el ejemplo anterior. Pero las funciones condicionalmente opcionales también se pueden implementar expandiendo el protocolo con una implementación vacía.

 protocol Dummy { func ohPlease() } extension Dummy { func ohPlease() { } } 


Tipo de herencia


Al crear una clase, estructura o enumeración, podemos denotar la herencia de un determinado protocolo; en este caso, los parámetros heredados funcionarán para nuestra clase y para todas las demás clases que heredan esta clase, incluso si no tenemos acceso a ellos.

Por cierto, en este contexto aparece un problema muy interesante. Digamos que tenemos un protocolo. Hay alguna clase Y la clase implementa el protocolo, y tiene una función work (). ¿Qué sucede si tenemos una extensión del protocolo, que también tiene un método work ()? ¿Cuál será llamado cuando se llame al método?

 protocol Person {    func work() } extension Person {   func work() {       print("Person")   } } class Employee { } extension Employee: Person {   func work() {       print("Employee")   } } 

Se lanzará el método de clase: estas son las características de los métodos de envío en Swift. Y esta respuesta es dada por muchos solicitantes. Pero sobre la cuestión de cómo asegurarse de que el código no tenga un método de clase, sino un método de protocolo, solo unos pocos saben la respuesta. Sin embargo, también hay una solución para esta tarea: implica eliminar la función de la definición del protocolo y llamar al método de la siguiente manera:

 protocol Person { //     func work() //      } extension Person {   func work() {       print("Person")   } } class Employee { } extension Employee: Person {   func work() {       print("Employee")   } } let person: Person = Employee() person.work() //output: Person 


Protocolos Genéricos


Swift también tiene protocolos genéricos con tipos asociados que le permiten definir variables de tipo. A dicho protocolo se le pueden asignar condiciones adicionales que se imponen a los tipos asociativos. Varios de estos protocolos le permiten construir estructuras complejas necesarias para la formación de la arquitectura de la aplicación.

Sin embargo, no puede implementar una variable como un protocolo genérico. Solo puede ser heredado. Estas construcciones se utilizan para crear dependencias en clases. Es decir, podemos describir una clase genérica abstracta para determinar los tipos utilizados en ella.

 protocol Printer {   associatedtype PrintableClass: Hashable   func printSome(printable: PrintableClass) } extension Printer {   func printSome(printable: PrintableClass) {       print(printable.hashValue)   } } class TheIntPrinter: Printer {   typealias PrintableClass = Int } let intPrinter = TheIntPrinter() intPrinter.printSome(printable: 0) let intPrinterError: Printer = TheIntPrinter() //   

Debe recordarse que los protocolos genéricos tienen un alto nivel de abstracción. Por lo tanto, en las propias aplicaciones, pueden ser redundantes. Pero al mismo tiempo, se utilizan protocolos genéricos al programar bibliotecas.

Protocolos de clase


Swift también tiene protocolos vinculados a clases. Se usan dos tipos de sintaxis para describirlos.

 protocol Employee: AnyObject { } 

O

 protocol Employee: class { } 

Según los desarrolladores del lenguaje, el uso de estas sintaxis es equivalente, pero la palabra clave de clase se usa solo en este lugar, a diferencia de AnyObject, que es un protocolo.

Mientras tanto, como vemos durante las entrevistas, las personas a menudo no pueden explicar qué es un protocolo de clase y por qué es necesario. Su esencia radica en el hecho de que tenemos la oportunidad de usar algún objeto, que sería un protocolo, y al mismo tiempo funcionaría como un tipo de referencia. Un ejemplo:

 protocol Handler: class {} class Presenter: Handler { weak var renderer: Renderer?  } protocol Renderer {} class View: Renderer { } 

¿Qué es la sal?

IOS utiliza la gestión de memoria utilizando el método de conteo automático de referencia, lo que implica la presencia de enlaces fuertes y débiles. Y en algunos casos, debe considerar cuáles variables, fuertes (fuertes) o débiles (débiles), se utilizan en las clases.

El problema es que cuando se usa algún protocolo como tipo, al describir una variable (que es un enlace fuerte), puede ocurrir un ciclo de retención, lo que lleva a pérdidas de memoria, porque los objetos se mantendrán en todas partes por enlaces fuertes. Además, pueden surgir problemas si aún decide escribir código de acuerdo con los principios de SOLID.

 protocol Handler {} class Presenter: Handler { var renderer: Renderer? } protocol Renderer {} class View: Renderer { var handler: Handler? } 

Para evitar tales situaciones, Swift utiliza protocolos de clase que le permiten establecer inicialmente variables "débiles". El protocolo de clase le permite mantener un objeto como una referencia débil. Un ejemplo donde a menudo vale la pena considerar esto se llama delegado.

 protocol TableDelegate: class {} class Table {   weak var tableDelegate: TableDelegate? } 

Otro ejemplo en el que deberían usarse protocolos de clase es una indicación explícita de que el objeto se pasa por referencia.

Herencia y despacho de métodos múltiples


Como se indicó al principio del artículo, los protocolos se pueden heredar varias veces. Es decir

 protocol Pet {   func waitingForItsOwner() } protocol Sleeper {   func sleepOnAChair() } class Kitty: Pet, Sleeper {   func eat() {       print("yammy")   }   func waitingForItsOwner() {       print("looking at the door")   }   func sleepOnAChair() {       print("dreams")   } } 

Esto es útil, pero ¿qué trampas están ocultas aquí? La cuestión es que las dificultades, al menos a primera vista, surgen debido al envío de métodos (envío de métodos). En palabras simples, puede que no esté claro qué método se llamará: el padre o el tipo actual.

Justo arriba, ya hemos cubierto el tema de cómo funciona el código; llama al método de clase. Es decir, como se esperaba.

 protocol Pet {   func waitingForItsOwner() } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty: Pet = Kitty() kitty.waitingForItsOwner() // Output: Kitty is looking at the door 

Pero si intenta eliminar la firma del método de la definición del protocolo, entonces ocurre la "magia". De hecho, esta es una pregunta de la entrevista: "¿Cómo hacer que una función se llame desde un protocolo?"

 protocol Pet { } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty: Pet = Kitty() kitty.waitingForItsOwner() // Output: Pet is looking at the door 

Pero si usa la variable no como un protocolo, sino como una clase, entonces todo estará bien.

 protocol Pet { } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty = Kitty() kitty.waitingForItsOwner() // Output: Kitty is looking at the door 

Se trata de métodos de despacho estáticos al extender el protocolo. Y esto debe tenerse en cuenta. ¿Y aquí está la herencia múltiple? Pero con esto: si toma dos protocolos con funciones implementadas, dicho código no funcionará. Para que se ejecute la función, deberá emitir explícitamente el protocolo deseado. Tal es el eco de la herencia múltiple de C ++.

 protocol Pet {   func waitingForItsOwner() } extension Pet {   func yawn() { print ("Pet yawns") } } protocol Sleeper {   func sleepOnAChair() } extension Sleeper {   func yawn() { print ("Sleeper yawns") } } class Kitty: Pet, Sleeper {   func eat() {       print("yammy")   }   func waitingForItsOwner() {       print("looking at the door")   }   func sleepOnAChair() {       print("dreams")   } } let kitty = Kitty() kitty.yawn() 

Una historia similar será si hereda un protocolo de otro, donde hay funciones que se implementan en extensiones. El compilador no lo dejará compilar.

 protocol Pet {   func waitingForItsOwner() } extension Pet {   func yawn() { print ("Pet yawns") } } protocol Cat {   func walk() } extension Cat {   func yawn() { print ("Cat yawns") } } class Kitty:Cat {   func eat() {       print("yammy")   }   func waitingForItsOwner() {       print("looking at the door")   }   func sleepOnAChair() {       print("dreams")   } } let kitty = Kitty() 

Los dos últimos ejemplos muestran que no vale la pena reemplazar completamente los protocolos con clases. Puede confundirse en la programación estática.

Genéricos y protocolos


Podemos decir que esta es una pregunta con un asterisco, que no es necesario preguntar en absoluto. Pero los codificadores aman las construcciones súper abstractas y, por supuesto, un par de clases genéricas innecesarias deben estar en el proyecto (sin él). Pero un programador no sería programador si no quisiera resumirlo todo en otra abstracción. Y Swift, siendo un lenguaje joven pero en desarrollo dinámico, brinda esa oportunidad, pero de manera limitada. (Sí, no se trata de teléfonos móviles).

En primer lugar, una prueba completa para la posible herencia solo se encuentra en Swift 4.2, es decir, solo en el otoño será posible usar esto normalmente en proyectos. En Swift 4.1, aparece un mensaje que indica que la oportunidad aún no se ha implementado.

 protocol Property { } protocol PropertyConnection { } class SomeProperty { } extension SomeProperty: Property { } extension SomeProperty: PropertyConnection { } protocol ViewConfigurator { } protocol Connection { } class Configurator<T> where T: Property {   var property: T   init(property: T) {       self.property = property   } } extension Configurator: ViewConfigurator { } extension Configurator: Connection where T: PropertyConnection { } [Configurator(property: SomeProperty()) as ViewConfigurator]   .forEach { configurator in   if let connection = configurator as? Connection {       print(connection)   } } 

Para Swift 4.1, se muestra lo siguiente:

 warning: Swift runtime does not yet support dynamically querying conditional conformance ('__lldb_expr_1.Configurator<__lldb_expr_1.SomeProperty>': '__lldb_expr_1.Connection') 

Mientras que en Swift 4.2 todo funciona como se esperaba:

 __lldb_expr_5.Configurator<__lldb_expr_5.SomeProperty> connection 

También vale la pena señalar que puede heredar el protocolo con un solo tipo de relación. Si hay dos tipos de enlaces, se prohibirá la herencia en el nivel del compilador. Aquí se muestra una explicación detallada de lo que es y lo que no es posible.

 protocol ObjectConfigurator { } protocol Property { } class FirstProperty: Property { } class SecondProperty: Property { } class Configurator<T> where T: Property {   var properties: T   init(properties: T) {       self.properties = properties   } } extension Configurator: ObjectConfigurator where T == FirstProperty { } //   : // Redundant conformance of 'Configurator<T>' to protocol 'ObjectConfigurator' extension Configurator: ObjectConfigurator where T == SecondProperty { } 

Pero, a pesar de estas dificultades, trabajar con conexiones en genéricos es bastante conveniente.

Para resumir


Se proporcionaron protocolos en Swift en su forma actual para hacer que el desarrollo sea más estructural y proporcionar modelos de herencia más avanzados que en el mismo Objective-C. Por lo tanto, estamos seguros de que el uso de protocolos es un movimiento justificable, y asegúrese de preguntar a los candidatos a desarrolladores qué saben sobre estos elementos del lenguaje Swift. En las siguientes publicaciones tocaremos los métodos de envío.

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


All Articles