Вложени класове
Накратко: Вложеният клас е клас, обявен вътре в друг клас, защото има смисъл само там; в C# той вижда private членовете на външния, но не е вързан за негов обект.
Проблем
Клас, който има смисъл само на едно място
В урока за свързания списък Node беше отделен клас. Всеки в програмата може да прави възли, а те имат смисъл само в списъка. Същото е с реда от поръчката:
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 без Order е безсмислен. Името е дълго, за да не се сблъска с реда от фактурата, InvoiceLine, или с реда от склада. А и нищо не казва, че двата класа вървят заедно. Може ли редът да живее вътре в поръчката?
Идеята
Вложен клас
Може. Клас, обявен вътре в друг клас, се казва вложен клас (nested class):
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("Кафе на зърна", 2, 12.5));order.Add(new Order.Line("Чаша", 4, 3.0));Console.WriteLine($"Общо: {order.Total():F2} €");Общо: 37.00 €- Отвън името е
Order.Line. Вътре вOrderе самоLine. - Редът не е вързан за конкретна поръчка.
new Order.Line(...)се прави без обектOrder, като обикновен клас. - Вложеният клас без нищо пред
classеprivate: никой извънOrderне може да го ползва. Затова тук еpublic class Line. Безpublicкомпилаторът спира наnew Order.Line(...):
class Order{ class Line { }}error CS0122: 'Order.Line' is inaccessible due to its protection levelВече ползва такъв клас, без да го видиш: Keys на Dictionary е обект от KeyCollection, клас, вложен в Dictionary:
var stock = new Dictionary<string, int>();Console.WriteLine(stock.Keys.GetType().Name);Console.WriteLine(stock.Keys.GetType().DeclaringType.Name);KeyCollectionDictionary`2Dictionary`2 е името, под което средата пази Dictionary<TKey, TValue>: 2 е броят на типовете в < >.
По-точно, за любопитните
- В Java такъв клас се пише
static class Lineи се казва статичен вложен клас. В C#staticпред клас значи друго: статичен клас е клас без обекти, само съсstaticчленове, катоMath. От негоnewне може:
class Order{ public static class Line { }}error CS0712: Cannot create an instance of the static class 'Order.Line'- Всеки вложен клас в C# е като статичния в Java. Вътрешен клас, вързан за обект отвън, C# няма. Какво се прави вместо него, е в следващата карта.
Идеята
Вложеният обект пази външния
Количката има отстъпка за всички продукти в нея. Всеки продукт трябва да знае отстъпката на своята количка. В C# вложеният клас не вижда обекта отвън, затова количката се подава изрично:
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("Слушалки", 80);cart.Add("Калъф", 20);cart.Print();Слушалки: 72.00 €Калъф: 18.00 €Add подава this, а Item я пази в полето cart. FinalPrice чете discountPercent на количката, в която е. Полето е private, но Item е написан вътре в Cart, а вложеният клас вижда private членовете на външния.var cart = new Cart(10);cart.Add("Слушалки", 80);cart.Add("Калъф", 20);cart.Print();Стъпка по стъпка, с думи
- Количката е обект Cart с отстъпка 10%.
- Add създава Item и му подава количката, this. Освен Name и Price, всеки Item пази в cart стрелка към своята количка.
- Вторият Item сочи към същата количка.
- FinalPrice на „Слушалки“ минава по стрелката cart и чете discountPercent: 80 * (100 - 10) / 100 = 72.
- FinalPrice на „Калъф“ минава по стрелката cart и чете discountPercent: 20 * (100 - 10) / 100 = 18.
По-точно, за любопитните
- Отвън обект
Itemсе създава с количката като първи аргумент. Обикновено външният клас сам създава вътрешните, катоaddтук:
var cart = new Cart(10);var cable = new Cart.Item(cart, "Кабел", 10);Console.WriteLine(cable.FinalPrice());9- Полето
cartе точно това, което в Java прави скритотоCart.this. Тук то просто се вижда. - Ако вложеният клас не ползва полетата на външния, не му давай външния обект. Така не пази излишна референция.
Пробвай
Две колички
Класът Cart е същият. Едната количка е без отстъпка, а другата е с 20% за членове на клуба.
Какво ще отпечата програмата?
var regular = new Cart(0);var member = new Cart(20);regular.Add("Книга", 15);member.Add("Книга", 15);regular.Print();member.Print();Всеки Item вижда отстъпката на количката, която го е създала. Книгата в regular е с 0%, а тази в member е с 20%.
Пробвай
Намери грешката
error CS0120: An object reference is required for the non-static field, method, or property 'Order.currency'На кой ред е проблемът?
Избери реда с грешката.
currency е поле на обект Order, а Line няма поръчка. Вложеният клас в C# не е вързан за обект отвън, затова няма поръчка, чиято валута да прочете. Поправката е Line да получи поръчката, new Order.Line(order), да я пази в поле и да чете order.currency. Това, че currency е private, не пречи: Line е вътре в Order. Друг начин е currency да стане static, ако валутата е една за всички поръчки.Идеята
Вместо клас в метод: кортеж
В Java клас може да се обяви и в метод. Тогава се вижда само там и се казва локален клас. В C# такъв няма: клас в метод не се компилира. Ако на метода му трябват няколко стойности, които вървят заедно само в него, C# има кортеж (tuple):
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($"Най-продаван: {best.Name} ({best.Count} бр.)");}string[] names = { "Кафе", "Чай", "Вода" };int[] sold = { 120, 45, 300 };PrintBestSeller(names, sold);Най-продаван: Вода (300 бр.)(string Name, int Count)е типът: две стойности, всяка със свое име.best.Nameиbest.Countсе четат като свойства.(names[i], sold[i])прави нов кортеж, и той се присвоява наведнъж.- Извън
PrintBestSellerникой не го ползва. Ако същите данни трябват и другаде или имат поведение, те са клас, катоOrder.Line.
Идеята
Анонимен клас няма
Отстъпките в магазина са интерфейс:
interface IDiscount{ double Apply(double price);}Досега за всяка отстъпка правеше клас: class BlackFriday : IDiscount { ... }. В Java за отстъпка, която се ползва само на едно място, има анонимен клас: клас без име, написан точно там, new Discount() { ... }. В C# такъв няма: new IDiscount() { ... } не се компилира. Класът винаги е с име. Ако е нужен само в един клас, той е вложен и private, като в този урок:
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 == "черен петък") { return new BlackFriday(); } return new NoDiscount(); }}
Console.WriteLine(Checkout.For("черен петък").Apply(100));Console.WriteLine(Checkout.For("без").Apply(100));70100BlackFridayиNoDiscountне се виждат извънCheckout. Отвън се знае само, чеForвръщаIDiscount.100 * 70 / 100сdoubleе70. C# печатаdoubleбез.0, когато числото е цяло.
По-точно, за любопитните
- За едно-единствено действие, като отстъпката, C# има по-кратък запис: делегат и ламбда. Тогава клас не трябва. Ще ги видиш в модул „Модерен C#“.
- Нещо с „анонимен“ в името C# има: анонимен тип.
new { Name = "Вода", Count = 300 }прави обект от клас без име само със свойства за четене. Методи няма и интерфейс не може да изпълни, затова не замества анонимния клас:
var sale = new { Name = "Вода", Count = 300 };Console.WriteLine(sale.Name);Console.WriteLine(sale);Вода{ Name = Вода, Count = 300 }Идеята
IComparer: подреждане по свое правило
Sort подреди текст и числа. Ученици обаче не може да подреди:
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("Иван", 5.50), new Student("Мария", 6.00), new Student("Петър", 4.75), new Student("Ана", 5.50)};students.Sort();InvalidOperationException: Failed to compare two elements in the array.Компилаторът не възразява, но при пускане програмата спира. C# не знае по какво да подреди ученици: по име, по успех, по възраст?
Правилото се дава отделно, като IComparer. Това е интерфейс с един метод, Compare(a, b). Той връща отрицателно число, ако a трябва да е преди b, нула, ако са равни, и положително, ако a е след b. Пишеш клас, който го изпълнява, и подаваш обект от него на 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));Мария 6, Иван 5.5, Ана 5.5, Петър 4.75b.Grade.CompareTo(a.Grade)сравнява наобратно, затова най-високият успех е пръв.- Иван и Ана са с еднакъв успех. Тук останаха в реда, в който бяха, но
List.Sortне го обещава: при повече ученици равните може да се разместят. Ако редът при равен успех е важен, кажи го вCompare, както в упражнението. string.JoinвикаToStringна всеки ученик.- За текст и числа в обратен ред:
Sort()и послеReverse().
var cities = new List<string> { "Варна", "София", "Пловдив" };cities.Sort();cities.Reverse();Console.WriteLine(string.Join(", ", cities));София, Пловдив, ВарнаПо-точно, за любопитните
IComparer<T>е вSystem.Collections.Generic, с типов параметър:IComparer<Student>сравнява ученици.- Текстът и числата се подреждат без
IComparer, защотоstringиintизпълняват интерфейсаIComparable<T>с методCompareTo. И твоят клас може да го изпълни, ако има един естествен ред. ТогаваSort()работи без нищо:
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("София", 1300000), new City("Русе", 140000), new City("Варна", 330000) };cities.Sort();Console.WriteLine(string.Join(", ", cities));Русе, Варна, СофияIComparer е за случаите, в които редовете са няколко.
CompareToпази от грешки.a.Grade - b.Gradeдори не се компилира, защото еdouble, аCompareвръщаint:
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?)С int изваждането се компилира, но при много големи числа препълва и знакът се обръща.
- Цял клас за едно сравнение е много код. В модул „Модерен C#“ правилото ще се подава на
Sortнаправо, на един ред.
Пробвай
Кое е първо?
Какво ще отпечата програмата?
class ByPrice : IComparer<Product>{ public int Compare(Product a, Product b) { return a.Price.CompareTo(b.Price); }}
var products = new List<Product>{ new Product("Монитор", 220), new Product("Мишка", 25), new Product("Клавиатура", 60)};
products.Sort(new ByPrice());foreach (Product p in products){ Console.WriteLine(p.Name);}a.Price.CompareTo(b.Price) е отрицателно, когато a е по-евтин. Затова по-евтиният е преди: 25, 60, 220. За най-скъпото отпред размени a и b.Упражнение
Класове, които вървят заедно
Реши задачите в редактора си.
1. Количка за пазаруване. Клас ShoppingCart с вложен клас Item (име и цена) и методи AddItem(Item item), GetTotal() и Count(). В Main добави new ShoppingCart.Item("Мляко", 2.5), хляб за 1,20 € и кафе за 7,50 €. Сумата печатай с :F2.
Продукти: 3Общо: 11.20 €2. Монитор на уеб сървър. Клас WebServer с вложен клас Request (адрес и статус код) и методи AddRequest(Request r) и PrintStats(). Статистиката казва колко са заявките, колко са от всеки статус код (подредени) и какъв процент са успешни, тоест с код от 200 до 299. Броевете пази в SortedDictionary<int, int>: той държи ключовете подредени. Заявките са /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Заявки: 5200: 3401: 1500: 1Успешни: 60%3. Сграден мениджър. Клас Building с вложен клас Apartment (номер, брой стаи, собственик) и методи AddApartment(Apartment a) и GetApartmentInfo(int number). Пази апартаментите в Dictionary<int, Apartment>. Добави 101 с 3 стаи на Иванова и 102 с 2 стаи на Петров. Потърси 101 и 999.
Апартамент 101: 3 стаи, собственик ИвановаНяма апартамент 999.4. Класиране. Подреди учениците от урока по успех, от най-високия надолу. При равен успех ги подреди по име, по азбучен ред. Всичко е в един IComparer<Student>: ако сравнението по успех не е 0, върни него, иначе сравни имената със string.Compare.
Мария 6Ана 5.5Иван 5.5Петър 4.75Още задачи
- Конфигуратор на компютър. Клас
Computerс вложени класовеCpu(марка, честота в GHz),Ram(GB) иStorage(GB) и методиSetCpu,SetRam,SetStorage.ToStringописва конфигурацията:Intel 3.5 GHz, 16 GB RAM, 512 GB. - Организатор на пътуване, отново. Направи задачата от урока за
Dictionaryс вложен класTripPlanner.Destination, който пази името и списъка от дейности. - Мрежа от контакти, отново. Премести
Contactот задачата в урока заDictionaryвътре вContactNetwork. Трябва лиContactда знае мрежата си? - Смятане наум, отново. Премести
Questionот задачата в урока за списъците вътре вMathGame. - Продукти по два начина. Подреди списък с продукти по цена от най-скъпия, а при еднаква цена по име. После подреди същия списък само по име в обратен ред.
- Своя отстъпка. Напиши три вложени класа в
Checkout, които изпълняватIDiscount: 10% над 100 €, фиксирани 5 € отстъпка и „втори продукт на половин цена“. Приложи ги върху една и съща количка.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Интерфейси · Изключения · Списъци · Dictionary и HashSet
- Продължава в: Ламбди и делегати · LINQ · Шаблони за дизайн · Тестове с xUnit
- Задачи по темата: Колекции