Skip to main content

C# Inner Classes

Introduction

In C#, inner classes (also called nested classes) are classes defined within another class. They provide a way to logically group classes that are only used in one place, increasing encapsulation and creating more readable and maintainable code.

Inner classes are a powerful feature of C#'s object-oriented programming model that helps organize code more efficiently, especially when a class is tightly coupled with another class and doesn't need to be used elsewhere in your application.

Understanding Inner Classes

An inner class is a class declared entirely within the body of another class or interface. The class that contains the inner class is called the outer class.

Basic Syntax

Here's how you define an inner class in C#:

csharp
public class OuterClass
{
// Members of outer class

// Inner class definition
public class InnerClass
{
// Members of inner class
}
}

To instantiate an inner class, you can use the following syntax:

csharp
// Create an instance of the inner class
OuterClass.InnerClass innerObject = new OuterClass.InnerClass();

Access Modifiers for Inner Classes

Inner classes can have any of the standard access modifiers:

  • public: The inner class is accessible from anywhere
  • private: The inner class is only accessible within the outer class
  • protected: The inner class is accessible within the outer class and its derived classes
  • internal: The inner class is accessible within the same assembly
  • protected internal: Combines the access levels of protected and internal

Basic Example of Inner Classes

Let's create a simple example to demonstrate inner classes:

csharp
using System;

public class Calculator
{
private int operationCount = 0;

// Inner class for performing advanced operations
public class ScientificOperations
{
public double SquareRoot(double number)
{
return Math.Sqrt(number);
}

public double Power(double baseNumber, double exponent)
{
return Math.Pow(baseNumber, exponent);
}
}

public int Add(int a, int b)
{
operationCount++;
return a + b;
}

public int GetOperationCount()
{
return operationCount;
}
}

// Usage
class Program
{
static void Main(string[] args)
{
// Using the outer class
Calculator calc = new Calculator();
int sum = calc.Add(5, 3);
Console.WriteLine($"Sum: {sum}");
Console.WriteLine($"Operation count: {calc.GetOperationCount()}");

// Using the inner class
Calculator.ScientificOperations sciOps = new Calculator.ScientificOperations();
double sqrtResult = sciOps.SquareRoot(16);
double powerResult = sciOps.Power(2, 8);

Console.WriteLine($"Square root of 16: {sqrtResult}");
Console.WriteLine($"2 raised to the power of 8: {powerResult}");
}
}

Output:

Sum: 8
Operation count: 1
Square root of 16: 4
2 raised to the power of 8: 256

In this example, ScientificOperations is an inner class of the Calculator class. The inner class provides additional functionality that's related to the calculator but separated into its own class for better organization.

Accessing Outer Class Members

An important aspect of inner classes is their ability to access members of the outer class. However, there's an important distinction to understand:

  1. Non-static inner classes cannot directly access static members of the outer class
  2. Static inner classes cannot access non-static members of the outer class

Let's see this in action:

csharp
using System;

public class OuterClass
{
private int outerField = 10;
private static int staticOuterField = 20;

// Non-static inner class
public class InnerClass
{
public void AccessOuterMembers(OuterClass outer)
{
// Cannot access outerField directly
// Console.WriteLine(outerField); // This would cause a compilation error

// But can access it through a reference to the outer class
Console.WriteLine($"Outer field value: {outer.outerField}");

// Can access static members directly
Console.WriteLine($"Static outer field value: {staticOuterField}");
}
}

// Static inner class
public static class StaticInnerClass
{
public static void AccessOuterMembers()
{
// Cannot access non-static members
// Console.WriteLine(outerField); // This would cause a compilation error

// Can access static members
Console.WriteLine($"Static outer field value: {staticOuterField}");
}
}

public void ShowInnerClassUsage()
{
// Create an instance of the inner class
InnerClass inner = new InnerClass();
inner.AccessOuterMembers(this);

// Use the static inner class
StaticInnerClass.AccessOuterMembers();
}
}

class Program
{
static void Main(string[] args)
{
OuterClass outer = new OuterClass();
outer.ShowInnerClassUsage();
}
}

Output:

Outer field value: 10
Static outer field value: 20
Static outer field value: 20

Practical Use Cases for Inner Classes

1. Data Containers

Inner classes can be used to define data structures that are only relevant to the outer class:

csharp
public class ShoppingCart
{
private List<Item> items = new List<Item>();

// Inner class representing an item in the cart
public class Item
{
public string Name { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }

public decimal TotalPrice => Price * Quantity;

public override string ToString()
{
return $"{Name} x{Quantity} - ${TotalPrice}";
}
}

public void AddItem(string name, decimal price, int quantity)
{
items.Add(new Item { Name = name, Price = price, Quantity = quantity });
}

public decimal GetTotalPrice()
{
return items.Sum(item => item.TotalPrice);
}

public void PrintReceipt()
{
Console.WriteLine("Receipt:");
foreach (var item in items)
{
Console.WriteLine(item);
}
Console.WriteLine($"Total: ${GetTotalPrice()}");
}
}

// Usage
class Program
{
static void Main(string[] args)
{
ShoppingCart cart = new ShoppingCart();
cart.AddItem("Laptop", 999.99m, 1);
cart.AddItem("Mouse", 24.95m, 2);
cart.AddItem("Keyboard", 59.99m, 1);

cart.PrintReceipt();
}
}

Output:

Receipt:
Laptop x1 - $999.99
Mouse x2 - $49.9
Keyboard x1 - $59.99
Total: $1109.88

2. Implementing Interfaces or Callbacks

Inner classes are useful for implementing interfaces or callbacks specific to the outer class:

csharp
public class AudioPlayer
{
private string currentTrack;

// Inner class implementing an interface
private class TrackCompletionHandler : ITrackCompletionListener
{
private AudioPlayer player;

public TrackCompletionHandler(AudioPlayer player)
{
this.player = player;
}

public void OnTrackCompleted()
{
Console.WriteLine("Track completed callback");
player.PlayNextTrack();
}
}

public AudioPlayer()
{
// Create and use the inner class as a callback handler
ITrackCompletionListener completionHandler = new TrackCompletionHandler(this);
// In a real implementation, this would be passed to some audio API
}

public void PlayNextTrack()
{
Console.WriteLine("Playing next track...");
}
}

public interface ITrackCompletionListener
{
void OnTrackCompleted();
}

3. Builder Pattern Implementation

Inner classes are excellent for implementing the Builder pattern:

csharp
public class Email
{
// Private properties
private string to;
private string from;
private string subject;
private string body;
private List<string> attachments = new List<string>();

// Private constructor - can only be instantiated via the Builder
private Email() { }

// Public getters
public string To => to;
public string From => from;
public string Subject => subject;
public string Body => body;
public IReadOnlyList<string> Attachments => attachments.AsReadOnly();

public override string ToString()
{
return $"From: {From}\nTo: {To}\nSubject: {Subject}\nAttachments: {Attachments.Count}\nBody: {Body}";
}

// Inner Builder class
public class Builder
{
private readonly Email email = new Email();

public Builder To(string to)
{
email.to = to;
return this;
}

public Builder From(string from)
{
email.from = from;
return this;
}

public Builder Subject(string subject)
{
email.subject = subject;
return this;
}

public Builder Body(string body)
{
email.body = body;
return this;
}

public Builder AddAttachment(string attachment)
{
email.attachments.Add(attachment);
return this;
}

public Email Build()
{
if (string.IsNullOrEmpty(email.to))
throw new InvalidOperationException("Email must have a recipient");

if (string.IsNullOrEmpty(email.from))
throw new InvalidOperationException("Email must have a sender");

return email;
}
}
}

// Usage
class Program
{
static void Main(string[] args)
{
Email email = new Email.Builder()
.From("[email protected]")
.To("[email protected]")
.Subject("Meeting Tomorrow")
.Body("Let's meet at 10 AM in the conference room.")
.AddAttachment("agenda.pdf")
.AddAttachment("presentation.pptx")
.Build();

Console.WriteLine(email);
}
}

Output:

From: [email protected]
To: [email protected]
Subject: Meeting Tomorrow
Attachments: 2
Body: Let's meet at 10 AM in the conference room.

Best Practices for Using Inner Classes

  1. Use inner classes when there's a clear "belongs to" relationship between the inner class and its outer class.

  2. Keep inner classes focused and cohesive - they should have a single, well-defined purpose.

  3. Consider accessibility carefully - make inner classes private if they're only used by the outer class.

  4. Use static inner classes when possible - if the inner class doesn't need access to instance members of the outer class, make it static.

  5. Avoid deeply nested classes - more than one level of nesting can make code hard to read and maintain.

  6. Name inner classes appropriately - their names should reflect their relationship to the outer class.

When to Avoid Inner Classes

While inner classes are useful in many scenarios, they should be avoided in the following situations:

  1. When the inner class needs to be used by multiple outer classes - consider making it a top-level class instead.

  2. When the inner class is very complex or large - this can make the outer class harder to understand.

  3. When using the inner class would create circular dependencies between classes.

Summary

Inner classes in C# provide a powerful way to organize related code and increase encapsulation. They're particularly useful for:

  • Creating helper classes that are only used by the outer class
  • Implementing interfaces specific to the containing class
  • Providing data structures used only within the outer class
  • Implementing design patterns like Builder or Strategy

By understanding when and how to use inner classes, you can write more organized, maintainable, and encapsulated code in C#.

Exercises

  1. Create a Library class with an inner class Book that includes properties like Title, Author, and ISBN.

  2. Implement a BankAccount class with a private inner class Transaction that tracks deposits and withdrawals.

  3. Create a UI component class with an inner class that handles events for that component.

  4. Implement the Observer pattern using inner classes to define concrete observers.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)