4.D Colecciones de objetos y miembros estáticos¶
Objetos que contienen objetos¶
Los objetos se pueden combinar: un curso tiene alumnos, una biblioteca tiene libros, una persona tiene cuentas. Es la relación de composición («tiene un»), y se resuelve guardando los objetos dentro de otro, normalmente en una lista.
Curso ◆────── 0..n ──────▶ Alumno
(guarda una lista) (nombre, nota)
var curso = new Curso();
curso.Matricular(new Alumno("Ana", 4));
curso.Matricular(new Alumno("Marta", 8));
curso.Matricular(new Alumno("Luis", 6));
Console.WriteLine($"Matriculados: {curso.Total}");
Console.WriteLine($"Media del curso: {curso.Media()}");
Console.WriteLine($"Mejor alumno: {curso.Mejor()}");
Console.WriteLine($"Aprobados: {string.Join(", ", curso.Aprobados().Select(a => a.Nombre))}");
try
{
new Alumno("Error", 11);
}
catch (ArgumentException e)
{
Console.WriteLine($"Error: {e.Message}");
}
Console.WriteLine($"Alumnos creados: {Alumno.Creados}");
class Alumno
{
public string Nombre { get; }
public int Nota { get; }
public bool Aprobado => Nota >= 5;
public static int Creados { get; private set; }
public Alumno(string nombre, int nota)
{
if (!NotaValida(nota))
{
throw new ArgumentException("nota fuera de rango");
}
Nombre = nombre;
Nota = nota;
Creados++;
}
public static bool NotaValida(int nota) => nota >= 0 && nota <= 10;
public override string ToString() => $"{Nombre} ({Nota})";
}
class Curso
{
private readonly List<Alumno> _alumnos = new();
public int Total => _alumnos.Count;
public void Matricular(Alumno alumno)
{
_alumnos.Add(alumno);
}
public double Media()
{
int suma = 0;
foreach (Alumno a in _alumnos)
{
suma += a.Nota;
}
return (double)suma / _alumnos.Count;
}
public Alumno Mejor()
{
Alumno mejor = _alumnos[0];
foreach (Alumno a in _alumnos)
{
if (a.Nota > mejor.Nota)
{
mejor = a;
}
}
return mejor;
}
public List<Alumno> Aprobados() => _alumnos.Where(a => a.Aprobado).ToList();
}
Salida:
Matriculados: 3
Media del curso: 6
Mejor alumno: Marta (8)
Aprobados: Marta, Luis
Error: nota fuera de rango
Alumnos creados: 3
Qué conviene copiar de este diseño:
- El curso decide qué se puede hacer con su lista. Ofrece
Matricular,Media,MejoryAprobados, pero no entrega la lista para que cualquiera la modifique. La lista esprivate: desde fuera solo se puede matricular o consultar. - Cada clase se ocupa de lo suyo.
Alumnosabe si está aprobado;Cursosabe calcular la media y buscar al mejor. El programa principal solo orquesta. - Los recorridos son los de siempre (acumular una suma, buscar un máximo, filtrar), solo que ahora recorren objetos y consultan sus propiedades.
- Las reglas del objeto, en el objeto: la nota se valida en el constructor de
Alumno, así un alumno con nota 11 no llega a existir (y no se cuenta enCreados).
Devolver la lista sin regalarla
Si hace falta que otros lean la lista, devuélvela como IReadOnlyList<Alumno> (public IReadOnlyList<Alumno> Todos => _alumnos;): se puede recorrer y consultar, pero no tiene Add ni Remove.
Miembros estáticos¶
Hasta ahora, cada atributo y cada método pertenecía a un objeto concreto. Un miembro estático pertenece a la clase, y es el mismo para todos los objetos. En el ejemplo hay dos:
Creados: un contador compartido que cuenta cuántos alumnos se han creado (no tiene sentido guardarlo en cada alumno).NotaValida: una función de utilidad que no necesita ningún alumno para funcionar.
En C# se marcan con static y se usan con el nombre de la clase: Alumno.Creados, Alumno.NotaValida(5). Una clase estática (static class Utilidades) solo contiene miembros estáticos y no se puede instanciar; es lo que son Math o Console.
No abuses de lo estático
Un atributo estático es estado global: cualquiera puede cambiarlo y los resultados dependen de lo que haya ocurrido antes en el programa, lo que complica las pruebas. Úsalo para contadores sencillos, constantes y utilidades; no para guardar los datos de la aplicación.
Errores frecuentes¶
| Error | Cómo evitarlo |
|---|---|
| Devolver la lista interna para que otros la modifiquen | Ofrece métodos concretos, o devuelve IReadOnlyList<T> o una copia |
Buscar el máximo empezando con un valor «inventado» (como 0) |
Empieza con el primer elemento (y comprueba que la lista no esté vacía) |
| Dividir por el tamaño de una lista vacía | Comprueba antes que hay elementos |
| Poner datos de objetos concretos en un atributo estático | Un atributo estático es para lo que es común a todos |
Dividir dos int y esperar decimales |
(double)suma / total: convierte antes de dividir |
Para practicar¶
Las colecciones de objetos y las relaciones entre clases se practican en U4.3 · POO II. Pasar de uno a otro muestra las mismas ideas en otro lenguaje.