× back

C# .NET Object Oriented Programming (OOP)

Introduction to Object Oriented Programming

  • What is Object-Oriented Programming?

    OOP ek programming paradigm hai jo objects aur classes ka use karta hai code ko organize aur structure karne ke liye. Isse code ka reuse, maintenance, aur scalability better hoti hai.

  • Principles of OOP:
    • Encapsulation

      Encapsulation ka matlab hai data (attributes) aur methods (functions) jo data pe kaam karte hain, unko ek single unit ya class me bundle karna. Ye kuch components ko restrict karta hai, jisse data ko galti se modify hone se bachaya ja sake.

      Example: A class `Person` with private variables and public getter and setter methods to access them.

    • Inheritance

      Inheritance ek class ko dusri class se properties aur methods inherit karne ki permission deta hai, jo code reuse promote karta hai. Jo class inherit karti hai use subclass kehte hain, aur jis class se inherit kiya jaata hai usse superclass kehte hain.

      Example: A class `Dog` inherits from the `Animal` class, and thus, it has all properties and methods of `Animal`.

    • Polymorphism

      Polymorphism ka matlab hai ki alag-alag classes ke objects ko ek common superclass ke objects ke tarah treat kiya ja sakta hai. Ye methods ko multiple ways me implement karne ki facility deta hai.

      Example: A method `makeSound()` could be defined in both `Dog` and `Cat` classes, but the implementation will be different for each.

    • Abstraction

      Abstraction ka concept hai ki complex implementation details ko hide kiya jaaye aur sirf zaruri features hi dikhaye jaaye. Ye abstract classes aur interfaces ke zariye achieve hota hai.

      Example: A class `Car` can be abstract, providing a general blueprint for all car types, while specific car types like `ElectricCar` or `SportsCar` implement detailed features.

Introduction to Object Oriented Programming

  • What is Object-Oriented Programming?

    Object-Oriented Programming (OOP) is a programming paradigm that uses objects and classes to organize and structure code. It allows for better code reusability, maintainability, and scalability.

  • Principles of OOP:
    • Encapsulation

      Encapsulation is the concept of bundling data (attributes) and methods (functions) that operate on the data into a single unit or class. It also restricts access to some of the object's components, which can prevent the accidental modification of data.

      Example: A class `Person` with private variables and public getter and setter methods to access them.

    • Inheritance

      Inheritance allows a class to inherit properties and methods from another class, promoting code reuse. The class that inherits is called the subclass, and the class from which it inherits is called the superclass.

      Example: A class `Dog` inherits from the `Animal` class, and thus, it has all properties and methods of `Animal`.

    • Polymorphism

      Polymorphism allows objects of different classes to be treated as objects of a common superclass. It also allows methods to be implemented in multiple ways.

      Example: A method `makeSound()` could be defined in both `Dog` and `Cat` classes, but the implementation will be different for each.

    • Abstraction

      Abstraction is the concept of hiding the complex implementation details and showing only the necessary features of an object. It is achieved using abstract classes and interfaces.

      Example: A class `Car` can be abstract, providing a general blueprint for all car types, while specific car types like `ElectricCar` or `SportsCar` implement detailed features.

Classes and Objects

Dividing code into classes

  • Dividing code into classes

    In Object-Oriented Programming, classes serve as blueprints for creating objects. By dividing the code into different classes, you make the code more modular, easier to maintain, and reusable.

    A class can have attributes (properties) and methods (functions) that define the behavior of the objects created from the class.

Creating objects and instances of classes

  • Creating objects

    Once a class is defined, you can create instances (objects) of that class. An object represents an individual instance of the class with its own set of data and behavior.

    The process of creating an object is known as instantiation.

Example 1:

                    
class Car {
    // Properties
    string model;
    int year;

    // Method
    void displayInfo() {
        Console.WriteLine("Model: " + model);
        Console.WriteLine("Year: " + year);
    }
}

class Program {
    static void Main() {
        // Creating an object of the Car class
        Car myCar = new Car();
        myCar.model = "Toyota";
        myCar.year = 2020;

        // Calling method on the object
        myCar.displayInfo();
    }
}
                
            

Explanation: In this simple example, we define a class `Car` with properties `model` and `year`. We create an object `myCar` of the `Car` class, assign values to its properties, and call the method `displayInfo()` to display the car's information.

Example 2:

                
class Animal {
    // Properties
    string name;
    int age;

    // Constructor to initialize properties
    public Animal(string n, int a) {
        name = n;
        age = a;
    }

    // Method to display animal's details
    public void displayDetails() {
        Console.WriteLine("Name: " + name);
        Console.WriteLine("Age: " + age);
    }
}

class Program {
    static void Main() {
        // Creating objects of the Animal class
        Animal dog = new Animal("Buddy", 3);
        Animal cat = new Animal("Whiskers", 2);

        // Calling method to display details
        dog.displayDetails();
        cat.displayDetails();
    }
}
                
            

Explanation: In this example, we define a class `Animal` with a constructor to initialize its properties. We create two objects (`dog` and `cat`) of the `Animal` class, pass values to the constructor, and call the `displayDetails()` method to show their information.

Fields, Properties, Methods, and Events

Adding fields to classes

  • Syntax for adding fields:

    Fields are variables declared inside a class, used to store data. They can have various access modifiers (like public, private, etc.) to control access.

                                
    class ClassName {
        // Field declaration
        accessModifier type fieldName;
    }
                                
                            

    Example: Here's how you can add fields to a class.

                                
    class Person {
        // Fields
        private string name;
        private int age;
    }
                                
                            

    Explanation: The `Person` class has two private fields: `name` (a string) and `age` (an integer). These fields store the data for an instance of `Person`.

Defining and using methods in classes

  • Syntax for defining methods:

    Methods define the behavior of a class. They can take parameters and return values. Methods are defined using the following syntax:

                                
    class ClassName {
        // Method declaration
        accessModifier returnType MethodName(parameters) {
            // Method body
        }
    }
                                
                            

    Example: Here's how you can define and use methods in a class.

                                
    class Person {
        private string name;
        private int age;
    
        // Method to display the person's details
        public void DisplayDetails() {
            Console.WriteLine("Name: " + name);
            Console.WriteLine("Age: " + age);
        }
    
        // Method to set the person's details
        public void SetDetails(string name, int age) {
            this.name = name;
            this.age = age;
        }
    }
                                
                            

    Explanation: The `Person` class has two methods: `DisplayDetails()` that prints the person's details and `SetDetails()` that assigns values to the `name` and `age` fields.

Creating Properties for Data Encapsulation

  • What are properties?

    Properties are members of a class that provide a controlled way to access private fields. They encapsulate data by restricting direct access and allowing only specific ways to read or modify it through get and set accessors.

    This ensures data integrity and allows us to add logic when reading or writing data, such as validation or transformation.

  • Syntax for defining properties:

    A property is defined with a type, a name, and two optional accessors: get (to retrieve the value) and set (to assign a value).

                                
    class ClassName {
        // Property declaration
        public type PropertyName {
            get { return field; }
            set { field = value; }
        }
    }
                            
                        
  • Example: Defining and Using Properties
                                
    class Person {
        private string name; // A private field to store the name.
    
        // Property to get and set the 'name' field.
        public string Name {
            get { return name; }
            set { name = value; }
        }
    }
    
    // Creating an object of the class and using the property.
    Person person = new Person(); // Creating an instance of the 'Person' class.
    person.Name = "Alice"; // Using the set accessor to assign a value.
    Console.WriteLine(person.Name); // Using the get accessor to retrieve the value.
                                
                            

    Explanation: In this example, the Person class has a private field name. The public property Name provides controlled access to this field. - The get accessor is used to retrieve the value of name. - The set accessor is used to assign a value to name, and you can add additional logic here, such as validation.

  • Using Object Initializer Syntax

    Object initializer syntax allows you to set property values directly at the time of object creation, making the code concise and more readable. This is especially useful when you need to initialize multiple properties.

                                
    class Person
    {
        private string name;
        private int age;
    
        // Property for 'name'
        public string Name
        {
            get { return name; }
            set { name = value; }
        }
    
        // Property for 'age'
        public int Age
        {
            get { return age; }
            set 
            { 
                if (value >= 0) // Adding validation for age.
                    age = value; 
                else
                    throw new ArgumentException("Age cannot be negative.");
            }
        }
    }
    
    // Using object initializer syntax
    Person person = new Person
    {
        Name = "Alice", // Setting the 'Name' property.
        Age = 22        // Setting the 'Age' property.
    };
    
    Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
                                
                            

    Explanation: In this example, the Person class has properties Name and Age. Instead of setting each property individually after creating the object, the object initializer syntax sets the values directly when the object is created.

    This approach improves code clarity and avoids the need for additional constructors, making it easier to initialize objects with multiple properties.

  • Why use properties?

    Properties offer several advantages over public fields:

    • Encapsulation: Keeps the internal representation hidden and provides a controlled interface for accessing data.
    • Validation: Allows adding conditions to ensure valid data is assigned (e.g., checking if a value is non-empty or within a range).
    • Read-only or write-only properties: Using only a get accessor makes a property read-only, while using only a set accessor makes it write-only.
    • Convenience: Object initializer syntax makes it easy to create objects with pre-defined values.

Implementing Events for Class Communication

  • What are events?

    Events are a way for one part of your program to notify other parts when something happens. For example, when you press a button, an event is raised, and other parts of the program react to it (e.g., showing a message or changing the screen).

    Events in C# rely on delegates, which are types that define the method signature for event handlers. When an event is raised, all methods (subscribers) attached to that event are executed.

  • How do events work?

    Think of events as a "broadcast" system:

    1. The publisher (e.g., a Button class) defines and raises the event when something happens.
    2. The subscribers (e.g., methods in other classes) listen to the event and react to it by running their code.
  • Syntax for defining events:

    Events are declared using the event keyword. The publisher uses a method to trigger the event when the appropriate condition occurs.

                                
    class ClassName {
        // Event declaration
        public event EventHandler EventName;
    
        // Method to trigger the event
        public void TriggerEvent() {
            if (EventName != null) {
                EventName(this, EventArgs.Empty);  // Notify subscribers
            }
        }
    }
                                
                            

    Let’s break this down:

    • event: This keyword declares an event that other classes can subscribe to.
    • EventHandler: A built-in delegate that defines the method signature (void Method(object sender, EventArgs e)).
    • EventName: The name of the event that other classes can subscribe to.
    • EventArgs.Empty: This means no additional data is sent with the event. It's a placeholder for events that don't need extra information.

  • Example: Button Click Simulation
                                
    using System;
    
    class Button
    {
        // Event declaration for the button click
        public event EventHandler Click;
    
        // Method to simulate a button click
        public void PerformClick()
        {
            Console.WriteLine("Button is clicked.");
            TriggerClickEvent();  // Notify all subscribers
        }
    
        // Method to trigger the Click event
        private void TriggerClickEvent()
        {
            if (Click != null)  // Check if there are subscribers
            {
                Click(this, EventArgs.Empty);  // Notify subscribers
            }
        }
    }
    
    class Program
    {
        static void Main()
        {
            // Create a Button object
            Button button = new Button();
    
            // Subscribe to the Click event
            button.Click += Button_ClickHandler;
    
            // Simulate button click
            Console.WriteLine("Simulating a button click...");
            button.PerformClick();
        }
    
        // Event handler for the button click
        static void Button_ClickHandler(object sender, EventArgs e)
        {
            Console.WriteLine("Button click event handled!");
        }
    }
                                
                            

    Explanation: Here's how this works:

    • Button class is the publisher. It declares the event Click and triggers it when PerformClick() is called.
    • The TriggerClickEvent method is responsible for raising the event and notifying all subscribers.
    • The Program class is the subscriber. It attaches the Button_ClickHandler method to the Click event using the += operator.
    • When button.PerformClick() is called, the event is triggered, and all subscribed methods (in this case, Button_ClickHandler) are executed.

  • Understanding EventArgs.Empty

    The second parameter of the EventHandler delegate is an EventArgs object. It allows events to send additional data to subscribers. For example, a file download event might include progress information.

    However, if no additional data is needed, EventArgs.Empty is used as a placeholder. It’s a predefined static field in the EventArgs class representing an empty event argument.

Introduction to constructors

Default constructors vs parameterized constructors

  • Default Constructor

    A default constructor is a constructor that does not take any parameters. It initializes an object with default values.

                                
    class ClassName {
        // Default constructor
        public ClassName() {
            // Initialization code with default values
        }
    }
                                
                            

    Example: Here's a default constructor.

                                
    class Person {
        private string name;
    
        // Default constructor
        public Person() {
            name = "Unknown";  // Default value
        }
    
        public void DisplayName() {
            Console.WriteLine("Name: " + name);
        }
    }
                                
                            

    Explanation: The `Person` class has a default constructor that sets the `name` to `"Unknown"` if no value is provided during object creation.

  • Parameterized Constructor

    A parameterized constructor takes one or more arguments and allows you to initialize an object with specific values at the time of creation.

                                
    class ClassName {
        // Parameterized constructor
        public ClassName(type parameter1, type parameter2) {
            // Initialization code with specific values
        }
    }
                                
                            

    Example: Here's a parameterized constructor.

                                
    class Person {
        private string name;
        private int age;
    
        // Parameterized constructor
        public Person(string name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public void DisplayInfo() {
            Console.WriteLine("Name: " + name);
            Console.WriteLine("Age: " + age);
        }
    }
                                
                            

    Explanation: The `Person` class has a parameterized constructor that takes `name` and `age` as parameters to initialize the object.

Constructor overloading

  • What is Constructor Overloading?

    Constructor overloading refers to having multiple constructors in a class with different parameter lists. This allows you to create objects in different ways depending on the provided arguments.

    The constructors must differ in the number or type of parameters, but they must have the same name as the class.

                            
    class ClassName {
        // Overloaded constructors
        public ClassName() {
            // Default constructor
        }
    
        public ClassName(type parameter) {
            // Constructor with one parameter
        }
    
        public ClassName(type parameter1, type parameter2) {
            // Constructor with two parameters
        }
    }
                            
                        

    Example: Here's an example of constructor overloading.

                            
    class Person {
        private string name;
        private int age;
    
        // Default constructor
        public Person() {
            name = "Unknown";
            age = 0;
        }
    
        // Parameterized constructor with name
        public Person(string name) {
            this.name = name;
            age = 0;
        }
    
        // Parameterized constructor with name and age
        public Person(string name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public void DisplayInfo() {
            Console.WriteLine("Name: " + name);
            Console.WriteLine("Age: " + age);
        }
    }
                            
                        

    Explanation: The `Person` class demonstrates constructor overloading with three constructors: a default constructor, a constructor that takes only the `name`, and a constructor that takes both `name` and `age`.

Defining Scope & Visibility

In object-oriented programming, access modifiers are used to set the visibility and accessibility of class members (fields, properties, methods, etc.). By using the right access modifier, you can control which parts of your class are accessible from outside the class and ensure that sensitive data is protected while maintaining functionality.

Understanding access modifiers (public, private, protected, internal, protected internal)

  • What are Access Modifiers?

    Access modifiers control the visibility and accessibility of class members to other classes or code outside of the class. The main access modifiers in C# are:

    • public: Allows access from anywhere, both inside and outside the class.
    • private: Restricts access to the class itself. No external class or object can access it.
    • protected: Allows access within the class and by derived (child) classes.
    • internal: Allows access within the same assembly (project), but not outside of it.
    • protected internal: Allows access to derived classes and classes within the same assembly.

    Syntax:

                            
    class ClassName {
        // Access modifier examples
        public string publicField;
        private int privateField;
        protected string protectedField;
        internal string internalField;
        protected internal string protectedInternalField;
    }
                            
                        

    Explanation: The above code snippet demonstrates the declaration of class members with different access modifiers.

Controlling visibility and accessibility of class members

  • Public Access

    When a class member is declared as public, it can be accessed from anywhere in the program, both within the class and from outside the class.

                                
    class Person {
        public string name;
    
        public void DisplayName() {
            Console.WriteLine("Name: " + name);
        }
    }
    
    class Program {
        static void Main() {
            Person person = new Person();
            person.name = "John";  // Accessible because it's public
            person.DisplayName();
        }
    }
                            
                        

    Explanation: The `name` field and `DisplayName()` method are declared as `public`, which allows them to be accessed directly from the `Program` class.

  • Private Access

    When a class member is declared as private, it can only be accessed within the class that defines it. It is not visible or accessible from outside the class.

                                
    class Person {
        private string name;
    
        public void SetName(string name) {
            this.name = name;  // Private field accessed via a public method
        }
    
        public void DisplayName() {
            Console.WriteLine("Name: " + name);
        }
    }
    
    class Program {
        static void Main() {
            Person person = new Person();
            person.SetName("Alice");  // Accessing private field through a public method
            person.DisplayName();
        }
    }
                            
                        

    Explanation: The `name` field is private, so it can only be accessed within the `Person` class. We use the `SetName()` method (which is public) to assign a value to the private field.

  • Protected Access

    The protected modifier allows access to a class member only within the class that defines it and any derived (child) classes.

                            
    class Animal {
        protected string name;
    
        public Animal(string name) {
            this.name = name;
        }
    }
    
    class Dog : Animal {
        public Dog(string name) : base(name) {}
    
        public void DisplayInfo() {
            Console.WriteLine("Dog's name: " + name);  // Accessing protected field in derived class
        }
    }
    
    class Program {
        static void Main() {
            Dog dog = new Dog("Max");
            dog.DisplayInfo();
        }
    }
                            
                        

    Explanation: The `name` field is protected, so it can be accessed in the derived `Dog` class but not from outside the class hierarchy.

  • Internal Access

    The internal modifier allows access to a class member only within the same assembly (i.e., project). It is not accessible from other assemblies, even if they reference the current assembly.

                            
    class Person {
        internal string name;
    
        public void DisplayName() {
            Console.WriteLine("Name: " + name);
        }
    }
    
    class Program {
        static void Main() {
            Person person = new Person();
            person.name = "John";  // Accessible because it's internal within the same assembly
            person.DisplayName();
        }
    }
                            
                        

    Explanation: The `name` field is marked as internal, meaning it is accessible within the same project or assembly but not from other projects.

  • Protected Internal Access

    The protected internal modifier combines the features of both protected and internal. It allows access to class members from derived classes or from any class within the same assembly.

                            
    class Person {
        protected internal string name;
    
        public void DisplayName() {
            Console.WriteLine("Name: " + name);
        }
    }
    
    class Program {
        static void Main() {
            Person person = new Person();
            person.name = "Alice";  // Accessible because it's protected internal within the same assembly
            person.DisplayName();
        }
    }
                            
                        

    Explanation: The `name` field is accessible in the `Program` class because it is within the same assembly and the field is marked as `protected internal`.

Garbage Collection

Garbage collection in C# is an automatic memory management process. The garbage collector (GC) helps in freeing up memory by removing objects that are no longer in use. This eliminates the need for manual memory management and helps in preventing memory leaks and resource exhaustion.

What is garbage collection?

  • Definition:

    Garbage collection is the process by which C# automatically reclaims memory occupied by objects that are no longer accessible. The garbage collector works by identifying objects that are not referenced by any part of the program and cleaning them up to free memory resources.

    Syntax:

                                
    GC.Collect();  // Force the garbage collector to run (rarely needed)
                                
                            

    Explanation: The GC.Collect() method forces the garbage collector to run, but it is generally not recommended to call it manually. The garbage collector runs automatically in the background when needed.

How C# handles memory management through the garbage collector

  • Memory management in C#

    In C#, memory management is automatically handled by the garbage collector (GC). The GC works in the background to manage the heap (where objects are stored) and ensures that objects no longer in use are removed. It tracks object references and cleans up memory when no references to an object remain.

    Key points about GC in C#:

    • The garbage collector automatically frees memory when objects are no longer referenced.
    • Objects are allocated memory on the heap.
    • The garbage collector uses generations to optimize performance, grouping objects based on their lifetimes (young, old, etc.).

                                
    class Example {
        public void CreateObject() {
            // Object will be eligible for garbage collection once it goes out of scope
            Example obj = new Example();
        }
    }
    
    class Program {
        static void Main() {
            Example example = new Example();
            example.CreateObject();  // The obj object will be eligible for garbage collection after this
        }
    }
                                
                            

    Explanation: In the above example, the obj object will be eligible for garbage collection once it goes out of scope. The GC will automatically remove the object from memory when it is no longer referenced.

Managing memory efficiently in OOP

  • Efficient memory management:

    To manage memory efficiently, developers should follow best practices to ensure that objects are not unnecessarily holding onto memory resources. C# provides features like Dispose() and the using statement to help manage resources efficiently, particularly for unmanaged resources.

    Example of using the Dispose pattern:

                                
    class Resource : IDisposable {
        private bool disposed = false;
    
        public void Dispose() {
            if (!disposed) {
                // Free unmanaged resources here
                disposed = true;
            }
            GC.SuppressFinalize(this);  // Prevent GC from calling finalizer
        }
    
        ~Resource() {
            Dispose();
        }
    }
    
    class Program {
        static void Main() {
            using (Resource resource = new Resource()) {
                // Use resource here
            }  // Dispose will be automatically called when exiting the using block
        }
    }
                                
                            

    Explanation: The Dispose() method is used to release unmanaged resources explicitly. The using statement ensures that the Dispose() method is automatically called when the resource is no longer needed, preventing memory leaks.

Inheritance and Polymorphism

Inheritance and polymorphism are core concepts in object-oriented programming (OOP) that promote code reuse and flexibility. Inheritance allows one class to inherit members (fields, properties, methods) from another, while polymorphism enables the same method to behave differently depending on the object it is acting on.

Inheritance: Creating derived classes from base classes

  • What is Inheritance?

    Inheritance allows a class to derive properties and behaviors from another class. The class that inherits is called the derived class, and the class it inherits from is called the base class. This promotes code reuse and allows for creating more specialized classes based on general ones.

    Syntax:

                                
    class BaseClass {
        public void DisplayMessage() {
            Console.WriteLine("Message from Base Class");
        }
    }
    
    class DerivedClass : BaseClass {
        // Derived class can use methods and properties from BaseClass
        public void AdditionalMethod() {
            Console.WriteLine("Additional method in Derived Class");
        }
    }
                                
                            

    Explanation: In this example, DerivedClass inherits the method DisplayMessage() from BaseClass. The derived class can also have additional methods.

Polymorphism: Using the same method signature for different implementations

  • What is Polymorphism?

    Polymorphism allows objects of different types to be treated as objects of a common base type. It enables the same method signature to be used with different implementations depending on the object type. Polymorphism can be achieved through method overriding or method overloading.

    Types of Polymorphism:

    • Method Overloading: Defining multiple methods with the same name but different parameters.
    • Method Overriding: Redefining a method in a derived class that was already defined in a base class.

    Method Overloading

    Example of Method Overloading:

                                
    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(2, 3));  // Calls Add(int, int)
            Console.WriteLine(calc.Add(2.5, 3.5));  // Calls Add(double, double)
        }
    }
                                
                            

    Explanation: The method Add() is overloaded in the Calculator class to handle both integer and double parameters, providing different implementations depending on the argument types.

    Method Overriding

    Example of Method Overriding:

                                
    class Animal {
        public virtual void MakeSound() {
            Console.WriteLine("Animal sound");
        }
    }
    
    class Dog : Animal {
        public override void MakeSound() {
            Console.WriteLine("Bark");
        }
    }
    
    class Program {
        static void Main() {
            Animal animal = new Animal();
            animal.MakeSound();  // Outputs: Animal sound
            
            Dog dog = new Dog();
            dog.MakeSound();  // Outputs: Bark
            
            Animal animalDog = new Dog();
            animalDog.MakeSound();  // Outputs: Bark (polymorphism in action)
        }
    }
                                
                            

    Explanation: In this example, the Dog class overrides the MakeSound() method from the Animal class. When called on a Dog object, the overridden method executes. This is an example of polymorphism where the method behavior changes based on the object type, even though the same method name is used.

Overloading Methods

Method overloading is a feature in C# that allows you to define multiple methods with the same name but different parameter types, number of parameters, or both. This enables the method to perform similar operations on different types of data, increasing flexibility and improving code readability.

Method overloading: Defining multiple methods with the same name but different parameters

  • What is Method Overloading?

    Method overloading allows a class to have multiple methods with the same name but different signatures. The method signature is determined by the number and types of parameters. The compiler differentiates between these methods based on the parameters passed during the method call.

    Syntax:

                                
    class MyClass {
        public int Add(int a, int b) {
            return a + b;
        }
    
        public double Add(double a, double b) {
            return a + b;
        }
    
        public string Add(string a, string b) {
            return a + " " + b;
        }
    }
                                
                            

    Explanation: In this example, the method Add() is overloaded to handle different parameter types: integers, doubles, and strings. Each version of the method performs the same operation (addition), but on different types of data.

Practical use cases of method overloading

  • Use case 1: Handling different data types with the same method name

    Method overloading is particularly useful when you want to perform the same operation on different types of data without having to create separate method names for each data type.

    Example:

                                
    class Display {
        public void Show(int number) {
            Console.WriteLine("Integer: " + number);
        }
    
        public void Show(string message) {
            Console.WriteLine("String: " + message);
        }
    
        public void Show(double value) {
            Console.WriteLine("Double: " + value);
        }
    }
    
    class Program {
        static void Main() {
            Display display = new Display();
            display.Show(100);  // Calls Show(int)
            display.Show("Hello, World!");  // Calls Show(string)
            display.Show(3.14);  // Calls Show(double)
        }
    }
                                
                            

    Explanation: The Show() method is overloaded to handle three different types of input: integers, strings, and doubles. This way, the same method name is used for different types of data without needing to create separate method names like ShowInt(), ShowString(), etc.

  • Use case 2: Performing operations based on the number of parameters

    Method overloading is also useful when performing operations that require different numbers of parameters. This allows the same method name to handle cases where the number of parameters may vary.

    Example:

                                
    class Calculator {
        public int Multiply(int a, int b) {
            return a * b;
        }
    
        public int Multiply(int a, int b, int c) {
            return a * b * c;
        }
    }
    
    class Program {
        static void Main() {
            Calculator calc = new Calculator();
            Console.WriteLine(calc.Multiply(2, 3));  // Calls Multiply(int, int)
            Console.WriteLine(calc.Multiply(2, 3, 4));  // Calls Multiply(int, int, int)
        }
    }
                                
                            

    Explanation: The Multiply() method is overloaded to handle multiplication of two or three integers. The correct method is selected based on the number of parameters passed during the method call.

  • Use case 3: Default values with overloaded methods

    You can combine method overloading with optional parameters to create methods that behave differently based on the parameters passed.

    Example:

                                
    class Logger {
        public void Log(string message) {
            Console.WriteLine("Log: " + message);
        }
    
        public void Log(string message, int level) {
            Console.WriteLine("Log Level " + level + ": " + message);
        }
    }
    
    class Program {
        static void Main() {
            Logger logger = new Logger();
            logger.Log("System started.");  // Calls Log(string)
            logger.Log("System error.", 2);  // Calls Log(string, int)
        }
    }
                                
                            

    Explanation: The Log() method is overloaded to accept either a single string message or a string message with a log level. This provides flexibility in logging without requiring separate method names for different log levels.

Error Handling and Exception Management

In C#, error handling and exception management are crucial for building reliable applications. Exceptions are runtime errors that disrupt the normal flow of the program. By using the try, catch, and finally blocks, we can catch and handle exceptions gracefully. Additionally, we can create custom exceptions to handle specific error scenarios.

Understanding exceptions in C#

  • What are exceptions?

    Exceptions are abnormal events that occur during the execution of a program, often due to runtime errors like division by zero, file access issues, or invalid input. These errors can cause a program to crash unless they are caught and handled.

    Common exception types:

    • System.Exception: The base class for all exceptions.
    • System.ArgumentException: Occurs when a method receives invalid arguments.
    • System.IO.IOException: Occurs when there is an error with file I/O operations.
    • System.NullReferenceException: Occurs when trying to use a reference that is null.

Throwing exceptions using the throw keyword

  • What is the throw keyword?

    The throw keyword in C# is used to explicitly throw an exception, signaling that something has gone wrong in the program. You can throw a predefined exception or create a new one.

    Syntax:

                                
    throw new Exception("An error occurred.");
                                
                            

    Example:

                                
    class Program {
        static void Main() {
            try {
                int x = 5;
                if (x == 5) {
                    throw new InvalidOperationException("Invalid operation due to x being 5.");
                }
            }
            catch (Exception ex) {
                Console.WriteLine(ex.Message);
            }
        }
    }
                                
                            

    Explanation: In this example, the throw keyword is used to manually throw an InvalidOperationException when the variable x is equal to 5. The exception is caught by the catch block and its message is displayed.

Handling exceptions with try...catch...finally blocks

  • What are try, catch, and finally blocks?

    The try block contains strong that might throw an exception. The catch block handles the exception if it occurs. The finally block, if used, will always execute, regardless of whether an exception was thrown or not, making it ideal for cleanup operations.

    Syntax:

                                
    try {
        // Code that may throw an exception
    } catch (ExceptionType ex) {
        // Code to handle the exception
    } finally {
        // Cleanup code (optional)
    }
                                
                            

    Example:

                                
    class Program {
        static void Main() {
            try {
                int[] numbers = { 1, 2, 3 };
                Console.WriteLine(numbers[5]);  // This will throw an exception
            }
            catch (IndexOutOfRangeException ex) {
                Console.WriteLine("Error: " + ex.Message);
            }
            finally {
                Console.WriteLine("This will always execute.");
            }
        }
    }
                                
                            

    Explanation: The program attempts to access an element of the array that is out of bounds, which throws an IndexOutOfRangeException. The catch block handles the exception, and the finally block executes regardless of whether an exception was thrown or not, printing a cleanup message.

Custom exceptions in C#

  • What are custom exceptions?

    Custom exceptions are user-defined exceptions that allow you to create more specific error messages for your application. Custom exceptions inherit from the base Exception class and can include additional data or functionality.

    Syntax:

                                
    class MyCustomException : Exception {
        public MyCustomException(string message) : base(message) { }
    }
                                
                            

    Example:

                                
    class MyCustomException : Exception {
        public MyCustomException(string message) : base(message) { }
    }
    
    class Program {
        static void Main() {
            try {
                throw new MyCustomException("This is a custom exception.");
            }
            catch (MyCustomException ex) {
                Console.WriteLine("Caught custom exception: " + ex.Message);
            }
        }
    }
                                
                            

    Explanation: In this example, we define a custom exception MyCustomException that inherits from the Exception class. When the exception is thrown, it is caught by the catch block, and the custom message is displayed.

Simplifying Maintenance Through Inheritance

Inheritance in C# simplifies code maintenance by allowing common functionality to be shared across multiple classes. By using base classes and derived subclasses, we can avoid code duplication, improve reusability, and make extending functionality easier. This promotes better organization and reduces the overall complexity of the codebase.

The role of inheritance in simplifying code maintenance

Implementing a base class and deriving subclasses

Benefits of inheritance in extending functionality

Virtual and Abstract Methods

Virtual and abstract methods provide mechanisms for defining methods in base classes that can be overridden or implemented by derived classes. These features help create flexible and extensible code. Virtual methods provide default implementations that can be overridden, while abstract methods must be implemented by any derived class.

Defining virtual methods in base classes

  • What are virtual methods?

    A virtual method is defined in a base class and provides a default implementation. Derived classes can override the virtual method to provide their own specific implementation.

    Syntax:

                                
    class BaseClass {
        public virtual void Display() {
            Console.WriteLine("Base class display method.");
        }
    }
    
    class DerivedClass : BaseClass {
        public override void Display() {
            Console.WriteLine("Derived class display method.");
        }
    }
                                
                            

    Example:

                                
    class Animal {
        public virtual void Speak() {
            Console.WriteLine("Animal speaks.");
        }
    }
    
    class Dog : Animal {
        public override void Speak() {
            Console.WriteLine("Dog barks.");
        }
    }
    
    class Program {
        static void Main() {
            Animal myAnimal = new Animal();
            myAnimal.Speak();  // Output: Animal speaks.
            
            Dog myDog = new Dog();
            myDog.Speak();     // Output: Dog barks.
        }
    }
                                
                            

    Explanation: The Speak() method in the Animal class is virtual, meaning it can be overridden in the Dog class. When Speak() is called on a Dog object, it uses the overridden method, not the base class implementation.

Understanding abstract methods and classes

  • What are abstract methods?

    Abstract methods are declared in an abstract class and do not provide an implementation. These methods must be implemented by any non-abstract class that inherits from the abstract class. An abstract class cannot be instantiated directly.

    Syntax:

                                
    abstract class BaseClass {
        public abstract void Display();
    }
    
    class DerivedClass : BaseClass {
        public override void Display() {
            Console.WriteLine("Implemented in Derived class.");
        }
    }
                                
                            

    Example:

                                
    abstract class Animal {
        public abstract void Speak();
    }
    
    class Dog : Animal {
        public override void Speak() {
            Console.WriteLine("Dog barks.");
        }
    }
    
    class Program {
        static void Main() {
            Dog myDog = new Dog();
            myDog.Speak();  // Output: Dog barks.
        }
    }
                                
                            

    Explanation: The Speak() method in the Animal class is abstract, meaning that any derived class (like Dog) must provide an implementation for this method. The Dog class implements the Speak() method.

Differences between virtual and abstract methods

  • Key differences:

    1. Implementation: A virtual method provides a default implementation in the base class, while an abstract method does not provide an implementation and must be implemented by derived classes.

    2. Override requirement: A virtual method can be optionally overridden in derived classes, whereas an abstract method must be overridden in all non-abstract derived classes.

    3. Class type: A class containing a virtual method can be instantiated, whereas a class containing an abstract method must be abstract itself and cannot be instantiated directly.

Method Overriding

Method overriding allows a derived class to provide its own implementation of a method that is already defined in its base class. It ensures that the most specific implementation of a method is called, even when using a base class reference. Method overriding is a key feature of polymorphism in object-oriented programming.

Overriding base class methods in derived classes

  • What is method overriding?

    Method overriding occurs when a derived class redefines a base class method with the same signature. It allows the derived class to provide a specific behavior that is different from the base class.

    Syntax:

                                
    class BaseClass {
        public virtual void Display() {
            Console.WriteLine("Base class Display method.");
        }
    }
    
    class DerivedClass : BaseClass {
        public override void Display() {
            Console.WriteLine("Derived class Display method.");
        }
    }
                                
                            

    Example:

                                
    class Animal {
        public virtual void Speak() {
            Console.WriteLine("Animal speaks.");
        }
    }
    
    class Dog : Animal {
        public override void Speak() {
            Console.WriteLine("Dog barks.");
        }
    }
    
    class Program {
        static void Main() {
            Animal myAnimal = new Animal();
            myAnimal.Speak();  // Output: Animal speaks.
    
            Animal myDog = new Dog();
            myDog.Speak();  // Output: Dog barks.
        }
    }
                                
                            

    Explanation: The Speak() method in the Animal class is virtual and is overridden in the Dog class. When the myDog object (of type Animal) calls the Speak() method, the overridden version in Dog is executed.

Using the override keyword in derived classes

  • Why use the override keyword?

    The override keyword explicitly indicates that a method in a derived class is overriding a virtual or abstract method in its base class. It ensures clarity and prevents accidental redefinition of methods that are not intended to be overridden.

    Syntax:

                                
    class BaseClass {
        public virtual void ShowMessage() {
            Console.WriteLine("Base class message.");
        }
    }
    
    class DerivedClass : BaseClass {
        public override void ShowMessage() {
            Console.WriteLine("Derived class message.");
        }
    }
                                
                            

    Example:

                                
    class Vehicle {
        public virtual void StartEngine() {
            Console.WriteLine("Starting engine in Vehicle.");
        }
    }
    
    class Car : Vehicle {
        public override void StartEngine() {
            Console.WriteLine("Starting engine in Car.");
        }
    }
    
    class Program {
        static void Main() {
            Vehicle myVehicle = new Car();
            myVehicle.StartEngine();  // Output: Starting engine in Car.
        }
    }
                                
                            

    Explanation: The StartEngine() method in Vehicle is overridden in the Car class using the override keyword. When a Car object (referenced as Vehicle) calls the method, the overridden version in Car is executed.

Practical examples of method overriding

  • Practical use case:

    Consider a scenario where you have a base class defining a general behavior, and subclasses that specify the behavior for different types. Method overriding ensures that the correct implementation is called at runtime.

    Example:

                                
    class Employee {
        public virtual void CalculateSalary() {
            Console.WriteLine("Calculating salary for general employee.");
        }
    }
    
    class Manager : Employee {
        public override void CalculateSalary() {
            Console.WriteLine("Calculating salary for manager with bonuses.");
        }
    }
    
    class Intern : Employee {
        public override void CalculateSalary() {
            Console.WriteLine("Calculating stipend for intern.");
        }
    }
    
    class Program {
        static void Main() {
            Employee emp1 = new Manager();
            emp1.CalculateSalary();  // Output: Calculating salary for manager with bonuses.
    
            Employee emp2 = new Intern();
            emp2.CalculateSalary();  // Output: Calculating stipend for intern.
        }
    }
                                
                            

    Explanation: The base class Employee defines a virtual method CalculateSalary(). Subclasses Manager and Intern override this method to provide specific implementations. This enables polymorphism, where the appropriate method is called based on the actual type of the object.