4.B Encapsulamiento¶
Encapsular es ocultar el estado interno de un objeto y dejar solo unas operaciones controladas para usarlo. Así el objeto se encarga de que sus datos siempre tengan sentido: un precio nunca es negativo, un stock nunca baja de cero.
Sin encapsulamiento, cualquiera podría escribir producto.Precio = -5 y el error aparecería mucho más tarde, lejos de donde se produjo. Con él, el fallo salta justo al intentar el cambio incorrecto.
Visibilidad¶
C# tiene private (solo la clase), protected (la clase y sus hijas), internal (el proyecto) y public. Lo que se escribe sin modificador dentro de una clase es private. Lo habitual es que los datos que no deban tocarse desde fuera sean private o tengan un private set.
Acceso controlado y validación¶
var p = new Producto("Cuaderno", 3, 10);
Console.WriteLine($"{p.Nombre}: {p.Precio} € x {p.Stock} = {p.ValorStock} €");
p.Vender(4);
Console.WriteLine($"tras vender 4, quedan {p.Stock}");
try
{
p.Vender(20);
}
catch (InvalidOperationException e)
{
Console.WriteLine($"Error: {e.Message}");
}
try
{
p.Precio = -1;
}
catch (ArgumentException e)
{
Console.WriteLine($"Error: {e.Message}");
}
p.Precio = 7;
Console.WriteLine($"nuevo valor del stock: {p.ValorStock} €");
try
{
new Producto("Roto", -2, 1);
}
catch (ArgumentException e)
{
Console.WriteLine($"Error: {e.Message}");
}
class Producto
{
private int _precio; // el campo que guarda el valor
public string Nombre { get; }
public int Stock { get; private set; } // se lee desde fuera, pero solo cambia desde dentro
public int Precio
{
get => _precio;
set
{
if (value < 0)
{
throw new ArgumentException("el precio no puede ser negativo");
}
_precio = value;
}
}
public int ValorStock => Precio * Stock; // propiedad calculada: no guarda nada
public Producto(string nombre, int precio, int stock)
{
Nombre = nombre;
Precio = precio; // pasa por el set: se valida igual que desde fuera
Stock = stock;
}
public void Vender(int cantidad)
{
if (cantidad > Stock)
{
throw new InvalidOperationException("no hay stock suficiente");
}
Stock -= cantidad;
}
}
Salida:
Cuaderno: 3 € x 10 = 30 €
tras vender 4, quedan 6
Error: no hay stock suficiente
Error: el precio no puede ser negativo
nuevo valor del stock: 42 €
Error: el precio no puede ser negativo
Qué hace cada pieza:
- Atributos protegidos (
Precio,Stock): no se pueden cambiar desde fuera sin pasar por el código de la clase. - Validar al crear y al modificar: el constructor asigna
Precio = precio, que pasa por elset, así que una sola comprobación cubre las dos puertas de entrada. Se lanza una excepción si el valor no es válido (ver 2.3 Excepciones). - Solo lectura: el stock se puede consultar, pero solo
Venderlo modifica. - Propiedad calculada:
ValorStockno se guarda; se calcula cada vez a partir dePrecioyStock, así nunca queda desactualizada.
Una propiedad { get; set; } sin más código ya es un getter y un setter automáticos. Cuando necesitas validar, escribes el set tú y guardas el valor en un campo privado (_precio).
El nuevo field de C# 14
Desde C# 14 (.NET 10) no hace falta declarar el campo privado a mano: dentro de un get o un set, la palabra field se refiere al valor guardado.
var p = new Producto { Precio = 5 };
Console.WriteLine(p.Precio);
try
{
p.Precio = -1;
}
catch (ArgumentException e)
{
Console.WriteLine($"Error: {e.Message}");
}
class Producto
{
public int Precio
{
get;
set
{
if (value < 0) throw new ArgumentException("el precio no puede ser negativo");
field = value;
}
}
}
Salida:
5
Error: el precio no puede ser negativo
Valida antes de modificar
Fíjate en Vender: primero comprueba que hay stock y solo después resta. Si la comprobación falla, el objeto queda como estaba. Modificar primero y validar después deja objetos a medias.
Propiedades de solo escritura al crear: init y required¶
Para datos que solo se fijan al crear el objeto y no cambian después, existe init. Con required, el compilador obliga a darles valor:
var u = new Usuario { Nombre = "Ana", Edad = 20 };
Console.WriteLine($"{u.Nombre}, {u.Edad}");
class Usuario
{
public required string Nombre { get; init; }
public int Edad { get; init; }
}
Salida:
Ana, 20
u.Nombre = "Eva" después de crear el objeto no compila (es init), y new Usuario { Edad = 20 } tampoco (falta el required).
Igualdad: identidad frente a valor¶
Dos objetos pueden ser el mismo (la misma referencia) o ser iguales (tener el mismo contenido). Por defecto, == en una clase solo reconoce lo primero; para que dos puntos con las mismas coordenadas sean iguales hay que decírselo.
Console.WriteLine($"sin igualdad por valor: {(new PuntoSimple(1, 2) == new PuntoSimple(1, 2) ? "sí" : "no")}");
Console.WriteLine($"con igualdad por valor: {(new Punto(1, 2) == new Punto(1, 2) ? "sí" : "no")}");
var conjunto = new HashSet<Punto> { new(1, 2), new(1, 2), new(3, 4) };
Console.WriteLine($"puntos distintos en el conjunto: {conjunto.Count}");
Console.WriteLine(new Punto(1, 2));
class PuntoSimple(int x, int y)
{
public int X { get; } = x;
public int Y { get; } = y;
}
record Punto(int X, int Y);
Salida:
sin igualdad por valor: no
con igualdad por valor: sí
puntos distintos en el conjunto: 2
Punto { X = 1, Y = 2 }
Un record genera solo Equals, GetHashCode, ToString y la copia con cambios (p with { X = 5 }), comparando todas sus propiedades. En una clase normal, == compara referencias.
Por la misma razón, en un conjunto o como clave de un mapa, la igualdad por valor decide si dos objetos son «el mismo elemento» (ver 3.3 Conjuntos).
Errores frecuentes¶
| Error | Cómo evitarlo |
|---|---|
| Campos públicos modificables desde cualquier sitio | Hazlos privados y expón propiedades o métodos |
Un set que acepta cualquier valor |
Valida y lanza una excepción si no es correcto |
Ofrecer set para todo «por si acaso» |
Solo lo que de verdad deba poder cambiar: lo demás, get o init |
Validar en el constructor pero no en el set (o al revés) |
Que el constructor pase por el set, como en el ejemplo |
Sobrescribir Equals sin GetHashCode (o al revés) |
Siempre juntos y con los mismos campos; o usar un record |
Para practicar¶
Los ejercicios de validación y atributos de solo lectura están en U4.2 · POO I. Para ver cómo se escribe en otro lenguaje: Pasar de uno a otro.