1. Un par de palabras sobre colecciones
Hasta ahora hemos trabajado con arrays — es la forma más simple de guardar varios elementos del mismo tipo. Pero en C# hay un montón de colecciones más cómodas y potentes, que resuelven diferentes tareas.
Colección — es un objeto que puede guardar un grupo de otros objetos. A diferencia de los arrays, las colecciones suelen poder cambiar su tamaño dinámicamente, te dan métodos cómodos para trabajar con los datos y están optimizadas para diferentes escenarios de uso.
Tipos principales de colecciones (resumen rápido):
List<T> — array dinámico, que puede crecer y encogerse:
List<string> names = new List<string>();
names.Add("Alex");
names.Add("María");
Console.WriteLine(names[0]); // Alex
Console.WriteLine(names.Count); // 2
Dictionary<TKey, TValue> — colección de pares "clave-valor", donde a cada clave le corresponde un valor:
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Alex"] = 25;
ages["María"] = 30;
Console.WriteLine(ages["Alex"]); // 25
Ojo: en los ejemplos de arriba usamos corchetes [] para acceder a los elementos de las colecciones — ¡igual que con los arrays! Esto es posible gracias a los indexadores, de los que vamos a hablar hoy.
Esto es solo una primera toma de contacto con las colecciones — veremos en detalle sus posibilidades, diferencias y usos en próximas lecciones. Ahora lo importante es entender que las colecciones te dejan acceder a sus elementos con corchetes, y no es magia, sino un mecanismo especial del lenguaje.
2. Indexadores
Los objetos normales de C# funcionan con propiedades y métodos. Pero ¿qué pasa si tu objeto es como una mini-colección? Por ejemplo, imagina:
- Estás escribiendo una clase Week, que debe devolver el nombre del día de la semana por número: week[0] → "Lunes".
- O una clase Library, donde puedes acceder a un libro por su número: library[3] → "Por el camino de Swann".
Suena cómodo, ¿verdad? Sería raro tener que escribir library.GetBookByIndex(3) cada vez — ¡queremos acceder a nuestro objeto como si fuera un array!
Y aquí es donde entran los indexadores.
Indexador — es un miembro especial de la clase que te permite usar objetos de esa clase con la sintaxis de corchetes, como los arrays: obj[0], obj["clave"], etc.Un indexador parece una propiedad, pero en vez de nombre — acepta parámetros dentro de corchetes. Es parecido a cuando escribimos .Name, solo que aquí es [i].
3. Colección simple con indexador
Vamos a montar una mini-clase que guarde colores favoritos. Usamos un array de strings para guardar. Sin indexador, tendríamos que hacer un método tipo GetColor(int i). Pero con indexador:
using System;
public class FavoriteColors
{
// Campo privado para guardar los colores
private string[] colors = new string[5];
// Indexador:
public string this[int index]
{
get
{
// Control de límites del array (¡encapsulación en acción!)
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("¡Índice de color incorrecto!");
return colors[index];
}
set
{
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("¡Índice de color incorrecto!");
colors[index] = value ?? throw new ArgumentNullException(nameof(value));
}
}
}
class Program
{
static void Main()
{
FavoriteColors favorites = new FavoriteColors();
favorites[0] = "Verde";
favorites[1] = "Azul";
favorites[2] = "Rojo";
favorites[10] = "Violeta"; // ¡Lanza excepción!
Console.WriteLine(favorites[1]); // Azul
}
}
¿Qué está pasando aquí?
- Creamos un array privado, para que nadie desde fuera pueda toquetearlo directamente.
- El indexador está definido como public string this[int index], donde this — palabra clave que indica que el indexador es del objeto.
- Dentro de get y set controlamos el rango, no dejamos salirnos del array ni meter null.
- Al final podemos hacer favorites[0], igual que con un array normal.
4. Sintaxis del indexador: detalles
La sintaxis es parecida a una propiedad, pero en vez de nombre (por ejemplo, Age) se pone la palabra clave this con parámetros:
// Firma del indexador (plantilla general)
[modificador] TipoResultado this[TipoÍndice nombreÍndice]
{
get { ... }
set { ... }
}
Ejemplo: Clásico
public class MyCollection
{
private int[] data = new int[10];
// Indexador para lectura y escritura
public int this[int index]
{
get { return data[index]; }
set { data[index] = value; }
}
}
Los indexadores no solo son por int
La gracia: el indexador no tiene que aceptar solo int. Puedes poner cualquier tipo (lo importante es que tenga sentido acceder por esa clave):
public string this[string colorName]
{
get { /* ... */ }
set { /* ... */ }
}
Por ejemplo, en una clase de agenda telefónica tiene sentido buscar por nombre:
public class PhoneBook
{
private Dictionary<string, int> entries = new Dictionary<string, int>();
public int this[string name]
{
get
{
if (entries.ContainsKey(name))
return entries[name];
return null;
}
set
{
entries[name] = value;
}
}
}
Sobre colecciones y cómo funciona Dictionary<string, string> te lo cuento en próximas lecciones :P
5. Ejemplo práctico: Contador de palabras en texto
Seguimos mejorando nuestra app. Ahora tenemos una clase que cuenta cuántas veces aparece cada palabra en un texto. Mola si el usuario puede acceder al objeto con corchetes para ver el número por palabra:
using System.Collections.Generic;
public class WordCounter
{
private Dictionary<string, int> counter = new Dictionary<string, int>();
// Indexador por string (palabra)
public int this[string word]
{
get
{
if (counter.ContainsKey(word))
return counter[word];
return 0; // Si no existe la palabra, devolvemos 0.
}
set
{
counter[word] = value;
}
}
// Método para contar palabras de un string
public void AddWords(string text)
{
foreach (var word in text.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
{
if (counter.ContainsKey(word))
counter[word]++;
else
counter[word] = 1;
}
}
}
// En Main:
var wc = new WordCounter();
wc.AddWords("mamá lavó la ventana lavó mamá papá");
Console.WriteLine($"'mamá' aparece {wc["mamá"]} vez/veces");
Console.WriteLine($"'ventana' aparece {wc["ventana"]} vez/veces");
Console.WriteLine($"'gato' aparece {wc["gato"]} vez/veces"); // 0
¿Para qué sirve esto en la práctica? Este enfoque se usa mucho para hacer colecciones propias, bibliotecas de memoria, mapeos (diccionarios e índices) e incluso DSL (lenguajes específicos dentro de C#).
6. Limitaciones y matices
Los indexadores molan, pero hay unas cuantas reglas y trampas (cómo no).
El indexador no tiene nombre
A diferencia de una propiedad, el indexador no tiene nombre, solo la firma tipo this[tipo parámetro]. Si intentas escribir public int MyIndexer[int i] — el compilador se va a quedar loco. Solo usamos this.
No existen indexadores estáticos
Los indexadores siempre se crean para una instancia de clase, no para miembros estáticos. O sea, no puedes declarar static int this[int i] — la lógica es que this siempre apunta a una instancia concreta.
Pueden sobrecargarse por tipo/número de parámetros
Puedes crear varios indexadores en una clase, si sus parámetros son de tipo o cantidad diferente. Por ejemplo:
public string this[int i] { get { ... } set { ... } }
public string this[string key] { get { ... } set { ... } }
Esto es legal, el compilador no se lía — y te avisa si los parámetros se solapan.
Solo con propiedades
No puedes declarar un indexador sin los accesores get o set. Si lo quieres solo de lectura — quita set, solo de escritura — get. Normalmente se usan ambos.
7. Utilidad práctica y por qué saber esto
- Los indexadores se usan mucho en colecciones de datos. Muchas clases de .NET los tienen: por ejemplo, List<T>, Dictionary<TKey,TValue>. Cuando escribes list[2], ¡estás usando un indexador!
- Los indexadores te dejan ocultar la implementación interna (¡encapsulación!), pero dar una interfaz cómoda y familiar. El usuario de tu clase no piensa cómo guardas los datos, solo usa los [índice] de siempre.
- Tu código queda más limpio e intuitivo — algo que se valora mucho en entrevistas (y tus futuros compis).
Propiedades e Indexadores: comparación
| Propiedad | Indexador | |
|---|---|---|
| Nombre | Sí (por ejemplo, Name) | No (en vez de nombre — this[parámetro]) |
| Acceso | Por nombre | Por índice (u otra clave) |
| Estático | Pueden ser static | Solo para instancia |
| Varios en la clase | Sí, los que quieras | Sí, pero con firmas de parámetros distintas |
| Uso | Guardar/acceder a datos | Mini-colecciones, datos asociativos |
8. Errores típicos y consejos
Error nº1: intentar hacer el indexador static.
No se puede — this[...] solo funciona con un objeto.
Error nº2: olvidarse de comprobar el índice.
Si no compruebas los límites del array en get o set, el programa puede petar.
Error nº3: tipos de parámetros mezclados.
Si creas dos indexadores con los mismos parámetros, el compilador te da error.
Error nº4: olvidaste implementar get o set.
Si quieres acceso de lectura y escritura — tienes que poner ambos.
Consejo: si tu clase envuelve un array — simplemente pasa los accesos al array por el indexador. Así el código va más rápido y es más intuitivo.
9. ¿Por qué saber todo esto?
Los indexadores hacen que la interfaz de un objeto sea simple, clara y cómoda. Te dejan ocultar la implementación interna, pero dar al desarrollador una forma fácil de acceder a los datos.
Así están hechas las colecciones integradas: string, List<T>, Dictionary<TKey, TValue>, Span<T> y muchas más. Cuando escribes array[2] o text[0], ya estás usando un indexador.
Y lo mejor — ahora puedes escribir tus propias clases que funcionan igual de flexibles y limpias. O sea — un paso más cerca de escribir código profesional y legible.
GO TO FULL VERSION