En esta página

Clases y objetos en C# 14

15 min lectura TextoCap. 2 — OOP en C#

Clases en C#

Una clase es un molde para crear objetos. Define la estructura (propiedades/campos) y el comportamiento (métodos) que tendrán las instancias creadas a partir de ella.

// Definición mínima de una clase
public class Persona
{
    public string Nombre { get; set; } = "";
    public int Edad { get; set; }
}

// Crear instancias
var p1 = new Persona { Nombre = "David", Edad = 30 };
var p2 = new Persona();
p2.Nombre = "María";
p2.Edad = 25;

Modificadores de acceso

Controlan desde dónde se puede acceder a un miembro:

Modificador Accesible desde
public En cualquier lugar
private Solo dentro de la misma clase
protected Clase + clases derivadas
internal Dentro del mismo ensamblado
protected internal Ensamblado o clases derivadas
private protected Clase misma + derivadas en el mismo ensamblado

Por defecto, los miembros de clase son private y las clases son internal.

Propiedades

Las propiedades en C# encapsulan los campos y permiten agregar validación:

public class CuentaBancaria
{
    // Propiedad auto-implementada — el compilador genera el campo
    public string Titular { get; set; } = string.Empty;

    // Propiedad con validación personalizada
    private decimal _saldo;
    public decimal Saldo
    {
        get => _saldo;
        private set   // solo se puede establecer desde dentro de la clase
        {
            if (value < 0)
                throw new InvalidOperationException("Saldo no puede ser negativo");
            _saldo = value;
        }
    }

    // Propiedad calculada (sin campo de respaldo)
    public bool TieneSaldo => _saldo > 0;

    // Propiedad con init — solo asignable en construcción
    public string NumeroCuenta { get; init; } = Guid.NewGuid().ToString();

    public CuentaBancaria(string titular, decimal saldoInicial)
    {
        Titular = titular;
        Saldo   = saldoInicial;
    }

    public void Depositar(decimal monto)
    {
        if (monto <= 0) throw new ArgumentException("El monto debe ser positivo");
        Saldo += monto;
    }

    public void Retirar(decimal monto)
    {
        if (monto > Saldo) throw new InvalidOperationException("Fondos insuficientes");
        Saldo -= monto;
    }
}

Constructores

Un constructor inicializa el objeto al crearlo:

public class Conexion
{
    public string Host    { get; }
    public int    Puerto  { get; }
    public bool   UsarSsl { get; }

    // Constructor principal
    public Conexion(string host, int puerto, bool usarSsl = true)
    {
        Host    = host;
        Puerto  = puerto;
        UsarSsl = usarSsl;
    }

    // Constructor de conveniencia — llama al principal con :this()
    public Conexion(string host) : this(host, 5432) { }

    // Constructor estático — se ejecuta una sola vez cuando se carga el tipo
    static Conexion()
    {
        Console.WriteLine("Tipo Conexion inicializado");
    }
}

var conn1 = new Conexion("localhost", 1433, false);
var conn2 = new Conexion("db.bemorex.com"); // usa puerto 5432 y SSL

Constructores primarios (C# 12+)

La sintaxis más concisa para clases simples:

// Sin constructor primario (C# clásico)
public class PuntoClasico
{
    public double X { get; }
    public double Y { get; }
    public PuntoClasico(double x, double y) { X = x; Y = y; }
}

// Con constructor primario (C# 12+)
public class Punto(double x, double y)
{
    public double X { get; } = x;
    public double Y { get; } = y;

    // Los parámetros x e y están disponibles en todo el cuerpo
    public double DistanciaOrigen() => Math.Sqrt(x * x + y * y);

    public override string ToString() => $"({x:F2}, {y:F2})";
}

var p = new Punto(3.0, 4.0);
Console.WriteLine(p);                          // (3.00, 4.00)
Console.WriteLine(p.DistanciaOrigen());        // 5

Miembros estáticos

Los miembros estáticos pertenecen a la clase, no a las instancias:

public class Contador
{
    private static int _total = 0;
    public static int Total => _total;

    public int Id { get; }

    public Contador()
    {
        _total++;
        Id = _total;
    }

    // Método de fábrica estático
    public static Contador Crear() => new Contador();

    // Método de utilidad estático
    public static void Reiniciar() => _total = 0;
}

var c1 = Contador.Crear();
var c2 = Contador.Crear();
var c3 = Contador.Crear();
Console.WriteLine(Contador.Total); // 3

Inicializadores de objeto

La sintaxis {} para inicializar propiedades sin constructor explícito:

public class DireccionPostal
{
    public string Calle    { get; set; } = string.Empty;
    public string Ciudad   { get; set; } = string.Empty;
    public string Pais     { get; set; } = "Bolivia";
    public string CodigoPostal { get; set; } = string.Empty;
}

// Inicializador de objeto
var dir = new DireccionPostal
{
    Calle  = "Av. 6 de Agosto 123",
    Ciudad = "Oruro",
    // Pais usa el valor por defecto "Bolivia"
    CodigoPostal = "OR-001"
};

// Objeto con init — solo inicializable así, inmutable después
public record Config
{
    public string ApiUrl   { get; init; } = string.Empty;
    public int    Timeout  { get; init; } = 30;
    public bool   DebugMode { get; init; }
}

var cfg = new Config { ApiUrl = "https://api.bemorex.com", Timeout = 60 };
// cfg.Timeout = 90; // ← Error de compilación: init-only property

Clases parciales

Permiten dividir una clase en múltiples archivos:

// Archivo: Pedido.cs
public partial class Pedido
{
    public int     Id       { get; set; }
    public decimal Total    { get; set; }
    public string  Cliente  { get; set; } = string.Empty;
}

// Archivo: Pedido.Validacion.cs
public partial class Pedido
{
    public bool EsValido() => Total > 0 && !string.IsNullOrEmpty(Cliente);
    public void Validar()
    {
        if (!EsValido())
            throw new InvalidOperationException("Pedido inválido");
    }
}

Clases sealed y abstract

// sealed — no se puede heredar
public sealed class Singleton
{
    private static readonly Singleton _instancia = new();
    public static Singleton Instancia => _instancia;
    private Singleton() { }
}

// abstract — no se puede instanciar directamente
public abstract class Shape
{
    public abstract double Area();
    public abstract double Perimetro();

    // Método concreto disponible para subclases
    public string Describir() => $"Área: {Area():F2}, Perímetro: {Perimetro():F2}";
}

Práctica

  1. Clase BancoCuenta: Implementa una clase con propiedades Titular, Saldo (validado, no negativo) y NumeroCuenta (init-only). Agrega métodos Depositar y Retirar con validaciones.
  2. Constructor primario: Reescribe Punto(double x, double y) con constructor primario y agrega un método MoverA(double dx, double dy) que retorne un nuevo Punto.
  3. Miembro estático: Agrega un contador estático a tu clase BancoCuenta para rastrear cuántas cuentas se han creado.

En la siguiente lección veremos herencia, clases abstractas, interfaces y cómo C# implementa el polimorfismo.

Usa init para inmutabilidad parcial
La palabra clave init permite asignar una propiedad solo durante la construcción del objeto (en el constructor o en un inicializador de objeto {}). Después de construido, la propiedad es de solo lectura, lo que previene mutaciones accidentales.
Constructores primarios (C# 12+)
Los constructores primarios declaran los parámetros directamente en la firma de la clase. Son ideales para clases simples tipo DTO o Value Object. Los parámetros están disponibles en todo el cuerpo de la clase como campos capturados.
Clases sealed para performance
Declara tus clases como sealed cuando no estén diseñadas para heredarse. El JIT puede hacer optimizaciones de despacho de métodos virtuales cuando sabe que no hay subclases. Además, comunica la intención de diseño.
csharp
// Clase completa con todas las características modernas de C# 14
public class Producto
{
    // Propiedades auto-implementadas
    public int    Id      { get; init; }   // init: solo en constructor/inicializador
    public string Nombre  { get; set; } = string.Empty;
    public decimal Precio { get; set; }

    // Propiedad calculada (computed)
    public decimal PrecioConIVA => Precio * 1.13m;

    // Campo privado con propiedad de validación
    private int _stock;
    public int Stock
    {
        get => _stock;
        set
        {
            if (value < 0) throw new ArgumentException("Stock no puede ser negativo");
            _stock = value;
        }
    }

    // Miembro estático
    public static int TotalCreados { get; private set; }

    // Constructor
    public Producto(int id, string nombre, decimal precio, int stock)
    {
        Id     = id;
        Nombre = nombre;
        Precio = precio;
        Stock  = stock;
        TotalCreados++;
    }

    // Sobrescribir ToString
    public override string ToString()
        => $"[{Id}] {Nombre} — ${Precio:F2} (stock: {Stock})";
}

// Constructor primario (C# 12+)
public class Categoria(int id, string nombre)
{
    public int    Id     { get; } = id;
    public string Nombre { get; } = nombre;

    public override string ToString() => $"Categoría: {Nombre}";
}

// Uso
var prod = new Producto(1, "Laptop Pro", 1299.99m, 10);
Console.WriteLine(prod);
Console.WriteLine($"IVA incluido: ${prod.PrecioConIVA:F2}");
Console.WriteLine($"Productos creados: {Producto.TotalCreados}");

var cat = new Categoria(1, "Electrónica");
Console.WriteLine(cat);