Hola a todos Hoy queremos compartir la traducción preparada en la víspera del lanzamiento del curso "iOS Developer. Curso avanzado " . Vamos!

Una de las principales ventajas del diseño basado en el protocolo de Swift es que nos permite escribir código genérico que sea compatible con una amplia gama de tipos y que no se implemente específicamente para todos. Especialmente si dicho código general está destinado a uno de los protocolos, que se puede encontrar en la biblioteca estándar, que permitirá usarlo tanto con los tipos incorporados como con los definidos por el usuario.
Un ejemplo de dicho protocolo es Sequence, que es aceptado por todos los tipos de bibliotecas estándar que se pueden iterar, como Array, Dictionary, Set y muchos otros. Esta semana, veamos cómo podemos envolver Sequence en contenedores universales, lo que nos permitirá encapsular varios algoritmos en el núcleo de las API fáciles de usar.
El arte de ser vago
Es bastante fácil confundirse al pensar que todas las secuencias son similares a Array, ya que todos los elementos se cargan instantáneamente en la memoria al crear la secuencia. Dado que el único requisito del protocolo de secuencia es que los receptores deben poder iterar, no podemos hacer ninguna suposición sobre cómo se cargan o almacenan los elementos de una secuencia desconocida.
Por ejemplo, como cubrimos en Secuencias rápidas: El arte de ser perezoso , las secuencias a veces pueden cargar sus elementos perezosamente, ya sea por razones de rendimiento o porque no se garantiza que toda la secuencia pueda caber en la memoria. Aquí hay algunos ejemplos de tales secuencias:
Dado que todas las secuencias anteriores son perezosas por alguna razón, no querríamos forzarlas a una matriz, por ejemplo, llamando a Array (folder.subfolders). Pero aún podemos querer modificarlos y trabajar con ellos de diferentes maneras, así que veamos cómo podemos hacer esto creando un tipo de envoltorio de secuencia.
Creación de fundaciones
Comencemos creando un tipo básico que podamos usar para crear todo tipo de API convenientes sobre cualquier secuencia. Lo llamaremos WrappedSequence, y será un tipo universal que contiene tanto el tipo de secuencia que envolvemos como el tipo de elemento que queremos que cree nuestra nueva secuencia.
La característica principal de nuestro contenedor será su función Iterator, que nos permitirá tomar el control de la búsqueda de la secuencia base, cambiando el iterador utilizado para cada iteración:
struct WrappedSequence<Wrapped: Sequence, Element>: Sequence { typealias IteratorFunction = (inout Wrapped.Iterator) -> Element? private let wrapped: Wrapped private let iterator: IteratorFunction init(wrapping wrapped: Wrapped, iterator: @escaping IteratorFunction) { self.wrapped = wrapped self.iterator = iterator } func makeIterator() -> AnyIterator<Element> { var wrappedIterator = wrapped.makeIterator() return AnyIterator { self.iterator(&wrappedIterator) } } }
Como puede ver arriba, Sequence utiliza un patrón de fábrica para que cada secuencia cree una nueva instancia de iterador para cada iteración, utilizando el método makeIterator ().
Arriba, usamos el tipo AnyIterator de la biblioteca estándar, que es un iterador de borrado de tipo que puede usar cualquier implementación básica de IteratorProtocol para obtener valores de Element. En nuestro caso, crearemos un elemento llamando a nuestra función Iterator, pasando como argumento nuestro propio iterador de la secuencia envuelta, y dado que este argumento está marcado como inout, podemos cambiar el iterador base en su lugar dentro de nuestra función.
Dado que WrappedSequence también es una secuencia, podemos usar toda la funcionalidad de la biblioteca estándar relacionada con ella, como iterar sobre ella o transformar sus valores usando map:
let folderNames = WrappedSequence(wrapping: folders) { iterator in return iterator.next()?.name } for name in folderNames { ... } let uppercasedNames = folderNames.map { $0.uppercased() }
¡Ahora comencemos con nuestra nueva secuencia envuelta!
Prefijos y Sufijos
Cuando se trabaja con secuencias muy a menudo, existe el deseo de insertar un prefijo o sufijo en la secuencia con la que trabajamos, pero ¿no sería genial si pudiéramos hacer esto sin cambiar la secuencia principal? Esto puede conducir a un mejor rendimiento y nos permite agregar prefijos y sufijos a cualquier secuencia, y no solo a tipos generales como Array.
Usando WrappedSequence, podemos hacer esto con bastante facilidad. Todo lo que necesitamos hacer es extender la secuencia con un método que crea una secuencia envuelta a partir de una matriz de elementos para insertar como prefijo. Luego, cuando iteramos, comenzamos a iterar sobre todos los elementos del prefijo antes de continuar con la secuencia base, de esta manera:
extension Sequence { func prefixed( with prefixElements: Element... ) -> WrappedSequence<Self, Element> { var prefixIndex = 0 return WrappedSequence(wrapping: self) { iterator in
Arriba, usamos un parámetro con un número variable de argumentos (agregando ... a su tipo) para permitir la transmisión de uno o más elementos al mismo método.
De la misma manera, podemos crear un método que agregue un conjunto dado de sufijos al final de la secuencia, primero realizando nuestra propia iteración de la secuencia base y luego iterando sobre los elementos con sufijo:
extension Sequence { func suffixed( with suffixElements: Element... ) -> WrappedSequence<Self, Element> { var suffixIndex = 0 return WrappedSequence(wrapping: self) { iterator in guard let next = iterator.next() else {
Con los dos métodos mencionados anteriormente, ahora podemos agregar prefijos y sufijos a cualquier secuencia que queramos. Estos son algunos ejemplos de cómo se pueden usar nuestras nuevas API:
Aunque todos los ejemplos anteriores se pueden implementar usando tipos específicos (como Array y String), la ventaja de usar nuestro tipo WrappedSequence es que todo se puede hacer de manera perezosa: no realizamos ninguna mutación ni evaluamos ninguna secuencia para agregar nuestro prefijos o sufijos, que pueden ser realmente útiles en situaciones críticas para el rendimiento o cuando se trabaja con grandes conjuntos de datos.
Segmentación
A continuación, veamos cómo podemos ajustar secuencias para crear versiones segmentadas de ellas. En ciertas iteraciones, no es suficiente saber cuál es el elemento actual; también podemos necesitar información sobre los elementos siguientes y anteriores.
Cuando trabajamos con secuencias indexadas, a menudo podemos lograr esto usando la API enumerated (), que también usa un contenedor de secuencia para darnos acceso tanto al elemento actual como a su índice:
for (index, current) in list.items.enumerated() { let previous = (index > 0) ? list.items[index - 1] : nil let next = (index < list.items.count - 1) ? list.items[index + 1] : nil ... }
Sin embargo, la técnica anterior no solo es bastante detallada en términos de invocación, sino que también se basa en el uso de matrices nuevamente, o al menos en alguna forma de secuencia que nos da acceso aleatorio a sus elementos, que muchas secuencias, especialmente las perezosas, no es bienvenido
En su lugar, usemos nuestra WrappedSequence nuevamente: para crear una envoltura de secuencia que perezosamente proporcione vistas segmentadas en su secuencia base, rastreando elementos anteriores y actuales y actualizándolos mientras continúa iterando:
extension Sequence { typealias Segment = ( previous: Element?, current: Element, next: Element? ) var segmented: WrappedSequence<Self, Segment> { var previous: Element? var current: Element? var endReached = false return WrappedSequence(wrapping: self) { iterator in
Ahora podemos usar la API anterior para crear una versión segmentada de cualquier secuencia siempre que necesitemos mirar hacia adelante o hacia atrás al hacer una iteración. Por ejemplo, así es como podemos usar la segmentación para poder determinar fácilmente cuándo hemos llegado al final de la lista:
for segment in list.items.segmented { addTopBorder() addView(for: segment.current) if segment.next == nil { // , addBottomBorder() } } ```swift , . , : ```swift for segment in path.nodes.segmented { let directions = ( enter: segment.previous?.direction(to: segment.current), exit: segment.next.map(segment.current.direction) ) let nodeView = NodeView(directions: directions) nodeView.center = segment.current.position.cgPoint view.addSubview(nodeView) }
Ahora estamos comenzando a ver el verdadero poder de las secuencias de envoltura, ya que nos permiten ocultar algoritmos cada vez más complejos en una API realmente simple. Todo lo que necesita para segmentar la secuencia es acceder a la propiedad segmentada en cualquier secuencia, y nuestra implementación básica se encargará del resto.
Recursividad
Finalmente, veamos cómo incluso las iteraciones recursivas se pueden modelar usando envoltorios de secuencias. Supongamos que deseamos proporcionar una forma simple de iterar recursivamente sobre una jerarquía de valores en la que cada elemento de la jerarquía contiene una secuencia de elementos secundarios. Puede ser bastante difícil hacerlo bien, por lo que sería genial si pudiéramos usar una implementación para realizar todas estas iteraciones en nuestra base de código.
Usando WrappedSequence, podemos lograr esto extendiendo Sequence con un método que usa la misma restricción de tipo genérico para garantizar que cada elemento pueda proporcionar una secuencia anidada que tenga el mismo tipo de iterador que nuestro original. Para poder acceder dinámicamente a cada secuencia anidada, también le pediremos a la persona que llama que especifique KeyPath para la propiedad que se debe usar para la recursión, lo que nos dará una implementación similar a esta:
extension Sequence { func recursive<S: Sequence>( for keyPath: KeyPath<Element, S> ) -> WrappedSequence<Self, Element> where S.Iterator == Iterator { var parentIterators = [Iterator]() func moveUp() -> (iterator: Iterator, element: Element)? { guard !parentIterators.isEmpty else { return nil } var iterator = parentIterators.removeLast() guard let element = iterator.next() else {
Usando lo anterior, ahora podemos iterar recursivamente sobre cualquier secuencia, independientemente de cómo se construya dentro, y sin tener que cargar toda la jerarquía por adelantado. Por ejemplo, así es como podríamos usar esta nueva API para iterar recursivamente a través de una jerarquía de carpetas:
let allFolders = folder.subfolders.recursive(for: \.subfolders) for folder in allFolders { try loadContent(from: folder) }
También podemos usarlo para iterar sobre todos los nodos del árbol o para atravesar recursivamente un conjunto de registros de la base de datos, por ejemplo, para enumerar todos los grupos de usuarios en una organización:
let allNodes = tree.recursive(for: \.children) let allGroups = database.groups.recusive(for: \.subgroups)
Una cosa que debemos tener cuidado cuando se trata de iteraciones recursivas es evitar referencias circulares, cuando un cierto camino nos devuelve a un elemento que ya hemos encontrado, lo que nos llevará a un bucle infinito.
Una forma de solucionar esto es hacer un seguimiento de todos los elementos que ocurren (pero puede ser problemático desde el punto de vista de la memoria), para asegurarse de que no haya referencias circulares en nuestro conjunto de datos, o para manejar estos casos cada vez desde el lado de la llamada (usando pausa, continuar o regresar para completar cualquier iteraciones cíclicas).
Conclusión
La secuencia es uno de los protocolos más simples en la biblioteca estándar, requiere solo un método, pero sigue siendo uno de los más potentes, especialmente cuando se trata de cuánta funcionalidad podemos crear en base a él. Al igual que la biblioteca estándar contiene secuencias de contenedor para cosas como enumeraciones, también podemos crear nuestros propios contenedores, que nos permiten ocultar funcionalidades avanzadas con API realmente simples.
Aunque las abstracciones siempre tienen un precio, y es importante considerar cuándo vale la pena (y quizás lo más importante, cuando no vale la pena) presentarlas, si podemos construir nuestras abstracciones directamente sobre lo que proporciona la biblioteca estándar, usando las mismas convenciones, entonces estas Las abstracciones suelen ser más propensas a resistir el paso del tiempo.