Skip to content
wannacode

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}");
Output
Total: €37.00
  • From the outside, the name is Order.Line. Inside Order, it is just Line.
  • The line is not tied to a specific order. new Order.Line(...) is made without an Order object, like a regular class.
  • A nested class with nothing in front of class is private: nobody outside Order can use it. That is why it is public class Line here. Without public, the compiler stops at new Order.Line(...):
class Order
{
class Line
{
}
}
Compile error
error CS0122: 'Order.Line' is inaccessible due to its protection level

You 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);
Output
KeyCollection
Dictionary`2

Dictionary`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 Line and is called a static nested class. In C#, static in front of a class means something else: a static class is a class with no objects, only static members, like Math. You cannot use new with it:
class Order
{
public static class Line
{
}
}
Compile error
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();
Output
Headphones: €72.00
Case: €18.00
Java has an inner class: each object of it keeps a hidden reference to the outer object. C# has no such thing, so the reference is visible: Add 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
  1. The cart is a Cart object with a 10% discount.
  2. 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.
  3. The second Item points to the same cart.
  4. FinalPrice of "Headphones" follows the arrow cart and reads discountPercent: 80 * (100 - 10) / 100 = 72.
  5. 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 Item object is created with the cart as the first argument. Usually the outer class creates the inner ones itself, like add here:
var cart = new Cart(10);
var cable = new Cart.Item(cart, "Cable", 10);
Console.WriteLine(cable.FinalPrice());
Output
9
  • The cart field is exactly what the hidden Cart.this does 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();
Answer

Try it

Find the bug

The line should show the currency of the order. The program does not compile:
Compile error
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.

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);
Output
Best seller: Water (300 sold)
  • (string Name, int Count) is the type: two values, each with its own name. best.Name and best.Count are 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, like Order.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));
Output
70
100
  • BlackFriday and NoDiscount are not visible outside Checkout. From the outside, all you know is that For returns an IDiscount.
  • 100 * 70 / 100 with double is 70. C# prints a double without .0 when 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);
Output
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();
Error
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));
Output
Maria 6, Ivan 5.5, Ana 5.5, Peter 4.75
  • b.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.Sort does not promise that: with more students, equal ones may change places. If the order matters when grades are equal, say so in Compare, as in the exercise.
  • string.Join calls ToString on each student.
  • For text and numbers in reverse order: Sort() and then Reverse().
var cities = new List<string> { "Sofia", "Varna", "Plovdiv" };
cities.Sort();
cities.Reverse();
Console.WriteLine(string.Join(", ", cities));
Output
Varna, Sofia, Plovdiv
More precisely, if you are curious
  • IComparer<T> is in System.Collections.Generic, with a type parameter: IComparer<Student> compares students.
  • Text and numbers are sorted without an IComparer, because string and int implement the IComparable<T> interface with a CompareTo method. Your class can implement it too, if it has one natural order. Then Sort() 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));
Output
Ruse, Varna, Sofia

IComparer is for cases where there are several orders.

  • CompareTo protects you from mistakes. a.Grade - b.Grade does not even compile, because it is a double, and Compare returns an int:
class ByGrade : IComparer<Student>
{
public int Compare(Student a, Student b)
{
return a.Grade - b.Grade;
}
}
Compile error
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 Sort directly, 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);
}
Answer

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.

Output
Products: 3
Total: €11.20

2. 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.

Output
Requests: 5
200: 3
401: 1
500: 1
Successful: 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.

Output
Apartment 101: 3 rooms, owner Ivanova
There 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.

Output
Maria 6
Ana 5.5
Ivan 5.5
Peter 4.75

More tasks

  • Computer configurator. A Computer class with the nested classes Cpu (brand, speed in GHz), Ram (GB) and Storage (GB) and the methods SetCpu, SetRam, SetStorage. ToString describes the configuration: Intel 3.5 GHz, 16 GB RAM, 512 GB.
  • Trip planner, again. Do the task from the Dictionary lesson with a nested class TripPlanner.Destination that keeps the name and the list of activities.
  • Contact network, again. Move Contact from the task in the Dictionary lesson inside ContactNetwork. Should Contact know its network?
  • Mental math, again. Move Question from the task in the lesson about lists inside MathGame.
  • 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 Checkout that implement IDiscount: 10% over €100, a fixed €5 off and "second product at half price". Apply them to one and the same cart.

Test

Check yourself

1. class Item is nested in class Cart. Does Item see the fields of a specific cart?
2. Why does Show() of Order.Line not compile if it reads the currency of the order?
3. What do you write in C# instead of an anonymous class new Discount() { … } from Java?
4. Compare(a, b) returns a negative number. What does that mean for List.Sort?
5. What in Compare sorts the students by grade from the highest down?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·