C# Fundamentals: Inheritance, Polymorphism, Delegates, Types

Single Inheritance in C#

Single inheritance is a fundamental concept in object-oriented programming where a class inherits properties and behaviors from a single parent class. This promotes code reusability and establishes an “is-a” relationship between classes.

using System;

class Vehicle
{
    public void Start()
    {
        Console.WriteLine("Vehicle has started.");
    }
}

class Car : Vehicle
{
    public void Drive()
    {
        Console.WriteLine("Car is driving.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Car myCar = new Car();
        myCar.Start(); // Inherited from Vehicle
        myCar.Drive();
    }
}

In this example, Car inherits from Vehicle, allowing a Car object to use the Start() method defined in Vehicle, in addition to its own Drive() method.

Multilevel Inheritance in C#

Multilevel inheritance involves a chain of inheritance, where a class inherits from another class, which in turn inherits from yet another class. This creates a hierarchy of classes.

using System;

class Animal
{
    public void Eat()
    {
        Console.WriteLine("Animal eats food.");
    }
}

class Dog : Animal
{
    public void Bark()
    {
        Console.WriteLine("Dog barks.");
    }
}

class Puppy : Dog
{
    public void Weep()
    {
        Console.WriteLine("Puppy weeps.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Puppy myPuppy = new Puppy();
        myPuppy.Eat();  // Inherited from Animal
        myPuppy.Bark(); // Inherited from Dog
        myPuppy.Weep();
    }
}

Here, Puppy inherits from Dog, and Dog inherits from Animal, allowing a Puppy object to access methods from both parent classes.

Hierarchical Inheritance in C#

Hierarchical inheritance occurs when multiple classes inherit from a single base class. This allows different specialized classes to share common functionality from a single parent.

using System;

class Animal
{
    public void Eat()
    {
        Console.WriteLine("Animal eats food.");
    }
}

class Dog : Animal
{
    public void Bark()
    {
        Console.WriteLine("Dog barks.");
    }
}

class Cat : Animal
{
    public void Meow()
    {
        Console.WriteLine("Cat meows.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Dog dog = new Dog();
        dog.Eat();  // Inherited from Animal
        dog.Bark();

        Cat cat = new Cat();
        cat.Eat();  // Inherited from Animal
        cat.Meow();
    }
}

In this scenario, both Dog and Cat inherit from the Animal class, demonstrating a hierarchical structure.

Multiple Inheritance with Interfaces in C#

C# does not support direct multiple inheritance of classes, but it achieves similar functionality through interfaces. A class can implement multiple interfaces, thereby inheriting method signatures from all of them.

using System;

interface IAnimal
{
    void Eat();
}

interface ICanine
{
    void Bark();
}

class Dog : IAnimal, ICanine
{
    public void Eat()
    {
        Console.WriteLine("Dog eats food.");
    }

    public void Bark()
    {
        Console.WriteLine("Dog barks.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Dog myDog = new Dog();
        myDog.Eat();
        myDog.Bark();
    }
}

Here, the Dog class implements both IAnimal and ICanine interfaces, effectively demonstrating multiple inheritance of behavior contracts.

Abstract Classes in C#

An abstract class in C# cannot be instantiated directly but can be inherited by other classes. It can contain both abstract methods (without implementation) and concrete methods (with implementation). Abstract methods must be overridden by derived classes.

using System;

abstract class Shape
{
    // Abstract method: must be implemented by derived classes
    public abstract void Draw();

    // Concrete method: has an implementation
    public void Display()
    {
        Console.WriteLine("This is a shape.");
    }
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle.");
    }
}

class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Shape shape1 = new Circle();
        shape1.Draw();
        shape1.Display();

        Shape shape2 = new Square();
        shape2.Draw();
        shape2.Display();
    }
}

This example shows how Circle and Square classes provide specific implementations for the abstract Draw() method, while inheriting the concrete Display() method from the Shape abstract class.

Method Overloading in C#

Method overloading allows a class to have multiple methods with the same name but different parameter lists (different number of parameters, different types of parameters, or different order of parameters). This enables methods to perform similar operations on different data types or with varying inputs.

using System;

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

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

class Program
{
    static void Main()
    {
        Calculator calc = new Calculator();
        Console.WriteLine(calc.Add(5, 3));       // Calls Add(int, int)
        Console.WriteLine(calc.Add(2.5, 4.5));   // Calls Add(double, double)
    }
}

The Calculator class demonstrates overloading the Add method to handle both integer and double-precision floating-point numbers.

Method Overriding in C#

Method overriding allows a derived class to provide a specific implementation for a method that is already defined in its base class. The base class method must be declared as virtual, and the derived class method must use the override keyword.

using System;

class Animal
{
    public virtual void MakeSound()
    {
        Console.WriteLine("Animal makes a sound.");
    }
}

class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Dog barks.");
    }
}

class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Cat meows.");
    }
}

class Program
{
    static void Main()
    {
        Animal myAnimal;

        myAnimal = new Dog();
        myAnimal.MakeSound(); // Output: Dog barks.

        myAnimal = new Cat();
        myAnimal.MakeSound(); // Output: Cat meows.
    }
}

This demonstrates polymorphism, where a single method call (myAnimal.MakeSound()) behaves differently based on the actual type of the object at runtime.

Delegates in C#

A delegate in C# is a type-safe function pointer. It holds references to methods and can invoke them. Delegates are crucial for implementing event handling and callback mechanisms.

using System;

// Declare a delegate type
delegate void MessageDelegate(string message);

class Messenger
{
    public void ShowMessage(string msg)
    {
        Console.WriteLine("Message: " + msg);
    }

    public void ShowAlert(string msg)
    {
        Console.WriteLine("ALERT: " + msg.ToUpper());
    }
}

class Program
{
    static void Main(string[] args)
    {
        Messenger m = new Messenger();

        // Instantiate delegate with ShowMessage method
        MessageDelegate del = m.ShowMessage;
        del("Hello, world!");

        // Reassign delegate to ShowAlert method
        del = m.ShowAlert;
        del("system error");
    }
}

This example demonstrates how a MessageDelegate can point to different methods (ShowMessage and ShowAlert) and invoke them dynamically.

Multicast Delegates in C#

A multicast delegate can hold references to multiple methods. When the delegate is invoked, all the methods it references are called in the order they were added. This is achieved using the += operator to add methods and -= to remove them.

using System;

// Declare a delegate type
delegate void Notify();

class Alert
{
    public void LowBattery() => Console.WriteLine("Low battery!");
    public void FullCharge() => Console.WriteLine("Battery is full.");
}

class Program
{
    static void Main()
    {
        Alert alert = new Alert();

        // Create a multicast delegate
        Notify notify = alert.LowBattery;
        notify += alert.FullCharge; // Add another method

        // Invoke the delegate, calling both methods
        notify();
    }
}

When notify() is called, both LowBattery() and FullCharge() methods are executed sequentially.

Value Types vs. Reference Types in C#

Understanding the distinction between value types and reference types is crucial for effective C# programming, as it impacts memory management and how data is handled.

Value Types

  • Storage: Stored directly in the stack memory.
  • Content: Stores the actual data value.
  • Copy Behavior: When assigned to another variable, an independent copy of the value is created. Changing one variable does not affect the other.
  • Examples: int, float, char, bool, struct, enum.

Value Type Example:

using System;

class Program
{
    static void Main()
    {
        int a = 10;
        int b = a; // b gets a copy of a's value (10)
        b = 20;    // Changing b does not affect a

        Console.WriteLine($"Value of a: {a}"); // Output: Value of a: 10
        Console.WriteLine($"Value of b: {b}"); // Output: Value of b: 20
    }
}

Reference Types

  • Storage: Stored in the heap memory. Variables hold a reference (memory address) to the actual data.
  • Content: Stores the memory address where the actual data resides.
  • Copy Behavior: When assigned to another variable, only the reference (address) is copied. Both variables then point to the same object in memory. Changing the object through one variable affects the other.
  • Examples: class, array, string, object, delegate, interface.

Reference Type Example:

using System;

class Person
{
    public string Name;
}

class Program
{
    static void Main()
    {
        Person p1 = new Person();
        p1.Name = "Alice";

        Person p2 = p1; // p2 gets a copy of p1's reference (points to the same object)
        p2.Name = "Bob"; // Changing p2.Name also changes p1.Name

        Console.WriteLine($"Person 1 Name: {p1.Name}"); // Output: Person 1 Name: Bob
        Console.WriteLine($"Person 2 Name: {p2.Name}"); // Output: Person 2 Name: Bob
    }
}

This distinction is fundamental to understanding how data is managed and manipulated in C# applications.