programacion-generica-templates

Qué son los Genéricos y Templates en programación

  • 7 min

Los templates y generics son mecanismos de programación genérica que permiten definir funciones, clases o estructuras que pueden operar con cualquier tipo de dato.

Si alguna vez te has encontrado copiando y pegando una función entera solo para cambiar int por float en la definición de los parámetros, entonces has sentido el dolor que venimos a solucionar en el tema de hoy.

Aunque hay diferencias técnicas en como funcionan Templates y Genéricos, el concepto es el mismo. Escribir con código que funcione con tipos que aún no conocemos.

programacion-genericos

Es como diseñar el plano de una caja sin decidir todavía qué vamos a guardar dentro, o una función que cambia para adaptarse a lo que le pasas.

Al principio pueden intimidar, especialmente cuando ves definiciones complejas como <T, U, V> where T : new(). Pero son una herramienta fundamental para construir librerías y frameworks

¿Por qué deberías usarlos?

  1. Seguridad de Tipos (Type Safety): El compilador detecta errores antes de que ejecutes el programa. Si intentas meter una Manzana en una Caja<Pera>, el código no compila.
  2. Rendimiento: Evitamos el Boxing/Unboxing (convertir tipos a Object y viceversa), lo cual es costoso computacionalmente.
  3. Código Limpio: Mantenemos una única versión del algoritmo, facilitando el mantenimiento y la legibilidad.

El Problema: La duplicidad de código

Imagina que queremos crear una función sencilla para intercambiar dos valores entre dos variables (Swap).

En un lenguaje fuertemente tipado (sin genéricos), tendríamos que hacer esto:

// Versión para enteros
void Swap(int a, int b) { ... }

// Versión para flotantes
void Swap(float a, float b) { ... }

// Versión para strings
void Swap(string a, string b) { ... }

Copied!

Esto es terrible. El algoritmo es idéntico en los tres casos, solo cambia el tipo de dato. Si encontramos un bug en la lógica, tenemos que corregirlo en tres sitios (o en 27)

La solución: Tipos como parámetros

La programación genérica nos permite definir una función (o una clase) donde el tipo de dato es un parámetro más.

Igual que una función recibe argumentos como valores (x = 5), una función genérica recibe argumentos de tipo (T = int).

La T (de Type) es simplemente una convención muy usada, pero podrías usar cualquier nombre. La sintaxis suele usar los símbolos < >.

// Pseudo-código Genérico
class Caja<T> 
{
    private T contenido;

    public void Guardar(T objeto) {
        this.contenido = objeto;
    }

    public T Sacar() {
        return this.contenido;
    }
}

Copied!

Ahora podemos reutilizar nuestra lógica infinitamente:

  • Caja<int>: Una caja que solo guarda enteros.
  • Caja<String>: Una caja que solo guarda texto.
  • Caja<Usuario>: Una caja que guarda objetos de usuario.

¿Templates o genéricos? ¿Es lo mismo?

Conceptualmente, básicamente sí, son lo mismo. Aunque la implementación interna es distinta

En C++, se llaman Templates. Funcionan como una fotocopiadora inteligente en tiempo de compilación.

Si tú usas List<int> y List<float>, el compilador genera literalmente dos copias del código de la clase, sustituyendo T por int en una y por float en la otra.

  • Ventaja: Rendimiento extremo (se optimiza para cada tipo específico).
  • Desventaja: El ejecutable final crece (Code Bloat) y los tiempos de compilación son lentos.

En lenguajes como Java o C#, se llaman Genéricos.

  • Java usa Type Erasure: Elimina los tipos al compilar. En tiempo de ejecución, todo es básicamente un Object, y hace castings automáticos. Es un truco del compilador para asegurar tipos.
  • C# usa Reified Generics: El tipo existe realmente en tiempo de ejecución (List<int> es diferente de List<string> en la memoria). Es más potente y rápido que el enfoque de Java.

Constraints

Los genéricos son una herramienta muy potente, pero normalmente necesitamos “delimitarlos” de alguna forma para que sean útiles. Si T puede ser cualquier cosa, entonces no puedo hacer mucho con ello.

Por ejemplo, no puedo hacer a + b dentro de una función genérica, porque… ¿y si T es una conexión a base de datos? No puedes sumar conexiones

Para solucionar esto, usamos las Constraints (restricciones). Le decimos al compilador:

Acepto cualquier tipo T, siempre y cuando T cumpla con estas condiciones

Dependiendo del lenguaje, la sintaxis varía (where, extends, concepts), pero la idea es:

// Solo acepto tipos que sean "Comparables" entre sí
class Ordenador<T> where T : IComparable 
{
    public T Mayor(T a, T b) 
    {
        // Como sé que T es IComparable, puedo usar el método CompareTo
        if (a.CompareTo(b) > 0) return a;
        return b;
    }
}

Copied!

Esto nos da lo mejor de los dos mundos,

  • Flexibilidad, funciona con muchos tipos
  • Seguridad, sabemos qué métodos podemos llamar

Sintaxis de templates y generics en distintos lenguajes

Aunque el concepto es similar, la sintaxis varía según el lenguaje. A continuación, veremos ejemplos en varios lenguajes populares.

En C++, los templates se usan para definir funciones y clases genéricas.

#include <iostream>

// Función template para intercambiar valores
template <typename T>
void intercambiar(T &a, T &b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5, y = 10;
    intercambiar(x, y);
    std::cout << "x: " << x << ", y: " << y << std::endl;  // Salida: x: 10, y: 5

    std::string s1 = "Hola", s2 = "Mundo";
    intercambiar(s1, s2);
    std::cout << "s1: " << s1 << ", s2: " << s2 << std::endl;  // Salida: s1: Mundo, s2: Hola

    return 0;
}
Copied!

En Java, los generics se usan para definir clases y métodos genéricos.

public class Main {
    // Método genérico para intercambiar valores
    public static <T> void intercambiar(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    public static void main(String[] args) {
        Integer[] numeros = {5, 10};
        intercambiar(numeros, 0, 1);
        System.out.println("numeros[0]: " + numeros[0] + ", numeros[1]: " + numeros[1]);  // Salida: numeros[0]: 10, numeros[1]: 5

        String[] palabras = {"Hola", "Mundo"};
        intercambiar(palabras, 0, 1);
        System.out.println("palabras[0]: " + palabras[0] + ", palabras[1]: " + palabras[1]);  // Salida: palabras[0]: Mundo, palabras[1]: Hola
    }
}
Copied!

En C#, los generics se usan de manera similar a Java.

using System;

class Program
{
    // Método genérico para intercambiar valores
    public static void Intercambiar<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }

    static void Main()
    {
        int x = 5, y = 10;
        Intercambiar(ref x, ref y);
        Console.WriteLine($"x: {x}, y: {y}");  // Salida: x: 10, y: 5

        string s1 = "Hola", s2 = "Mundo";
        Intercambiar(ref s1, ref s2);
        Console.WriteLine($"s1: {s1}, s2: {s2}");  // Salida: s1: Mundo, s2: Hola
    }
}
Copied!

En TypeScript, los generics se usan para definir funciones y clases genéricas.

// Función genérica para intercambiar valores
function intercambiar<T>(a: T, b: T): [T, T] {
    return [b, a];
}

let x = 5, y = 10;
[x, y] = intercambiar(x, y);
console.log(`x: ${x}, y: ${y}`);  // Salida: x: 10, y: 5

let s1 = "Hola", s2 = "Mundo";
[s1, s2] = intercambiar(s1, s2);
console.log(`s1: ${s1}, s2: ${s2}`);  // Salida: s1: Mundo, s2: Hola
Copied!

Rust usa genéricos intensivamente junto con Traits (su versión de las interfaces). Tipos como Option<T> o Result<T, E> son la base del manejo de errores, evitando las excepciones tradicionales.