Clases anidadas
En resumen: Una clase anidada es una clase declarada dentro de otra porque solo tiene sentido ahí; en C# ve los miembros privados de la clase externa, pero no está ligada a un objeto externo.
Problema
Una clase que solo tiene sentido en un lugar
En la lección sobre la lista enlazada, Node era una clase aparte. Cualquiera en el programa puede crear nodos, pero solo tienen sentido en la lista. Lo mismo pasa con una línea de pedido:
class OrderLine{ public string Product { get; set; } public int Quantity { get; set; } public double Price { get; set; }}
class Order{ public List<OrderLine> Lines { get; } = new List<OrderLine>();}OrderLine sin Order no tiene sentido. El nombre es largo para que no choque con la línea de una factura, InvoiceLine, ni con la línea de un almacén. Y nada dice que las dos clases van juntas. ¿Puede la línea vivir dentro del pedido?
La idea
Clase anidada
Puede. Una clase declarada dentro de otra clase se llama clase anidada:
class Order{ public class Line { public string Product { get; } public int Quantity { get; } public double Price { get; }
public Line(string product, int quantity, double price) { Product = product; Quantity = quantity; Price = price; }
public double Total() { return Quantity * Price; } }
private List<Line> lines = new List<Line>();
public void Add(Line line) { lines.Add(line); }
public double Total() { double sum = 0; foreach (Line line in lines) { sum += line.Total(); } return sum; }}var order = new Order();order.Add(new Order.Line("Café en grano", 2, 12.5));order.Add(new Order.Line("Taza", 4, 3.0));Console.WriteLine($"Total: {order.Total():F2} €");Total: 37.00 €- Desde fuera, el nombre es
Order.Line. Dentro deOrder, es soloLine. - La línea no está ligada a un pedido concreto.
new Order.Line(...)se crea sin un objetoOrder, como una clase normal. - Una clase anidada sin nada delante de
classesprivate: nadie fuera deOrderpuede usarla. Por eso aquí espublic class Line. Sinpublic, el compilador se detiene ennew Order.Line(...):
class Order{ class Line { }}error CS0122: 'Order.Line' is inaccessible due to its protection levelYa usaste una clase así sin verla: Keys de un Dictionary es un objeto de KeyCollection, una clase anidada en Dictionary:
var stock = new Dictionary<string, int>();Console.WriteLine(stock.Keys.GetType().Name);Console.WriteLine(stock.Keys.GetType().DeclaringType.Name);KeyCollectionDictionary`2Dictionary`2 es el nombre que guarda el entorno de ejecución para Dictionary<TKey, TValue>: 2 es el número de tipos entre < >.
Más precisamente, para curiosos
- En Java una clase así se escribe
static class Liney se llama clase anidada estática. En C#,staticdelante de una clase significa otra cosa: una clase estática es una clase sin objetos, solo con miembrosstatic, comoMath. No puedes usarnewcon ella:
class Order{ public static class Line { }}error CS0712: Cannot create an instance of the static class 'Order.Line'- Toda clase anidada en C# es como la estática en Java. C# no tiene una clase interna ligada a un objeto externo. Lo que se hace en su lugar está en la siguiente tarjeta.
La idea
El objeto anidado guarda al externo
El carrito tiene un descuento para todos los productos que hay en él. Cada producto tiene que conocer el descuento de su propio carrito. En C# una clase anidada no ve al objeto externo, así que el carrito se pasa de forma explícita:
class Cart{ private int discountPercent; private List<Item> items = new List<Item>();
public Cart(int discountPercent) { this.discountPercent = discountPercent; }
public class Item { private Cart cart; public string Name { get; } public double Price { get; }
public Item(Cart cart, string name, double price) { this.cart = cart; Name = name; Price = price; }
public double FinalPrice() { return Price * (100 - cart.discountPercent) / 100; } }
public void Add(string name, double price) { items.Add(new Item(this, name, price)); }
public void Print() { foreach (Item item in items) { Console.WriteLine($"{item.Name}: {item.FinalPrice():F2} €"); } }}var cart = new Cart(10);cart.Add("Auriculares", 80);cart.Add("Funda", 20);cart.Print();Auriculares: 72.00 €Funda: 18.00 €Add pasa this, e Item la guarda en el campo cart. FinalPrice lee el discountPercent del carrito en el que está. El campo es private, pero Item está escrita dentro de Cart, y una clase anidada ve los miembros private de la externa.var cart = new Cart(10);cart.Add("Auriculares", 80);cart.Add("Funda", 20);cart.Print();Paso a paso, en palabras
- El carrito es un objeto Cart con un descuento del 10 %.
- Add crea un Item y le pasa el carrito, this. Además de Name y Price, cada Item guarda en cart una flecha a su propio carrito.
- El segundo Item apunta al mismo carrito.
- FinalPrice de «Auriculares» sigue la flecha cart y lee discountPercent: 80 * (100 - 10) / 100 = 72.
- FinalPrice de «Funda» sigue la flecha cart y lee discountPercent: 20 * (100 - 10) / 100 = 18.
Más precisamente, para curiosos
- Desde fuera, un objeto
Itemse crea con el carrito como primer argumento. Normalmente la clase externa crea las internas ella misma, comoaddaquí:
var cart = new Cart(10);var cable = new Cart.Item(cart, "Cable", 10);Console.WriteLine(cable.FinalPrice());9- El campo
cartes justo lo que hace elCart.thisoculto en Java. Aquí simplemente se ve. - Si la clase anidada no usa los campos de la clase externa, no le des el objeto externo. Así no guarda una referencia que no necesita.
Pruébalo
Dos carritos
La clase Cart es la misma. Un carrito no tiene descuento, y el otro tiene un 20 % para los socios del club.
¿Qué imprimirá el programa?
var regular = new Cart(0);var member = new Cart(20);regular.Add("Libro", 15);member.Add("Libro", 15);regular.Print();member.Print();Cada Item ve el descuento del carrito que lo creó. El libro de regular tiene un 0 %, y el de member, un 20 %.
Pruébalo
Encuentra el error
error CS0120: An object reference is required for the non-static field, method, or property 'Order.currency'¿En qué línea está el problema?
Elige la línea con el error.
currency es un campo de un objeto Order, y Line no tiene pedido. Una clase anidada en C# no está ligada a un objeto externo, así que no hay ningún pedido cuya moneda pueda leer. La corrección es que Line reciba el pedido, new Order.Line(order), lo guarde en un campo y lea order.currency. Que currency sea private no es problema: Line está dentro de Order. Otra forma es hacer currency static, si la moneda es la misma para todos los pedidos.La idea
En lugar de una clase dentro de un método: una tupla
En Java una clase también se puede declarar dentro de un método. Entonces solo se ve ahí y se llama clase local. C# no tiene algo así: una clase dentro de un método no compila. Si un método necesita unos cuantos valores que van juntos solo dentro de él, C# tiene la tupla:
static void PrintBestSeller(string[] names, int[] sold){ (string Name, int Count) best = (names[0], sold[0]); for (int i = 1; i < names.Length; i++) { if (sold[i] > best.Count) { best = (names[i], sold[i]); } } Console.WriteLine($"Más vendido: {best.Name} ({best.Count} vendidos)");}string[] names = { "Café", "Té", "Agua" };int[] sold = { 120, 45, 300 };PrintBestSeller(names, sold);Más vendido: Agua (300 vendidos)(string Name, int Count)es el tipo: dos valores, cada uno con su propio nombre.best.Nameybest.Countse leen como propiedades.(names[i], sold[i])crea una tupla nueva, y se asigna de una vez.- Nadie la usa fuera de
PrintBestSeller. Si los mismos datos hacen falta en otros lugares o tienen comportamiento, es una clase, comoOrder.Line.
La idea
No hay clase anónima
Los descuentos de la tienda son una interfaz:
interface IDiscount{ double Apply(double price);}Hasta ahora creabas una clase para cada descuento: class BlackFriday : IDiscount { ... }. Para un descuento que se usa en un solo lugar, Java tiene la clase anónima: una clase sin nombre, escrita justo ahí, new Discount() { ... }. C# no tiene algo así: new IDiscount() { ... } no compila. Una clase siempre tiene nombre. Si solo hace falta en una clase, va anidada y private, como en esta lección:
class Checkout{ private class BlackFriday : IDiscount { public double Apply(double price) { return price * 70 / 100; } }
private class NoDiscount : IDiscount { public double Apply(double price) { return price; } }
public static IDiscount For(string promo) { if (promo == "black friday") { return new BlackFriday(); } return new NoDiscount(); }}
Console.WriteLine(Checkout.For("black friday").Apply(100));Console.WriteLine(Checkout.For("ninguno").Apply(100));70100BlackFridayyNoDiscountno se ven fuera deCheckout. Desde fuera, lo único que sabes es queFordevuelve unIDiscount.100 * 70 / 100condoublees70. C# imprime undoublesin.0cuando el número es entero.
Más precisamente, para curiosos
- Para una sola acción, como el descuento, C# tiene una forma más corta: un delegado y una lambda. Así no necesitas una clase. Los verás en el módulo «C# moderno».
- C# sí tiene algo con «anónimo» en el nombre: un tipo anónimo.
new { Name = "Agua", Count = 300 }crea un objeto de una clase sin nombre, con propiedades de solo lectura. No tiene métodos y no puede implementar una interfaz, así que no sustituye a la clase anónima:
var sale = new { Name = "Agua", Count = 300 };Console.WriteLine(sale.Name);Console.WriteLine(sale);Agua{ Name = Agua, Count = 300 }La idea
IComparer: ordenar con tu propia regla
Sort ordenaba texto y números. Pero no puede ordenar alumnos:
class Student{ public string Name { get; } public double Grade { get; }
public Student(string name, double grade) { Name = name; Grade = grade; }
public override string ToString() { return $"{Name} {Grade}"; }}var students = new List<Student>{ new Student("Juan", 5.50), new Student("María", 6.00), new Student("Pedro", 4.75), new Student("Ana", 5.50)};students.Sort();InvalidOperationException: Failed to compare two elements in the array.El compilador no protesta, pero al ejecutarlo, el programa se detiene. C# no sabe por qué ordenar a los alumnos: ¿por nombre, por nota, por edad?
La regla se da aparte, como un IComparer. Es una interfaz con un método, Compare(a, b). Devuelve un número negativo si a debe ir antes que b, cero si son iguales y un número positivo si a va después de b. Escribes una clase que la implementa y pasas un objeto de ella a Sort:
class ByGradeDescending : IComparer<Student>{ public int Compare(Student a, Student b) { return b.Grade.CompareTo(a.Grade); }}
students.Sort(new ByGradeDescending());Console.WriteLine(string.Join(", ", students));María 6, Juan 5.5, Ana 5.5, Pedro 4.75b.Grade.CompareTo(a.Grade)compara al revés, así que la nota más alta va primero.- Juan y Ana tienen la misma nota. Aquí se quedaron en el orden en que estaban, pero
List.Sortno lo promete: con más alumnos, los iguales pueden cambiar de lugar. Si el orden importa cuando las notas son iguales, dilo enCompare, como en el ejercicio. string.Joinllama aToStringde cada alumno.- Para texto y números en orden inverso:
Sort()y despuésReverse().
var cities = new List<string> { "Sevilla", "Valencia", "Bilbao" };cities.Sort();cities.Reverse();Console.WriteLine(string.Join(", ", cities));Valencia, Sevilla, BilbaoMás precisamente, para curiosos
IComparer<T>está enSystem.Collections.Generic, con un parámetro de tipo:IComparer<Student>compara alumnos.- El texto y los números se ordenan sin
IComparer, porquestringeintimplementan la interfazIComparable<T>con un métodoCompareTo. Tu clase también puede implementarla, si tiene un orden natural. EntoncesSort()funciona sin nada más:
class City : IComparable<City>{ public string Name { get; } public int Population { get; }
public City(string name, int population) { Name = name; Population = population; }
public int CompareTo(City other) { return Population.CompareTo(other.Population); }
public override string ToString() { return Name; }}
var cities = new List<City> { new City("Sevilla", 1300000), new City("Toledo", 140000), new City("Valencia", 330000) };cities.Sort();Console.WriteLine(string.Join(", ", cities));Toledo, Valencia, SevillaIComparer es para los casos en que hay varios órdenes.
CompareTote protege de errores.a.Grade - b.Gradeni siquiera compila, porque es undouble, yComparedevuelve unint:
class ByGrade : IComparer<Student>{ public int Compare(Student a, Student b) { return a.Grade - b.Grade; }}error CS0266: Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?)Con int la resta compila, pero con números muy grandes se desborda y el signo cambia.
- Una clase entera para una sola comparación es mucho código. En el módulo «C# moderno» la regla se pasará directamente a
Sort, en una línea.
Pruébalo
¿Cuál va primero?
¿Qué imprimirá el programa?
class ByPrice : IComparer<Product>{ public int Compare(Product a, Product b) { return a.Price.CompareTo(b.Price); }}
var products = new List<Product>{ new Product("Monitor", 220), new Product("Ratón", 25), new Product("Teclado", 60)};
products.Sort(new ByPrice());foreach (Product p in products){ Console.WriteLine(p.Name);}a.Price.CompareTo(b.Price) es negativo cuando a es más barato. Así que el más barato va primero: 25, 60, 220. Para poner primero el más caro, cambia a por b.Ejercicio
Clases que van juntas
Resuelve las tareas en tu editor.
1. Carrito de compras. Una clase ShoppingCart con una clase anidada Item (nombre y precio) y los métodos AddItem(Item item), GetTotal() y Count(). En Main, agrega new ShoppingCart.Item("Leche", 2.5), pan por 1,20 € y café por 7,50 €. Imprime el total con :F2.
Productos: 3Total: 11.20 €2. Monitor de un servidor web. Una clase WebServer con una clase anidada Request (dirección y código de estado) y los métodos AddRequest(Request r) y PrintStats(). Las estadísticas dicen cuántas solicitudes hay, cuántas hay de cada código de estado (ordenados) y qué porcentaje son correctas, es decir, con un código del 200 al 299. Guarda los conteos en un SortedDictionary<int, int>: mantiene las claves ordenadas. Las solicitudes son /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Solicitudes: 5200: 3401: 1500: 1Correctas: 60%3. Administrador de un edificio. Una clase Building con una clase anidada Apartment (número, número de habitaciones, propietario) y los métodos AddApartment(Apartment a) y GetApartmentInfo(int number). Guarda los apartamentos en un Dictionary<int, Apartment>. Agrega el 101, de 3 habitaciones, de Fernández, y el 102, de 2 habitaciones, de Pérez. Busca el 101 y el 999.
Apartamento 101: 3 habitaciones, propietario: FernándezNo existe el apartamento 999.4. Clasificación. Ordena a los alumnos de la lección por nota, de la más alta a la más baja. Cuando las notas son iguales, ordénalos por nombre, en orden alfabético. Todo va en un solo IComparer<Student>: si la comparación por nota no es 0, devuélvela; si no, compara los nombres con string.Compare.
María 6Ana 5.5Juan 5.5Pedro 4.75Más tareas
- Configurador de computadoras. Una clase
Computercon las clases anidadasCpu(marca, velocidad en GHz),Ram(GB) yStorage(GB) y los métodosSetCpu,SetRam,SetStorage.ToStringdescribe la configuración:Intel 3.5 GHz, 16 GB RAM, 512 GB. - El planificador de viajes, otra vez. Haz la tarea de la lección sobre
Dictionarycon una clase anidadaTripPlanner.Destinationque guarde el nombre y la lista de actividades. - La red de contactos, otra vez. Mueve
Contactde la tarea de la lección sobreDictionarydentro deContactNetwork. ¿DeberíaContactconocer su red? - Cálculo mental, otra vez. Mueve
Questionde la tarea de la lección sobre listas dentro deMathGame. - Productos de dos formas. Ordena una lista de productos por precio, del más caro, y por nombre cuando el precio es el mismo. Después ordena la misma lista solo por nombre, en orden inverso.
- Tu propio descuento. Escribe tres clases anidadas en
Checkoutque implementenIDiscount: un 10 % a partir de 100 €, 5 € fijos de descuento y «el segundo producto a mitad de precio». Aplícalas a un mismo carrito.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Interfaces · Excepciones · Listas · Dictionary y HashSet
- Continúa en: Lambdas y delegados · LINQ · Patrones de diseño · Tests con xUnit
- Ejercicios sobre el tema: Colecciones