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#:
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:
// 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 anywhereprivate
: The inner class is only accessible within the outer classprotected
: The inner class is accessible within the outer class and its derived classesinternal
: The inner class is accessible within the same assemblyprotected internal
: Combines the access levels ofprotected
andinternal
Basic Example of Inner Classes
Let's create a simple example to demonstrate inner classes:
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:
- Non-static inner classes cannot directly access static members of the outer class
- Static inner classes cannot access non-static members of the outer class
Let's see this in action:
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:
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:
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:
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
-
Use inner classes when there's a clear "belongs to" relationship between the inner class and its outer class.
-
Keep inner classes focused and cohesive - they should have a single, well-defined purpose.
-
Consider accessibility carefully - make inner classes private if they're only used by the outer class.
-
Use static inner classes when possible - if the inner class doesn't need access to instance members of the outer class, make it static.
-
Avoid deeply nested classes - more than one level of nesting can make code hard to read and maintain.
-
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:
-
When the inner class needs to be used by multiple outer classes - consider making it a top-level class instead.
-
When the inner class is very complex or large - this can make the outer class harder to understand.
-
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
-
Create a
Library
class with an inner classBook
that includes properties likeTitle
,Author
, andISBN
. -
Implement a
BankAccount
class with a private inner classTransaction
that tracks deposits and withdrawals. -
Create a UI component class with an inner class that handles events for that component.
-
Implement the Observer pattern using inner classes to define concrete observers.
Additional Resources
- Microsoft Documentation on Nested Types
- Design Patterns: Elements of Reusable Object-Oriented Software - A classic book that demonstrates many uses of inner classes in design patterns
- C# in Depth by Jon Skeet - Covers advanced C# topics including effective use of nested types
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)