DEV Community

Cover image for Exploring the Different Types of Classes in C#
  Isaiah   Clifford Opoku
Isaiah Clifford Opoku

Posted on

Exploring the Different Types of Classes in C#

Classes are the fundamental building blocks of object-oriented programming in C#. They allow us to create reusable and modular code by grouping related data and functionality together. In C#, there are several types of classes that serve different purposes and can be used in different scenarios. In this guide, we'll explore various types of classes in C# and how they can be used to create efficient and maintainable code.

You can watch videos about C#, Azure, .NET, Blazor, and .NET maturity on my YouTube channel CliffTech. Don't forget to subscribe to my channel for the latest updates and tutorials!

Table of Contents

Let start with the first type of class which is Static Classes.

Static Classes

Static classes are a special type of class in C# designed to provide a collection of related utility methods and properties that do not rely on instance data. Unlike regular classes, static classes cannot be instantiated, and they can only contain static members. Static classes are sealed, meaning they cannot be inherited, making them ideal for grouping methods that are stateless and do not require object-oriented features.

Example of a Static Class in csharp

Here's an example of a static class in C#:

namespace StaticClasses
{
// Define a static class
    public static class MathUtils
    {
        // Static method to add two numbers
        public static int Add(int a, int b)
        {
            return a + b;
        }

        // Static method to subtract two numbers
        public static int Subtract(int a, int b)
        {
            return a - b;
        }

       // Static method to multiply two numbers
        public static int Multiply(int a, int b)
        {
            return a * b;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • The MathUtils class is defined as static, meaning it cannot be instantiated.
  • It contains three static methods: Add, Subtract, and Multiply.
  • These methods can be called directly on the MathUtils class without creating an instance.

Using Static Methods in Program.cs

You can use the static methods defined in the MathUtils class as follows:


 // program.cs
namespace StaticClasses
{
    class Program
    {
        static void Main(string[] args)
        {
             // Call static methods from the MathUtils class
            int sum = MathUtils.Add(5, 3);
            // Call the static method Subtract from the MathUtils class
            int difference = MathUtils.Subtract(5, 3);

            // Call the static method Multiply from the MathUtils class
            int product = MathUtils.Multiply(5, 3);

            // Display the results of the sum method
            Console.WriteLine($"Sum: {sum}"); // Output: 8
            // Display the results of the difference method
            Console.WriteLine($"Difference: {difference}"); // Output: 2
            // Display the results of the product method
            Console.WriteLine($"Product: {product}");  // Output: 15
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Points to Remember About Static Classes in csharp

  • Cannot be Instantiated: Static classes cannot be instantiated. You cannot create objects of a static class.
  • Only Static Members: Static classes can only contain static members. They do not support instance methods or instance fields.
  • Sealed by Default: Static classes are implicitly sealed, meaning they cannot be inherited.
  • Utility and Helper Methods: Static classes are typically used to group related utility or helper methods that do not require object state.

Static classes provide a way to organize and access utility methods and properties in a clean, straightforward manner, making them essential for creating efficient and maintainable code.

Sealed Classes

Sealed classes are a special type of class in C# that cannot be inherited. They are used to prevent other classes from deriving from them, which can be useful for creating immutable types or ensuring that a class's behavior remains unchanged. By sealing a class, you ensure that it cannot be modified or extended, making it useful for scenarios where you want to provide a specific implementation without allowing further alterations.

Example of a Sealed Class in csharp

Here's an example of a sealed class in C#:

namespace SealedClasses
{

    // Define an abstract class
    public abstract class Shape
    {
        // Abstract method to calculate the area
        public abstract double CalculateArea();
    }

     // Define a sealed class
    public sealed class Rectangle : Shape
    {

        //  Properties
        public double Width { get; }
        public double Height { get; }

        // Constructor
        public Rectangle(double width, double height)
        {
            Width = width;
            Height = height;
        }

       // Implement the CalculateArea method
        public override double CalculateArea()
        {
            return Width * Height;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • The Shape class is an abstract base class with an abstract method CalculateArea().
  • The Rectangle class inherits from Shape and provides an implementation for CalculateArea().
  • The Rectangle class is sealed, which means it cannot be inherited from. This ensures that the class's implementation cannot be modified or extended.

Using the Sealed Rectangle Class in Program.cs

Here's how you can use the Rectangle class in a Program.cs file:

namespace SealedClasses
{
    class Program
    {
        static void Main(string[] args)
        {
            Rectangle rectangle = new Rectangle(5, 3);
            double area = rectangle.CalculateArea();

            Console.WriteLine($"Area of the rectangle: {area}"); // Output: Area of the rectangle: 15
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Rectangle class is sealed to ensure that its behavior cannot be altered through inheritance. This provides a guarantee that the Rectangle class's implementation of CalculateArea() remains unchanged, which is useful for maintaining consistent behavior.

Key Points to Remember About Sealed Classes

  • No Inheritance: Sealed classes cannot be inherited from, ensuring that their behavior remains unchanged.
  • Prevent Modification: Sealed classes are used to prevent further inheritance, thus avoiding accidental modifications or extensions.
  • Immutable and Specific: Sealed classes are useful for creating immutable classes or for scenarios where you want to provide a specific, unchangeable implementation.

At point you might wonder why we need sealed classes if static classes are already sealed. The key difference is:

Sealed Classes vs. Static Classes

  • Static Classes: They are sealed and cannot be instantiated. They are used for grouping static methods and properties.
  • Sealed Classes: They can be instantiated, but cannot be inherited. This allows for creating objects that are protected from further subclassing.

Sealed classes provide flexibility in creating classes that can be used directly without risking modification through inheritance.

Concrete Classes

Concrete classes form the core of object-oriented programming in C#. They represent fully implemented classes that can be instantiated, meaning you can directly create objects from them. Unlike abstract classes or interfaces, concrete classes provide complete implementations of all their methods and properties, making them versatile and fundamental to most C# applications.

A concrete class is a class that is not abstract. It includes full implementations of all its members—methods, properties, fields, etc.—and can be used to create objects. These classes are used to represent real-world entities or concepts in your application, encapsulating both data (stored in fields or properties) and behavior (defined by methods).

Example: Defining a Concrete Class in csharp

Here's a simple example of a concrete class in C#:



// Define a concrete class
public class Animal
{
    public void Speak()
    {
        Console.WriteLine("The animal makes a sound.");
    }
}

// Define a derived class that inherits from the Animal class
public class Dog : Animal
{
    public void Bark()
    {
        Console.WriteLine("The dog barks.");
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Animal class is a concrete class with a method Speak that represents a generic sound made by any animal. The Dog class inherits from Animal and adds a Bark method to represent a sound specific to dogs. Both Animal and Dog are concrete classes because they can be instantiated and used to create objects.

Instantiating and Using Concrete Classes

Here’s how you can use the Dog class in a program:


// program.cs
class Program
{
    static void Main(string[] args)
    {
        // Create an instance of the Dog class
        Dog myDog = new Dog();

        // Call the inherited method
        myDog.Speak(); // Output: The animal makes a sound.

        // Call the method defined in the Dog class
        myDog.Bark();  // Output: The dog barks.
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we instantiate the Dog class to create a myDog object. We then call the Speak method inherited from the Animal class, followed by the Bark method from the Dog class. This demonstrates how concrete classes encapsulate both inherited and specific behavior.

Real-World Example: Concrete Class for a Product

To illustrate the practical application of concrete classes, consider the following example of a Product class:

// Define a concrete class for a product
public class Product
{
    // Data properties
    public string Name { get; set; }
    public decimal Price { get; set; }

    // Method to display product information
    public void DisplayInfo()
    {
        Console.WriteLine($"Product: {Name}, Price: {Price:C}");
    }
}
Enter fullscreen mode Exit fullscreen mode

This Product class is a concrete class with properties Name and Price to store information about a product. The DisplayInfo method provides a way to display the product’s details.

Using the Product Class

Here’s how you can use the Product class:

class Program
{
    static void Main(string[] args)
    {
        // Create an instance of the Product class
        Product product = new Product
        {
            Name = "Laptop",
            Price = 1299.99m
        };

        // Display product information
        product.DisplayInfo(); // Output: Product: Laptop, Price: $1,299.99
    }
}
Enter fullscreen mode Exit fullscreen mode

In this scenario, the Product class is instantiated to create a product object. The DisplayInfo method is called to show the product's name and price. This demonstrates how concrete classes are used to model and manipulate real-world data.

Key Points to Remember About Concrete Classes

  • Instantiable: Concrete classes can be instantiated, allowing you to create objects that represent specific entities or concepts in your application.
  • Complete Implementation: Concrete classes provide full implementations of all methods and properties, as opposed to abstract classes or interfaces.
  • Common Use: They are the most common type of class in C#, used to define objects with specific behavior and data.

Concrete classes are essential for C# development, allowing you to define and work with objects that model real-world entities within your applications. Understanding how to effectively use concrete classes is crucial for building robust, object-oriented software.

Abstract Classes

In C#, abstract classes are a powerful feature that allows you to define a blueprint for other classes without providing complete implementations. They serve as base classes that cannot be instantiated directly but can be inherited by other classes that will provide specific implementations for the abstract methods defined within them. This design helps enforce consistency across related classes while allowing flexibility in how certain behaviors are implemented.

What Does "Instantiated" Mean?

Before diving into abstract classes, let's clarify what it means to instantiate a class. Instantiation refers to the process of creating an object from a class. When you use the new keyword in C#, you are creating an instance (or object) of that class. However, abstract classes cannot be instantiated directly—they must be inherited by a non-abstract (concrete) class that provides implementations for the abstract methods.

Understanding Abstract Classes and Abstract Methods

  • Abstract Classes: Abstract classes are classes that cannot be instantiated. Instead, they serve as templates for other classes. They can contain both fully implemented methods and abstract methods (methods without a body). Abstract classes are used to define a common interface and shared behavior for a group of related classes.

  • Abstract Methods: Abstract methods are methods declared within an abstract class that do not have a body. These methods must be implemented by any non-abstract class that inherits from the abstract class. This ensures that all derived classes provide specific implementations for these methods, enforcing a consistent interface across all subclasses.

Real-World Example: Bank Account Management

Let's explore a real-world example to illustrate the concept of abstract classes and abstract methods in C#.

using System;

// define an abstract class
namespace AbstractClasses
{
    // Abstract class
    public abstract class BankAccount
    {
        // Properties
        public string AccountNumber { get; private set; }
        public decimal Balance { get; protected set; }

        // Constructor
        public BankAccount(string accountNumber, decimal initialBalance)
        {
            AccountNumber = accountNumber;
            Balance = initialBalance;
        }

        // Abstract methods
        public abstract void Deposit(decimal amount);
        public abstract void Withdraw(decimal amount);
        public abstract void DisplayAccountInfo();
    }

    // Derived class: SavingsAccount
    public class SavingsAccount : BankAccount
    {
        private decimal interestRate;

        public SavingsAccount(string accountNumber, decimal initialBalance, decimal interestRate)
            : base(accountNumber, initialBalance)
        {
            this.interestRate = interestRate;
        }

        // Implementing abstract methods
        public override void Deposit(decimal amount)
        {
            Balance += amount;
            Console.WriteLine($"Deposited {amount} to Savings Account {AccountNumber}. New Balance: {Balance}");
        }

        public override void Withdraw(decimal amount)
        {
            if (amount > Balance)
            {
                throw new InvalidOperationException("Insufficient funds.");
            }
            Balance -= amount;
            Console.WriteLine($"Withdrew {amount} from Savings Account {AccountNumber}. New Balance: {Balance}");
        }

        public override void DisplayAccountInfo()
        {
            Console.WriteLine($"Savings Account {AccountNumber} - Balance: {Balance}, Interest Rate: {interestRate}%");
        }
    }

    // Derived class: CheckingAccount
    public class CheckingAccount : BankAccount
    {
        private decimal overdraftLimit;

        public CheckingAccount(string accountNumber, decimal initialBalance, decimal overdraftLimit)
            : base(accountNumber, initialBalance)
        {
            this.overdraftLimit = overdraftLimit;
        }

        // Implementing abstract methods
        public override void Deposit(decimal amount)
        {
            Balance += amount;
            Console.WriteLine($"Deposited {amount} to Checking Account {AccountNumber}. New Balance: {Balance}");
        }

        public override void Withdraw(decimal amount)
        {
            if (amount > Balance + overdraftLimit)
            {
                throw new InvalidOperationException("Overdraft limit exceeded.");
            }
            Balance -= amount;
            Console.WriteLine($"Withdrew {amount} from Checking Account {AccountNumber}. New Balance: {Balance}");
        }

        public override void DisplayAccountInfo()
        {
            Console.WriteLine($"Checking Account {AccountNumber} - Balance: {Balance}, Overdraft Limit: {overdraftLimit}");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the BankAccount class is an abstract class that defines a common interface for different types of bank accounts. It includes abstract methods like Deposit, Withdraw, and DisplayAccountInfo, which must be implemented by any class that inherits from BankAccount.

The SavingsAccount and CheckingAccount classes inherit from BankAccount and provide specific implementations for these abstract methods. This design enforces that every type of bank account must implement deposit, withdrawal, and display functions, while still allowing each account type to implement these functions in a way that makes sense for that specific type.

Using the Abstract Classes in a Program

Let's see how you can use the SavingsAccount and CheckingAccount classes in a program:

namespace AbstractClasses
{
    class Program
    {
        static void Main(string[] args)
        {
            // Create a savings account
            BankAccount savings = new SavingsAccount("SA123", 1000, 1.5m);
            // Create a checking account
            BankAccount checking = new CheckingAccount("CA123", 500, 200);

            // Deposit and withdraw from the savings account
            savings.DisplayAccountInfo();

           // Deposit and withdraw from the checking account
            savings.Deposit(200);

            // Display the updated account information
            savings.Withdraw(100);
            // Display the updated account information
            savings.DisplayAccountInfo();

            // Deposit and withdraw from the checking account
            checking.DisplayAccountInfo();

             // Deposit and withdraw from the checking account
            checking.Deposit(300);

            // Display the updated account information
            checking.Withdraw(600);

            // Display the updated account information
            checking.DisplayAccountInfo();

            try
            {
                checking.Withdraw(200);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }

            checking.DisplayAccountInfo();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This program will produce the following output:

Savings Account SA123 - Balance: 1000, Interest Rate: 1.5%
Deposited 200 to Savings Account SA123. New Balance: 1200
Withdrew 100 from Savings Account SA123. New Balance: 1100
Savings Account SA123 - Balance: 1100, Interest Rate: 1.5%
Checking Account CA123 - Balance: 500, Overdraft Limit: 200
Deposited 300 to Checking Account CA123. New Balance: 800
Withdrew 600 from Checking Account CA123. New Balance: 200
Checking Account CA123 - Balance: 200, Overdraft Limit: 200
Withdrew 200 from Checking Account CA123. New Balance: 0
Checking Account CA123 - Balance: 0, Overdraft Limit: 200
Enter fullscreen mode Exit fullscreen mode

In this example, the SavingsAccount and CheckingAccount objects are created, and the abstract methods Deposit, Withdraw, and DisplayAccountInfo are called. The abstract class BankAccount ensures that both account types have these methods, while the derived classes provide the specific functionality.

Key Points to Remember About Abstract Classes

  • Cannot Be Instantiated: Abstract classes cannot be instantiated directly. They must be inherited by a subclass that provides implementations for the abstract methods.
  • Contain Abstract Methods: Abstract methods are declared without a body in an abstract class. They must be implemented by any non-abstract class that inherits the abstract class.
  • Define Common Interfaces: Abstract classes are used to define a common interface for a group of related classes, ensuring consistency while allowing flexibility in implementation.

Abstract classes are a fundamental concept in C#, providing a way to enforce a certain structure across related classes while still allowing for specialization. By understanding and utilizing abstract classes, you can create more organized, maintainable, and extensible code.

Singleton Classes

Singleton classes are a design pattern that restricts the instantiation of a class to one single instance. This is particularly useful when you need a single, shared resource across your application, such as a configuration manager, logging service, or database connection.

Why Use Singleton Classes in csharp ?

Imagine you have a class responsible for managing a database connection. You don’t want multiple instances of this class running around, potentially causing issues with resource management or inconsistent data. A Singleton class ensures that only one instance is created and provides a global point of access to it.

Example: Defining a Singleton Class

Let’s dive into how you can implement a Singleton class in C#:

// Define a singleton class
public class Singleton
{
    private static Singleton instance;
    private static readonly object lockObject = new object();

    // Private constructor prevents instantiation from outside the class
    private Singleton()
    {
    }

    // Public property to access the single instance of the class
    public static Singleton Instance
    {
        get
        {
            // Ensure thread safety
            lock (lockObject)
            {
                if (instance == null)
                {
                    instance = new Singleton();
                }
            }
            return instance;
        }
    }

    // Example method to demonstrate the singleton instance
    public void PrintMessage()
    {
        Console.WriteLine("Hello, I am a singleton class.");
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Singleton class is defined with a private constructor, which prevents other classes from creating new instances. The static property Instance returns the single instance of the class, creating it if it doesn't already exist. The lockObject ensures that the class is thread-safe, meaning that even in a multi-threaded environment, only one instance will be created.

The PrintMessage method is just a simple example to show that the Singleton instance can be used like any other class instance.

Using the Singleton Class in Program.cs

Now let’s see how you can use this Singleton class in your application:

class Program
{
    static void Main(string[] args)
    {
        // Retrieve the single instance of the Singleton class
        Singleton singleton1 = Singleton.Instance;
        singleton1.PrintMessage(); // Output: Hello, I am a singleton class.

        // Retrieve the instance again
        Singleton singleton2 = Singleton.Instance;

        // Check if both instances are the same
        Console.WriteLine(singleton1 == singleton2); // Output: True
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we retrieve the Singleton instance twice. Because the class is a Singleton, both singleton1 and singleton2 refer to the same instance. The == operator confirms this by returning true.

Extending the Singleton Example

You can expand the Singleton pattern to handle more complex scenarios. For example, you could initialize the Singleton instance with configuration data:

public class ConfigurationManager
{
    private static ConfigurationManager instance;
    private readonly Dictionary<string, string> settings = new Dictionary<string, string>();

    private ConfigurationManager()
    {
        // Simulate loading settings from a configuration file
        settings["AppName"] = "MyApplication";
        settings["Version"] = "1.0.0";
    }

    public static ConfigurationManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new ConfigurationManager();
            }
            return instance;
        }
    }

    public string GetSetting(string key)
    {
        return settings.ContainsKey(key) ? settings[key] : null;
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, ConfigurationManager is a Singleton class that loads and manages application settings. The GetSetting method allows you to retrieve specific configuration values, ensuring that all parts of your application use the same settings.

Key Points to Remember About Singleton Classes

  • Single Instance: Singleton classes ensure that only one instance of the class exists in the application.
  • Global Access: Singleton provides a global point of access to the instance, making it easy to use across different parts of your application.
  • Thread Safety: In multi-threaded environments, ensure your Singleton is thread-safe to avoid creating multiple instances.
  • Use Cases: Common use cases for Singleton include managing configurations, logging services, and database connections.

Singleton classes are a fundamental design pattern in software engineering, offering a simple yet powerful way to manage shared resources. Understanding and correctly implementing Singletons can help you write more efficient and maintainable code.

Generic Classes

Generic classes in C# provide a powerful way to create reusable and type-safe code. By using generic classes, you can design a single class that works with any data type, eliminating the need for type-specific implementations. This makes your code more flexible and reduces redundancy.

Why Use Generic Classes?

Imagine you need to implement a stack that stores integers. Later, you might need another stack to store strings. Instead of writing two separate classes, you can write one generic stack class that can handle both data types—and any others you might need. Generic classes help you avoid code duplication and make your codebase easier to maintain.

Example: Defining a Generic Class

Let’s take a look at a simple implementation of a generic stack class:

// Define a generic class
public class Stack<T>
{
    private List<T> items = new List<T>();

    public void Push(T item)
    {
        items.Add(item);
    }

    public T Pop()
    {
        if (items.Count == 0)
        {
            throw new InvalidOperationException("The stack is empty.");
        }
        T item = items[items.Count - 1];
        items.RemoveAt(items.Count - 1);
        return item;
    }

    public T Peek()
    {
        if (items.Count == 0)
        {
            throw new InvalidOperationException("The stack is empty.");
        }
        return items[items.Count - 1];
    }

    public bool IsEmpty()
    {
        return items.Count == 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Stack<T> class is defined with a type parameter T. This type parameter is a placeholder that represents the type of data the stack will store. The class includes methods like Push to add an item to the stack, Pop to remove and return the top item, Peek to view the top item without removing it, and IsEmpty to check if the stack is empty.

Because Stack<T> is generic, you can use it with any data type, whether it's int, string, or even a custom class.

Using the Stack Class in Program.cs

Let’s see how this generic Stack class can be used in a program:

class Program
{
    static void Main(string[] args)
    {
        // Stack for integers
        Stack<int> intStack = new Stack<int>();
        intStack.Push(10);
        intStack.Push(20);
        Console.WriteLine(intStack.Pop()); // Output: 20
        Console.WriteLine(intStack.Peek()); // Output: 10

        // Stack for strings
        Stack<string> stringStack = new Stack<string>();
        stringStack.Push("Hello");
        stringStack.Push("World");
        Console.WriteLine(stringStack.Pop()); // Output: World
        Console.WriteLine(stringStack.Peek()); // Output: Hello
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we create two instances of the Stack class: one that stores integers and another that stores strings. The flexibility of generics allows us to use the same class to work with different data types, making our code more reusable and concise.

Extending the Generic Class

Let’s take it a step further and extend our Stack class to include a method that returns all items as an array:

public T[] ToArray()
{
    return items.ToArray();
}
Enter fullscreen mode Exit fullscreen mode

Now, you can easily convert the stack’s items into an array:

int[] intArray = intStack.ToArray();
string[] stringArray = stringStack.ToArray();
Enter fullscreen mode Exit fullscreen mode

This extension further showcases the power of generics, allowing the same method to work with different data types seamlessly.

Key Points to Remember About Generic Classes

  • Flexibility: Generic classes can work with any data type, making them versatile and reusable.
  • Type Safety: By using type parameters, generic classes ensure that your code is type-safe, catching errors at compile-time rather than runtime.
  • Code Reuse: Generics eliminate the need for duplicating code to handle different data types, leading to cleaner, more maintainable code.
  • Type Parameters: Generic classes are defined using type parameters, which act as placeholders for the actual data types you will use when you instantiate the class.

Generic classes are an essential tool in C# for creating flexible, reusable, and type-safe code. By understanding and utilizing generics, you can write more robust and maintainable applications.

Internal Classes

Internal classes in C# are a powerful way to encapsulate implementation details within an assembly. By using the internal access modifier, you can restrict access to certain classes, ensuring they are only accessible within the same assembly. This is particularly useful for hiding complex logic or utility classes that are not intended to be exposed to the public API of your library or application.

Why Use Internal Classes?

In a large application, you may have classes that should only be used internally by your code and not by external consumers. For example, helper classes, utility functions, or components of a larger system that do not need to be exposed outside the assembly can be marked as internal. This ensures that your public API remains clean and focused while still allowing full functionality within the assembly.

Example: Defining an Internal Class

Let’s consider a scenario where you have a library that processes orders. You might have a class that handles the complex logic of calculating discounts, but you don't want this class to be accessible to users of your library. Instead, you only expose the main OrderProcessor class, keeping the discount logic hidden with an internal class.

// Define a public class that uses an internal class
public class OrderProcessor
{
    public void ProcessOrder(int orderId)
    {
        // Internal class is used here
        DiscountCalculator calculator = new DiscountCalculator();
        decimal discount = calculator.CalculateDiscount(orderId);
        Console.WriteLine($"Order {orderId} processed with a discount of {discount:C}");
    }

    // Internal class that handles discount calculations
    internal class DiscountCalculator
    {
        public decimal CalculateDiscount(int orderId)
        {
            // Complex discount calculation logic
            return orderId * 0.05m;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the DiscountCalculator class is marked as internal, meaning it is only accessible within the assembly. The OrderProcessor class, which is public, uses this internal class to process orders. External users of the library can call ProcessOrder without needing to know about or interact with the DiscountCalculator class.

Using the Internal Class in Program.cs

Now, let's see how this works in practice:

class Program
{
    static void Main(string[] args)
    {
        OrderProcessor processor = new OrderProcessor();
        processor.ProcessOrder(12345); // Output: Order 12345 processed with a discount of $617.25
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the ProcessOrder method is publicly accessible, but the internal workings of discount calculation remain hidden, providing a clean and secure API.

Key Points to Remember About Internal Classes

  • Restricted Access: Internal classes are only accessible within the same assembly, which helps in keeping your public API clean and focused.
  • Encapsulation: They are often used to encapsulate implementation details, such as helper functions or complex logic that shouldn’t be exposed publicly.
  • Visibility Control: By using the internal access modifier, you control the visibility of classes and members, ensuring that only the intended parts of your code are exposed to other assemblies.

Internal classes are a vital tool for managing the complexity of large applications, allowing you to carefully control what parts of your code are accessible outside of your assembly. By encapsulating details and restricting access, you can maintain a clean, maintainable, and secure codebase.

Nested Classes

Nested classes in C# are defined within another class. This structure is useful for grouping related classes together and encapsulating the implementation details. Nested classes can be either static or non-static, and they have direct access to the private members of their enclosing class.

Why Use Nested Classes?

Nested classes are particularly useful when a class is closely tied to the logic of another class and isn’t meant to be used independently. They allow you to encapsulate helper classes, hide them from other parts of the program, and keep related code together. This can lead to a cleaner, more organized codebase.

Example: Defining a Nested Class

Let’s consider a scenario where we have a class that represents a Car and another class that represents a Engine. Since the Engine class is closely related to the Car class and doesn’t make much sense on its own, we can define it as a nested class within Car.

// Define a class with a nested class
public class Car
{
    // Define private fields
    private string model;
    private Engine carEngine;

   // Constructor
    public Car(string model)
    {
        this.model = model;
        carEngine = new Engine();
    }


    // Method to start the car
    public void StartCar()
    {
        carEngine.StartEngine();
        Console.WriteLine($"{model} is starting...");
    }

    // Nested class
    public class Engine
    {
        public void StartEngine()
        {
            Console.WriteLine("Engine started.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Car class has a private field model and a method StartCar that starts the car. The Engine class is nested within the Car class and contains a StartEngine method. By nesting Engine inside Car, we express the close relationship between the two.

Using the Nested Class in Program.cs

Let’s see how we can use the Car class and its nested Engine class in a program:

class Program
{
    static void Main(string[] args)
    {
        Car myCar = new Car("Toyota");
        myCar.StartCar(); // Output: Engine started. Toyota is starting...

        // Although you can create an instance of the nested class separately, it usually makes sense to use it through the outer class
        Car.Engine engine = new Car.Engine();
        engine.StartEngine(); // Output: Engine started.
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we create an instance of the Car class and call the StartCar method, which internally calls the StartEngine method of the nested Engine class. While it's possible to instantiate the nested class separately, it’s more common to access it through the outer class, emphasizing the relationship between the two.

Key Points to Remember About Nested Classes

  • Encapsulation: Nested classes help encapsulate implementation details that are not meant to be exposed outside of the outer class.
  • Access to Private Members: Nested classes can access private members of the outer class, making them suitable for helper classes that need to interact with the outer class’s internal state.
  • Organization: Use nested classes to group related classes together, leading to cleaner and more organized code.
  • Static or Non-static: Nested classes can be static or non-static. Static nested classes cannot access the instance members of the outer class directly, while non-static nested classes can.

Nested classes are a powerful way to structure your code, especially when dealing with complex objects that contain tightly coupled components. By keeping related classes together, you create a more cohesive and maintainable codebase.

Partial Classes

Partial classes in C# allow you to split a class definition across multiple files. This feature is particularly useful in large projects, where it can be beneficial to break a complex class into smaller, more manageable sections. By using the partial keyword, you can organize your code better, especially when working with generated code or collaborating in a team environment.

Why Use Partial Classes?

Imagine you’re working on a large application where a single class contains hundreds of lines of code. This can become difficult to manage and maintain. By using partial classes, you can divide the class into logical parts, each residing in a separate file. This not only makes the code more readable but also allows multiple developers to work on different parts of the class simultaneously without causing merge conflicts.

Example: Defining a Partial Class in csharp

Let’s say we have a class that handles various operations for an employee management system. Instead of putting all methods in one file, we can split them across multiple files using partial classes.

File 1: PartialClass_Methods1.cs

// Define a partial class
public partial class EmployeeOperations
{
    public void AddEmployee(string name)
    {
        Console.WriteLine($"Employee {name} added.");
    }
}
Enter fullscreen mode Exit fullscreen mode

File 2: PartialClass_Methods2.cs

// Define the other part of the partial class
public partial class EmployeeOperations
{
    public void RemoveEmployee(string name)
    {
        Console.WriteLine($"Employee {name} removed.");
    }
}
Enter fullscreen mode Exit fullscreen mode

In these examples, the EmployeeOperations class is split into two files, each containing a part of the class. The first file handles adding employees, while the second file handles removing them.

Using the Partial Class in Program.cs

Now, let’s use the EmployeeOperations class in our Program.cs file:

class Program
{
    static void Main(string[] args)
    {
        EmployeeOperations operations = new EmployeeOperations();

        operations.AddEmployee("John Doe");    // Output: Employee John Doe added.
        operations.RemoveEmployee("John Doe"); // Output: Employee John Doe removed.
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the EmployeeOperations class, although defined in multiple files, behaves like a single class. The methods AddEmployee and RemoveEmployee are seamlessly combined, providing a clean and organized way to manage operations.

Key Points to Remember About Partial Classes

  • Code Organization: Partial classes help keep large classes organized by splitting them into smaller, focused sections.
  • Team Collaboration: Multiple developers can work on different parts of the same class without interfering with each other’s code.
  • Generated Code: Often used with auto-generated code, where part of the class is generated by a tool, and the rest is written manually.

Partial classes are a powerful feature in C# that allow for better code management, especially in large-scale applications. By breaking down a class into logical components, you can maintain clean, readable, and maintainable code.

Conclusion

Classes are the building blocks of object-oriented programming in C#. By understanding the different types of classes—abstract, static, sealed, concrete, and singleton—you can create well-structured, maintainable, and efficient code. Whether you’re designing utility classes, defining abstract interfaces, or encapsulating complex logic, classes play a crucial role in shaping your application’s architecture.

Top comments (0)