IDS Guía de estudio

Semana 11 · Módulo 10

POO en C# y colecciones fuertemente tipadas

El puente final: el mismo modelo de objetos que construiste en Python, ahora en C#. Verás que el diseño (clases, reglas, herencia, composición) se mantiene; cambia cómo se escribe y cuánto te ayuda el compilador.

Pregunta orientadora¿Es mi diseño de objetos independiente del lenguaje? ¿Qué me da C# que Python no me daba?

Lecturas
TRO 5 · TRO 6 · MSDN
Proyecto
Fase 7: defensa (esta semana)
Evaluación
Control 4 (esta semana)

Cómo usar esta guía

Antes de claseTRO 5 y 6 (sintaxis de C#) y la página de Microsoft Learn sobre colecciones. Luego el checkpoint.
Durante el laboratorioImplementas en C# el modelo de objetos de la semana 8. Usa la tabla de equivalencias.
Control 4Esta semana, individual y sin IA. Lista “puedo…”.
Defensa del proyectoEsta semana. Lee la guía de sustentación y ensaya con tu equipo.
  • Pregunta
  • →
  • Exploración
  • →
  • Implementación
  • →
  • Resultado
  • →
  • Interpretación
  • →
  • Verificación
Sobre ejecutar el código

Los ejemplos de C# no se ejecutan en la página: cópialos a un proyecto y usa dotnet run. Todas las salidas mostradas vienen de ejecutarlos con .NET 8.

Qué puedes usar esta semana

Sí, desde ahora

  • Todo lo de la semana 10
  • class, campos private, constructores, métodos
  • Propiedades: { get; set; }, de solo lectura y con validación
  • public, private, protected
  • Arreglos T[] y colecciones genéricas List<T>
  • Herencia : Base, virtual / override, base(...)
  • throw new ArgumentException(...), try/catch

Todavía no / fuera del curso

  • Interfaces, genéricos propios, LINQ, Dictionary en profundidad → Programación Orientada a Objetos y Estructuras de Datos
  • Bases de datos y aplicaciones gráficas: materias posteriores

Lecturas de preparación

Reading checkpoint

¿Qué equivale en C# al __init__ de Python?

public Cuenta(string titular) { ... } se ejecuta con new Cuenta("Ana").

¿Qué equivale a self?

En C# los campos y métodos del propio objeto se usan directo; this.saldo es opcional salvo que haya un parámetro con el mismo nombre.

Un campo declarado private decimal saldo;…

En Python el guion bajo es un acuerdo; en C# private lo hace cumplir el compilador.

¿Qué es public decimal Saldo { get; private set; }?

Es el equivalente de @property sin setter público.

¿Qué ventaja tiene List<decimal> sobre decimal[]?

Un arreglo se crea con un tamaño que no cambia. List<T> es como la lista de Python, pero solo acepta el tipo T.

Para que una clase hija pueda redefinir CalcularPago(), en la clase base el método debe ser…

En Python cualquier método se puede redefinir; en C# se declara explícitamente la intención con virtual/override.

¿Qué hace : base(nombre, 0) en el constructor de una clase hija?

Es la misma idea de la semana 9 con otra sintaxis.

Clases, campos y propiedades

En palabras simples

La cuenta bancaria de la semana 8 es la misma; solo cambia el idioma. C# te pide decir en voz alta lo que en Python era una convención: qué es privado, qué se puede leer y qué tipo tiene cada cosa.

Equivalencias entre una clase en Python y en C#: constructor, atributo privado, propiedad de solo lectura, método, excepción, herencia y redefinición con override.
Cada idea de la semana 8 tiene su equivalente en C#.

Caso: la CuentaBancaria de la semana 8 en C#

Pregunta

¿Cómo se expresa en C# una cuenta cuyo saldo solo cambia con depósitos y retiros válidos?

Exploración
  • self._saldo + @property → public decimal Saldo { get; private set; }
  • raise ValueError → throw new ArgumentException(...)
  • except ValueError as e → catch (ArgumentException ex)
Implementación
var ana = new Cuenta("Ana", 100m);
ana.Depositar(50m);
ana.Retirar(30m);
Console.WriteLine($"{ana.Titular}: {ana.Saldo:F2}");

try
{
    ana.Retirar(500m);
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Rechazado: {ex.Message}");
}
// ana.Saldo = 1000m;   // no compila: el set es privado (CS0272)


public class Cuenta
{
    private readonly List<string> movimientos = new();

    public string Titular { get; }
    public decimal Saldo { get; private set; }

    public Cuenta(string titular, decimal saldoInicial = 0m)
    {
        if (saldoInicial < 0)
            throw new ArgumentException("el saldo inicial no puede ser negativo");
        Titular = titular;
        Saldo = saldoInicial;
    }

    public void Depositar(decimal monto)
    {
        if (monto <= 0)
            throw new ArgumentException("el depósito debe ser positivo");
        Saldo += monto;
        movimientos.Add($"depósito {monto:F2}");
    }

    public void Retirar(decimal monto)
    {
        if (monto <= 0)
            throw new ArgumentException("el retiro debe ser positivo");
        if (monto > Saldo)
            throw new InvalidOperationException($"saldo insuficiente ({Saldo:F2})");
        Saldo -= monto;
        movimientos.Add($"retiro {monto:F2}");
    }
}
Ana: 120.00
Rechazado: saldo insuficiente (120.00)
Interpretación

Titular { get; } solo se asigna en el constructor. Saldo se lee desde afuera, pero su set es privado: la línea comentada no compilaría. La protección que en Python era una convención aquí la garantiza el compilador.

Verificación

100 + 50 − 30 = 120 ✔; el retiro de 500 se rechaza y el saldo queda en 120. Mismo comportamiento que la clase de la semana 8.

Convenciones de nombres en C#
  • Clases, métodos y propiedades: PascalCase (Depositar, Saldo).
  • Campos privados y variables locales: camelCase (movimientos, saldoInicial).
  • Una clase pública por archivo en proyectos reales (Cuenta.cs).

Arreglos y List<T>

En palabras simples

Un arreglo es una caja de huevos: trae 12 espacios y no más. Una List<T> es una bolsa con etiqueta: crece lo que necesites, pero solo acepta lo que dice la etiqueta.

Un arreglo decimal de C# tiene tamaño fijo de 5 posiciones; una List de decimal crece con Add y solo acepta elementos de ese tipo.
El arreglo tiene tamaño fijo; la lista genérica crece con Add.
decimal[] fijos = { 12.50m, 4.25m, 3.90m };        // arreglo: tamaño fijo
Console.WriteLine($"Arreglo: {fijos.Length} precios, primero {fijos[0]}");

var precios = new List<decimal> { 12.50m, 4.25m };
precios.Add(3.90m);
precios.Add(6.75m);
precios.Remove(4.25m);
precios.Insert(0, 1.99m);
Console.WriteLine($"Lista: {string.Join(", ", precios)} ({precios.Count} elementos)");

decimal total = 0;
foreach (decimal p in precios)
{
    total += p;
}
Console.WriteLine($"Total: {total:F2}  ¿Contiene 3.90? {precios.Contains(3.90m)}");

precios.Sort();
Console.WriteLine($"Ordenados: {string.Join(", ", precios)}");

var productos = new List<Producto>
{
    new Producto("A01", "Aceite", 12.50m, 40),
    new Producto("B07", "Arroz", 4.25m, 8),
};
foreach (var p in productos)
{
    if (p.Stock < 10) Console.WriteLine($"Reordenar {p.Nombre}");
}


public class Producto
{
    public string Codigo { get; }
    public string Nombre { get; }
    public decimal Precio { get; }
    public int Stock { get; set; }

    public Producto(string codigo, string nombre, decimal precio, int stock)
    {
        Codigo = codigo;
        Nombre = nombre;
        Precio = precio;
        Stock = stock;
    }
}
Arreglo: 3 precios, primero 12.50
Lista: 1.99, 12.50, 3.90, 6.75 (4 elementos)
Total: 25.14  ¿Contiene 3.90? True
Ordenados: 1.99, 3.90, 6.75, 12.50
Reordenar Arroz
Python (lista)C# (List<T>)
len(v)v.Count (en arreglos: v.Length)
v.append(x)v.Add(x)
v.insert(0, x)v.Insert(0, x)
v.remove(x)v.Remove(x)
x in vv.Contains(x)
v.sort()v.Sort()
for x in v:foreach (var x in v)

Herencia y polimorfismo en C#

En palabras simples

Es la planilla de la semana 9. La diferencia: en C# la clase base avisa qué se puede redefinir (virtual) y la hija declara que lo redefine (override). Nada pasa por accidente.

Caso: la planilla polimórfica de la semana 9

Pregunta

¿Se puede demostrar que el diseño de la semana 9 es independiente del lenguaje?

Implementación
var planilla = new List<Empleado>
{
    new Empleado("Ana", 900m),
    new PorHora("Luis", 4.5m, 170),
    new Vendedor("Eva", 500m, 12000m),
};

decimal total = 0;
foreach (Empleado e in planilla)          // polimorfismo
{
    Console.WriteLine($"{e.Nombre,-6} {e.GetType().Name,-9} {e.CalcularPago(),9:N2}");
    total += e.CalcularPago();
}
Console.WriteLine($"Total planilla: {total:N2}");


public class Empleado
{
    public string Nombre { get; }
    protected decimal SalarioBase { get; }

    public Empleado(string nombre, decimal salarioBase)
    {
        Nombre = nombre;
        SalarioBase = salarioBase;
    }

    public virtual decimal CalcularPago() => SalarioBase;
}

public class PorHora : Empleado
{
    private readonly decimal tarifa;
    private readonly int horas;

    public PorHora(string nombre, decimal tarifa, int horas) : base(nombre, 0m)
    {
        this.tarifa = tarifa;
        this.horas = horas;
    }

    public override decimal CalcularPago()
    {
        decimal extra = Math.Max(0, horas - 160) * tarifa * 0.5m;
        return horas * tarifa + extra;
    }
}

public class Vendedor : Empleado
{
    private readonly decimal ventas;

    public Vendedor(string nombre, decimal salarioBase, decimal ventas) : base(nombre, salarioBase)
    {
        this.ventas = ventas;
    }

    public override decimal CalcularPago() => base.CalcularPago() + ventas * 0.05m;
}
Ana    Empleado     900.00
Luis   PorHora      787.50
Eva    Vendedor   1,100.00
Total planilla: 2,787.50
Interpretación

: base(nombre, 0m) = super().__init__(nombre, 0). protected: visible para las hijas, no para el resto. => define un método de una sola expresión. La lista es List<Empleado> y guarda objetos de las tres clases: polimorfismo.

Verificación

Mismos números que en Python: 900.00, 787.50, 1,100.00 y total 2,787.50 ✔. Esta equivalencia es lo que el programa llama “demostración de equivalencia semántica entre lenguajes”.

Tabla de traza

¿Compila? ¿Qué pasa?

Con la clase Cuenta de arriba y var c = new Cuenta("X", 100m);. Escribe el saldo final, no compila o el nombre de la excepción.

InstrucciónResultado
c.Depositar(20m);
c.Saldo = 0m;
c.Retirar(100m);
c.Retirar(101m);
c.Depositar(-5m);
c.movimientos.Clear();

Ejercicios

E1 · Estudiante con notas, en C#

clase + List + propiedades · ★★

Traduce la clase Estudiante de la semana 8: lista privada de notas, validación de 0 a 10, Promedio y Aprobado como propiedades calculadas, y ToString() (el __str__ de C#).

Solución
var e = new Estudiante("Carla", "2026-0412");
e.AgregarNota(7.5m);
e.AgregarNota(5.0m);
e.AgregarNota(8.0m);
Console.WriteLine(e);
try { e.AgregarNota(11m); }
catch (ArgumentOutOfRangeException ex) { Console.WriteLine($"Error: {ex.ParamName} fuera de rango"); }


public class Estudiante
{
    private readonly List<decimal> notas = new();
    public string Nombre { get; }
    public string Carnet { get; }

    public Estudiante(string nombre, string carnet)
    {
        Nombre = nombre;
        Carnet = carnet;
    }

    public void AgregarNota(decimal nota)
    {
        if (nota < 0 || nota > 10)
            throw new ArgumentOutOfRangeException(nameof(nota));
        notas.Add(nota);
    }

    public decimal Promedio => notas.Count == 0 ? 0 : Math.Round(notas.Sum() / notas.Count, 2);
    public bool Aprobado => Promedio >= 6;

    public override string ToString() =>
        $"{Carnet} {Nombre}: {Promedio} ({(Aprobado ? "aprobado" : "en riesgo")})";
}
2026-0412 Carla: 6.83 (aprobado)
Error: nota fuera de rango

override string ToString() hace que Console.WriteLine(e) muestre el objeto en términos del negocio. notas.Sum() suma la lista.

E2 · Formas de pago con clase abstracta

abstract + override · ★★★

Traduce el simulador de pagos de la semana 9. La clase base es abstract: declara Comision sin implementarla y obliga a cada hija a hacerlo.

Solución
var metodos = new List<MetodoPago> { new Efectivo(), new Tarjeta(), new Transferencia() };
foreach (var m in metodos)
{
    Console.WriteLine($"{m.GetType().Name,-13} cobra {m.Cobrar(80m):F2}");
}


public abstract class MetodoPago
{
    public abstract decimal Comision(decimal monto);
    public decimal Cobrar(decimal monto) => monto + Comision(monto);
}

public class Efectivo : MetodoPago
{
    public override decimal Comision(decimal monto) => 0m;
}

public class Tarjeta : MetodoPago
{
    public override decimal Comision(decimal monto) => Math.Round(monto * 0.035m, 2);
}

public class Transferencia : MetodoPago
{
    public override decimal Comision(decimal monto) => monto < 100 ? 0.75m : 0m;
}
Efectivo      cobra 80.00
Tarjeta       cobra 82.80
Transferencia cobra 80.75

Si una hija olvida implementar Comision, el programa no compila (en Python, el NotImplementedError aparecía al ejecutar). Otra vez el compilador te cuida.

Práctica intensiva

Ejercicios rápidos con colecciones y herencia en C#, y un problema de modelado completo tipo examen final.

Ejercicios rápidos · ¿Qué imprime en C#?

Si no compila, escribe error.

CódigoTu respuesta
var v = new List<int> { 3, 1, 2 };
v.Add(5);
Console.WriteLine(v.Count);
var v = new List<int> { 3, 1, 2 };
v.Sort();
Console.WriteLine(v[0]);
int[] a = { 4, 8, 15 };
Console.WriteLine(a.Length);
var v = new List<string> { "a" };
v.Add(5);
var v = new List<int> { 10, 20 };
Console.WriteLine(v.Contains(20));
A x = new B();
Console.WriteLine(x.F());

class A { public virtual string F() => "A"; }
class B : A { public override string F() => "B"; }

Problema tipo examen

P1 · Inventario orientado a objetos en C#

composición + List<T> + excepciones · ★★★★

Clase Producto (stock que solo baja con ventas válidas) y clase Inventario que contiene una List<Producto>: agregar (sin códigos repetidos), buscar, vender, valor total y lista de productos por reordenar.

Tu programa debe cumplir estos casos de prueba:

SituaciónEsperado
Vender 5 de A01stock A01 = 35
Vender 20 de B07 (hay 8)InvalidOperationException
Agregar otro A01ArgumentException
Valor471.50
Por reordenar (< 10)Arroz (8), Azúcar (0)
Pista

Buscar devuelve Producto? (puede ser null, como el None de Python). ?? da un valor alternativo si lo de la izquierda es null.

Solución
var inventario = new Inventario();
inventario.Agregar(new Producto("A01", "Aceite 1 L", 12.50m, 40));
inventario.Agregar(new Producto("B07", "Arroz 5 lb", 4.25m, 8));
inventario.Agregar(new Producto("C12", "Azúcar 5 lb", 3.90m, 0));

inventario.Vender("A01", 5);
try { inventario.Vender("B07", 20); }
catch (InvalidOperationException ex) { Console.WriteLine($"Rechazado: {ex.Message}"); }
try { inventario.Agregar(new Producto("A01", "Duplicado", 1m, 1)); }
catch (ArgumentException ex) { Console.WriteLine($"Rechazado: {ex.Message}"); }

Console.WriteLine($"Valor del inventario: {inventario.Valor():N2}");
foreach (var p in inventario.PorReordenar(10))
    Console.WriteLine($"Reordenar: {p.Nombre} ({p.Stock})");


public class Producto
{
    public string Codigo { get; }
    public string Nombre { get; }
    public decimal Precio { get; }
    public int Stock { get; private set; }

    public Producto(string codigo, string nombre, decimal precio, int stock)
    {
        if (precio <= 0) throw new ArgumentException("precio inválido");
        Codigo = codigo; Nombre = nombre; Precio = precio; Stock = stock;
    }

    public void Vender(int cantidad)
    {
        if (cantidad <= 0 || cantidad > Stock)
            throw new InvalidOperationException($"no hay {cantidad} de {Nombre} (quedan {Stock})");
        Stock -= cantidad;
    }
}

public class Inventario
{
    private readonly List<Producto> productos = new();

    public void Agregar(Producto p)
    {
        if (Buscar(p.Codigo) != null)
            throw new ArgumentException($"código repetido: {p.Codigo}");
        productos.Add(p);
    }

    public Producto? Buscar(string codigo)
    {
        foreach (var p in productos)
            if (p.Codigo == codigo) return p;
        return null;
    }

    public void Vender(string codigo, int cantidad)
    {
        Producto p = Buscar(codigo) ?? throw new ArgumentException($"no existe {codigo}");
        p.Vender(cantidad);
    }

    public decimal Valor()
    {
        decimal total = 0;
        foreach (var p in productos) total += p.Precio * p.Stock;
        return total;
    }

    public List<Producto> PorReordenar(int minimo)
    {
        var resultado = new List<Producto>();
        foreach (var p in productos)
            if (p.Stock < minimo) resultado.Add(p);
        return resultado;
    }
}
Rechazado: no hay 20 de Arroz 5 lb (quedan 8)
Rechazado: código repetido: A01
Valor del inventario: 471.50
Reordenar: Arroz 5 lb (8)
Reordenar: Azúcar 5 lb (0)

Verificación: 35 × 12.50 + 8 × 4.25 + 0 = 437.50 + 34 = 471.50 ✔.

Guía de la defensa del proyecto

La sustentación es en esta semana: 15 minutos por equipo, 8 de demostración en vivo y 7 de preguntas individuales sobre el código. Vale el 25 % de la nota del proyecto, y quien no pueda explicar su código puede perder la nota del proyecto.

Antes del día

Guion sugerido para los 8 minutos

  1. 1 min · El problema de negocio y quién usa el sistema.
  2. 3 min · Demostración del flujo principal con datos reales, incluyendo un error manejado (dato inválido).
  3. 2 min · Arquitectura: clases, módulos y por qué están así (diagrama o README).
  4. 1 min · Pruebas corriendo en vivo.
  5. 1 min · Valor para el negocio: qué automatiza, qué errores humanos evita, qué decisión mejora.

Banco de preguntas de todo el ciclo

¿Qué hace esta línea? (señalan cualquier línea)

Explica la línea en términos del negocio y del lenguaje: qué variable cambia, por qué, y qué pasaría sin ella.

¿Qué pasa si el usuario escribe texto donde va un número?

Muéstralo en vivo: el try/except ValueError o la validación, y el mensaje que ve el usuario.

¿Cómo garantizan que ese bucle termina?

Señala la variable que cambia en cada vuelta y la condición de salida (semana 3).

¿Por qué esa función devuelve en vez de imprimir?

Para poder probarla y reutilizarla; la interfaz decide cómo mostrar (semana 4).

¿Qué estructura de datos usaron para X y por qué?

Lista, tupla, diccionario o conjunto según el uso (semanas 5 y 7).

¿Qué pasa si borro el archivo de datos?

Se atrapa FileNotFoundError y se empieza vacío o con datos de ejemplo (semana 7).

¿Por qué esto es una clase? ¿Qué invariante protege?

Datos + reglas + acciones juntos; muestra la validación y la prueba que la ataca (semana 8).

¿Herencia o composición? ¿Por qué?

“Es un” vs. “tiene un” (semana 9).

¿Qué partes hizo la IA y cómo lo verificaron?

La bitácora con casos concretos y las pruebas que respaldan cada corrección (Fase 6).

Si lo tuvieran que pasar a C#, ¿qué cambiaría?

Tipos declarados, decimal para dinero, propiedades, List<T>, virtual/override: el diseño se mantiene (semanas 10 y 11).

Control 4 y rumbo al Final

El Control 4 es esta semana y el Examen Final la próxima (lógica en Python y fundamentos formales de C#). De esta semana:

Fuentes

  • [TRO] Troelsen, A. y Japikse, P. (2022). Pro C# 10 with .NET 6, 11.ª ed. Caps. 5 y 6.
  • [MSDN] Microsoft Learn. Clases · Colecciones.