Programación Orientada a Protocolo, Parte 3

Artículo final sobre programación orientada al protocolo.


En esta parte, veremos cómo se almacenan y copian las variables de tipo genérico y cómo funciona el método de envío con ellas.


Versión no compartida


protocol Drawable { func draw() } func drawACopy(local: Drawable) { local.draw() } let line = Line() drawACopy(line) let point = Point() drawACopy(point) 

Código muy simple drawACopy toma un parámetro de tipo Drawable y llama a su método de dibujo, eso es todo.


Versión generalizada


Veamos la versión generalizada del código anterior:


 func drawACopy<T: Drawable>(local: T) { local.draw() } ... 

Nada parece haber cambiado. Todavía podemos llamar a la función drawACopy , como su versión drawACopy , y nada más, pero la más interesante como de costumbre.
El código generalizado tiene dos características importantes:


  1. polimorfismo estático (también conocido como paramétrico)
  2. un tipo específico y único en el contexto de la llamada (un tipo genérico T se define en tiempo de compilación)

Considere esto con un ejemplo:


 func foo<T: Drawable>(local: T) { bar(local) } func bar<T: Drawable>(local: T) { ... } let point = Point(...) foo(point) 

La parte más interesante comienza cuando llamamos a la función foo . El compilador conoce exactamente el tipo de point variable, es solo Point. Además, el compilador puede inferir libremente el tipo T: Drawable en la función foo desde el momento en que pasamos una variable del tipo Point conocido a esta función: T = Point. Todos los tipos son conocidos en el momento de la compilación y el compilador puede realizar todas sus maravillosas optimizaciones; lo más importante es alinear la llamada foo .


 This: ```swift let point = Point(...) foo<T = Point>(point) Becomes this: ```swift bar<T = Point>(point) 

El compilador simplemente incorpora la llamada foo con su implementación y también muestra el tipo genérico de T: barra dibujable. En otras palabras, el compilador primero incrusta una llamada al método foo con tipo T = Point, luego incrusta el resultado de la incrustación anterior: el método de barra con tipo T = Point.


Implementación de métodos genéricos.


 func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...)) 

Internamente, drawACopy Swift utiliza una tabla de método de protocolo (que contiene todas las implementaciones del método T) y una tabla de ciclo de vida (que contiene todos los métodos de ciclo de vida para la instancia T). En pseudocódigo, se ve así:


 func drawACopy<T: Drawable>(local: T, pwt: T.PWT, vwt: T.VWT) {...} drawACopy(Point(...), Point.pwt, Point.vwt) 

VWT y PWT son tipos asociados (tipo asociado) en T - como alias de tipo (typealias), solo que mejor. Point.pwt y Point.vwt son propiedades estáticas.


Como en nuestro ejemplo T es Punto, T está bien definido, por lo tanto, no se requiere la creación de un contenedor. En la versión anterior drawACopy de drawACopy (local: Drawable), la creación de un contenedor existencial se llevó a cabo según fue necesario; lo examinamos en la segunda parte del artículo.


Se requiere una tabla de ciclo de vida en las funciones debido a la creación de un argumento. Como sabemos, los argumentos en Swift se pasan a través de valores, no a través de enlaces, por lo tanto, deben copiarse, y el método de copia para este argumento pertenece a la tabla del ciclo de vida como este argumento. También hay otros métodos de ciclo de vida allí: asignar, destruir y desasignar.


Se requiere una tabla de ciclo de vida en funciones genéricas debido al uso de métodos para parámetros de código genérico.


¿Generalizado o no generalizado?


¿Es cierto que el uso de tipos genéricos hace que la ejecución del código sea más rápida que el uso de tipos de protocolo solamente? ¿La función generalizada func foo<T: Drawable>(arg: T) más rápida que su homólogo fun foo(arg: Drawable) ?


Notamos que el código genérico da una forma más estática de polimorfismo. También incluye optimizaciones del compilador llamadas "Especialización en código genérico". A ver:


Nuevamente tenemos el mismo código:


 func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...)) 

La especialización de una función genérica crea una copia con tipos genéricos especializados de esta función. Por ejemplo, si llamamos a drawACopy con una variable de tipo Point, el compilador creará una versión especializada de esta función: drawACopyOfPoint (local: Point), y obtenemos:


 func drawACopyOfPoint(local: Point) { local.draw() } func drawACopyOfLine(local: Line) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...)) 

Lo que se puede reducir mediante la optimización del compilador crudo antes de esto:


 Point(...).draw() Line(...).draw() 

Todos estos trucos están disponibles porque las funciones genéricas solo se pueden drawACopy si todos los tipos genéricos están definidos: en el método drawACopy tipo genérico (T) está bien definido.


Propiedades almacenadas genéricas


Considere un par de estructura simple:


 struct Pair { let fst: Drawable let snd: Drawable } let pair = Pair(fst: Line(...), snd: Line(...)) 

Cuando usamos esto de esta manera, obtenemos 2 asignaciones en el montón (las condiciones de memoria exactas en este escenario se describieron en la segunda parte), pero podemos evitar esto con la ayuda de un código generalizado.


La versión genérica de Pair se ve así:


 struct Pair<T: Drawable> { let fst: T let snd: T } 

Desde el momento en que el tipo T se define en la versión generalizada, los tipos de propiedad fst y snd mismos y también se definen. Como el tipo está definido, el compilador puede asignar una cantidad especializada de memoria para estas dos propiedades: fst y snd .


En más detalle sobre la cantidad especializada de memoria:


Cuando trabajamos con una versión fst de Pair , los tipos de propiedad fst y snd son Drawable. Cualquier tipo puede corresponder a Drawable, incluso si se necesitan 10 KB de memoria. Es decir, Swift no podrá sacar una conclusión sobre el tamaño de este tipo y utilizará una ubicación de memoria universal, por ejemplo, un contenedor existencial. Cualquier tipo se puede almacenar en este contenedor. En el caso del código genérico, el tipo es bien reconocido, el tamaño real de las propiedades también es reconocible y Swift puede crear una ubicación de memoria especializada. Por ejemplo (versión generalizada):


 let pair = Pair(Point(...), Point(...)) 

El tipo T ahora es punto. Point toma N bytes de memoria y en Pair obtenemos dos de ellos. Swift asignará 2 * N cantidad de memoria y pondrá pair allí.


Entonces, con la versión genérica de Pair, eliminamos las asignaciones innecesarias en el montón, porque los tipos son fácilmente reconocibles y pueden ubicarse específicamente, sin la necesidad de crear plantillas de memoria universal, ya que todo es conocido.


Conclusión


1. Código genérico especializado - Tipos de valor


tiene la mejor velocidad de ejecución, ya que:


  • sin asignación de montón al copiar
  • código genérico: escribe una función para un tipo especializado
  • sin recuento de referencias
  • envío de método estático

2. Código generalizado especializado - tipos de referencia


Tiene una velocidad de ejecución promedio, ya que:


  • asignaciones por montón al crear instancias
  • hay un recuento de referencia
  • envío de métodos dinámicos a través de una tabla virtual

3. Código generalizado no especializado - valores pequeños


  • sin asignación de montón: el valor se coloca en el búfer de valor del contenedor existencial
  • sin recuento de referencias (ya que no se coloca nada en el montón)
  • envío de método dinámico a través de la tabla de método de protocolo

4. Código generalizado no especializado - valores grandes


  • colocación en el montón: el valor se coloca en el búfer de valores
  • hay un recuento de referencia
  • despacho dinámico a través de la tabla de protocolo-método

Este material no significa que las clases sean malas, que las estructuras sean buenas y que las estructuras en combinación con el código generalizado sean las mejores. Queremos decir que como programador, usted tiene la responsabilidad de elegir una herramienta para sus tareas. Las clases son realmente buenas cuando necesitas mantener valores grandes y hay una semántica de enlaces. Las estructuras son mejores para valores pequeños y cuando necesita su semántica. Los protocolos se adaptan mejor a estructuras y códigos genéricos, etc. Todas las herramientas son específicas de la tarea que está resolviendo y tienen lados positivos y negativos.


Y tampoco pague el dinamismo cuando no lo necesite . Encuentre la abstracción correcta con los requisitos mínimos de tiempo de ejecución.


  • tipos estructurales - semántica de significados
  • tipos de clase - identidad
  • código generalizado - polimorfismo estático
  • tipos de protocolo - polimorfismo dinámico

Utilice el almacenamiento indirecto para trabajar con valores grandes.


Y no lo olvide, es su responsabilidad elegir la herramienta adecuada.
Gracias por su atención a este tema. Esperamos que estos artículos te hayan ayudado y hayan sido interesantes.


Buena suerte

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


All Articles