Saltar al contenido

Restricciones de tipo: genéricos con contrato

Limitar un parámetro genérico a los tipos que conforman ciertos protocolos, componer varias restricciones y aprender a descomponer una firma genérica compleja de la librería estándar.

Un parámetro genérico sin restricciones es casi inútil: puedes guardarlo, moverlo y devolverlo, pero no puedes preguntarle nada. La restricción es el contrato que convierte un marcador vacío en un tipo con capacidades verificadas en compilación. Este nivel empieza aquí porque todo lo demás —cláusulas where, conformidad condicional, especialización— son refinamientos de esta única idea.

  • Comprender por qué un parámetro genérico sin restricciones apenas puede operar.
  • Restringir con un protocolo, con varios y con una superclase.
  • Distinguir la sintaxis de dos puntos de la cláusula where y saber cuándo cada una.
  • Descomponer una firma genérica compleja leyéndola por capas.

El marcador vacío y su contrato

Cuando escribes un parámetro de tipo sin restricción, Swift solo garantiza una cosa: que existe algún tipo concreto ocupando ese hueco. Nada más. No sabe si se puede comparar, imprimir, sumar o iterar.

SWIFT
func describir<T>(_ valor: T) -> String {    // valor == valor      // error: T no conforma a Equatable    // valor.description   // error: T no tiene esa propiedad    "\(valor)"             // esto sí: la interpolación funciona con todo}

La restricción se escribe tras dos puntos y significa exactamente “cualquier T, siempre que conforme a esto”. A partir de ese instante, el cuerpo de la función puede usar todo lo que el protocolo promete, y el compilador lo verifica en la definición, no en cada llamada.

SWIFT
func maximo<T: Comparable>(_ a: T, _ b: T) -> T {    a > b ? a : b          // legal: Comparable garantiza el operador}

Esta es la diferencia esencial con los genéricos de C++ o con el duck typing: allí la plantilla se comprueba al instanciarse y los errores aparecen en el punto de uso; en Swift el contrato se verifica una sola vez, al definir. Si el cuerpo compila, compilará para todos los tipos que satisfagan la restricción, sin excepción.

Restricción no es herencia

Restringir con un protocolo no crea una jerarquía. T sigue siendo un tipo concreto —Int, String, tu struct— no una caja polimórfica. El protocolo solo actúa como filtro de admisión y como catálogo de lo que puedes invocar dentro.

Componer restricciones

Un mismo parámetro puede exigir varias capacidades a la vez con el ampersand de composición, y puede exigir además ser subclase de una clase concreta. La lista de parámetros admite tantos marcadores como necesites, cada uno con su propio contrato.

SWIFT
// Dos protocolos sobre el mismo parámetrofunc clave<T: Hashable & CustomStringConvertible>(_ x: T) -> String {    "\(x.hashValue)-\(x.description)"}// Restricción de superclase mas protocolofunc configurar<V: UIView & Identifiable>(_ vista: V) { /* ... */ }// Varios parametros, cada uno con su contratofunc emparejar<K: Hashable, V: Equatable>(_ k: K, _ v: V) -> [K: V] {    [k: v]}

Protocolo

La forma canónica. T: Collection admite cualquier tipo que sepa iterar y indexar, sea Array, String o uno tuyo.

Composición

T: Hashable & Sendable exige ambas conformidades. El orden no importa y no hay límite práctico de términos.

Superclase

T: NSObject restringe a una clase y sus descendientes. Es la única restricción nominal no protocolar y solo aplica a tipos de referencia.

La sintaxis de dos puntos solo puede hablar del parámetro en sí. En cuanto necesitas decir algo sobre los tipos asociados de ese parámetro —“el elemento de la colección debe ser comparable”— la lista de parámetros se queda corta y hay que bajar a la cláusula where, que es el tema de la lección siguiente.

Leer una firma compleja

La habilidad que separa a quien usa genéricos de quien los diseña es la lectura. Una firma densa se descompone siempre igual: primero los parámetros de tipo con sus contratos, luego los parámetros de valor, luego el retorno.

SWIFT
func agrupar<C: Collection, K: Hashable>(    _ coleccion: C,    por clave: (C.Element) throws -> K) rethrows -> [K: [C.Element]]

Léela por capas, en este orden:

  1. Los marcadores. Hay dos: C y K. Todo lo demás se expresa en términos de ellos.
  2. Los contratos. C debe ser una colección; K debe ser hashable, porque va a ser clave de diccionario. El compilador ya sabe que C tiene un tipo asociado Element.
  3. Los valores. Recibe la colección y una función que, dado un elemento de esa colección, produce una clave. Esa función puede lanzar.
  4. El retorno. Un diccionario de claves K a arrays de elementos de C. Y rethrows dice que esta función solo lanza si la función que le pasaste lanza.

Con esa disciplina, firmas que parecen intimidantes se vuelven mecánicas. Cuando abras la documentación de la librería estándar y veas la declaración real de Dictionary.init(grouping:by:) o de Sequence.flatMap, reconocerás la misma anatomía.

Restricciones implícitas y errores frecuentes

No todas las restricciones que gobiernan tu código están escritas por ti. El protocolo que exiges arrastra las suyas: pedir Hashable te da también Equatable, porque el primero hereda del segundo; pedir Collection te da Sequence y, con él, todo el catálogo de algoritmos que se definen sobre secuencias. Diseñar bien consiste en pedir el protocolo más débil que habilite lo que necesitas, no el más cómodo.

SWIFT
// Innecesariamente estricto: no usamos el orden para nadafunc contarApariciones<T: Comparable>(_ x: T, en lista: [T]) -> Int {    lista.filter { $0 == x }.count}// Correcto: la igualdad basta, y ahora admite muchos mas tiposfunc contarApariciones<T: Equatable>(_ x: T, en lista: [T]) -> Int {    lista.filter { $0 == x }.count}

Tres errores que aparecen una y otra vez al empezar:

  • Restringir de más. Cada protocolo extra que exiges expulsa tipos válidos. Si el cuerpo no usa la capacidad, sobra.
  • Confundir el parámetro con su contenido. Escribir T: Comparable cuando lo que querías era ordenar los elementos de T. La restricción de dos puntos no puede alcanzar tipos asociados.
  • Restringir en la extensión en lugar de en el tipo. Poner la restricción en la declaración del tipo genérico obliga a que todas sus instancias la cumplan; ponerla en una extensión deja el tipo abierto y añade capacidades solo donde procede.

La prueba del cuerpo

Ante la duda, borra todas las restricciones y vuelve a añadirlas una a una, guiándote por los errores del compilador. Lo que quede es el contrato mínimo. Es un ejercicio mecánico que produce firmas mucho mejores que la intuición.

La restricción es el sistema de tipos hablando de sí mismo

Aquí ocurre algo profundo que conviene nombrar. Una restricción genérica no es documentación ni una comprobación diferida: es una proposición lógica que el compilador demuestra antes de generar una sola instrucción. Cuando escribes T: Comparable, no estás pidiendo un favor al optimizador, estás añadiendo una hipótesis a un sistema formal, y el cuerpo de tu función es una prueba que solo se acepta si se deriva de esa hipótesis. Por eso Swift comprueba la definición y no cada instanciación: no está expandiendo plantillas, está verificando un teorema una vez y aplicándolo infinitas. La consecuencia práctica es enorme. Un genérico que compila es un genérico correcto para todos los tipos presentes y futuros que cumplan el contrato, incluidos los que escribirá alguien que jamás verá tu código. Y la consecuencia de diseño lo es todavía más: cada restricción que añades estrecha el universo de tipos admitidos pero ensancha el universo de operaciones legales dentro. Diseñar un genérico es negociar ese intercambio con precisión quirúrgica —pedir exactamente lo que necesitas, ni un protocolo más—, y esa negociación es el oficio entero.

Descompón y diseña

  1. Escribe func primerosDistintos<S: Sequence>(_ s: S, _ n: Int) -> [S.Element] añadiendo la restricción mínima que haga falta para eliminar duplicados. ¿Cuál es y por qué no basta con Sequence?
  2. Declara una función que acepte un parámetro que sea a la vez Codable y Sendable y explica qué operaciones te habilita cada uno.
  3. Busca en la librería estándar la firma real de Sequence.max(by:) y descomponla en las cuatro capas de esta lección.
  4. Intenta escribir T: Comparable donde en realidad necesitabas hablar de T.Element. Lee el error del compilador: te está señalando la lección 2.