Nested classes
In short: A nested class is a class declared inside another class because it only makes sense there; in C# it sees the outer class's private members but is not tied to an outer object.
Problem
A class that only makes sense in one place
In the lesson about the linked list, Node was a separate class. Anyone in the program can make nodes, but they only make sense in the list. The same goes for an order line:
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 without Order makes no sense. The name is long so it does not clash with the line of an invoice, InvoiceLine, or with the line of a warehouse. And nothing says that the two classes go together. Can the line live inside the order?
The idea
Nested class
It can. A class declared inside another class is called a 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("Coffee beans", 2, 12.5));order.Add(new Order.Line("Cup", 4, 3.0));Console.WriteLine($"Total: €{order.Total():F2}");Total: €37.00- From the outside, the name is
Order.Line. InsideOrder, it is justLine. - The line is not tied to a specific order.
new Order.Line(...)is made without anOrderobject, like a regular class. - A nested class with nothing in front of
classisprivate: nobody outsideOrdercan use it. That is why it ispublic class Linehere. Withoutpublic, the compiler stops atnew Order.Line(...):
class Order{ class Line { }}error CS0122: 'Order.Line' is inaccessible due to its protection levelYou have already used such a class without seeing it: Keys of a Dictionary is an object of KeyCollection, a class nested in Dictionary:
var stock = new Dictionary<string, int>();Console.WriteLine(stock.Keys.GetType().Name);Console.WriteLine(stock.Keys.GetType().DeclaringType.Name);KeyCollectionDictionary`2Dictionary`2 is the name the runtime keeps for Dictionary<TKey, TValue>: 2 is the number of types in the < >.
More precisely, if you are curious
- In Java such a class is written
static class Lineand is called a static nested class. In C#,staticin front of a class means something else: a static class is a class with no objects, onlystaticmembers, likeMath. You cannot usenewwith it:
class Order{ public static class Line { }}error CS0712: Cannot create an instance of the static class 'Order.Line'- Every nested class in C# is like the static one in Java. C# has no inner class tied to an outer object. What you do instead is in the next card.
The idea
The nested object keeps the outer one
The cart has a discount for all products in it. Each product must know the discount of its own cart. In C# a nested class does not see the outer object, so the cart is passed explicitly:
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("Headphones", 80);cart.Add("Case", 20);cart.Print();Headphones: €72.00Case: €18.00Add passes this, and Item keeps it in the cart field. FinalPrice reads the discountPercent of the cart it is in. The field is private, but Item is written inside Cart, and a nested class sees the private members of the outer one.var cart = new Cart(10);cart.Add("Headphones", 80);cart.Add("Case", 20);cart.Print();Step by step, in words
- The cart is a Cart object with a 10% discount.
- Add creates an Item and passes it the cart, this. Besides Name and Price, each Item keeps an arrow to its own cart in cart.
- The second Item points to the same cart.
- FinalPrice of "Headphones" follows the arrow cart and reads discountPercent: 80 * (100 - 10) / 100 = 72.
- FinalPrice of "Case" follows the arrow cart and reads discountPercent: 20 * (100 - 10) / 100 = 18.
More precisely, if you are curious
- From the outside, an
Itemobject is created with the cart as the first argument. Usually the outer class creates the inner ones itself, likeaddhere:
var cart = new Cart(10);var cable = new Cart.Item(cart, "Cable", 10);Console.WriteLine(cable.FinalPrice());9- The
cartfield is exactly what the hiddenCart.thisdoes in Java. Here you can simply see it. - If the nested class does not use the fields of the outer class, do not give it the outer object. That way it does not keep a reference it does not need.
Try it
Two carts
The Cart class is the same. One cart has no discount, and the other has 20% for club members.
What will the program print?
var regular = new Cart(0);var member = new Cart(20);regular.Add("Book", 15);member.Add("Book", 15);regular.Print();member.Print();Each Item sees the discount of the cart that created it. The book in regular has 0%, and the one in member has 20%.
Try it
Find the bug
error CS0120: An object reference is required for the non-static field, method, or property 'Order.currency'Which line has the problem?
Pick the line with the bug.
currency is a field of an Order object, and Line has no order. A nested class in C# is not tied to an outer object, so there is no order whose currency it could read. The fix is for Line to get the order, new Order.Line(order), keep it in a field and read order.currency. The fact that currency is private is no problem: Line is inside Order. Another way is to make currency static, if the currency is the same for all orders.The idea
Instead of a class in a method: a tuple
In Java a class can also be declared in a method. Then it is only visible there and is called a local class. C# has no such thing: a class in a method does not compile. If a method needs a few values that go together only inside it, C# has a 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 seller: {best.Name} ({best.Count} sold)");}string[] names = { "Coffee", "Tea", "Water" };int[] sold = { 120, 45, 300 };PrintBestSeller(names, sold);Best seller: Water (300 sold)(string Name, int Count)is the type: two values, each with its own name.best.Nameandbest.Countare read like properties.(names[i], sold[i])makes a new tuple, and it is assigned all at once.- Nobody uses it outside
PrintBestSeller. If the same data is needed elsewhere too or has behavior, it is a class, likeOrder.Line.
The idea
There is no anonymous class
The discounts in the shop are an interface:
interface IDiscount{ double Apply(double price);}So far you made a class for each discount: class BlackFriday : IDiscount { ... }. For a discount used in only one place, Java has an anonymous class: a class with no name, written right there, new Discount() { ... }. C# has no such thing: new IDiscount() { ... } does not compile. A class always has a name. If it is needed only in one class, it is nested and private, as in this lesson:
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("none").Apply(100));70100BlackFridayandNoDiscountare not visible outsideCheckout. From the outside, all you know is thatForreturns anIDiscount.100 * 70 / 100withdoubleis70. C# prints adoublewithout.0when the number is whole.
More precisely, if you are curious
- For one single action, like the discount, C# has a shorter way: a delegate and a lambda. Then you do not need a class. You will see them in the "Modern C#" module.
- C# does have something with "anonymous" in its name: an anonymous type.
new { Name = "Water", Count = 300 }makes an object of a class with no name and only read-only properties. It has no methods and cannot implement an interface, so it does not replace the anonymous class:
var sale = new { Name = "Water", Count = 300 };Console.WriteLine(sale.Name);Console.WriteLine(sale);Water{ Name = Water, Count = 300 }The idea
IComparer: sorting by your own rule
Sort sorted text and numbers. But it cannot sort students:
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("Ivan", 5.50), new Student("Maria", 6.00), new Student("Peter", 4.75), new Student("Ana", 5.50)};students.Sort();InvalidOperationException: Failed to compare two elements in the array.The compiler does not object, but when you run it, the program stops. C# does not know what to sort students by: by name, by grade, by age?
The rule is given separately, as an IComparer. It is an interface with one method, Compare(a, b). It returns a negative number if a should be before b, zero if they are equal, and a positive number if a is after b. You write a class that implements it and pass an object of it to 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));Maria 6, Ivan 5.5, Ana 5.5, Peter 4.75b.Grade.CompareTo(a.Grade)compares the other way around, so the highest grade comes first.- Ivan and Ana have the same grade. Here they stayed in the order they were in, but
List.Sortdoes not promise that: with more students, equal ones may change places. If the order matters when grades are equal, say so inCompare, as in the exercise. string.JoincallsToStringon each student.- For text and numbers in reverse order:
Sort()and thenReverse().
var cities = new List<string> { "Sofia", "Varna", "Plovdiv" };cities.Sort();cities.Reverse();Console.WriteLine(string.Join(", ", cities));Varna, Sofia, PlovdivMore precisely, if you are curious
IComparer<T>is inSystem.Collections.Generic, with a type parameter:IComparer<Student>compares students.- Text and numbers are sorted without an
IComparer, becausestringandintimplement theIComparable<T>interface with aCompareTomethod. Your class can implement it too, if it has one natural order. ThenSort()works with nothing else:
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("Sofia", 1300000), new City("Ruse", 140000), new City("Varna", 330000) };cities.Sort();Console.WriteLine(string.Join(", ", cities));Ruse, Varna, SofiaIComparer is for cases where there are several orders.
CompareToprotects you from mistakes.a.Grade - b.Gradedoes not even compile, because it is adouble, andComparereturns anint:
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?)With int the subtraction compiles, but with very large numbers it overflows and the sign flips.
- A whole class for one comparison is a lot of code. In the "Modern C#" module the rule will be passed to
Sortdirectly, on one line.
Try it
Which one is first?
What will the program print?
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("Mouse", 25), new Product("Keyboard", 60)};
products.Sort(new ByPrice());foreach (Product p in products){ Console.WriteLine(p.Name);}a.Price.CompareTo(b.Price) is negative when a is cheaper. So the cheaper one comes first: 25, 60, 220. To put the most expensive first, swap a and b.Exercise
Classes that go together
Solve the tasks in your editor.
1. Shopping cart. A ShoppingCart class with a nested class Item (name and price) and the methods AddItem(Item item), GetTotal() and Count(). In Main, add new ShoppingCart.Item("Milk", 2.5), bread for €1.20 and coffee for €7.50. Print the total with :F2.
Products: 3Total: €11.202. Web server monitor. A WebServer class with a nested class Request (address and status code) and the methods AddRequest(Request r) and PrintStats(). The stats say how many requests there are, how many there are of each status code (sorted) and what percent are successful, that is, with a code from 200 to 299. Keep the counts in a SortedDictionary<int, int>: it keeps the keys sorted. The requests are /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Requests: 5200: 3401: 1500: 1Successful: 60%3. Building manager. A Building class with a nested class Apartment (number, number of rooms, owner) and the methods AddApartment(Apartment a) and GetApartmentInfo(int number). Keep the apartments in a Dictionary<int, Apartment>. Add 101 with 3 rooms owned by Ivanova and 102 with 2 rooms owned by Petrov. Look up 101 and 999.
Apartment 101: 3 rooms, owner IvanovaThere is no apartment 999.4. Ranking. Sort the students from the lesson by grade, from the highest down. When the grades are equal, sort them by name, in alphabetical order. It all goes in one IComparer<Student>: if the comparison by grade is not 0, return it, otherwise compare the names with string.Compare.
Maria 6Ana 5.5Ivan 5.5Peter 4.75More tasks
- Computer configurator. A
Computerclass with the nested classesCpu(brand, speed in GHz),Ram(GB) andStorage(GB) and the methodsSetCpu,SetRam,SetStorage.ToStringdescribes the configuration:Intel 3.5 GHz, 16 GB RAM, 512 GB. - Trip planner, again. Do the task from the
Dictionarylesson with a nested classTripPlanner.Destinationthat keeps the name and the list of activities. - Contact network, again. Move
Contactfrom the task in theDictionarylesson insideContactNetwork. ShouldContactknow its network? - Mental math, again. Move
Questionfrom the task in the lesson about lists insideMathGame. - Products in two ways. Sort a list of products by price from the most expensive, and by name when the price is the same. Then sort the same list by name only, in reverse order.
- Your own discount. Write three classes nested in
Checkoutthat implementIDiscount: 10% over €100, a fixed €5 off and "second product at half price". Apply them to one and the same cart.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Interfaces · Exceptions · Lists · Dictionary and HashSet
- Continues in: Lambdas and delegates · LINQ · Design patterns · Tests with xUnit
- Tasks on this topic: Collections