Към съдържанието
wannacode

Вложени класове

Накратко: Вложеният клас е клас, обявен вътре в друг клас, защото има смисъл само там; в 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);
Изход
KeyCollection
Dictionary`2

Dictionary`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 €
В Java има вътрешен клас (inner class): всеки обект от него пази скрита референция към обекта отвън. В C# такъв няма, затова референцията е видима: Add подава this, а Item я пази в полето cart. FinalPrice чете discountPercent на количката, в която е. Полето е private, но Item е написан вътре в Cart, а вложеният клас вижда private членовете на външния.

var cart = new Cart(10);
cart.Add("Слушалки", 80);
cart.Add("Калъф", 20);
cart.Print();
Стъпка по стъпка, с думи
  1. Количката е обект Cart с отстъпка 10%.
  2. Add създава Item и му подава количката, this. Освен Name и Price, всеки Item пази в cart стрелка към своята количка.
  3. Вторият Item сочи към същата количка.
  4. FinalPrice на „Слушалки“ минава по стрелката cart и чете discountPercent: 80 * (100 - 10) / 100 = 72.
  5. 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();
Отговор

Пробвай

Намери грешката

Редът трябва да покаже валутата на поръчката. Програмата не се компилира:
Грешка при компилиране
error CS0120: An object reference is required for the non-static field, method, or property 'Order.currency'

На кой ред е проблемът?

Избери реда с грешката.

Идеята

Вместо клас в метод: кортеж

В 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));
Изход
70
100
  • BlackFriday и 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.75
  • b.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);
}
Отговор

Упражнение

Класове, които вървят заедно

Реши задачите в редактора си.

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.

Изход
Заявки: 5
200: 3
401: 1
500: 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 € отстъпка и „втори продукт на половин цена“. Приложи ги върху една и съща количка.

Тест

Провери се

1. class Item е вложен в class Cart. Вижда ли Item полетата на конкретна количка?
2. Защо Show() на Order.Line не се компилира, ако чете currency на поръчката?
3. Какво пишеш в C# вместо анонимен клас new Discount() { … } от Java?
4. Compare(a, b) връща отрицателно число. Какво значи това за List.Sort?
5. Кое в Compare подрежда учениците по успех от най-високия надолу?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·