Learn C#.NET Advancec Level

Mritunjay KumarMritunjay Kumar
233 min read

If you do not understande what is .NET ,what is C#, Compilation of C#, execution of C#, Data types,Operatord, type casting, Strings, Conditional statments, Controll statments, Loop's, Array's and Method's. Then first understand this then come hear, hear is the link of learn this things: Bigginner lavel learn.

OOP (Object Oriented programming):

  • OOP means object-oriented programming system/structure.

  • OOP is a paradigm/methodology/way of programming, a way of creating real-world applications, providing security, and building business applications.

  • C language is a Structured Programming Language, but the concept of Classes was added to C language features and named C with classes. Later, it was renamed C++.

  • C++ is an object-oriented programming language. Java is also an object-oriented programming language from Sun Microsystems (Oracle).

  • C# is an object-oriented programming language from Microsoft.

  • Class and Object is a bulding block of OOP's because without creating class and object we cannot create realword object in programming world.

The variable inside the method called local/auto variable.

Why we need OOP?

  • Because early Programming Language like C Follow Step by Step Procedure for Programming.

  • That is Caled Procedural Programming.

  • It is Good for Simple Application, but hard for Complex application.

Benefit of OOP's

  • Improved software-developments productivity.

  • Improve software maintainability.

  • Faster developments.

  • Lower cost of development.

  • Hogher-quality software.

Disadvantages:

  • Steep learning curve.

  • Larger program size.

Object-Oriented Programming Features (six pillars of OOP's)

  1. Class & Objects

  2. Methods

  3. Encapsulation

  4. Abstraction

  5. Inheritance

  6. Polymorphism


Class & Object

Every object should have the following characteristics.

  1. State

  2. Identity

  3. Behaviour

ClassObject
Class is Blueprint/TemplateObject is Instance of Class
Class is Not a Real World EntityOnject is a Real World Entity
Class do Not Occupy MemoryObject Occupy Memory
  • Class has Attributes & Behavior.

  • Based on Class Object has Same Attributes and Behavior.

  • For example: Create a class for Car and Dog to understand.

  • Syntax of Class:

    <access modifier> class <identifier>{}

      public class Car
      {
          //Body of class
      }
    
  • Example:

      public class Car
      {
          int num1;
          int num2;
          static void Main()
          {
              Car c = new Car();//Create Object
          }
      }
    

Syntax of Object/instance: Car c = new Car();

Carc\=newCar()
Class namerefrence name (Declear object)Assignment operatorAllocate memory (Object create)Constructor name

Car c -> Refrence variable.

new Car() -> Instance.

Example:

using System;

namespace AccessModifiers
{
    internal class SimpleObj
    {
        int num1;
        int num2;
        int retult;

        void Add()
        {
            retult = num1 + num2;
            Console.WriteLine(retult);
        }
        void Sub()
        {
            retult = num1 - num2;
            Console.WriteLine(retult);
        }
        static void Main()
        {
            SimpleObj obj = new SimpleObj();
            obj.num1 = 10;
            obj.num2 = 30;
            obj.Add();
            obj.Sub();
        }
    }
}

Object has the same attributes and behavior as defined in the class, allowing us to easily call all variables and methods. When you create an object, the object contains the same attributes and behavior as the class. A class can have multiple objects.

What is class?

  • Class is type of an object.

  • Class is a template to an object.

  • Class is a blue print to an object.

  • Class is a collection of similar type of objects.

What is an Object?

  • Object is an instance of a class.

  • With one class we can create n no. of objects.

  • Objects may interact with each other.

Diffrence bitween Object and Instance.


Constructor

  1. OOP (Object Oriented Programming) means all types of code are inside class. A class contains many things like fields, methods, constructors, and more.

  2. Constructor Defination: A Method which is responsible for initializing the variable inside the class and create instance of the class.

  3. Instance mins create object and object name is same name of class.

  4. I told you constructor is method it mins it's take name, and the name shood be exactly same as class name is mandatory another wize you got error.

  5. Constructor does not return any value.

  6. Constructor is invoked implicitly as soon as an object of the class is created.

  7. We can pass parameters to a constructors.

  8. We can also give access modifier (public).

  9. Each & every class requires this constructor if you want to create the instance of the class inside the class then you need constructor. Syntex of creating explicit constructor:-[<modifiers>] <Name of Class> (<parameter list>){//Code}.

  10. Example:-

    using System;
    namespace Learn
    {
        class Program
        {
            // Explicit constructor
            public Program()
            {
                Console.WriteLine("Explicit constructor called");
            }
            /*
                - 'public' is a access modifier.
                - 'Program' is the name of the class.
                - We do not pass any parameters here.
                - Console.WriteLine("Explicit constructor called"); is the code inside the constructor.
            */
            static void Main(string[] args) 
            {
                // Creating multiple instances of the class
                Program obj = new Program(); // Create the first instance of the class.
                Program obj1 = new Program(); // Create the second instance of the class.
                Program obj2 = new Program(); // Create the third instance of the class. 
            }
        }
    }
    
  11. Here, Program obj = new Program(); is an instance, meaning I created an object (a real entity) of the class (blueprint). If you create an object, the syntax is <Class name> <Variable name> = new <Constructor name>(). When you write only <Class name> <Variable name> = new, it means memory space is created. We will learn about objects and classes later.

  12. If you use static with a variable or method, it means you can call it without creating an object. If you don't use static, then you need to create an object to call it.

  13. If you not create the constructor inside the class then compiler create the implicit constructor inside the class. And all variable you decleared inside the class automatically inslized by implicit constructor.

  class MyClass
  {
      int i; string s; bool b;
      public MyClass()  //create automatimplicit constructor by compilercannot see this constructobyDefault it's public
      {
          i = 0;
          s = null;
          b = false; 
      }
  }
    using System;
        namespace Learn
        {
            class Program
            {
                int i; string s; bool b;
                static void Main(string[] args) 
                {
                    //implicit constructor:-
                    Program p = new Learn.Program(); // Calling the implicit constructor. Syntex:-<Class Name> <variable>  = new <namespacename>.<constructor>
                    Console.WriteLine("Value of i: "+ p.i + " Value of s: "+ p.s + " Value of b: "+ p.b);  //Automaticaly inslize the value by default implicit constructor, You get the output like that :- Value of i: 0 Value of s: null Value of b: false
                }
            }
    }
  • Implicit constructor are parameter less construtor this type of constructor also know as default constructor.

  • The implicit constructor's access level depends on the class's access modifier.

  • Those constructor whic is define by you which is inside the class are called explicit constructor.

  • Explicit constructor are parameteriz also.

  • Difference bitween defining a constructor and calling a constructor:- Difference is two type Implicit & Explicit but calling in only one Explicit like that calling Program p = new Program();.

  • Syntex of calling constructor :- <Class Name> <variable> = new <constructor>

  • If you want to create only instance without creating constructor, you can create it like that but outside of class.

    class MyClass
    {
        int i;
    }
    MyClass obj = new MyClass(); //Like that you create instance of class without creating any constructor.

Types of Constructor

  1. Default or Parameter less constructor.

  2. Parmetrized Constructor.

  3. Copy Constructor.

  4. Static Constructor.

  5. Private Constructor.

1. Default or Parameter less constructor :-

  • If a constructor method cannot take any parameters then we call default or parameter less constructor.

  • If you define the constructor like that public MyClass(); in inside the class, that is also called as a Implicit Constructor or Default Constructor or Parameter less Constructor.

2. Parmetrized Constructor :-

  • If a constructor method is defined with parameter by programmer are called parametrize constructor. And the constructor is define explicit constructor not implicit constructor.

  • Example:-

    using System;
        namespace Learn
        {
            class Program
            {
                public Program(int i)
                {
                    Console.WriteLine("parametrize constructor "+ i);
                }
                static void Main(string[] args) 
                {
                    Program pc = new Program(15); //Output is :- parametrize constructor 10
                }
            }
    }
    using System;
        namespace Learn
        {
            class Program
            {
                int x;
                // This is a parameterized constructor for the `Program` class.
                public Program(int i)
                {
                    Console.WriteLine("parametrize constructor "+ i);
                }
                // This is a normal method of the `Program` class.
                public void Display()
                {
                    Console.WriteLine("Vale of x is: "+ x);
                }
                static void Main(string[] args) 
                {
                    // Creates a instance of the Program class and passing the value `15` to the parameterized constructor.
                    Program pc = new Program(15); //Output is :- parametrize constructor 15 

                    //Calls the Display method on the `pc object`(which is instance of the `Program class`).
                    //If you not inslize the value in `x` then compiler get the responsiblity to inslize the valu in `x` by `Implicitly`.
                    pc.Display(); //Output is :- Vale of x is: 0  // pc is the instance of class and inside the class i am calling Display method.    
                }
            }
    }
  • If you not inslize the value in x then compiler get the responsiblity to inslize the valu in x by Implicitly.
     using System;
         namespace Learn
         {
             class Program
             {
                 int x;
                 public Program(int x)
                 {
                     this.x = x;// By define constructor it inslize the value.
                     Console.WriteLine("parametrize constructor "+ i);
                 }
                 public void Display()
                 {
                     Console.WriteLine("Vale of x is: "+ x);
                 }
                 static void Main(string[] args) 
                 {
                     Program pc = new Program(15); //you send the value in constructor. Output is :- parametrize constructor 15
                     //pc is is the object which is instance of class.
                     pc.Display(); //Output is :- Vale of x is: 15
                 }
             }
     }

this keyword:

Refers to the current instance of the class. It acts as a reference to the current object, allowing access to the object's members (fields, properties, methods) and constructors. It is used to differentiate between instance variables and parameters or local variables with the same name.

Invoking Other Constructors:

In constructor chaining, the this keyword can be used to call another constructor in the same class. This helps in reducing code duplication and organizing initialization logic.

   class A
   {
       public A() : this("mritunjay"){ } // Calls the two-parameter constructor
       public A(string nam)
       {
           Console.WriteLine(nam);
       }
   }
   class Test
   {
       static void Main()
       {
           A a = new A();
       }
   }

Passing the Current Object:

this can be passed as an argument to methods or used to return the current object from a method.

using System;
namespace Demo
{
    class A
    {
        int x = 10;
        public A GetP() { return this; }
    }
    class Test
    {
        static void Main()
        {
            A a = new A();
            Console.WriteLine(a.GetP());//Out: Demo.A
        }
    }
}

Key Points:

  • Scope: The this keyword can only be used within non-static methods and constructors, as it refers to an instance of the class. Static methods do not have an instance context and therefore cannot use this.

  • Contextual Clarity: Using this is not mandatory when referring to instance members unless there is a naming conflict with parameters or local variables. However, some developers prefer using this consistently for clarity.

  • Hear allocate two place value(Get two coppy or instace of x in pc and pc1).
     using System;
         namespace Learn
         {
             class Program
             {
                 int x;
                 public Program(int i)
                 {
                     x = i;// By your define constructor it inslize the value.
                     Console.WriteLine("parametrize constructor "+ i);
                 }
                 public void Display()
                 {
                     Console.WriteLine("Vale of x is: "+ x);
                 }
                 static void Main(string[] args) 
                 {
                     // Hear allocate two place value(Get two coppy or instace of x in pc and pc1). And also create two memory space.
                     Program pc = new Program(15); //Output is :- parametrize constructor 15
                     Program pc1 = new Program(19); //Output is :- parametrize constructor 19
                     pc.Display(); //Output is :- Vale of x is: 15
                     pc1.Display(); //Output is :- Vale of x is: 19
                 }
             }
     }

3. Copy Constructor :-

  • If we want to create multiple instances with the same value then we use these copy constructors, in a copy constructor the constructor take the same class as a parameter to it.

  • The copy constructor is used to create a new instance by copying the values from an existing instance.

     using System;
         namespace Learn
         {
             class Program
             {
                 int x;
                 public Program(int i) // Parametrize Constructor
                 {
                     x = i;
                     Console.WriteLine("parametrize constructor "+ i);
                 }

                 public Program(Program other) // Copy constructor: Program is class name and other is refrence    
                 {
                     x = other.x; // Copy the value from another instance
                     Console.WriteLine("Copy constructor " + other.x);
                 }
                 public void Display()
                 {
                     Console.WriteLine("Vale of x is: "+ x);
                 }
                 static void Main(string[] args) 
                 {
                     // Calls parameterized constructor
                     Program pc = new Program(15); //Output is :- parametrize constructor 15
                     // Calls copy constructor
                     Program pc1 = new Program(pc); //Output is :- parametrize constructor 15
                     pc.Display(); //Output is :- Vale of x is: 15
                     pc1.Display(); //Output is :- Vale of x is: 15
                 }
             }
     }

If you are having trouble to understanding this code, let me clear up:-

public Program(Program other)
{
    x = other.x; // Copy the value from another instance
    Console.WriteLine("Copy constructor " + other.x);
}
  • The parameter of the copy constructor is of the same type as the class (Program in this case).

  • By convention, we name this parameter other (or sometimes source, original, etc.).

  • other.x refers to the x field of the existing instance (the one being copied).

  • Using other.x allows you to access the value of x from the existing instance and assign it to the new instance.

  • Create an instance like Program pc1 = new Program(pc);. This makes a new instance (pc1) by copying the value of x from the existing instance (pc).

  • The line x = other.x; ensures that pc1.x gets the same value as pc.x.

  • The output “Copy constructor 15” indicates that the value of x from the existing instance (pc) was successfully copied to the new instance (pc1).

    Whyother.x and Program other ?

  1. Static Constructor:

    • Static constructor are responsible in initializing static variable and these constructors are never called explicitly they are implicitly called and more over these constructor are first to execute under any class.

        using System;
        namespace Learn
        {
            class Program
            {
                static Program()  //This is static constructor.
                {
                    Console.WriteLine("Static constructer executed!");
                }
      
                static void Main(string[] args)
                {
      
                }
            }
        }
      

  • Main method runs first, but just before it runs, the static constructor is executed automatically.

  • Static constructors cannot be parameterized, so overloading static constructors (passing parameters in a static constructor) is not possible.

Q. Why is an explicit constructor required in the program? And what is the need for defining a constructor explicitly again?

Ans: We require an explicit constructor because if we do not define a constructor, the compiler creates an implicit constructor and initializes default values in variables which is declear in class. That's why we need the explicit constructor to initialize our own values or pass parameters to change the values by creating an instance of the class and also make multiple instance of class (reusability approach apply) .

    //First.cs
    using System;

    namespace Learn
    {
        class First
        {
            public int x = 100;
        }
        class Second
        {
            public int x;
        }
        class TestClasses
        {
            static void Main()
            {
                First f1 = new First();
                First f2 = new First();
                First f3 = new First();
                Console.WriteLine(f1.x +" "+ f2.x + " "+ f3.x);//Out:- 100 100 100

                Second s1 = new Second();
                Second s2 = new Second();
                Second s3 = new Second();
                Console.WriteLine(s1.x + " " + s2.x + " " + s3.x);//Out:- 0 0 0

                Second ss1 = new Second(10); //Show Error when you pass the argument Second(10)    
                Second ss2 = new Second(15); //Show Error when you pass the argument Second(10)
                Second ss3 = new Second(25); //Show Error when you pass the argument Second(10)
                Console.WriteLine(ss1.x + " " + ss2.x + " " + ss3.x);
                //Out:- 0 0 0
            }
        }
    }

That's way we need constructor to inslize own value.

    using System;

    namespace Learn
    {
        class First
        {
            public int x = 100;
        }
        class Second
        {
            public int x;
            public Second(int x) //This is a constructor
            {
                this.x = x;
            } 
        }
        class TestClasses
        {
            static void Main()
            {
                First f1 = new First();
                First f2 = new First();
                First f3 = new First();
                Console.WriteLine(f1.x +" "+ f2.x + " "+ f3.x);//Out:- 100 100 100

                Second s1 = new Second(); //Show Error when you pass the argument Second(10) because explicit constructor define   
                Second s2 = new Second(); //Show Error when you pass the argument Second(10) because explicit constructor define
                Second s3 = new Second(); //Show Error when you pass the argument Second(10) because explicit constructor define
                Console.WriteLine(s1.x + " " + s2.x + " " + s3.x);

                Second ss1 = new Second(10); 
                Second ss2 = new Second(15); 
                Second ss3 = new Second(25); 
                Console.WriteLine(ss1.x + " " + ss2.x + " " + ss3.x); //Out:- 10 15 25
            }
        }
    }

'this.x = x' mins this refer to the class variable x and 'x' refer to the parameter of constructor.

When you define a class, first identify if the class variable is required. If it is, then create an explicit constructor and pass the value through that constructor. This way, every time an instance of the class is created, we get a chance to pass a new value. This is the main advantage of explicit constructor.

Nots : Generaly every class requires some value for execution and the value that are required for a class to execute are always sent to that class by using the constructor only.

  1. Private Constructor :

    A private constructor is a special type of constructor that can't be accessed from outside the class it's defined in. This means you can't create instances of the class directly from outside. Also, you can't inherit a class that has a private constructor. Private constructors are usually used to:

    • Prevent Instantiation: Stop the creation of objects from a class.

        class Example
        {
            // Private constructor
            private Example()
            {
            }
      
            public static void DisplayMessage()
            {
                Console.WriteLine("Hello, world!");
            }
        }
      
        class Program
        {
            static void Main()
            {
                // Example e = new Example(); // This will cause a compile-time error    
                Example.DisplayMessage(); // Output: Hello, world!
            }
        }
      
    • Implement Singleton Pattern: Make sure only one instance of a class is created.

        using System;
      
        class Singleton
        {
            // This field holds the single instance of the Singleton class
            private static Singleton instance; 
      
            // Private constructor prevents instantiation from other classes
            private Singleton()
            {
            }
      
            // Public property to provide access to the single instance of the class
            public static Singleton Instance
            {
                get
                {
                    // If no instance exists, create one
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                    return instance;
                }
            }
      
            // Method to demonstrate functionality
            public void ShowMessage()
            {
                Console.WriteLine("Singleton instance");
            }
        }
      
        class Program
        {
            static void Main()
            {
                Singleton s1 = Singleton.Instance;
                Singleton s2 = Singleton.Instance;
                s1.ShowMessage(); // Output: Singleton instance
                // Verify both instances are the same
                Console.WriteLine(s1 == s2); // Output: True
            }
        }
      
      • The Singleton class has a private constructor to prevent creating new instances from outside.

      • A static property Instance makes sure only one instance of Singleton is created.

      • The ShowMessage method shows how to use the singleton instance.

      • In the Main method, two references s1 and s2 are obtained from the Instance property, and both point to the same instance.Provide Static Members: Hold only static members and offer utility or helper methods.

Diffrence bitween Static constructors and Non-static Constructors :

  1. Static Constructors: If the constructor is declared using the static modifier, we call it a static constructor. All other constructors are non-static.
//code:
using System;
namespace Practical_Exercises
{
    class Program
    {
        static Program()
        {
            Console.WriteLine("Static constructor is called!");
        }
        public Program()
        {
            Console.WriteLine("Non-Static constructor is called!");
        }
        static void Main()
        {
        }
    }
}
  1. Constructors are responsible for initializing fields/variables of a class, so static fields are initialized by static constructors and non-static fields are initialized by non-static constructors.

     class Program
     {
         int x;//By default x value is 0, Because initialized by non-static constructor
         static int y;//By default y value is 0, Because initialized by static constructor
         static Program()
         {
             y = 20;
         }
         public Program()
         {
             this.x = 10;
         }
     }
    
    • By default x value is 0, Because initialized by Non-Static constructor & x is a non-static variable.

    • By default y value is 0, Because initialized by Static constructor & y is an static variable.

  2. Static constructors are implicitly called, whereas non-static constructors are called explicitly.

  3. Static constructors execute immediately once the execution of a class starts and, moreover, they are the first block of code to run in a class. Non-static constructors, on the other hand, execute only after creating an instance of the class and every time an instance of the class is created.

     class Program
     {
         int x;
         static int y;
         static Program()
         {
             Console.WriteLine("Static constructor is called!");
         }
         public Program()
         {
             Console.WriteLine("Non-Static constructor is called!");
         }
         static void Main()
         {
             Program program = new Program();
             Program program1 = new Program();
         }
     }
     //Output is:-
     //Static constructor is called!
     //Non-Static constructor is called!
    
  4. In the life cycle (life cycle means from the start of execution to the end of execution.) of a class, the static constructor executes only once, whereas the non-static constructor executes zero times if no instances are created and 'n' times if 'n' instances are created.

  5. Static members can be accessed directly, whereas non-static members cannot be accessed directly. This is because static members are shared among all instances of a class, while non-static members belong to individual instances. When you create multiple instances of a class, each instance has its own copy of the non-static members. However, there is only one copy of the static members, which is shared by all instances. This is why static members can be accessed directly using the class name, but non-static members require an instance of the class to be accessed.

     using System;
    
     namespace Practical_Exercises
     {
         class Program
         {
             int x; // Non-static member
             static int y; // Static member
             // Static constructor
             static Program()
             {
                 Console.WriteLine("Static constructor is called!");
             }
             // Non-static (instance) constructor
             public Program()
             {
                 Console.WriteLine("Non-Static constructor is called!");
             }
    
             static void Main()
             {
                 // Create an instance of the Program class
                 Program program = new Program();
                 // Access the static member directly
                 Console.WriteLine(y);
                 //Access the non static member
                 Console.WriteLine(program.x);
                 Console.ReadLine();
             }
         }
     }
    
     //Output:-
     //Static constructor is called!
     //Non-Static constructor is called!
     //0
     //0
    
  6. Non-static constructor can be parametrized but static constructor can't have parametrized. Because of static constrctor is implicitly call & wo will pass the parameter. Remember it's a 1st block of code to run the class.

     using System;
    
     namespace Practical_Exercises
     {
         class Program
         {
             int x; // Non-static member
             static int y; // Static member
    
             // Static constructor
             static Program(int x)//give error: static constructor must be parameterless 
             { 
                 Console.WriteLine("Static constructor is called!");
             }
    
             // Non-static (instance) constructor
             public Program(int x)
             {
                 this.x=x;
                 Console.WriteLine("Non-Static constructor is called!");
             }
    
             static void Main()
             {
                 // Create an instance of the Program class
                 Program program = new Program(10);
                 Program program1 = new Program(20);
                 Program program2 = new Program(30);
    
                 // Access the static member directly
                 Console.WriteLine(y);
    
                 //Access the non static member
                 Console.Write(program.x + " "+ program.x + " " + program.x);
    
                 Console.ReadLine();
             }
         }
     }
    
     //Output:-
     //Static constructor is called!
     //Non-Static constructor is called!
     //Non-Static constructor is called!
     //Non-Static constructor is called!
     //0
     //10 20 30
    
  7. Non static constructor can be overloaded where as static constructor can't be overloaded.

     using System;
    
     namespace Practical_Exercises
     {
         class Program
         {
             int x; // Non-static member
             static int y; // Static member
    
             // Static constructor
             static Program()
             {
                 Console.WriteLine("Static constructor is called!");
             }
    
             // Non-static (instance) constructor
             public Program(int x)
             {
                 this.x=x;
                 Console.WriteLine("Non-Static constructor is called!");
             }
    
             // Non-static (instance) constructor overloaded
             public Program(int x, int z)
             {
                 Console.WriteLine("Non-Static constructor is called!");
             }
    
             static void Main()
             {
                 Program program = new Program(10);
                 Program program1 = new Program(20,10);
                 Console.ReadLine();
             }
         }
     }
    
  8. Every class contains an implicit constructor if not defined explicitly, and those implicit constructors are defined based on the following criteria:

    • Every class except a static class contains an implicit non-static-constructor if not defined with an explicit constructor.

      Static Classes: Static classes are special in C#. You can't create an instance of them, and they only have static members. They can't have instance constructors, whether implicit or explicit. However, they can have static constructors to initialize static members if needed.

      public static class StaticExample
      {
          static StaticExample()
          {/* Static constructor logic*/}
          public static void DoSomething()
          {/* Static method logic*/}
      }
      

      An instance constructor (also called a constructor) is a special method that runs when you create an object of a class. Its main job is to set up the new object.

      Implicit non-static-constructor: If you create a class and don't add any constructor, the C# compiler automatically makes a default non-static constructor. This constructor has no parameters and only allows you to create an instance of the class.Clear the concept of Static and instace.

  9. Static constructors are implicitly defined only if that class contains any static members or else that constructor will be present at all.

Destructor

A destructor in C# is a special method used to clean up an object before the garbage collector reclaims it. It runs automatically when the object is no longer needed. The destructor's name is the same as the class name, but with a tilde (~) at the beginning.

Key Characteristics of a Destructor:

  • No Access Modifiers: Destructors do not have access modifiers and are implicitly private.

  • No Parameters: Destructors cannot take parameters.

  • No Overloading: A class can only have one destructor.

  • Automatic Invocation: Destructors are called automatically by the garbage collector when an object is no longer needed.

Difference Between Constructor and Destructor

ConstructorDestructor
The constructor's name is the same as the class name. It is called as soon as an object is created.The destructor's name is the same as the class name, preceded by ~. It is used to perform cleanup activities.
It is used to initialize the object.It is used to clean up resources when an object goes out of scope.
Constructor overloading is possible.Destructor overloading is not possible.
We can pass arguments to constructors.We cannot pass arguments to destructors.
Access modifiers can be used with constructors.Access modifiers cannot be used with destructors.

Example of Constructor and Destructor in C#

using System;

class SampleClass
{
    // Constructor
    public SampleClass()
    {
        Console.WriteLine("Constructor: Initializing object.");
    }

    // Destructor
    ~SampleClass()
    {
        Console.WriteLine("Destructor: Cleaning up object.");
    }
}

class Program
{
    static void Main()
    {
        SampleClass obj = new SampleClass(); // Constructor called here

        // Forcing garbage collection to see the destructor message
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

In this example:

  • Constructor: The SampleClass constructor is called when an object of SampleClass is created, initializing the object.

  • Destructor: The destructor is called automatically by the garbage collector to clean up resources when the object is no longer in use.


Variable, Instance & Refrence of class

To learn about variables, instances, and references, first understand what a class is:

Class: A class is a user-defined type or data type. For example, string is a data type in C#, but it was created as a class. string is a predefined class in the .NET library, and we now call it a data type. Every class, whether predefined, user-defined or any type of class, is a data type or type in C#.

How to consume class: Can we do something like int = 100;? No, because int is a data type or type, which is just a blueprint for data but doesn't have a memory location. To locate memory, we write int i = 100;. Similarly, string = "Hello" is invalid because string is a class, and a class is a user-defined type or data type. You cannot consume it directly. If you want to use it, write string s = "Hello". Then, what is s? s is a copy of type string, and i is a copy of type int.

Understande by Example:- Can you build a house without a map? If you have a map, you can build the house. Similarly, int = 100 or string = "Hello" is like a house map or blueprint. Writing int i = 100 or string s = "Hello" is like actually building the house or allocating memory. You live in a constructed house, not in a map of the house. Similarly, values stay in memory, not in the blueprint.

So, a data type is always a blueprint, and the copy of the data type is an implementation of that blueprint.

    class Program
    {
        int x = 10;
        static void Main()
        {
            //Give error, because non static member of class cannot be axcess by the static block.
            Console.WriteLine(x);
            //Then how to axcess the `x` value. By creating instance/copy of class
            Program f = new Program();//`f` is the instance/copy of class 
            Console.WriteLine(f.x);
            Console.ReadLine();
        }
    }
  • Here, what is f? f is a copy of the class. Similarly, in this example string s = "Hello", s is a copy of the type string, and string is a class. And that copy is called a copy of the class.

  • Program f = new Program(); Here, f is a copy of the class Pro gram. Program is a class, and f is the copy of the class. And that copy is called an instance of the class. Memory is allocated for the instance only, and the new keyword is used to create the instance of the class.

class Program
{
    int x = 10;
    static void Main()
    {
        Program f;//now that time `f` is the copy of class which is not inslize. 
        Console.WriteLine(f.x);// GIve error:- Use of unassigned local variable 'f'
        Console.ReadLine();
    }
}

Program f; now at that time f is the copy of the class which is not initialized, or you could say it is a variable of the class. Without using new, it's only a variable.

class Program
{
    int x = 10;
    static void Main()
    {
        Program f;//now that time `f` is the copy of class which is not inslize. 
        f = new Program();//Declear the valu, which is calss.
        Console.WriteLine(f.x);// Give error:- Use of unassigned local variable 'f'
        Console.ReadLine();
    }
}
  • Both are the same: Program f = new Program(); and Program f; f = new Program();.

  • Here, Program f = new Program(); 'f' is an instance of the class.

  • Program f; 'f' is a variable of the class which has a default null value.

  • f = new Program(); 'f' is an instance of the class, and memory allocation is initialized when you create the instance. The instance is created when you use the new keyword, and x is initialized.

Variable of a class : A copy of the class that is not initialized.

Instance of a class: A coppy of class that is initialized by using the new keyword which has it's own memory and never shared with another instance.

class Program
{
    int x = 10;
    static void Main()
    {
        Program f1 = new Program();//`f` is the instance of class 
        Program f2 = new Program();//`f` is the instance of class 
        Console.WriteLine(f1.x + " "+ f2.x);//Output is: 10 10
        f1.x = 20;
        Console.WriteLine(f1.x + " "+ f2.x);//Output is: 20 10
        f2.x = 30;
        Console.WriteLine(f1.x + " "+ f2.x);//Output is: 20 30
        Console.ReadLine();
    }
}

You create n number of instances of a class, and each instance has a separate memory location. Any instance can never reflect another instance, meaning the f1 instance never reflects the f2 instance. If you modify f1.x, it does not modify the f2.x value.

class Program
{
    int x = 10;
    static void Main()
    {
        Program f1 = new Program();//`f` is the instance of class 
        Program f2 = f1;//`f2` is the refrence of class or you say f2 is the pointer of `f1`.
        Console.WriteLine(f1.x + " "+ f2.x);//Output is: 10 10
        f1.x = 20;
        Console.WriteLine(f1.x + " "+ f2.x);//Output is: 20 20
        f2.x = 30;
        Console.WriteLine(f1.x + " "+ f2.x);//Output is: 30 30
        Console.ReadLine();
    }
}

In that example, f2 is the reference of f1, or you can say the pointer of f1. f2 does not have a separate memory. f1 and f2 share the same memory location. A reference is a copy of a class created by an existing instance of the class. A reference does not have its own memory but is used like an instance of the class.

Refrence of class: A coppy of the class that is initialized by using an existing instance and references of the class will not have any memory allocation. They will be sharinge the same memory of the instance that was assigned for initializing the variable.

Reference of a class can be called a pointer to the instance, and every modification we perform on the members using the instance reflects when we access those members through the reference and vice-versa.


Access Specifiers:

It's a special kind of modifers using which we can define the scope of a type and it's members.

In C# 7, there are access specifiers, but mainly only 4:

  1. Private: accessible only to the current class.

  2. Internal: to all the classes in the current assembly only.

  3. Protected: to the current class and to it’s child classes.

  4. Public: accessible to all the classes in all the assemblies.

  5. Protected internal: if protected or internal access.

  6. Private protected: if private or internal access.

  7. File: within the file scope.

A member of a class that is defined with any scope is accessible within that class. If there are any restrictions, they start outside of this class.

  • The default access modifier inside a namespace is internal (first level).

  • The default access modifier inside a class is private (second level).

By using inharitance:

Example:-

using System;

namespace Practical_Exercises
{
    //Case1: Consumimg members of a class from same class
    public class Program //Axcess from anywhere
    {
        private void Test1()
        {
            Console.WriteLine("Private Method");
        }
        internal void Test2()
        {
            Console.WriteLine("Internal Method");
        }
        protected void Test3()
        {
            Console.WriteLine("Protected Method");
        }
        protected internal void Test4()
        {
            Console.WriteLine("Protected Internal Method");
        }
        public void Test5()
        {
            Console.WriteLine("Public Method");
        }
        static void Main()
        {
            Program p = new Program();
            //All member can be accessable hear.
            p.Test1();p.Test2();p.Test3();p.Test4();p.Test5();
        }
    }
}

/*
Output:-
Private Method
Internal Method
Protected Method
Protected Internal Method
Public Method
*/

Hear all 5 method are executable, But if you want to access from another class then you getting problom. Like that i am creating another class in my project name is Progrem2.

new is a keyword that is used to allocate memory for any object, variable, array, etc.

use . (dot) operator in order to call the members of the class.

All the class names in C# should follow Pascal Case. Means every word’s first letter should be in Capital.

using System;

namespace Practical_Exercises
{
    //Case2: Consumimg members of a class from child class from same project
    //According to Inharit, parents class access the all member of child class except private class like owner.
    internal class Program2: Program//Inharit the Program class
    {
        static void Main()
        {
            Program2 p = new Program2();
            p.Test1();//Only Test1() method give error
            p.Test2(); p.Test3(); p.Test4();p.Test5();//Other are accessable
        }
    }
}
/*
Output:-
Internal Method
Protected Method
Protected Internal Method
Public Method
*/

Remember:

1: All members of a class are private by default.

2: We cannot declare a class as private, protected, or protected internal. If you do not define a class as public, it is internal by default.

3: A member declared as private is only accessible within the class.

If you run the Program2 class, all 4 methods from the Program class will be called.

By using making instance of class:

See make a third class name is Program3 :

using System;

namespace Practical_Exercises
{
    //Case3: Consumimg members of a class from non-child class from same project
    internal class Program3
    {
        static void Main()
        {
            Program p = new Program();
            p.Test1();p.Test3();//Only Test1() and Test3 method give error
            p.Test2();p.Test4();p.Test5();
        }
    }
}
/*
Output:-
Internal Method
Protected Internal Method
Public Method
*/

Remember: When we create an instance of another class, we cannot access the protected and private methods. Only a child class can access the protected methods, not other classes.

private: Can be accessed within the class, cannot be accessed outside the class.

protected: It can be accessed within the class or within the child class, and non-child classes cannot access it.

public: Not any restriction, it can be accessible anywhere.

To understand Internal and Protected Internal, create another project namedPractical_Exercises2withen the same solution:

  • Rename the Program class to Program4 in Practical_Exercises2 just for clear understanding.

My intention is to access the Program class members in Program4 class by inheritance. Both the Program and Program4 classes are in different projects. To do that, get the reference of the Practical_Exercises Assembly to the Practical_Exercises2 project.

Steps: Right-click on Practical_Exercises2 > click on Add > next click on Reference > a window named Reference manager will open > go to Browse and select the Practical_Exercises project, go to the bin folder, and then select the .exe file, which is the reference of the assembly, and click on the ok button.

You consume the reference of another project (Practical_Exercises).

Hear example:-

https://cdn.hashnode.com/res/hashnode/image/upload/v1719894605538/4d0c41f3-dd96-4b03-8dda-07578f0dfbae.gif

If you not able to see the example click hear : https://cdn.hashnode.com/res/hashnode/image/upload/v1719894605538/4d0c41f3-dd96-4b03-8dda-07578f0dfbae.gif

Now, there are two ways to consume the method: the first is inheritance, and the second is object creation. But here I use inheritance:

Here we can consume only three methods: protected, protected internal, and public. We cannot consume internal outside the project. The default scope of a class is internal, which is why every class is accessible inside the project only unless it's public. see hear:

We can access only one class name is Program, but in the Practical_Exercises project, there are three class Program, Program2, and Program3, because Program is public.

internal: We cannot consume internal outside the project only we can conume in the same project.

protected internal: If either protected or internal is accessible, then protected internal is also accessible. If neither of these is accessible, protected internal cannot be accessible.

Summary:

CasesPrivateInternalProtectedProtected InternalPublic
Case 1 (Same class)TrueTrueTrueTrueTrue
Case 2 (Child class in same project)FalseTrueTrueTrueTrue
Case 3 (None child class in same project)FalseTrueFalseTrueTrue
Case 4 (Child class in diffrent project)FalseFalseTrueTrueTrue
Case 5 (Consume any where)FalseFalseFalseFalseTrue

Difference kinds of Variables:

There are 4 kinds of variable:-

  1. Non-static variable (Instance variable)

  2. Static variable

  3. Constant variable

  4. ReadOnly variable

Non-static variable (Instance variable) and Static variable:

  • Also known as non-static variables.

  • Each instance (object) of a class has its own copy of instance variables.

  • They are declared within a class but outside any method, constructor, or property.

  • Instance variables are initialized when an object of the class is created and destroyed when the object is destroyed.

  •                                                                                             class Program
                                                                                                {
                                                                                                    //Non-static variable or instance variable
                                                                                                    int x;
                                                                                                    public int y;
                                                                                                    static void Main()
                                                                                                    {
                                                                                                        Console.WriteLine(x);//Error
                                                                                                    }
                                                                                                }
    
  • Also known as class variables.

  • There is only one copy of a static variable that is shared by all instances (objects) of a class.

  • They are declared using the static keyword.

  • Static variables are initialized only once, at the start of the execution, and remain in memory until the program ends

class Program
{
    static int x = 200;//Static variable
    static void Main()
    {
        int y; //Static variable
        Console.WriteLine(x);//200
    }
}

If a variable is explicitly declared using the static modifier or is declared inside a static block, then that variable is static. Another all other variables are non-static.

using System;

class Program
{
    int x = 100; // Non-static variable or instance variable
    static int y = 200; // Static variable

    static void Main()
    {
        Program obj = new Program(); // Creating an instance of the Program class
        Console.WriteLine(x); //Error
        Console.WriteLine(obj.x); // Accessing the instance variable through the instance
        Console.WriteLine(y); // Accessing the static variable directly
    }
}
  • You can directly access a static variable, but you cannot directly access a non-static variable. To access a non-static variable, you need to create an instance of the class. Static variables are initialized and allocated memory when the class is loaded, while non-static variables are initialized when an instance is created.

  • Static members of a class do not require an instance for initialization or execution, whereas non-static members do. Static variables are initialized when the class is loaded, while instance variables are initialized each time a new instance is created.

using System;

class Program
{
    int x = 100; // Non-static variable or instance variable
    static void Main()
    {
        Program obj = new Program(); //1st instance created & 1st time memory is allocated for x    
        Program obj1 = new Program(); //2st instance created & 2nd time memory is allocated for x    
        Console.WriteLine(obj.x); 
        Console.WriteLine(obj1.x); 
    }
}

2 times memory is allocated for x because 2 instances are created.

  • In the life cycle of a class, a static variable is initialized only one time, whereas instance variables are initialized 0 times if no instances are created and n times if n time instances are created.

  • Initialization of instance variable is associalte with instance creation & constructor calling, so instance variables can be initialize thru the constructor also.

Create two diffrent instance variable:

using System;

class Program
{
    int x = 100; // Non-static variable or instance variable
    public Program(int x)
    {
        this.x = x;
    }
    static void Main()
    {
        Program obj = new Program(50); //1st instance created & 1st time memory is allocated for x    
        Program obj1 = new Program(150); //2st instance created & 2nd time memory is allocated for x    
        Console.WriteLine(obj.x); 
        Console.WriteLine(obj1.x); 
    }
}

In that program, x has two copies because we create two instances, but y has only one copy because a static variable is initialized only once in the class's life cycle.

A static variable is initialized only once when the class is first loaded into memory. It is not re-initialized for each instance of the class and can be modified after initialization. This means that no matter how many objects of the class are created, the static variable retains its value and can be modified, but it is never reset.

Example:

using System;

class Example
{
    // Static variable
    public static int staticVar = 10;

    // Instance constructor
    public Example()
    {
        // Modify the static variable
        staticVar++;
    }

    static void Main()
    {
        // Display the initial value of the static variable
        Console.WriteLine("Initial value of staticVar: " + Example.staticVar); // Output: 10

        // Create the first instance of the Example class
        Example obj1 = new Example();
        Console.WriteLine("Value of staticVar after creating obj1: " + Example.staticVar); // Output: 11

        // Create the second instance of the Example class
        Example obj2 = new Example();
        Console.WriteLine("Value of staticVar after creating obj2: " + Example.staticVar); // Output: 12

        // Create the third instance of the Example class
        Example obj3 = new Example();
        Console.WriteLine("Value of staticVar after creating obj3: " + Example.staticVar); // Output: 13
    }
}

Constant variable:

If a variable is declared using the keyword const, we call it a constant variable. This constant variable can't be modified after its declaration, so it must be initialized at the time of declaration. Ex: const float pi = 3.14f;. Decimal values are treated as double; if you want to represent the value as float, add the suffix f. If you don't add the suffix, a compile-time error will occur. If you use const the it must be inslize variable.

using System;

class Program
{
    const int x = 100;
    const float pi = 3.14f;
    static void Main()
    {
        Console.WriteLine(pi);
    }
}

Constants have only one copy, no matter how many instances of the class are created. Even if we create 10 instances of the class, there will still be only one copy of the constant x. This saves memory because constant values cannot be changed.

The behavior of a constant is similar to that of a static variable. That is initialized one and only one time in the life cycle of the class and doesn't require an instance of the class.

The difference between static and constant variables is that a static variable can be modified, but a constant variable cannot be modified.

ReadOnly variable:

If a variable is declared using the readonly keyword, we call that variable a readonly variable. These variables also can't be modified, like constants, but only after initialization.It's not compulsory to initialize a readonly variable at the time of declaration; it can also be initialized in the constructor. see example:

using System;

namespace Class
{
    internal class Call
    {
        readonly float x; //Readonly variable
        int z;
        public Call(float x)
        {
            this.x = x;
        }
        static void Main()
        {
            Call obj = new Call(50.5f); 
            Call obj1 = new Call(150.45f);
            Console.WriteLine(obj.x);//out: 50.5
            obj.z = 20;// No erroe
            obj.x = 20;//Error give
            Console.WriteLine(obj1.x);//Out: 150.45
        }
    }
}

x has two copies created because the behavior of a readonly variable is the same as a non-static variable. It is initialized only after creating an instance of the class and once for each instance of the class created.

The difference between a readonly variable and a instance variable is that an instance variable can be modified, but a readonly variable cannot.

The difference between a readonly variable and a constant variable is that a constant is a single copy for the whole class, but a readonly variable has a copy for each instance. A constant variable is fixed for the entire class, while a readonly variable is specific to an instance of the class.

  1. Non-static variable (Instance variable): Maintain one copy for each instance of the class that is initialize only if the instance is created. If there are n instances, there are n copies; if there are zero instances, there are zero copies.

  2. Static variable: Maintain only one copy for the whole class. It can be modified but cannot create multiple copies. It can be initialized only one time.

  3. Constant variable: Cannot be modified after declaration, so initialize the value at the time of declaration and maintaining only one copy throughout the class.

  4. ReadOnly variable: Cannot be modified after initialization, maintains a copy for each instance.


Static

Static is a keyword used for:

  • Static Data Members

  • Static Methods

  • Static Constructors

  • Static Classes

Static Data Member

Definition:

  • A static data member (or static field) belongs to the class itself, not to any specific instance.

  • Only one copy of the static data member exists, no matter how many instances of the class are created.

Usage:

  • Shared among all instances of the class.

  • Commonly used to store values that every instance shares.

class Student
{
    public int a, b;          // Instance variables
    public static int c;      // Static variable

    public Student(int a, int b)
    {
        this.a = a;
        this.b = b;
    }
}

class Program
{
    static void Main()
    {
        Student.c = 100;  // Setting static variable

        Student s1 = new Student(1, 2);
        Student s2 = new Student(3, 4);

        Console.WriteLine(Student.c);  // Output: 100
    }
}

Static method

Definition:

  • A static method belongs to the class itself rather than any specific instance.

  • It can be called without creating an instance of the class.

Usage:

  • Used for operations that do not require any data from instance variables.

  • Commonly used for utility or helper functions.

class MathHelper
{
    //Another class static method
    public static int Add(int x, int y)
    {
        return x + y;
    }
}

class Program
{
    //Same class Static method
    static void Hello()
    {
        Console.WriteLine("Hello method");
    }
    static void Main()
    {
        int result = MathHelper.Add(5, 3);
        Console.WriteLine(result);  // Output: 8

        Hello();
    }
}

A static method is preceded by the static keyword, and we can call a static method without creating an object if it is in the same class.

Static Constructors

Definition:

  • A static constructor is used to initialize static data members.

  • It is called automatically before any static members are accessed or any instance of the class is created.

  • It is created only once for the entire application. As soon as we access a static data member, the static constructor is called.

  • We cannot give access modifiers to a static constructor.

  • We cannot pass arguments to a static constructor.

Usage:

  • Used to initialize static fields or perform actions that only need to be done once.
class Example
{
    public static int x;
    public static int y;

    // Static constructor
    static Example()
    {
        x = 10;
        y = 20;
        Console.WriteLine("Static constructor called");
    }
}

class Program
{
    static void Main()
    {
        Console.WriteLine(Example.x);  // Output: 10
        Console.WriteLine(Example.y);  // Output: 20
    }
}

Static Classes

Definition:

  • A static class cannot be instantiated, meaning you cannot create objects of a static class.

  • It can only contain static members (methods, properties, fields, etc.).

Usage:

  • Static classes are used to group methods that don't need to use or change the data stored in an instance of the class.

  • They are often used to create utility or helper functions that perform common tasks and can be accessed without creating an object.

static class Utility
{
    public static void PrintMessage(string message)
    {
        Console.WriteLine(message);
    }
}

class Program
{
    static void Main()
    {
        Utility.PrintMessage("Hello, World!");  // Output: Hello, World!
    }
}

Summary:

  1. Static Data Members:

    • Shared by all instances.

    • Only one copy exists.

  2. Static Methods:

    • Belong to the class.

    • Called without an instance.

  3. Static Constructors:

    • Initialize static data.

    • Called automatically.

  4. Static Classes:

    • Cannot be instantiated.

    • Contain only static members.


Inheritance

It's a way to use the members of one class in another class by creating a parent/child relationship between the classes. Which provides reusability.

The main aim of inheritance is "Code reusability." Inheritance is also called "Extending classes."

The existing class from which we are inheriting is called the “Base class,” “Super class,” or “Parent class.” The new class that is created from the existing class is called the “Derived class,” “Sub class,” or “Child class.”

Syntax: <modifiers> class <child class> : <parent class>

class A
{
    - Members
}
class B:A
{
    - Consuming the members of A from heare
}
  • Class A is the Parent, Base, or Super class.

  • Class B is the Child, Derived, or Sub class.

Note: In inheritance, the child class can consume members of its parent class as if it is the owner of those members, except for the private members of the parent.

Class2 calls the Class1 method as if it were its own property, but internally the method belongs to Class 1.

using System;


namespace Inheritance
{
    class Class1
    {
        public void Test1()
        {
            Console.WriteLine("Hello 1");
        }
        public void Test2()
        {
            Console.WriteLine("Hello 2");
        }
    }

    class Class2:Class1
    {
        public void Test3()
        {
            Console.WriteLine("Hello 3");
        }
        static void Main()
        {
            Class2 c = new Class2();
            c.Test1(); // Hello 1
            c.Test2(); // Hello 2
            c.Test3(); // Hello 1
        }
    }
}

Currently, Class2 has 3 methods: all the properties that come from Class1 (Parent's property) and all those in Class2 (my property) are Class2's property.

Points:

  1. Parent class constructor must be accessible to the child class; otherwise, inheritance will not be possible.

     using System;
    
     class Class1
     {
        public Class1 ()
        {
             Console.WriteLine("Class 1 constructor call");
        }
     }
    
     class Class2:Class1
     {
         Class2()
         {
             Console.WriteLine("Class 2 constructor call");
         }
         static void Main()
         {
             Class2 c = new Class2();
         }
     }
     /*Out: 
      Class 1 constructor call
      Class 2 constructor call
      */
    
    • If you don't specify access modifiers or use private access modifiers for class members, they can't be inherited because, by default, all methods in the class are private, and private members can't be accessed. Also, if the constructor is private, inheritance not possible.

    • If a class is public, the default constructor is public. If a class is internal, the default constructor is internal. The default access modifier for a class in C# is internal.

    • Execution always starts from top(parents) to bottom(child).

  2. In inheritance, a child class can access the parent class's members, but the parent class cannot access members that are defined only in the child class.

     using System;
    
     class Parent
     {
         public void ParentMethod()
         {
             Console.WriteLine("Method in Parent class");
         }
     }
    
     class Child : Parent
     {
         public void ChildMethod()
         {
             Console.WriteLine("Method in Child class");
         }
     }
    
     class Program
     {
         static void Main()
         {
             Child child = new Child();
             child.ParentMethod(); // Accessible
             child.ChildMethod();  // Accessible
    
             Parent parent = new Parent();
             parent.ParentMethod(); // Accessible
             parent.ChildMethod(); // Not accessible, will cause a compile-time error    
         }
     }
    
  3. We can inslize a parent class variable by using the child class instance to make it as a refrence.

     using System;
    
     class Parent
     {
         public void Display()
         {
             Console.WriteLine("Parent class method");
         }
     }
    
     class Child : Parent
     {
         public void Show()
         {
             Console.WriteLine("Child class method");
         }
     }
    
     class Program
     {
         static void Main()
         {
             Parent p; // p is a variable of class1
             Child c =  new Child(); // c is instance of child class
             p = new Parent(); //Inslize the normaly
             p = c; // Assigning child class instance to parent class reference  
             p.Display(); //Out: Parent class method
             //p.Show(); //Out: Error Class Parent not contain show member.
         }
     }
    
    • p = c; :- Assigning child class instance to parent class reference, or you can say that p is a reference of the parent class created using the child class instance (Reference does not take memory: References of a class do not have memory; they will consume the memory of the assigned instance of the class. Memory is consumed by the instance, not the reference).

    • Even though both p and c refer to the same memory, we cannot access child class methods from parent class refrence.

  4. Every class we define or that is predefined in the language's libraries has a default parent class called the Object class, which is in the System namespace. The Object class has four methods: Equals, GetHashCode, GetType, and ToString.

     using System;
    
     class Program
     {
         static void Main()
         {
             Object obj = new Object();
    
             // Calling ToString method: Returns a string that represents the current object.
             string str = obj.ToString();//Return type of object
             Console.WriteLine("ToString: " + str);
    
             // Calling Equals method: Determines whether the current object is equal to another object.
             bool isEqual = obj.Equals(new Object());
             Console.WriteLine("Equals: " + isEqual);
    
             // Calling GetType method: Gets the type of the current instance.
             Type type = obj.GetType();
             Console.WriteLine("GetType: " + type);
    
             // Calling GetHashCode method: Returns a hash code for the object, which is used in hash-based collections.
             int hashCode = obj.GetHashCode();
             Console.WriteLine("GetHashCode: " + hashCode);
         }
     }
    
     /*OUTPUT:-
     ToString: System.Object
     Equals: False
     GetType: System.Object
     GetHashCode: 46104728
      */
    

    These four methods are accessible anywhere.

     using System;
    
     class Parent
     {
         public void Display()
         {
             Console.WriteLine("Parent class method");
         }
     }
    
     class Program
     {
         static void Main()
         {
             Object obj = new Object();
             Parent p = new Parent();
             p.GetHashCode(); //We can access those 4 method any where.
         }
     }
    
    • If a class doesn't explicitly inherit from another class, it automatically inherits the Object class, which is the base class for all classes in C#.

Object class is the default parent class of C# .NET .

Object class is present at the top of the hierarchy.

Object class is always at the top.

Types of inheriance:

The type of inheritance is determined by the number of parent classes a child class has or the number of child classes a parent class has.

  1. Single Inheritance: A class (child) inherits from one parent class.

  2. Multilevel Inheritance: A class inherits from another class, which then inherits from another class, forming a chain of inheritance.

  3. Hierarchical Inheritance: Multiple classes (children) inherit from a single parent class.

  4. Hybrid Inheritance: A combination of two or more types of inheritance (Note: Not directly supported in C#, but can be achieved using interfaces).

  5. Multiple Inheritance: A class (child) inherits from more than one parent class(Note: Not directly supported in C#, but can be achieved using interfaces).

In C#, we can't use multiple inheritance with classes. We can only use single inheritance with classes. It means Hybrid and Multiple inheritance are not supported.

Points:

  1. In C#, multiple inheritance with classes is not supported. We can only use single inheritance with classes.

  2. In the first point, we learned that whenever a child class instance is created, the child class constructor will implicitly call its parent class's constructor, but only if the constructor is parameterless. However, if the parent class's constructor is parameterized, the child class constructor can't implicitly call its parent's constructor. To overcome this problem, it is the programmer's responsibility to explicitly call the parent class's constructor from the child class constructor and pass values to those parameters. To call the parent's constructor from the child class, we need to use the base keyword.

    Error code (Why error come):-

     using System;
    
     class Parent
     {
         public Parent(string message)
         {
             Console.WriteLine("Parent class parameterized constructor called with message: " + message);
         }
     }
    
     class Child : Parent
     {
         public Child() // Error: No parameterless constructor in Parent class
         {
             Console.WriteLine("Child class constructor called.");
         }
     }
    
     class Program
     {
         static void Main()
         {
             Child child = new Child(); 
         }
     }
    

    Solution Code:-

     using System;
    
     class Parent
     {
         public Parent(string message)
         {
             Console.WriteLine("Parent class parameterized constructor called with message: " + message);
         }
     }
    
     class Child : Parent
     {
         public Child() : base("Message from Parent")
         {
             Console.WriteLine("Child class constructor called: ");
         }
     }
    
     class Program
     {
         static void Main()
         {
             Child child = new Child();
         }
     }
    

    One more example:-

     using System;
    
     class Parent
     {
         public Parent(string message)
         {
             Console.WriteLine("Parent class parameterized constructor called with message: " + message);
         }
     }
    
     class Child : Parent
     {
         public Child(string childMessage) : base("Message from Parent")
         {
             Console.WriteLine("Child class constructor called with message: " + childMessage);
         }
     }
    
     class Program
     {
         static void Main()
         {
             Child child = new Child("Message from Child");
         }
     }
    

How to use inheritance in our application:

Entity: It's a living or non-living object associated with a set of attributes.

Step 1: Identify the entities that are associated with the application we are developing.

E.g.:School Application: Student, TeachingStaff, NonTeachingStaff.

Step 2: Identify the attributes of each and every entity.

E.g.:Student: Id, Name, Address, Phone, Class, Marks, Grade, Fees.

E.g.:TeachingStaff: Id, Name, Address, Phone, Designation, Salary, Qualification, Subject.

E.g.:NonTeachingStaff: Id, Name, Address, Phone, Designation, Salary, DepartmentName, ReportingManager.

Step 3: Identify Common Attributes: From the attributes listed above, we can see that Id, Name, Address, and Phone are common across all entities.

Step 4: Create a Base/parent Class for Common Attributes: Create a base class called Person that includes the common attributes. Take only generic names for the base class.

using System;

namespace SchoolApplication
{
    // Base class with common attributes
    public class Person
    {
        public int Id;
        public string Name, Address, Phone;

        public Person(int id, string name, string address, string phone)
        {
            Id = id;
            Name = name;
            Address = address;
            Phone = phone;
        }

        public void DisplayBasicInfo()
        {
            Console.WriteLine($"Id: {Id}, Name: {Name}, Address: {Address}, Phone: {Phone}");
        }
    }

    // Derived class for Student (take 8 attribute)
    public class Student : Person
    {
        public string Class, Grade;
        public double Marks, Fees;

        public Student(int id, string name, string address, string phone, string studentClass, double marks, string grade, double fees)
            : base(id, name, address, phone)
        {
            Class = studentClass;
            Marks = marks;
            Grade = grade;
            Fees = fees;
        }

        public void DisplayStudentInfo()
        {
            DisplayBasicInfo();
            Console.WriteLine($"Class: {Class}, Marks: {Marks}, Grade: {Grade}, Fees: {Fees}");
        }
    }

    // Staff Base class with common attributes (take 6 attribute)
    public class Staff : Person
    {
        public string Designation;
        public double Salary;

        public Staff(int id, string name, string address, string phone, string designation, double salary)
            : base(id, name, address, phone)
        {
            Designation = designation;
            Salary = salary;
        }

        public void DisplayStaffInfo()
        {
            DisplayBasicInfo();
            Console.WriteLine($"Designation: {Designation}, Salary: {Salary}");
        }
    }

    // Derived class for Teaching (take 8 attribute)
    public class Teaching : Staff
    {
        public string Qualification, Subject;

        public Teaching(int id, string name, string address, string phone, string designation, double salary, string qualification, string subject)
            : base(id, name, address, phone, designation, salary)
        {
            Qualification = qualification;
            Subject = subject;
        }

        public void DisplayTeachingStaffInfo()
        {
            DisplayStaffInfo();
            Console.WriteLine($"Qualification: {Qualification}, Subject: {Subject}");
        }
    }

    // Derived class for NonTeaching (take 8 attribute)
    public class NonTeaching : Staff
    {
        public string DepartmentName, ReportingManager;

        public NonTeaching(int id, string name, string address, string phone, string designation, double salary, string departmentName, string reportingManager)
            : base(id, name, address, phone, designation, salary)
        {
            DepartmentName = departmentName;
            ReportingManager = reportingManager;
        }

        public void DisplayNonTeachingStaffInfo()
        {
            DisplayStaffInfo();
            Console.WriteLine($"DepartmentName: {DepartmentName}, ReportingManager: {ReportingManager}");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Student student = new Student(1, "John Doe", "123 Elm St", "555-1234", "10th Grade", 90, "A", 1000.00);
            Teaching teacher = new Teaching(2, "Jane Smith", "456 Oak St", "555-5678", "Professor", 50000.00, "PhD", "Mathematics");
            NonTeaching staff = new NonTeaching(3, "Bob Brown", "789 Pine St", "555-9012", "Clerk", 30000.00, "Administration", "Mr. Green");

            student.DisplayStudentInfo();
            Console.WriteLine();
            teacher.DisplayTeachingStaffInfo();
            Console.WriteLine();
            staff.DisplayNonTeachingStaffInfo();
        }
    }
}

Overriding: It’s a concept of hiding a base class method with a derived class method when we have the same name in both base and derived classes.

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

// Derived class
public class Dog : Animal
{
    // Hide the base class method
    public new void MakeSound()   
    {
        Console.WriteLine("Dog barks");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Animal myAnimal = new Animal();
        Dog myDog = new Dog();
        Animal myNewDog = new Dog();

        myAnimal.MakeSound(); // Output: Animal makes a sound
        myDog.MakeSound();    // Output: Dog barks
        myNewDog.MakeSound(); // Output: Dog barks (because myNewDog is a Dog type reference)

        Console.ReadLine();
    }
}

Method Overloading

You can have multiple methods with the same name but different parameters. This is useful when you need the same method to handle different types or numbers of inputs.

using System;

class Program
{
    // Method to calculate the area of a rectangle
    static double CalculateArea(double length, double width)
    {
        return length * width;
    }

    // Overloaded method to calculate the area of a circle
    static double CalculateArea(double radius)
    {
        return Math.PI * radius * radius;
    }

    // Overloaded method to calculate the area of a circle
    static double CalculateArea(int radius)
    {
        return Math.PI * radius * radius;
    }

    // Overloaded method to calculate the area of a triangle
    static double CalculateArea(double baseLength, double height, bool isTriangle)
    {
        return 0.5 * baseLength * height;
    }

    // Overloaded method to calculate the area of a triangle
    static double CalculateArea( bool isTriangle, double baseLength, double height)
    {
        return 0.5 * baseLength * height;
    }

    static void Main(string[] args)
    {
        double rectangleArea = CalculateArea(10, 5); // Area of rectangle
        double circleArea = CalculateArea(7); // Area of circle
        int TypecircleArea = CalculateArea(7); // Area of circle
        double triangleArea = CalculateArea(10, 5, true); // Area of triangle
        double OrdertriangleArea = CalculateArea(true, 10, 5); // Area of triangle

        Console.WriteLine("Area of Rectangle: " + rectangleArea);
        Console.WriteLine("Area of Circle: " + circleArea);
        Console.WriteLine("Area of Circle: " + TypecircleArea);
        Console.WriteLine("Area of Triangle: " + triangleArea);
        Console.WriteLine("Area of Triangle: " + OrdertriangleArea);
    }
}

/*
Area of Rectangle: 50
Area of Circle: 153.93804002589985
Area of Circle: 153
Area of Triangle: 25
Area of Triangle: 25
*/

Advantages of Method Overloading

  1. Code Readability: Overloading lets you use the same method name with different parameters, making the code easier to read and understand. It groups related operations under one name.

  2. Improved Code Maintenance: Grouping similar methods under one name makes it easier to maintain the code and reduces errors during updates.

  3. Polymorphism: Overloading is a type of compile-time polymorphism. It allows you to perform similar operations in different ways based on the parameters, making the code more flexible.

  4. Enhanced Functionality: By having multiple versions of a method, you can handle different types or numbers of inputs without changing the method name.

  5. Ease of Use: Overloading makes the API simpler for users by letting them use the same method name with different arguments, instead of remembering multiple names for similar tasks.

  6. Code Reusability: Overloading promotes reusability by allowing the same method to work with different types of data or different numbers of parameters.

  7. Consistency: It ensures consistent method naming, making the codebase more uniform and easier to navigate.

Disadvantages of Method Overloading

  1. Complexity: Overloading too many methods can make the code harder to read and maintain.

  2. Ambiguity: If not managed properly, it can cause confusion in method calls, especially with similar parameter types.

Method overloading is a powerful feature in C# that allows you to define multiple methods with the same name but different parameters. This enhances code readability, reduces duplication, and simplifies method calls, although it must be used carefully to avoid complexity and ambiguity.

Note: If you show difference in return type of method, it doesn’t come under Method Overloading.

That example give error:-

using System;

namespace Method
{
    internal class OverLiading
    {
        static void Sum(int a, int b)
        {
            Console.WriteLine(a+b);
        }
        static int Sum(int a, int b)//give error
        {
            Console.WriteLine(a + b);
            return a + b;
        }
        static double Sum(int a, int b)//give error
        {
            Console.WriteLine(a + b);
            return a + b;
        }
        static void Main(string[] args)
        {
            Sum(10,20);
            Console.WriteLine(Sum(10, 20));
            Console.WriteLine(Sum(10, 20));
        }
    }
}

That is correct:

using System;

namespace Method
{
    internal class OverLiading
    {
        static void Sum(int a, int b)
        {
            Console.WriteLine(a+b);
        }
        static int Sum(int a, int b, int c)
        {
            return a + b  +c;
        }
        static double Sum(int a, int b, int c, int d)
        {
            return a + b + c + d;
        }
        static void Main(string[] args)
        {
            Sum(10, 20);
            Console.WriteLine(Sum(10, 20, 30));
            Console.WriteLine(Sum(10, 20, 30, 40));
        }
    }
}

It means method overloading does not depend on the return type of the method; it depends on the signature of the method, which means the parameters of the method, like this signature (int a, int b), (int a, int b, int c), or etc types.

Predefined method example:

string s = "Hello World";
s.IndexOf('o'); //Out: 4  //Return 1st occurance
s.IndexOf('o',5); //Out: 7 //Return next occurance
s.IndexOf("ll"); //Out: 2 //Return occurance

Here we use the same method with different behavior. We pass a string and a character, and we also pass one or two parameters.

It is a process of write a method with same name but with different function signature. The function signature can be different in any one of the following.

Definition of Method Overloading: It is an approach of defining a method with multiple behaviors where the behavior changes based on the parameter . If the input changes, the output changes based on the type of parameter, number of parameters, or order of parameters.

Polymorphism

Definition: Polymorphism is the ability in programming to use the same interface(same method) for different data types.

Types of Polymorphism:

Polymorphism can be implemented in two different ways.

  1. Compile-Time Polymorphism (Static Polymorphism):

    • Achieved through method overloading and operator overloading.

    • The method to be executed is determined at compile time.

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)
        }
    }
  1. Run-Time Polymorphism (Dynamic Polymorphism):

    • Achieved through method overriding and Interfaces where a method in a derived class overrides a method in the base class.

    • The method to be executed is determined at runtime.

Example of Method Overriding:

    class Animal
    {
        public virtual void Speak()
        {
            Console.WriteLine("Animal sound");
        }
    }

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

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

    class Program
    {
        static void Main()
        {
            Animal myDog = new Dog();
            Animal myCat = new Cat();

            myDog.Speak(); // Outputs "Bark"
            myCat.Speak(); // Outputs "Meow"
        }
    }

Key Points About Polymorphism:

  • Method Overloading: Same method name with different signatures within the same class. This is compile-time polymorphism.

  • Method Overriding: A derived class provides a specific implementation of a method that is already defined in its base class. This is run-time polymorphism.

  • Interfaces and Inheritance: Polymorphism is often used in conjunction with interfaces and inheritance, allowing for flexible and reusable code.

Why is Polymorphism Important?

  • Flexibility: Polymorphism allows for writing more flexible and maintainable code. For example, you can write code that works with a superclass, and it will automatically work with any subclass without modification.

  • Code Reusability: It promotes code reusability by allowing you to use the same method or interface to work with different types of objects.

  • Simplification: It simplifies code, as a single method can work with objects of different types.

Polymorphism is one of the fundamental principles of object-oriented programming, along with encapsulation, inheritance, and abstraction.

Method Overriding:

Definition: It's an approach of re-implementing a parent class's method in the child class with the same signature and changing the behavior.

Difference between Method Overloading and Method Overriding:

Difference 1:

class Class1
{
    public void Show(){} // Method Overloading
    public void Show(int i){} // Method Overloading
    public virtual void Test(){} // Method intended for overriding
}

class Class2 : Class1
{
    public void Show(string i){} // Method Overloading

    // Correct method overriding
    public override void Test(){} // Method overriding
}
OverloadingOverriding
In this case we define multiple methods with the same name by changing their parameters.In this case we define multiple methods with the same name and same parameters.

Difference 2:

OverloadingOverriding
This can be performed either within a class or between parent and child classes.This can be performed only between parent and child classes and can never be performed within the same class.

Difference 3:

Note:

  • If we want to override a parent's method in the child class, that method should first be declared using the virtual modifier in the parent class. By using the virtual keyword, the parent class gives permission to override the method.

  • Any virtual method of the parent class can be overridden by the child class if needed, using the override modifier.

using System;
class Class1
{
    // Method Overloading
    public void Show()
    {
        Console.WriteLine("Parent show method...");
    }
    // Method Overloading
    public void Show(int i)
    {
        Console.WriteLine("Parent show parameterized method...");
    }
    // Method overriding
    public virtual void Test()
    {
        Console.WriteLine("Parent Test method...");
    }
}

class Class2 : Class1
{
    // Method Overloading
    public void Show(string s)
    {
        Console.WriteLine("Child show parameterized method with string...");
    }
    // Method overriding
    public override void Test()
    {
        Console.WriteLine("Child Test method...");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Class1 c1 = new Class1();
        c1.Test(); // Output: Parent Test method...
        Class2 c2 = new Class2();
        c2.Show();            // Output: Parent show method...
        c2.Show(5);           // Output: Parent show parameterized method...
        c2.Show("example");   // Output: Child show parameterized method with string...
        c2.Test();            // Output: Child Test method...
        Console.ReadLine();
    }
}
OverloadingOverriding
While overloading a parent class method under the child class, child doesn't require to take any permission from the parent class.Whild Overriding a parent's method under child class, child class requires a permission from it's parent class.

Difference 4:

Overriding is all about changing the behavior of a parent's method in the child class.

Example of method overriding:

    class ParentClass
    {
        public virtual int Test()
        {
            Console.WriteLine("Run parent class Test method!!");
            return 100;
        }
    }
    class Program : ParentClass
    {
        public override int Test()
        {
            Console.WriteLine("Run child class Test method!!");
            return 20;
        }
        static  void Main(string[] arg)
        {
            Program p = new Program();
            p.Test();
        }
    }

Method Hiding/shadowing

Definition of method overriding: Method overriding is a technique or an approach of re-implementing a parent class method in the child class with exactly the same name and same signature.

Definition of method hiding/shadowing: Method hiding is a technique or an approach of re-implementing a parent class method in the child class with exactly the same name and same signature.

But the difference between Method overriding and Method hiding is that in Method overriding, the child class re-implements its parent's methods that are declared as virtual, whereas in Method hiding, the child class can re-implement any parent's method even if the method is not declared as virtual.

//Parent class:-
class ParentClass
{
    public virtual void Test()
    {
        Console.WriteLine("Run parent class Test method!!");
    }
    public void Test1()
    {
        Console.WriteLine("Run parent class Test 1 method!!");
    }
}

Case 1: Normal Inheriance

internal class Program : ParentClass
{
    static void Main(string[] args)
    {
        Program p = new Program();
        p.Test();
        p.Test1();
    }
}
/* Out:
Run parent class Test method!!
Run parent class Test 1 method!!
*/

Case 2: Method overriding:- Change the behavior of the parent class method if I don't require the parent class behavior.

internal class Program : ParentClass
{
    public override void Test()
    {
        Console.WriteLine("Run child class Test method!!");
    }
    static void Main(string[] args)
    {
        Program p = new Program();
        p.Test();
        p.Test1();
    }
}
/* Out:
Run child class Test method!!
Run parent class Test 1 method!!
*/

In method overriding, we can change the behavior by using the override keyword if the parent class methods are declared as virtual; otherwise, we cannot change the behavior.

Case 2: Method Hiding:- The child class can re-implement any parent's method even if the method is not declared as virtual.

internal class Program : ParentClass
{
    public new void Test()
    {
        Console.WriteLine("Run child class Test method!!");
    }
    public new void Test1()
    {
        Console.WriteLine("Run child class Test 1 method!!");
    }
    static void Main(string[] args)
    {
        Program p = new Program();
        p.Test();
        p.Test1();
    }
}
/* Out:
Run child class Test method!!
Run child class Test 1 method!!
*/

Without the new keyword, it also works. See it.

internal class Program : ParentClass
{
    public void Test()
    {
        Console.WriteLine("Run child class Test method!!");
    }
    public void Test1()
    {
        Console.WriteLine("Run child class Test 1 method!!");
    }
    static void Main(string[] args)
    {
        Program p = new Program();
        p.Test();
        p.Test1();
    }
}
/* Out:
Run child class Test method!!
Run child class Test 1 method!!
*/

If you do not use the new keyword, the compiler will give a warning: "Use the new keyword if hiding was intended." The new keyword helps you remember that you are hiding the parent method, which can be useful in the future.

We can re-implement a parent class method under child class using 2 approaches:

  1. Method Overriding,

  2. Method Hiding / Shadowing,

After re-implementing the parent class's method in the child class, the child class instance will start calling the local method, which is the re-implemented method. However, if needed, we can also call the parent class's method from the child class using two approaches.

Approach 1: By creating the instance of the parent class in the child class, we can call the parent's class method in the child class.

class Program : ParentClass
{
    public override void Test()
    {
        Console.WriteLine("Run child class Test method!!");
    }
    public new void Test1()
    {
        Console.WriteLine("Run child class Test 1 method!!");
    }
    static void Main(string[] args)
    {
        Program p = new Program();
        p.Test();
        p.Test1();
        ParentClass pc = new ParentClass();
        pc.Test();
        pc.Test1();
    }
    /*
     Run child class Test method!!
     Run child class Test 1 method!!
     Run parent class Test method!!
     Run parent class Test 1 method!!
     */
}

Approach 2: By using the base keyword, we can also call the parent's method from the child class, but keywords like this and base can't be used from static blocks.

class Program : ParentClass
{
    public override void Test()
    {
        Console.WriteLine("Run child class Test method!!");
    }
    public new void Test1()
    {
        Console.WriteLine("Run child class Test 1 method!!");
    }
    public void ParentChild()
    {
        base.Test();//Use base keyword to call the parent class method.
        base.Test1();
    }
    static void Main(string[] args)
    {
        //base.Test();//Give error: Becouse inside the static block not work base & this keyword   
        //base.Test1();//Give error
        Program p = new Program();
        p.ParentChild();
    }
     /*
      Run parent class Test method!!
      Run parent class Test 1 method!!
      */
}

Difference between Overriding and Hiding:

Inheritance rule no. 3: A parent class reference, even if created using the child class instance,can't access any members that are purely defined in the child class but can call overridden members of the child class, because overridden members are not considered pure child class members, but members re-implemented using the hiding approach are considered pure child class members and are not accessible to the parent's reference.

Example imp d/f Overriding and Hiding:-

class Program : ParentClass
{
    public override  void Test()
    {
        Console.WriteLine("Run child class Test method!!");
    }
    public new void Test1()
    {
        Console.WriteLine("Run child class Test 1 method!!");
    }
    static void Main(string[] args)
    {
        Program c = new Program(); //c is the instance of child class
        ParentClass p = c; // p is refrence of parent class created by using child's class instance
        p.Test();//Calling the child class method not parent class method
        p.Test1();//Calling the parent class method not child class method
    }
     /*
     Run child class Test method!!
     Run parent class Test 1 method!!
     */
}
  • p.Test(); :- Calling the child class method not parent class method (invoke child class method).

  • p.Test1(); :- Calling the parent class method not child class method (invoke parent class method).

Operator Overloading

Method overloading is an approach of defining multiple behaviors for a method, and those behaviors will vary based on the parameters of that method.

String s = "Hello how are you";
s.Substring(14); //Out:you
s.Substring(10); //Out:are you
s.Substring(10, 3); //Out:are

Operator overloading allows an operator to have different behaviors based on the types of operands . For example, the + operator adds two numbers together, but it combines two strings when used with them.

Number + Number => Addition

String + String => Concatenation

 int x = 12;
 int y = 13;
 int z = x + y;

Here is the method: public static int <method name>(int a, int b).

Operator overloading:public static int operator + (int a, int b). For example, z = int a + int b means z is an operator that takes two integer values and returns an integer value. The + is used to add the two numbers. These types of methods are implemented in the base class library. Operator overloading must be static.

Syntax: [<modifiers>] static <return type> operator <opt>(<operand type>){-logic}

operator: Always lowercase.

Example: Create operator for addition the Matrix**.**

using System;
namespace Demo
{
    // Define the Matrix class within the Demo namespace
    internal class Matrix
    {
        // Declare non-static variables for the matrix elements
        int a, b, c, d;

        // Define the constructor to initialize matrix values
        public Matrix(int a, int b, int c, int d)
        {
            this.a = a;
            this.b = b;
            this.c = c;
            this.d = d;
        }

        // Define the addition operator for the Matrix class
        public static Matrix operator +(Matrix obj1, Matrix obj2)
        {
            return new Matrix(obj1.a + obj2.a, obj1.b + obj2.b, obj1.c + obj2.c, obj1.d + obj2.d);
        }

        // Define the subtraction operator for the Matrix class
        public static Matrix operator -(Matrix obj1, Matrix obj2)
        {
            return new Matrix(obj1.a - obj2.a, obj1.b - obj2.b, obj1.c - obj2.c, obj1.d - obj2.d);
        }
    }

    // Define the TestMatrix class to test the Matrix operations
    class TestMatrix
    {
        static void Main()
        {
            // Initialize two Matrix objects with specific values
            Matrix m1 = new Matrix(20, 18, 16, 14);
            Matrix m2 = new Matrix(10, 8, 6, 4);

            // Perform addition and subtraction of the matrices
            Matrix m3 = m1 + m2;//If you do this without Define the addition operator for the Matrix class then come error 
            Matrix m4 = m1 - m2;//If you do this without Define the subtraction operator for the Matrix class then come error

            // Print the results (current implementation will print the class name)
            Console.WriteLine(m1); // Output: Demo.Matrix
            Console.WriteLine(m2); // Output: Demo.Matrix
            Console.WriteLine(m3); // Output: Demo.Matrix
            Console.WriteLine(m4); // Output: Demo.Matrix
        }
    }
}

Overloading WriteLine Method

The WriteLine method in the Console class can handle different types of parameters like int, float, bool, and objects. When you pass an object to WriteLine, it calls the ToString method of that object.

When you use WriteLine with an instance of a class, it calls the WriteLine method that takes an object as a parameter. This method then calls the ToString method on the object you passed.

How WriteLine Works with Objects

  1. Passing Object to WriteLine: Console.WriteLine(m1); Here, m1 is an instance of the Matrix class.

  2. Internal Handling: The WriteLine method for objects is: public static void WriteLine(object value); When you pass m1 to WriteLine, it calls value.ToString() where value is m1.

  3. DefaultToString Behavior: By default, ToString returns the class name if not overridden. So, calling Console.WriteLine(m1) without overriding ToString in the Matrix class will print Demo.Matrix.

  4. OverridingToString: To print useful information about the matrix, we should override the ToString method in the Matrix class.

Overriding ToString

To override the ToString method, we need to inherit from the Object class (which all classes do by default) and provide our own implementation:

public override string ToString()
{
    return $"{a} {b}\n {c} {d}";
}

This way, when Console.WriteLine(m1) is called, it will print the matrix elements instead of the class name.

Here's the updated Matrix class with the ToString method overridden:

internal class Matrix
{
    int a, b, c, d;
    public Matrix(int a, int b, int c, int d)
    {
        this.a = a; this.b = b;
        this.c = c; this.d = d;
    }
    public static Matrix operator +(Matrix obj1, Matrix obj2)
    {
        return new Matrix(obj1.a + obj2.a, obj1.b + obj2.b, obj1.c + obj2.c, obj1.d + obj2.d);
    }
    public static Matrix operator -(Matrix obj1, Matrix obj2)
    {
        return new Matrix(obj1.a - obj2.a, obj1.b - obj2.b, obj1.c - obj2.c, obj1.d - obj2.d);
    }
    public override string ToString()
    {
        return $"{a} {b}\n {c} {d}";
    }
}

Now, when you run the TestMatrix class, it will print the elements of the matrices.

NOTS: If you want to the instance of all value print as it is simply go to override the ToString() method.

Abstract

Abstraction is a process of hiding the implementation details and showing only functionality to the user. Another way, it shows only essential things to the user and hides the internal details.

Abstraction can be achieved by two ways:

  1. Abstract class

  2. Interface.

Abstract class

Abstract Method: A method without any body is called an abstract method.

Abstract Class: A class that has any abstract members is called an abstract class.

  • We cannot create object of abstract class.

Example: Consider a scenario with geometric shapes where different shapes share some common characteristics but also have their own specific attributes.

  • Entities: Rectangle, Circle, Triangle, Cone.

  • Attributes:

    Rectangle: Width, Height

    Circle: Radius, Pi

    Triangle: Width, Height

    Cone: Radius, pi, Height

Common attributes are: Width, Height, Radius, Pi.

According to the example, create a parent class for the attributes of all four classes: Rectangle, Circle, Triangle, and Cone.

// Abstract base class representing a geometric figure
public abstract class Figure
{
    // Common attributes for all figures
    public double Width, Height, Radius;
    public const double Pi = 3.14;

    //Hear we cannot define the formula because each Entity having diffrent formula
    //Hear come the Abstract method, Method can declare only hear but not consume hear
    // Abstract method to calculate the area of the figure
    // Must be overridden in derived classes
    public abstract double GetArea();

    // The advantage of declaring an abstract method is that all derived classes
    // will have to provide their own implementation for calculating the area.
}
// Rectangle class inheriting from Figure
public class Rectangle : Figure
{
    public Rectangle(double width, double height)
    {
        this.Width = width;
        this.Height = height;
    }

    // Override GetArea to calculate the area of a rectangle
    public override double GetArea()
    {
        return Width * Height;
    }
}
// Circle class inheriting from Figure
public class Circle : Figure
{
    public Circle(double radius)
    {
        this.Radius = radius;
    }

    // Override GetArea to calculate the area of a circle
    public override double GetArea()
    {
        return Pi * Radius * Radius;
    }
}
// Triangle class inheriting from Figure
public class Triangle : Figure
{
    public Triangle(double width, double height)
    {
        this.Width = width;
        this.Height = height;
    }

    // Override GetArea to calculate the area of a triangle
    public override double GetArea()
    {
        return 0.5 * Width * Height; 
    }
}
// Cone class inheriting from Figure
public class Cone : Figure
{
    public Cone(double radius, double height)
    {
        this.Radius = radius;
        this.Height = height;
    }

    // Override GetArea to calculate the surface area of a cone
    public override double GetArea()
    {
        // Surface area of a cone: πr(r + √(r² + h²))
        return Pi * Radius * (Radius + Math.Sqrt(Radius * Radius + Height * Height));
    }
}

// Testing the classes
class AbstractFigure
{
    static void Main()
    {
        // Initializes an array of Figure objects with instances of different derived classes (Rectangle, Circle, Triangle, Cone).
        Figure[] figures = new Figure[]
        {
            new Rectangle(10, 5),
            new Circle(7),
            new Triangle(6, 8),
            new Cone(3, 4)
        };

        foreach (Figure figure in figures)
        {
            Console.WriteLine($"Area: {figure.GetArea()}");
        }
    }
}

INTERFACE:

  • Class:

    A user-defined data type that can contain both non-abstract methods (methods with a body) and abstract methods (methods without a body, if the class is abstract). Classes can be created.

  • Abstract Class:

    A user-defined data type that cannot be created directly. It can have bothnon-abstract methods (with a body) and abstract methods (without a body). Abstract classes are meant to be subclassed.

  • Interface:

    A user-defined data type that contains only abstract methods (methods without a body). Interfaces cannot be created and are used to define a contract that classes can implement.

NOTE: Every abstract method in an interface must be implemented by the class that inherits from the interface.

Parent class has an interface with have some rules and regulations, and the child class fulfills the own requirements with follow the rules and regulations which provided by the parent class.

If you have any abstract method, it is implemented only by the child class.

  • Generally, a class inherits from another class to consume the members of its parent, whereas if a class is inheriting from an interface, it is to implement the members of its parents.

Note: A class can inherit from a class and interface at a time.

Create Interface:

Go to Visual Studio and press Ctrl + Shift + A, select Interface shown in the Add New Item panel, give it a name, and press OK.

Name Create: Make it a habit when you create an interface to place "I" at the beginning of the name to indicate that it is an interface.

How to define Interface:

[<modifiers>] Interface <Name>{-Abstract Member Declaration hear}

Create Method in Interface:

public abstract void Add(int a, int b);
//Or Only
void Add(int a, int b);

NOTES:

  • The default scope of a member of an interface is public. But in the case of a class, it is private.

  • By default, every member of an interface is abstract, so we don't need to use the abstract modifier again like we do with an abstract class.

  • We can't declare any fields/variables under an interface.

      internal interface ITestInterface
      {
          int x; //It's Give error
      }
    
  • If required, an interface can inherit from another interface; it is possible.

      internal interface ITestInterface1
      {
          void Add(int a, int b);
      }
      internal interface ITestInterface2:ITestInterface1
      {
          void Sub(int a, int b);
      }
      //ITestInterface2 having two method add and Sub
    
  • Every member of an interface should be implemented in the child class of the interface without fail, but while implementing, we don't need to use the override modifier as we do with an abstract class.

  • Abstract class modifiers can't be changed at initialization time.

    Example: Initialization abstract method

      internal interface ITestInterface1
      {
          void Add(int a, int b);
      }
      class ImplementationClass:ITestInterface1
      {
        //Use public because the default scope of a class is private, and interface members are by default public.  
        public void Add(int a, int b){}
      }
    

    OR you also Initialization abstract method like that.

      internal interface ITestInterface1
      {
          void Add(int a, int b);
      }
      class ImplementationClass:ITestInterface1
      {
        void ITestInterface1.Add(int a, int b){}
      }
    
      internal interface ITestInterface1
      {
          void Add(int a, int b);
          void Sub(int a, int b);
      }
      class ImplementationClass:ITestInterface1
      {
        public void Add(int a, int b)
        {
            Console.WriteLine(a+b);
        }
        void ITestInterface1.Sub(int a, int b)
        {
            Console.WriteLine(a - b);
        }
        static void Main()
        {
            ImplementationClass obj = new ImplementationClass();
            obj.Add(100, 300);
            obj.Sub(300, 100); //Give error
       }
      }
    

    obj.Sub(300, 100); gives an error because you can't call obj.Sub(300, 100) directly. This happens because Sub is implemented explicitly for the interface ITestInterface1. In explicit interface implementation, the method is not part of the public interface of the class and can only be accessed through an instance of the interface. To call the Sub method, you need to cast to the interface like this:

      internal interface ITestInterface1
      {
          void Add(int a, int b);
          void Sub(int a, int b);
      }
    
      class ImplementationClass : ITestInterface1
      {
          public void Add(int a, int b)
          {
              Console.WriteLine(a + b);
          }
    
          void ITestInterface1.Sub(int a, int b)
          {
              Console.WriteLine(a - b);
          }
    
          static void Main()
          {
              ImplementationClass obj = new ImplementationClass();
              obj.Add(100, 300);
    
              // To call the Sub method, you need to cast to the interface
              ITestInterface1 interfaceObj = obj; //Create interface
              interfaceObj.Sub(300, 100);
              interfaceObj.Add(100, 300);
          }
      }
    
      internal interface ITestInterface1 {void Add(int a, int b);}
      class ImplementationClass : ITestInterface1
      {
          void Add(int a, int b) //GIve error, because you not use public
          {
              Console.WriteLine(a + b);
          }
          static void Main()
          {
              ImplementationClass obj = new ImplementationClass();
              obj.Add(100, 300);//GIve error
          }
      }
    

Multiple Inheritance with Interface:

  • As we know, there are 5 types of inheritance: Single, Multilevel, Hierarchical, Hybrid, and Multiple. Even though multiple inheritance is not supported through classes in C#, it is still supported through interfaces.

  • A class can have only one immediate parent class, meaning one class can inherit only one other class. Whereas the same class can have any number of interfaces as its parent, i.e., multiple inheritance is supported in C# through interfaces.

  • A class can have only one inheritance, but it can implement any number of interfaces.

  • Q: Why is multiple inheritance not supported through classes, and how is it supported through interfaces?

    Ans: Ambiguity Issue: When a class inherits from multiple classes, there can be a problem if both parent classes have methods with the same name. This causes confusion because the compiler won't know which method to call. For example, if ClassA and ClassB both have a method Show(), and ClassC inherits from both, the compiler won't know which Show() method to use when ClassC calls Show().

      class ClassA
      {
          public void Show() { Console.WriteLine("ClassA Show"); }
      }
      class ClassB
      {
          public void Show() { Console.WriteLine("ClassB Show"); }
      }
      // This will cause ambiguity if C# allowed multiple inheritance.
      class ClassC : ClassA, ClassB
      {
          // Compiler confusion: which Show() to inherit?
      }
    
  • Diamond Problem: In multiple inheritance, the diamond problem occurs when a class inherits from two classes that both inherit from the same class. This can cause confusion in the inheritance chain.

Multiple Inheritance in Interfaces:

No Implementation Conflict: Interfaces do not provide method implementations, only declarations. Therefore, when a class implements multiple interfaces, there is no risk of ambiguity because the implementing class must provide the specific implementation for each method. This eliminates the confusion that arises from multiple inheritance in classes.

interface IInterfaceA
{
    void Show();
}

interface IInterfaceB
{
    void Show();
}

class ImplementationClass : IInterfaceA, IInterfaceB
{
    public void Show()
    {
        Console.WriteLine("Implementation of Show");
    }
}

Explicit Implementation(separately implimentation): If you need to separate methods from different interfaces, a class can explicitly implement the interface methods. This helps to avoid confusion.

interface IInterfaceA
{
    void Show();
}

interface IInterfaceB
{
    void Show();
}

class ImplementationClass : IInterfaceA, IInterfaceB
{
    void IInterfaceA.Show() //separately Implement method 
    {
        Console.WriteLine("IInterfaceA Show");
    }

    void IInterfaceB.Show() //separately Implement method
    {
        Console.WriteLine("IInterfaceB Show");
    }
}

class Program
{
    static void Main()
    {
        ImplementationClass obj = new ImplementationClass();
        // Requires casting to call the explicit implementations
        //Calling
        IInterfaceA i1 = obj;
        i1.Show(); // Outputs: IInterfaceA Show
        IInterfaceB i2 = obj;
        i2.Show(); // Outputs: IInterfaceB Show
        //Or we call like that
        ((IInterfaceA)obj).Show(); // Outputs: IInterfaceA Show
        ((IInterfaceB)obj).Show(); // Outputs: IInterfaceB Show
    }
}

Consuming will cause ambiguity, but implementing does not cause ambiguity.

There are two ways of implementing interfaces:

  1. Implicit: All interfaces assume my method is implemented.

  2. Explicitly: Clearly implement a particular interface.

interface A
{
    void show();
}
interface B
{
    void show();
}
//Select 'A' and right click press 'Quick actions and refectoring' next open automaticly quick action   
class Face: A, B     
{
    public void show() { } //Option one: Implement interface
    void A.show() { } //Option two: Implement member explicitly 
    static void Main(){} 
}

Abstract Class and Abstract Method:

Abstract Method: A method without a body is called an abstract method. It only contains the method's declaration.

  • Non-abstract method: public void show() { }

  • Abstract method: public abstract void show();

Abstract Class: If a class contains any abstract member, it is called an abstract class. To define an abstract class, we need to use the abstract keyword. If you try to run this class without adding the abstract keyword, you will get an error.

  • Non-abstract class: class Math { // Non-abstract members }

  • Abstract class: abstract class Math { // Non-abstract members with abstract members }

If a method is declared as abstract in any class, the child class of that class must implement the method without fail. It is mandatory to impliment abstract method in child class.

The concept of an abstract method is nearly similar to the concept of method overriding, but the difference is that abstract methods must be implemented in derived classes, while overriding a parent class's virtual method is optional.

If an abstract class contains both abstract and non-abstract methods, then the child class must implement every abstract method of the parent class. Only then can the child class use the non-abstract methods of the parent class.

It is not possible to create an instance of an abstract class because abstract classes are designed to be incomplete and serve as a base for other child classes. Abstract classes can contain abstract methods (methods without a body) that must be implemented by derived classes. Since the abstract class itself may have incomplete implementations, it cannot be instantiated directly. Instead, it must be subclassed, and the subclass must provide concrete implementations for all the abstract methods. This ensures that any object created will have fully defined behavior.

You cannot create an instance of an abstract class, but you can create a reference to it.

abstract class Aob
{
    public abstract void show();
}
class Face: Aob
{
    // Override the abstract method from the base class
    public override void show()
    {
        Console.WriteLine("Ok");
    }
    static void Main()
    {
            Face f = new Face(); //Instance of own class
            //Aob a = new Aob();//Give error: you cannot create instance of abstract class   
            Aob a = f; //pass refrence of abstract class
            f.show();
    }
}

Structures:

  • A structure is a user-defined data type, similar to a class.

  • In C Language: Structures contain only fields.

    In C#: Structures can contain fields, methods, constructors, properties, indexers, operator methods, and more.

  • Syntax:

      [<modifiers>] struct <Name>
      {
          // Define members (fields, methods, etc.)
      }
    
  • Creating a Structure in Visual Studio:

    1. Press Ctrl + Shift + A to open the "Add New Item" dialog.

    2. Select "Code File" from the list. : This code file is an empty file; it's just a .cs file.

    3. Enter a name for your file.

    4. Press "OK" to add the file.

  • Example:

      using System;
    
      namespace Demo
      {
          struct StructTest//Declare structure
          {
              public void show()
              {
                  Console.WriteLine("Method in struct");
              }
              static void Main()
              {
                  StructTest st = new StructTest();
                  st.show();
              }
          }
      }
    

Difference between Class and Structure:

Class:

  1. Reference Type: Classes are reference types.

  2. Memory Allocation: Instances of classes are allocated memory on the managed heap.

  3. Use Case: Classes are used to represent entities with larger volumes of data.

  4. Instantiation: new keyword is mandatory for creating the instance.

     Test t = new Test();
    
  5. Field Initialization: Fields of a class can be initialized at the time of declaration.

     class Test
     {
         int x = 5; // Field initialization
     }
    
  6. Example of initializing field value:

     class Test
     {
         int x;
         public void Display()
         {
             Console.WriteLine(x);
         }
         static void Main()
         {
             Test t = new Test();
             t.x = 10;
             t.Display();
         }
     }
    
  7. We can define any constructor in the class that is either parameterless or parameterized. If no constructor is defined, there will be an implicit constructor will be called which is default constructor.

  8. If no constructors are defined in a class, there will be one implicit constructor after compilation. If we define "n" constructors in a class, there will be "n" constructors after compilation.

  9. Class can be inherited by another class.

  10. A class can implement an interface.

Structure:

  1. Value Type: Structures are value types.

  2. Memory Allocation: Instances of structures are allocated memory on the stack.

  3. Use Case: Structures are used to represent smaller volumes of data.

    Note:

    • All pre-defined data types in our language that are reference types, such as String and Object, are classes.

    • All pre-defined data types that are value types, such as int (Int32), float (Single), and bool (Boolean), are structures.

    To see the implementation of string or int, follow these steps: declare a string or any other data type, then select and right-click it and select "Go to Implementation." You will see the source code like that:

    //For int:
    public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<int>, IEquatable<int>
    {/* Implementation*/}
    //For string:
    public sealed class String : IComparable, ICloneable, IConvertible, IEnumerable, IComparable<string>, IEnumerable<char>, IEquatable<string>
    {/* Implementation*/}
    
  4. Instantiation: new keyword is optional for creating the instance.

     Test t; // Without new keyword
     Test t = new Test(); // With new keyword
    
  5. Field Initialization: Fields of a struct cannot be initialized at the time of declaration.

     struct Test
     {
         int x; // Field cannot be initialized here
     }
    
  6. Example of initializing field value:

     struct Test
     {
         int x;
         public void Display()
         {
             Console.WriteLine(x);
         }
         static void Main()
         {
             Test t;  
             //Or
             Test t = new Test();
             t.x = 10;//Inslization is mendatory
             t.Display();
         }
     }
    

    Struct example: When you call the constructor new Test(); in a struct, it initializes the default values. If you do not call the constructor and only declare the struct instance without using the new keyword, like Test t;, you will get an error when trying to access its members.

    using System;
    
    struct Test
    {
        public int x; // Field needs to be public to be accessible
    
        public void Display()
        {
            Console.WriteLine(x);
        }
    
        static void Main()
        {
            // Case 1: Without using new keyword (will cause an error if we try to access members without initialization)
            Test t;
            // t.Display(); // This line will cause a compile-time error: "Use of unassigned local variable 't'"
    
            // Case 2: Using new keyword
            Test t2 = new Test();
            t3.Display(); // Output: 0
    
            // Case 3: Using new keyword
            Test t1;
            t1.x = 10;
            t1.Display(); // Output: 10
    
            // Case 4: Using new keyword
            Test t4 = new Test();
            t4.x = 10; // Initialization is mandatory
            t4.Display(); // Output: 10
        }
    }
    

    Note:If a structure contains any fields, you need to initialize those fields either by explicitly calling the default constructor using the new keyword, or by explicitly assigning values to the fields after creating the instance without using new.For example:

    • Using new keyword: Test t = new Test(); This initializes all fields to their default values.

    • Without using new keyword: Test t; You must manually initialize each field before accessing it: t.x = 10;

  7. Whereas in the case of a structure, a parameterless or default constructor is always implicit and can't be defined explicitly again, we can only define a parameterized constructor.

     struct Test
     {
         int x; // Field declaration
    
         // Parameterized constructor
         public Test(int x)
         {
             this.x = x;
         }
    
         // Method to display the value of x
         public void Display()
         {
             Console.WriteLine(x);
         }
    
         static void Main()
         {
             // Case 1: Creating an instance without using the 'new' keyword
             // We manually assign a value to the field 'x'
             Test t;
             t.x = 10;
             t.Display(); // Output: 10 (since we explicitly assigned 10 to t.x)
    
             // Case 2: Creating an instance using the 'new' keyword
             // This calls the default constructor, which initializes 'x' to its default value (0 for int)
             Test t1 = new Test();
             t1.Display(); // Output: 0 (since the default constructor initializes 'x' to 0)
    
             // Case 3: Creating an instance using the parameterized constructor
             // This initializes 'x' with the provided value (e.g., 5)
             Test t2 = new Test(5);
             t2.Display(); // Output: 5 (since the parameterized constructor sets 'x' to 5)
         }
     }
    
  8. In a structure, if we define "0" constructors, then after compilation there will be 1 constructor (implicit). If we define "n" constructors, after compilation there will be "n" + 1 constructors.

  9. Structure can't be inherited by other Structures (Structures do not support inheritance).

  10. Structure can also implement an interface.

Enumeration or Enum Types:

An enum is a user-defined type, so it is always better to define an enum directly under the namespace, but it is also possible to define an enum under a class or structure.

Characteristics:

  1. Type Safety: Enums provide type safety, ensuring that only valid values (those defined in the enum) can be assigned to enum variables.

  2. Value Type: Enums are value types, and their underlying type can be any integral type except char.

Syntax:

[<modifiers>] enum <Name> [: <type>]
{
    //List of named constant values
}

Basic Example:

public enum Days
{
    Monday, Tuesday, Wednesday, Thursday, Friday,Saturday, Sunday
}

Underlying Type:

By default, the underlying type of enum members is int, and and the values start from 0 and increment by 1.

public enum Days
{
    Monday,     // Default value 0
    Tuesday,    // Default value 1
    Wednesday,  // Default value 2
    Thursday,   // Default value 3
    Friday      // Default value 4
}

You can specify a different underlying type and custom values.

Custom Underlying Type and Values:

public enum Days : byte
{
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
    Sunday = 7
}

Enum supports only: byte, short, int, long, uint, ushort, ulong, and sbyte.

Supported underlying types:byte,short,int,long,uint,ushort,ulong, andsbyte.

Enum with Explicit Values Example:

public enum Days
{
    Monday = 1,
    Tuesday = 52,
    Wednesday = 21,
    Thursday = 41,
    Friday = 18
}

Using Enums in Code:

//Declaring and Assigning Enum Variables:
Days today = Days.Monday;

//Printing Enum Values:
Console.WriteLine(today);  // Output: Monday

//Getting the Underlying Value:
Console.WriteLine((byte)today);  // Output: 1

//Type Casting:
Days tomorrow = (Days)2;
Console.WriteLine(tomorrow);  // Output: Tuesday

// Get integer representation of enum value
Days d4 = Days.Friday;
Console.WriteLine((int)d4); // Output: 4 (default value)

// Working with custom enum values
CustomDays cd1 = CustomDays.Monday;
Console.WriteLine(cd1);  // Output: Monday

CustomDays cd2 = (CustomDays)52;
Console.WriteLine(cd2);  // Output: Tuesday

CustomDays cd3 = CustomDays.Wednesday;
Console.WriteLine(cd3);  // Output: Wednesday

Console.WriteLine((int)cd3);  // Output: 21 (custom value)

// Use foreach to get values
// `Enum.GetValues()` retrieves an array of the values in the enum
// `Enum.GetNames()` retrieves an array of the names in the enum
// `typeof` keyword is used to get the type of the enum

Console.WriteLine("Days Enum Values:");
foreach (int i in Enum.GetValues(typeof(Days)))
     Console.WriteLine(i);

Console.WriteLine("Days Enum Names:");
foreach (string s in Enum.GetNames(typeof(Days)))
     Console.WriteLine(s);

// Print name and values
Console.WriteLine("Days Enum Names and Values:");
foreach (int i in Enum.GetValues(typeof(Days)))
     Console.WriteLine(i + " : " + (Days)i);

Working with Custom Enum Values:

public enum Days 
{
    Monday = 1,
    Tuesday = 52,
    Wednesday = 21,
    Thursday = 41,
    Friday = 18
}

class TestClass
{
    static void Main()
    {
        Days cd1 = Days.Monday;
        Console.WriteLine(cd1);  // Output: Monday

        Days cd2 = (Days)52;
        Console.WriteLine(cd2);  // Output: Tuesday

        Days cd3 = Days.Wednesday;
        Console.WriteLine(cd3);  // Output: Wednesday

        Console.WriteLine((byte)cd3);  // Output: 21 (custom value)

        // Use foreach to get values
        Console.WriteLine("Days Enum Values:");
        foreach (int i in Enum.GetValues(typeof(Days)))
            Console.WriteLine(i);

        Console.WriteLine("Days Enum Names:");
        foreach (string s in Enum.GetNames(typeof(Days)))
            Console.WriteLine(s);

        // Print name and values
        Console.WriteLine("Days Enum Names and Values:");
        foreach (int i in Enum.GetValues(typeof(Days)))
            Console.WriteLine(i + " : " + (Days)i);
    }
}
public enum Days : byte
{
    Monday = 1,
    Tuesday = 52,
    Wednesday = 21,
    Thursday = 41,
    Friday = 18
}

class TestClass
{
    static void Main()
    {
        // Use foreach to get values
        Console.WriteLine("Days Enum Values:");
        foreach (int i in Enum.GetValues(typeof(Days))) //Give error because now enum is byte type
            Console.WriteLine(i);

        foreach (byte i in Enum.GetValues(typeof(Days))) //thats correct
            Console.WriteLine(i);

        Console.WriteLine("Days Enum Names:");
        foreach (string s in Enum.GetNames(typeof(Days)))
            Console.WriteLine(s);
    }
}

Useing:

using System;

namespace Demo
{
    // Define an enum named Days with specific byte values
    public enum Days : byte
    {
        Monday = 1,
        Tuesday = 52,
        Wednesday = 21,
        Thursday = 41,
        Friday = 18
    }

    class TestClass
    {
        // Define a static property of type Days with a default value
        public static Days MeetingDate
        {
            get; set; // get; set; are used to access and modify the value
        } = 0; // Set a valid default value from the enum

        // Define another static property with a default value cast from an integer
        public static Days MeetingDate2
        {
            get; set; // get; set; are used to access and modify the value
        } = (Days)52; // Casting 52 to Days, which corresponds to Days.Tuesday

        // Define a static property with a default value directly set from the enum
        public static Days MeetingDate3
        {
            get; set; // get; set; are used to access and modify the value
        } = Days.Monday; // Set to a valid named constant from the enum

        static void Main()
        {
            // Print the current values of the static properties
            Console.WriteLine(MeetingDate); // Output: 0
            Console.WriteLine(MeetingDate2); // Output: Tuesday
            Console.WriteLine(MeetingDate3); // Output: Monday

            // Change the meeting date:
            // MeetingDate3 = "Saturday"; // Error: Cannot assign a string to a Days enum property.
            // MeetingDate3 = Days.Saturday; // Error: 'Saturday' is not defined in the Days enum.

            MeetingDate3 = Days.Friday; // Correct assignment, using a defined value in the enum
            Console.WriteLine(MeetingDate3); // Output: Friday
        }
    }
}
  • If you set a default value for an enum property to 0 and 0 is not defined in the enum, you will get 0 as output.

      public enum Days : byte
      {
      Monday = 1, Tuesday = 52, Wednesday = 21, Thursday = 41, Friday = 18
      }
      public static Days MeetingDate
      {
      get; set; 
      } = 0; // Output is: 0
    
  • If 0 is defined in the enum (like Monday = 0 or like only written Monday not inslize), then the output will be the corresponding enum name.

      public enum Days : byte
      {
         Monday,
         //Or
         //Monday = 0, 
         Tuesday = 52, Wednesday = 21, Thursday = 41, Friday = 18
      }
      public static Days MeetingDate
      {get; set; } = 0; // Output is: Monday
    
  • If you assign a default value except 0, it will cause a compile-time error

      public enum Days : byte
      {
         Monday = 1, Tuesday = 52, Wednesday = 21, Thursday = 41, Friday = 18
      }
      public static Days MeetingDate
      {
      get; set; 
      } = 1; // Output: You got error
    

Key Points:

  1. Type Safety: Enums ensure that only valid named constants are used, which makes the code more robust and less error-prone.

  2. Readability: Using enums improves code readability by replacing numeric values with meaningful names.

  3. Scope: Enums can be declared directly within a namespace, class, or struct.

  4. Values: Enum members can have explicit values, and the underlying type can be specified.

Notes:

  1. Default Values: If no underlying type is specified, int is used by default, starting from 0.

  2. Unique Values: Enum member names must be unique within the same scope.

  3. Compatibility: Enums can be easily used with switch statements and for loops.

Sealed classes:

In C#, a sealed class is a class that cannot be inherited by other classes. This means that you cannot use a sealed class as a base class, ensuring that the class's behavior remains unchanged and that no further subclasses can alter its implementation.

When to Use Sealed Classes

  1. Preventing Inheritance: If you want to ensure that a class's functionality is not extended or overridden, you can declare it as sealed.

  2. Performance Optimization: Sealed classes can be more optimized at runtime because the compiler knows that the class won't have subclasses, allowing for more efficient method calls.

How to Define a Sealed Class

To declare a class as sealed, use the sealed keyword in the class definition. Here's an example:

public sealed class FinalClass
{
    public void DisplayMessage()
    {
        Console.WriteLine("This is a sealed class.");
    }
}

In this example, FinalClass is sealed, so no other class can inherit from it.

Example of Sealed Class Usage

public class BaseClass
{
    public virtual void ShowMessage()
    {
        Console.WriteLine("BaseClass method");
    }
}

public sealed class DerivedClass : BaseClass
{
    public override void ShowMessage()
    {
        Console.WriteLine("DerivedClass method");
    }
}

// The following code will result in a compile-time error because DerivedClass is sealed.
// public class AnotherClass : DerivedClass
// {
// }

class Program
{
    static void Main()
    {
        DerivedClass derived = new DerivedClass();
        derived.ShowMessage(); // Outputs: DerivedClass method
    }
}

In this example:

  • BaseClass is a normal class with a virtual method ShowMessage.

  • DerivedClass inherits from BaseClass and overrides the ShowMessage method.

  • DerivedClass is marked as sealed, preventing any further inheritance from it.

Key Points

  • Sealed Classes Prevent Inheritance: Once a class is sealed, it cannot be used as a base class.

  • Use Cases: Sealed classes are often used when a class is designed to be immutable or when you want to provide a clear and unalterable implementation.

  • Performance: There can be slight performance benefits in terms of method invocation speed because the runtime doesn't need to check for potential overrides.

Sealed classes are a way to control the inheritance hierarchy and ensure that certain classes maintain a specific, unchangeable implementation.

Properties:

  • Property is a member of class using to expose values associated with a class to the outside environment.

Way to access the fields value in diffrent class.

public class Circle//Circle represent is entity
{
    double Radius = 12.35; //Radius represent is a attribute
}
public class Test
{
    static void Main()
    {
        Circle cir = new Circle();
        Console.WriteLine(cir.Radius);
//Error, you cannot access the Radius value. Because Radius is a private.
    }
}
  • If you make field as public, any one can access and modify value:
    public class Circles//Circle represent is entity
    {
        public double Radius = 12.35; //Radius represent is a attribute
    }
    public class Test
    {
        static void Main()
        {
            Circles cir = new Circles();
            Console.WriteLine(cir.Radius);//Get the old value. Out: 12.35
            cir.Radius = 19.52;//Set the new value 
            Console.WriteLine(cir.Radius);//Get the new value. Out: 19.52
        }
    }
  • But the problom is any one can get the value and set the value.

  • But if I want to allow only one action, like either getting the value or setting the value, or both, you can't do that in the above example. The first thing you should do is never declare the variables or fields as public.Then we use method.

    public class Circles
    {
        double Radius = 12.35; 
        //If you want to use ony giv the value then use only this method
        public double GetRadius()//Use only get access
        {
            return Radius;
        }
        //If you want to use ony set the value then use only this method
        public void SetRadius(double radius) //use only set value
        {
            Radius = radius;
        }
    }
    public class Test
    {
        static void Main()
        {
            Circles cir = new Circles();
            Console.WriteLine(cir.GetRadius());//Out: 12.35
            double radius = 19.52;
            cir.SetRadius(radius);
            Console.WriteLine(cir.GetRadius());//Out: 19.52
        }
    }

Make more easy using Property:

Syntax:

[<modifiers>] <type> <Name>
{
    [get {<stmt's>}] //Get Accessor
    [set {<stmt's>}] //Set Accessor
}

Advantage:

  • Only one name is require.

  • No parameter require.

  • Only block we need.

Example:

    public class Circles
    {
        double Radius = 12.35; 

        //Use property:
        public double AccessRadius
        {
            get { return Radius; }
            set { Radius = value; }//The value keyword represents the new value being assigned
        }
    }
    public class Test
    {
        static void Main()
        {
            Circles cir = new Circles();
            Console.WriteLine(cir.AccessRadius);//Out: 12.35
            cir.AccessRadius = 19.52;//Hear we not use bracess '()'
            Console.WriteLine(cir.AccessRadius);//Out: 12.35
        }
    }
  • If you use only get accesser then you can only get the property vlaue get { return Radius; } . get { return Radius; } it represent value return method without parameter.

  • If you use only set accesser then you can only set the property vlaue set { Radius = value; }. set { Radius = value; } it represent a non-value return method with parameter.

Notes:common practice of naming convention

The name of the field and the property should not be the same because you cannot declare a class member with the same name. To make it clear and easier to understand you need the same name, then add an _ underscore at the beginning of the field name. Giving different names to fields and properties can make you confuse in large programs.

Example:

double _Radius = 12.35; 
public double Radius
{
    get { return _Radius; }
    set { _Radius = value; }
}

Add Condition in inside the geter and setter:

    public class Circles
    {
        double _Radius = 12.35; 
        public double Radius
        {
            get { return _Radius; }
            set 
            { 
                if(value > _Radius) _Radius = value; 
            }
        }
    }

If the condition is false in the getter or setter, the old value remains.

What is the difference between a public field and using both get and set accessors in a property: The difference is that with a public field, you cannot write conditions, but you can with a property.

We can also use access modifier with get and set access.Example:

// Property with different access levels
public int Value
{
    get { return _value; }
    internal set { _value = value; }
}

Q: A class has one field, or you can say variable. And the class wants to give permission to class one class to change and modify the value, but class another should only be able to see the value.

Ans: You can use properties with different access modifiers to control the visibility and mutability of the field.

  • Class Circle have a private field that it wants to control.

  • Class A want to modify the value.

  • Class B want to only read the value.

using System;

namespace Demo
{
    public class Circles
    {
        double _Radius = 12.35; 

        public double Radius
        {
            get { return _Radius; }//That one can access by any one 
            protected set { _Radius = value; } //That one can modify by only own class or child class
        }
    }
    class A : Circles 
    { 
        public void Change()
        {
            Radius = 10;
        }
    }
    class B
    {
        public void View()
        {
            Circles cir = new Circles();
            //cir.Radius = 10;//You got error
            Console.WriteLine("From B Class: "+ cir.Radius);
        }
    }
    class Test
    {
        static void Main()
        {
            Circles cir = new Circles();
            Console.WriteLine("From Main Class: "+cir.Radius);
            //cir.Radius = 19.52;//GIve error you cannot change the value because seter is protected

            //Set the value:-
            A a = new A();
            a.Change();
            Console.WriteLine("From A class: "+a.Radius);

            B b = new B();
            b.View();
        }
    }
}
/*Out:
From Main Class: 12.35
From A class: 10
From B Class: 12.35
 */

Add condition in property:

    public class Circles
    {
        double _Radius = 12.35; 
        int _amount = 500;
        public double Radius
        {
            get { return _Radius; }
            set 
            { 
                if(_amount >= 500) _Radius = value; 
            }
        }
    }

Q. Assume you have a class with a field that is a string, and it can only take six state names. Also, show how to ensure it only takes those six state names.

using System;

namespace Demo
{
    public class Circles
    {
        // Define a public enum so it can be accessed outside the class
        public enum StateName
        {
            Assam,
            Bihar,
            Chhattisgarh,
            Jharkhand,
            Odisha,
            Rajasthan
        }

        // Private field to store the state name
        private StateName _State = StateName.Chhattisgarh;

        // Public property to get and set the state name
        public StateName State
        {
            get { return _State; }
            set { _State = value; }
        }
    }

    class Test
    {
        static void Main()
        {
            Circles cs = new Circles();
            Console.WriteLine(cs.State);//Out: Chhattisgarh
            //cs.State = cs.StateName.Assam;//you got error: 
            cs.State = Circles.StateName.Assam;//Assign data
            Console.WriteLine(cs.State);//Out: Assam
        }
    }
}

Why shood we use like that Circles.StateName.Assam; because enums are scoped by the type or namespace they are declared in. When you declare an enum inside a class, the enum type becomes part of that class's definition. Therefore, to access an enum value, you use the syntax ClassName.EnumType.EnumValue.

Enum Scope and Access: Enums are not instance members of a class; they are static members, meaning they belong to the type itself, not to any specific instance of the type. This is why you access them using the class name (Circles) rather than an instance name (cs).

Static Context: Since enums do not change per instance and are constant values associated with the type, they are accessed in a static context. Thus, you refer to them using the class name.

But if you use an enum outside the class, you can directly access and use it like this, but the enum should be in the same namespace:

using System;

namespace Demo
{
    // Define a public enum so it can be accessed outside the class
    public enum StateName
    {
        Assam,
        Bihar,
        Chhattisgarh,
        Jharkhand,
        Odisha,
        Rajasthan
    }
    public class Circles
    {
        // Private field to store the state name
        private StateName _State = StateName.Chhattisgarh;

        // Public property to get and set the state name
        public StateName State
        {
            get { return _State; }
            set { _State = value; }
        }
    }

    class Test
    {
        static void Main()
        {
            Circles cs = new Circles();
            Console.WriteLine(cs.State);//Out: Chhattisgarh
            cs.State = StateName.Assam;//Assign data
            //StateName is a datatype, Assam is a value.
            Console.WriteLine(cs.State);//Out: Assam
        }
    }
}

All value showing hear.

Auto-Implemented or Automatic Property:

  • Introduced in C# 3.0.

  • In C#, auto-implemented (or automatic) properties provide an easy way to declare properties without needing a separate field for storage. The compiler automatically creates a private, hidden field to hold the data, so you can just define the property's get and set accessors.

  • Syntax:

      public class MyClass
      {
          public int MyProperty { get; set; } 
      }
    
  • Implicit Backing Field: The compiler automatically creates a private field for the property. You can't access or change this field directly. It stores the property's value.

  • Default Implementation: If you don't need extra logic in the getter or setter, auto-implemented properties make the code simpler by removing the need for a separate field and manual property code.

  • Encapsulation: Even though you can't access the backing field directly, the property still provides encapsulation, letting you control how the data is accessed and changed.

  • Normal example:

      using System;
      namespace Demo
      {
          public class Circles
          {
              public string State { get; set; }
              public Circles()
              {
                  this.State = "Assam";
              }
          }
          class Test
          {
              static void Main()
              {
                  Circles cs = new Circles();
                  Console.WriteLine(cs.State);//Out: Assam
                  cs.State = "Chhattisgarh";
                  Console.WriteLine(cs.State);//Out: Chhattisgarh
              }
          }
      }
    
  • Using Enum Example:

      using System;
      namespace Demo
      {
          enum StateName
          {
              Assam,
              Bihar,
              Chhattisgarh,
              Jharkhand,
              Odisha,
              Rajasthan
          }
          public class Circles
          {
              public string State { get; set; }
              public Circles()
              {
                  this.State = StateName.Assam.ToString();
              }
          }
          class Test
          {
              static void Main()
              {
                  Circles cs = new Circles();
                  Console.WriteLine(cs.State);//Out: Assam
                  cs.State = StateName.Chhattisgarh.ToString();
                  Console.WriteLine(cs.State);//Out: Chhattisgarh
              }
          }
      }
    

    StateName.Chhattisgarh refers to a specific member of the StateName enum.

    Integer Value: Each member of an enum has an integer value behind the scenes. By default, it starts at 0 and goes up by 1 for each next member, unless you set it differently. For example, in StateName, Assam is 0, Bihar is 1, and so on up to Rajasthan, which would be 5 if not specified otherwise.

  • Enum to String Conversion: The ToString() method is called on the enum value (StateName.Assam.ToString()) to convert the enum name to its string representation, which can be stored in the State property.

  • In auto-implemented (or automatic) properties before C# 3.0, both get and set were mandatory. But after C# 6.0, you can use only get, only set, or both.

  • Set default value:

      // Auto-implemented property with a default value
      public string State { get; set; } = "Assam";
    
  • You cannot use any condition in Auto-implemented property.


A class can be considered an entity in object-oriented programming, and its fields (or properties) define the attributes of the objects created from the class.

INDEXERS:

Indexers in C# are a feature that allows an object to be indexed like an array. They provide a way to access elements in an object using an index, enabling syntax similar to accessing elements in an array or a list. Indexers use the this keyword along with a parameter list to define the index and are equipped with get and set accessors to manage how values are retrieved or assigned.

An indexer allows you to access an object's properties like an array.

Syntax:

// Indexer declaration
[<modifiers>] <type> this[<parameter list>]
{
    [get{<stmts>}] //Get Accessor
    [set{<stmts>}] //Set Accessor
}

Example for get the value by indexers:

using System;

namespace Demo
{
    public class Circles
    {
        int Eno;
        double Salary;
        string Ename, Job, Dname, Location;

        // Constructor to initialize the fields
        public Circles(int Eno, double Salary, string Ename, string Job, string Dname, string Location)
        {
            this.Eno = Eno;
            this.Salary = Salary;
            this.Ename = Ename;
            this.Job = Job;
            this.Dname = Dname;
            this.Location = Location;
        }
        //if you write object as a type it can return any type of value
        //this use to define current class
        public object this[int index]//int index parameter come in inder form this is int
        {
            get 
            {
                // Returns the appropriate field based on the index
                if (index == 0) return Eno;
                else if (index == 1) return Salary;
                else if (index == 2) return Ename;
                else if (index == 3) return Job;
                else if (index == 4) return Dname;
                else if (index == 5) return Location;
                // Returns null if the index is out of range
                return null;
            }
        }
    }

    class Test
    {
        static void Main()
        {
            // Create an instance of the Circles class with sample data
            Circles cs = new Circles(101, 1000, "Mritunjay", "IT", "Manager", "Assam");

            // Access and print properties using the indexer
            Console.WriteLine("Eno: " + cs[0]); // Output: Eno: 101
            Console.WriteLine("Salary: " + cs[1]); // Output: Salary: 1000
            Console.WriteLine("Ename: " + cs[2]); // Output: Ename: Mritunjay
            Console.WriteLine("Job: " + cs[3]); // Output: Job: IT
            Console.WriteLine("Dname: " + cs[4]); // Output: Dname: Manager
            Console.WriteLine("Location: " + cs[5]); // Output: Location: Assam
        }
    }
}

[int index]:The parameter list for the indexer. In this case, it is a single parameter of type int, which represents the index used to access different values within the class.

Example for set the value by indexers:

using System;
namespace Demo
{
    public class Circles
    {
        int Eno;
        double Salary;
        string Ename, Job, Dname, Location;
        public Circles(int Eno, double Salary, string Ename, string Job, string Dname, string Location)
        {
            this.Eno = Eno;
            this.Salary = Salary;
            this.Ename = Ename;
            this.Job = Job;
            this.Dname = Dname;
            this.Location = Location;
        }
        public object this[int index] 
        {
            get 
            {
                if (index == 0) return Eno;
                else if(index == 1) return Salary;
                else if(index == 2) return Ename;
                else if(index == 3) return Job;
                else if(index == 4) return Dname;
                else if(index == 5) return Location;
                else return null;// Returns null for unsupported indices
            }
            //'value' is a implicit variable, that provide access to the value assign by user
            set
            {
                //if (index == 0) Eno = value;//If you assign only value you got error the error is object cant assign to integer. If you want to convert refrence type to value type you go to unboxing.       
                if (index == 0) Eno = (int)value;
                else if (index == 1) Salary = (double)value;
                else if (index == 2) Ename = (string)value;
                else if (index == 3) Job = (string)value;
                else if (index == 4) Dname = (string)value;
                else if (index == 5) Location = (string)value;
            }
        }
    }
    class Test
    {
        static void Main()
        {
            Circles cs = new Circles(101, 1000.00, "Mritunjay", "IT", "Manager", "Assam");
            Console.WriteLine("Eno: " + cs[0] + "Salary: " + cs[1] + "Ename: " + cs[2] + "Job: " + cs[3] + "Dname: " + cs[4] + "Location: " + cs[5]);

            cs[0] = 102; cs[1] = 2000.00; cs[2] = "Amit"; cs[3] = "IT"; cs[4] = "Employe"; cs[5] = "Jharkhand";

            Console.WriteLine("Eno: " + cs[0] + "Salary: " + cs[1] + "Ename: " + cs[2] + "Job: " + cs[3] + "Dname: " + cs[4] + "Location: " + cs[5]);
        }
    }
}

If Eno = value; is used, you will get an error because you cannot assign an object to an integer. To convert a reference type to a value type, you need to use unboxing.

By defining the indexers, the array behaves like a virtual array.

Example send string in parameter:

using System;

namespace Demo
{
    public class Circles
    {
        int Eno;
        double Salary;
        string Ename, Job, Dname, Location;
        public Circles(int Eno, double Salary, string Ename, string Job, string Dname, string Location)
        {
            this.Eno = Eno;
            this.Salary = Salary;
            this.Ename = Ename;
            this.Job = Job;
            this.Dname = Dname;
            this.Location = Location;
        }
        public object this[string name] 
        {
            get 
            {
                if (name.ToUpper() == "ENO") return Eno;
                else if(name.ToUpper() == "SALARY") return Salary;
                else if(name.ToUpper() == "ENAME") return Ename;
                else if(name.ToUpper() == "JOB") return Job;
                else if(name.ToUpper() == "DNAME") return Dname;
                else if(name.ToUpper() == "LOCATION") return Location;
                else return null;
            }
            set
            {
                if (name.ToUpper() == "ENO") Eno = (int)value;
                else if (name.ToUpper() == "SALARY") Salary = (double)value;
                else if (name.ToUpper() == "ENAME") Ename = (string)value;
                else if (name.ToUpper() == "JOB") Job = (string)value;
                else if (name.ToUpper() == "DNAME") Dname = (string)value;
                else if (name.ToUpper() == "LOCATION") Location = (string)value;
            }
        }

    }
    class Test
    {
        static void Main()
        {
            Circles cs = new Circles(101, 1000.00, "Mritunjay", "IT", "Manager", "Assam");
            Console.WriteLine("Eno: " + cs["Eno"] + "Salary: " + cs["Salary"] + "Ename: " + cs["Ename"] + "Job: " + cs["Job"] + "Dname: " + cs["Dname"] + "Location: " + cs["Location"]);

            cs["Eno"] = 102; cs["Salary"] = 2000.00; cs["Ename"] = "Amit"; cs["Job"] = "IT"; cs["Dname"] = "Employe"; cs["Location"] = "Jharkhand";
        }
    }
}

Use ToUpper() because C# is a case-sensitive language. If you enter lowercase or any other case, it automatically converts to uppercase before comparing it. You can use another case also.

Example (use swith-case in indexers):

using System;
namespace Demo
{
    public class Circles
    {
        int Eno;
        double Salary;
        string Ename, Job, Dname, Location;

        public Circles(int Eno, double Salary, string Ename, string Job, string Dname, string Location)
        {
            this.Eno = Eno;
            this.Salary = Salary;
            this.Ename = Ename;
            this.Job = Job;
            this.Dname = Dname;
            this.Location = Location;
        }

        // Indexer to get and set fields by index
        public object this[int index] 
        {
            get 
            {
                return index switch
                {
                    0 => Eno,
                    1 => Salary,
                    2 => Ename,
                    3 => Job,
                    4 => Dname,
                    5 => Location,
                    _ => null // Returns null for unsupported indices
                };
            }
            set
            {
                switch (index)
                {
                    case 0:
                        Eno = (int)value; // Cast to int
                        break;
                    case 1:
                        Salary = (double)value; // Cast to double
                        break;
                    case 2:
                        Ename = (string)value; // Cast to string
                        break;
                    case 3:
                        Job = (string)value; // Cast to string
                        break;
                    case 4:
                        Dname = (string)value; // Cast to string
                        break;
                    case 5:
                        Location = (string)value; // Cast to string
                        break;
                    default:
                        Console.WriteLine("Index out of range");
                        break;
                }
            }
        }
    }

    class Test
    {
        static void Main()
        {
            Circles cs = new Circles(101, 1000, "Mritunjay", "IT", "Manager", "Assam");
            Console.WriteLine("Eno: " + cs[0] + "Salary: " + cs[1] + "Ename: " + cs[2] + "Job: " + cs[3] + "Dname: " + cs[4] + "Location: " + cs[5]);

            cs[0] = 102;
            cs[1] = 2000.0; // Must be a double since Salary is a double
            cs[2] = "Amit";cs[3] = "IT";cs[4] = "Employee";cs[5] = "Jharkhand";
            cs[6] = "Out of range"; // This index is not supported and should trigger an out-of-range message

            Console.WriteLine("Eno: " + cs[0] + "Salary: " + cs[1] + "Ename: " + cs[2] + "Job: " + cs[3] + "Dname: " + cs[4] + "Location: " + cs[5]);
        }
    }
}

Delegates:

Definition: It's a user-defined type that is a type-safe function pointer.

  • A delegate holds the reference of a method and then calls the method for execution.

  • We already have two ways to call a method; now, there is one more way to call a method, which is called delegates.

  •             using System;
    
                namespace ModifiersTest1
                {
                    class Program2
                    {
                        int Sum(int x, int y)
                        {
                            return x + y;
                        }
                        static string Name(string name)
                        {
                            return "Hello " + name;
                        }
                        static void Main(string[] args)
                        {
                            //1st way of calling the method by creating Object of class:
                            Program2 program2 = new Program2();
                            int x = program2.Sum(10, 20);
                            Console.WriteLine("sum is " + x);
    
                            //2nd way of calling the method if method is static
                            //string name = Name("Codecomponents");
                            //Or
                            string name = Program2.Name("Codecomponents");
                            Console.WriteLine(name);
                        }
                    }
                }
    
  • Classes, structures, and interfaces are user-defined types, and a delegate is also a user-defined type. The main difference between a class and a structure is that a class is a reference type, while a structure is a value type. Similarly, a delegate is a reference type.

  • Typically, types like classes, structures, and interfaces are defined within a namespace because a namespace is a logical container for types. Similarly, a delegate is also defined within a namespace. You can also define it within a class, but then it is called a nested type. However, the best practice is to define a delegate within a namespace.

Syntax for defining a delegate:

[<modifiers>] delegate <void or type> <Name>([<parameter list>]);

Rools:

  • The return type of the delegate should match the method's return type.

  • The signature of the delegate should match the method's signature. The name can be anything, but the type should match.

After defining the delegate, two more steps are required: first, create an instance of the delegate; second, call the delegate.

Example:

using System;
namespace ModifiersTest1
{
    //Definning the delegate:
    public delegate int SumDelegate(int a, int b);
    public delegate string NameDelegate(string name);
    class Program2
    {
        int Sum(int x, int y)
        {
            return x + y;
        }
        static string Name(string name)
        {
            return "Hello " + name;
        }
        static void Main(string[] args)
        {
           Program2 program2 = new Program2();

            //Create instance of delegate:
            SumDelegate sumDelegate = new SumDelegate(program2.Sum);
            NameDelegate nameDelegate = new NameDelegate(Name);

            //Call the delegate by passing the parameter whic is required so that the internaly the method which is bind with delegate is call.    
            int x = sumDelegate(10, 5);
            string s = nameDelegate("Codecomponents");
            Console.WriteLine(s + " " + x);
            //Or you can call like that
            Console.WriteLine(nameDelegate.Invoke("Codecomponants") + " " + sumDelegate.Invoke(25, 5));
        }
    }
}

SumDelegate sumDelegate = new SumDelegate(program2.Sum); in this line, the SumDelegate delegate is holding the method reference of the program2.Sum method.

SumDelegate sumDelegate = new SumDelegate(program2.Sum); this delegate holds the reference to only one method. But a delegate can hold the multiple methods references, which is called a Multicast Delegate.

We can also hold the refrence in delegate like that:

using System;
namespace ModifiersTest1
{
    public delegate double AddNums1Delegate(int x, float y, double z);
    class Program2
    {
        public static double AddNums1(int x, float y, double z)
        {
            return x + y + z;
        }
        static void Main(string[] args)
        {
            AddNums1Delegate obj1 = AddNums1;
            double ret = obj1.Invoke(100, 34.5f, 193.456);
        }
    }
}

Multicast Delegate:

A delegate can hold the reference of more than one method. And we call the method with the help of delegate.

If a class has multiple methods with the same signature and type, we can call all these methods with the same delegate.

using System;
namespace ModifiersTest1
{
    public delegate void PrintName(string FName, string LName);
    class Program2
    {
        public void Teacher(string FName, string LName)
        {
            Console.WriteLine("Teacher: " + FName + " " + LName);
        }
        public void Strudent(string FName, string LName)
        {
            Console.WriteLine("Student: " + FName + " " + LName);
        }
        static void Main(string[] args)
        {
            Program2 program2 = new Program2();

            //1st way by multicast (One single delegate bind with two or more method)
            PrintName obj = program2.Teacher;
            obj += program2.Strudent;
            obj.Invoke("Mritunjay", "Kumar");

            /*Out:
             * Teacher: Mritunjay Kumar
             * Student: Mritunjay Kumar
             */
            obj.Invoke("Rahul", "Kumar"); //Overide the delegate

            /*Out:
            * Teacher: Rahul Kumar
            * Student: Rahul Kumar
            */

            //----------------------------------------------
            //2nd way that is not a multicast
            PrintName teacher = program2.Teacher;
            PrintName student = program2.Strudent;
            teacher("Mritunjay", "Kumar"); //Out: Teacher: Mritunjay Kumar
            student("Rahul", "Kumar");// Out: Student: Rahul Kumar
        }
    }
}

In any situation where multiple methods require the same signature with the same value, it is needed.

Anonymous Method:

A method doesn't have a name; it only uses the delegate keyword. We can also add a signature without needing to define the type of method.

An anonymous method is defined using a delegate.

Example:

using System;
namespace ModifiersTest1
{
    public delegate string PrintName(string FName, string LName);
    class Program2
    {
        static void Main(string[] args)
        {
            PrintName pn = delegate (string FName, string LName)
            {
                return FName + " " + LName;
            };//This method is an anonymous method

            string name = pn.Invoke("Mritunjay", "Kumar");//Calling
            Console.WriteLine(name);
        }
    }
}

Anonymous method not segested it's only segested when code voluem is less like 10 line 20 line.

Use of+= **with delegates:-**The += operator is used with delegates in C# to combine or chain multiple methods together. This allows multiple methods to be called in sequence when the delegate is invoked. When you use +=, you're adding another method to the invocation list of the delegate.

Here's a brief example to illustrate the use of += with delegates:

using System;
namespace ModifiersTest1
{
    public delegate void PrintName(string FName, string LName);

    class Program2
    {
        static void Main(string[] args)
        {
            PrintName pn = delegate (string FName, string LName)
            {
                Console.WriteLine("First method: " + FName + " " + LName);
            }; // This method is an anonymous method

            // Using += to add another anonymous method to the delegate
            pn += delegate (string FName, string LName)
            {
                Console.WriteLine("Second method: " + FName.ToUpper() + " " + LName.ToUpper());
            };

            // Invoking the delegate, which will call both methods
            pn("Mritunjay", "Kumar");
        }
    }
}

Explanation:

  • Initial Delegate Assignment:pn is initially assigned an anonymous method that prints the full name in a normal format.

  • Using+= to Add a Method: The += operator adds another anonymous method to the delegate pn. This second method converts the first and last names to uppercase before printing them.

  • Invocation of the Delegate: When pn is invoked with "Mritunjay", "Kumar", it sequentially calls both methods in the order they were added.

Key Points:

  • The methods added to a delegate must have the same signature (return type and parameters) as the delegate type.

  • You can use the += operator to combine methods and the -= operator to remove them.

  • When the delegate is invoked, all methods in its invocation list are called in the order they were added.

This feature is particularly useful in scenarios like event handling, where multiple event handlers might need to respond to a single event.

If you do not have any parameters for an anonymous method, use it like this:

using System;
namespace ModifiersTest1
{
    public delegate void PrintName();
    class Program2
    {
        static void Main(string[] args)
        {
            PrintName pn = delegate
            {
                Console.WriteLine("Hello world");
            };
            pn.Invoke();
        }
    }
}

Anonymous methods are useful if you don't want to write an access modifier, method name, or specify static.

In C# 2.0 anonymous method introduce.

Lambda Expressions:

A lambda expression is a shorthand for writing anonymous methods, simplifying their syntax.

Lambda expression come in C# 3.0.

Lambda operator ' => ' use to make more simplifying the anonymous methods syntax.

using System;
namespace ModifiersTest1
{
    public delegate string PrintName(string FName, string LName);
    class Program2
    {
        static void Main(string[] args)
        {
            //Use lambda expression '=>':-
            PrintName pn = (FName, LName) =>
            {
                return FName + " " + LName;
            };

            //You can also write like that:-
            /*PrintName pn = (string FName, string LName) =>
            {
                return FName + " " + LName;
            };*/
            //But no need to write

            string name = pn.Invoke("Mritunjay", "Kumar");
            Console.WriteLine(name);
        }
    }
}

Predefine Generic Delegates:

First, understand this example, then we will move on to predefined generic delegates:-

using System;
namespace ModifiersTest1
{
    public delegate double AddNums1Delegate(int x, float y, double z);
    public delegate void AddNums2Delegate(int x, float y, double z);
    public delegate bool CheakLengthDelegate(string s);
    class Program2
    {
        public static double AddNums1(int x, float y, double z)
        {
            return x + y + z;
        }
        public static void AddNums2(int x, float y, double z)
        {
            Console.WriteLine(x + y + z);
        }
        public static bool CheakLength(string str)
        {
            if (str.Length > 5) 
                return true;
            else
                return false;
        }
        static void Main(string[] args)
        {
            AddNums1Delegate obj1 = AddNums1;
            double result1 = obj1.Invoke(100, 34.5f, 193.465);
            Console.WriteLine(result1);

            AddNums2Delegate obj2 = AddNums2;
            obj2.Invoke(100, 34.5f, 193.465);

            CheakLengthDelegate obj3 = CheakLength;
            bool result2 = obj3.Invoke("Hello World");
            Console.WriteLine(result2);
        }
    }
}

C# provides three predefined generic delegates in the base class library: Func, Action, and Predicate. These delegates simplify the usage of methods as parameters, especially when working with collections and LINQ.

  1. Func Delegate: Used when the method has a return value. It can have up to 16 input parameters, with the last type parameter representing the return type.

  2. Action Delegate: Used when the method does not return a value (i.e., it returns void). It can take up to 16 input parameters.

  3. Predicate Delegate: Specifically used for methods that return a bool. It is often used in scenarios where a condition needs to be checked.

Func delegate has 16 inputs and one output.

But if we need 3 input and one output parameter, then use the 3rd one:

Example

Below is an example illustrating how to use these predefined delegates:

using System;

namespace ModifiersTest1
{
    class Program2
    {
        // Method matching Func<int, float, double, double> signature
        public static double AddNums1(int x, float y, double z)
        {
            return x + y + z;
        }

        // Method matching Action<int, float, double> signature
        public static void AddNums2(int x, float y, double z)
        {
            Console.WriteLine(x + y + z);
        }

        // Method matching Predicate<string> signature
        public static bool CheckLength(string str)
        {
            return str.Length > 5;
        }

        static void Main(string[] args)
        {
            // Using Func delegate for a method that returns a double
            Func<int, float, double, double> funcDelegate = AddNums1;
            double result1 = funcDelegate.Invoke(100, 34.5f, 193.465);
            Console.WriteLine(result1);

            // Using Action delegate for a method that returns void
            Action<int, float, double> actionDelegate = AddNums2;
            actionDelegate.Invoke(100, 34.5f, 193.465);

            // Using Predicate delegate for a method that returns bool
            Predicate<string> predicateDelegate = CheckLength;
            bool result2 = predicateDelegate.Invoke("Hello World");
            Console.WriteLine(result2);

            // Alternatively, using Func<string, bool> for boolean-returning methods
            Func<string, bool> funcPredicate = CheckLength;
            bool result3 = funcPredicate.Invoke("Mritunjay");
            Console.WriteLine(result3);
        }
    }
}

Key Points:

  • Func Delegate: The last type parameter specifies the return type. In the example, Func<int, float, double, double> means the method takes two integers, one float, and one double, and returns a double.

  • Action Delegate: Used for methods that don't return a value. In the example, Action<int, float, double> means the method takes two integers and one double, and doesn't return anything.

  • Predicate Delegate: Specifically for methods that return a bool. Predicate<string> means the method takes a string and returns a boolean.

These delegates are particularly useful for lambda expressions, LINQ queries, and event handling, where methods can be passed as parameters or returned as values.

We can still simplify the code here using an anonymous method. Let's see:

using System;
namespace ModifiersTest1
{
    class Program2
    {
        static void Main(string[] args)
        {
            Func<int, float, double, double> obj1 = (x, y, z) =>
            {
                return x + y + z;
            };

            double result1 = obj1.Invoke(100, 34.5f, 193.465);
            Console.WriteLine(result1);


            Action<int, float, double> obj2 = (x, y, z)=>
            {
                Console.WriteLine(x + y + z);
            };

            obj2.Invoke(100, 34.5f, 193.465);


            Predicate<string> obj3 = (str) =>
            {
                if (str.Length > 5)
                    return true;
                else
                    return false;
            };

            bool result2 = obj3.Invoke("Hello World");
            Console.WriteLine(result2);
        }
    }
}

Make more simple code:-

class Program2
{
    static void Main(string[] args)
    {
        Func<int, float, double, double> obj1 = (x, y, z) =>x + y + z;
        double result1 = obj1.Invoke(100, 34.5f, 193.465);
        Console.WriteLine(result1);


        Action<int, float, double> obj2 = (x, y, z)=>Console.WriteLine(x + y + z);
        obj2.Invoke(100, 34.5f, 193.465);


        Predicate<string> obj3 = (str) => str.Length > 5? true : false;

        bool result2 = obj3.Invoke("Hello World");
        Console.WriteLine(result2);
    }
}

Extension Methods:

Extension methods, introduced in C# 3.0, let you add new methods to existing types without changing the original source code or creating a new derived type. This is helpful for extending classes, structures, or interfaces you can't modify, like those in third-party libraries or the .NET framework.

Key Points:

  1. Enhancing Existing Types: Extension methods let you add capabilities to existing types without needing their source code. This is useful for adding methods to classes or structures defined elsewhere, like in third-party libraries or system assemblies.

  2. Non-Inheritable Types: Unlike inheritance, which is limited to classes and not applicable to structures or sealed classes, extension methods can be applied to any type, including sealed classes and structures. This overcomes the limitations of inheritance, which cannot be used with these types.

  3. No Source Code Modification: Extension methods don't require changes to the original source code of the type being extended. This keeps your code separate from third-party or framework code.

  4. No Recompilation Needed: Using extension methods doesn't need the original type to be recompiled, making it a convenient and non-intrusive way to add functionality.

Example: Suppose you have sent the source code for testing, and the testing is complete. Later, you need to add a method to that source code. Modifying the code to add the method is not a good ideal because the code has already been tested. In such cases, extension methods are important because they allow you to add the method without modifying the original source code.

To create an extension method, define a static method in a static class.
Example:-

using System;
namespace ModifiersTest1
{
    class Calculator
    {
        public void  Sum()
        {
            Console.WriteLine("22 + 5 => 27");
        }
    }
    static class Calculator2 
    {
        /*Normal method static method
        public static void Sub()
        {
            Console.WriteLine("22 - 5 => 17");
        }*/

        /*How to bind this Sub method with the Calculator class: 
        If the parameter is 'int x', it means this Sub method 
        wants an 'x' int value, just like 'Calculator c' in 
        this parameter 'c' is a Calculator type value.

        public static void Sub(Calculator c)
        {
            Console.WriteLine("22 - 5 => 17");
        }*/

        /*To bind the Sub method with the Calculator class, 
        we need the this keyword. 'this' means the Sub 
        method belongs to the Calculator class.*/  
        public static void Sub(this Calculator c)
        {
            Console.WriteLine("22 - 5 => 17");
        }
    }
    class Program2
    {
        static void Main(string[] args)
        {
            Calculator cal = new Calculator();
            cal.Sum();
            cal.Sub();
        }
    }
}

You can use this with any type, class, structure, etc.

public static void Sub(this Calculator c)
{
    Console.WriteLine("22 - 5 => 17");
}

You might wonder how we can call the Sub method using an instance of the class when it is a static method. This is because extension methods are defined as static, but once they are bound to a class or structure, they turn non-static methods.

If an extension method is defined with the same name and signature as an existing method in the class, the extension method will not be called. The original method will always take precedence.

using System;
namespace ModifiersTest1
{
    class Calculator
    {
        public void  Sum()
        {
            Console.WriteLine("22 + 5 => 27");
        }
    }
    static class Calculator2 
    {
        public static void Sum(this Calculator c)
        {
            Console.WriteLine("22 - 5 => 17");
        }
    }
    class Program2
    {
        static void Main(string[] args)
        {
            Calculator cal = new Calculator();
            cal.Sum();//Out:- 22 + 5 => 27
        }
    }
}

If the extension method has only one parameter like this Calculator c in this public static void Sub(this Calculator c){}, you don't need to pass any arguments when calling it. This parameter is used for binding. You can also add more parameters like this: public static void Sub(this Calculator c, int x, string s){}. In this case, you must pass both an integer and a string when you call the Sub method.

In short, the parameter prefixed with the this keyword is not considered. All other parameters will be considered.

An extension method should have one and only one binding parameter, and it should be the first in the parameter list.

If an extension method is defined with n parameters, then while calling it, there will be n-1 parameters only because the binding parameter is excluded.

Lets do with struct:

  • Int32 is a data type and also a struct type. If you want to see it, check the definition of Int32. Let's add an extension method to find the factorial of a value in Int32 struct.

      using System;
      namespace ModifiersTest1
      {
          static class Calculator2 
          {
              public static int Fact(this Int32 x) // `x` is a value which is come fron `int i = 5`;
              {
                  if(x == 1) return 1;
                  if(x == 2) return 2;
                  else return x * Fact(x-1);
              }
          }
          class Program2
          {
              static void Main(string[] args)
              {
                  int i = 5;
                  Console.WriteLine(i.Fact());//Out:- 120
              }
          }
      }
    

Lets do with sealed classes:

string is a data type and also a sealed class. If you want to see it, check the definition of string.

using System;
namespace ModifiersTest1
{
    static class Calculator2 
    {
        public static int Len(this String s) // `x` is a value which is come fron `int x = 10`;
        {
            return s.Length;
        }
    }
    class Program2
    {
        static void Main(string[] args)
        {
            string name = "Mritunjay";
            Console.WriteLine(name.Len());//Out:-9
            //Or
            Console.WriteLine("Mritunjay".Len());//Out:-9
        }
    }
}

No need to get the permission from any where for make extension method.


Diffrence bitwwen String & StringBuilder:

String:

Strings in C# are immutable, meaning their values cannot be modified after they are created.

When you declare a string, such as string s = "Hello";, the value "Hello" is stored in memory. If you then try to change the string by using an operation like s = s + " world";, it may seem like you're modifying the original string, but what's actually happening is that a new string object is created in memory to hold the new value "Hello world". The original string "Hello" remains unchanged, and the variable s now points to the new string.

Each time you modify a string, a new copy is made, and this new string is stored in heap memory. This can lead to increased memory usage if many string modifications are made, as each modification results in a new string object being created.

This is a drawback when large-scale string manipulations and also performance or memory efficiency is a decrease.

String is recommended when you want a static string value and only need to make a few modifications; otherwise, it is not recommended.

For situations where you need to modify strings frequently, consider using the StringBuilder class, which is designed to handle dynamic string content efficiently.

StringBuilder:

StringsBuilder is mutable, meaning their values can be modified after they are created.

syntax:

StringBuilder sb = new StringBuilder("Hello");

Here, you allocate 5 characters, but internally it gives you 16 characters of memory space. In this case, 11 character spaces are empty.

If you need more space, StringBuilder can automatically increase its capacity. For example, if you add more than 16 characters, it will automatically increase by another 16 characters. So, StringBuilder sb = new StringBuilder("Hello world America"); will take 32 character spaces because the 19 character is there so StringBuilder adds more space.

Notes:-

  • String present in System name space.

  • StringBuilder present in System.Text name space.

Example show the diffrence which one take how much time:

using System;
using System.Text;
using System.Diagnostics;

namespace ModifiersTest1
{
    class Program2
    {
        static void Main(string[] args)
        {
            string str1 = "Mritunjay";

            Stopwatch sw1 = new Stopwatch(); //Use to measer the time to take to preform task come from `System.Diagnostics`    
            sw1.Start();
            for (int i = 0; i < 100000; i++)
            {
                str1 = str1 + i;
            }
            sw1.Stop();


            StringBuilder str2 = new StringBuilder("Hello");

            Stopwatch sw2 = new Stopwatch();
            sw2.Start();
            for (int i = 0; i < 100000; i++)
            {
                str2.Append(i);
            }
            sw2.Stop();

            Console.WriteLine("Time taken by String " + sw1.ElapsedMilliseconds);//Give the result in millisecond
            Console.WriteLine("Time taken by StringBuilder " + sw2.ElapsedMilliseconds);//Give the result in millisecond
        }
    }
}
/*Out:-
Time taken by String 42313
Time taken by StringBuilder 11
 */

Multithreading enables concurrent execution of code, improving performance and responsiveness. Collections manage groups of objects, like arrays and lists. LINQ provides a query language for data manipulation, enhancing data retrieval and processing.


Multitherading:

Before we go to into multithreading, let's discuss multitasking:

Multitasking:

  • Windows OS is a multitasking system, allowing multiple tasks to run at the same time. But how do all these tasks run at the same time?

  • To execute multiple programs, the OS uses processes. A process is a part of the OS responsible for executing a program.

  • If you look at the task manager, you'll see many processes running. Each process executes one program.

  • There are also many background processes, which are essentially Windows services.

  • Besides these, multiple applications can be running, with each process running one application.

Operating System and Threads:

  • To run an application, a process uses threads.

  • A thread is the smallest unit of execution within a process.

  • Every application has logic to be executed, and threads are responsible for executing this logic.

  • Every application must have at least one thread to execute, which is known as the main thread. This means every application is by default a single-threaded model.

Main Thread:

  • Every application by default contains one thread to execute the program, known as the main thread.

  • By default, every program runs in a single-threaded model.

Multithreading in Detail:

  • Multithreading allows an application to perform multiple operations concurrently by creating multiple threads within the same process.

  • This can significantly improve the performance of an application, especially on multi-core processors where threads can run on different cores simultaneously.

Key Points

  • Processes: High-level entities used by the OS to manage the execution of programs.

  • Threads: Low-level units of execution within processes.

  • Single-threaded: By default, applications run on a single main thread.

  • Multi-threaded: Applications can create additional threads to run multiple tasks simultaneously.

Example 1: Single-threaded Program

using System;
class ThreadProgram
{
    static void Main()
    {
        Console.WriteLine("Hello world");
    }
}
// Output: Hello world

This program contains a thread, and with the help of this thread, the program executes. This thread is known as the main thread.

Example 2: Naming the Main Thread

using System;
using System.Threading;

class ThreadProgram
{
    static void Main()
    {
        Thread t = Thread.CurrentThread;
        t.Name = "MyThread";
        Console.WriteLine("Now current thread is: " + t.Name);
    }
}
// Output: Now current thread is: MyThread

In this example, we demonstrate that there is a thread running the program. Every program by default runs with a single thread.

Explanation

In the above examples:

  • Example 1: The program runs using the main thread by default.

  • Example 2: We explicitly name the main thread to show that the program runs on a single thread, which we've named "MyThread".

Single-threaded Disadvantage:

  • In a single-threaded program, all logic runs in one thread.

  • If your program contains multiple methods, and all methods are called by the main method, the main thread is responsible for executing all methods.

  • This means the main thread executes each method one by one, sequentially.

Example to Illustrate the Disadvantage:

using System;

class ThreadProgram
{
    static void Main()
    {
        Method1();
        Method2();
        Method3();
    }

    static void Method1()
    {
        Console.WriteLine("Executing Method1");
    }

    static void Method2()
    {
        Console.WriteLine("Executing Method2");
    }

    static void Method3()
    {
        Console.WriteLine("Executing Method3");
    }
}
// Output:
// Executing Method1
// Executing Method2
// Executing Method3

In this example:

  • The main thread calls Method1, Method2, and Method3 sequentially.

  • The main thread is responsible for executing all methods, one after the other.

  • This sequential execution can be a disadvantage if you have tasks that could be performed concurrently to save time or improve performance.

Issue with Single-threaded Programs

In a single-threaded program, if any method takes a long time to finish, the whole program has to wait for that method before moving to the next task. This can waste time and resources. For example, if a method is waiting for a response from a busy database, the entire program is stuck waiting for that response.

using System;
class ThreadProgram
{
    static void LongRunningMethod()
    {
        //Thread going to sleep
        // Simulate a long-running task, such as a database call
        Console.WriteLine("Starting LongRunningMethod...");
        System.Threading.Thread.Sleep(5000); // main thread going to sleep 'Sleep is a ststic method'
        Console.WriteLine("LongRunningMethod completed.");
    }

    static void Method2()
    {
        Console.WriteLine("Executing Method2");
    }

    static void Method3()
    {
        Console.WriteLine("Executing Method3");
    }

    static void Main()
    {
        //Main thread execution start
        LongRunningMethod();
        Method2();
        Method3();
        //Main thread execution end
    }
}
// Output:
// Starting LongRunningMethod...
// (Waits for 5 seconds)
// LongRunningMethod completed.
// Executing Method2
// Executing Method3

To overcome this issue, we can use multiple threads to allow other parts of the program to continue running while waiting for the long-running method to complete. This is called Multithreading.

In multithreading, when we use multiple threads in a program, the OS distributes CPU time for each thread to execute. Based on time-sharing, all the threads execute equally. In the above example, if you apply multithreading, all methods execute together. Multithreading maximizes CPU resource utilization.

All the threads do not execute in parallel; they execute simultaneously. Where the OS allocates small time slices to each thread, allowing them to run as if they are executing at the same time.

Time-Sharing in Multithreading

  • Time-Sharing: The operating system splits the CPU time among all active threads. Each thread gets a small time slice, called a quantum, to run.

  • Thread Scheduling: The OS uses a scheduler to decide which thread runs at any moment. This makes sure all threads get a fair amount of CPU time.

  • Simultaneous Execution: Threads may not run in parallel (unless on a multi-core processor), but they switch quickly between each other, giving the impression of parallel execution.

How CPU Time Distribution Works

  • Imagine there are three methods (Method1, Method2, and Method3) running on separate threads.

  • The CPU gives a specific amount of time (e.g., 2 seconds) to each thread.

  • During its time, a thread runs its task. If it doesn't finish in that time, the CPU pauses it and moves to the next thread.

  • This process repeats, with the CPU cycling through the threads, giving each one time until all tasks are done.

  • The operating system, not the program, decides the length of the time slice.

This means all methods are given equal priority to execute.

using System;
using System.Threading;

class ThreadProgram
{
    static void Method1()
    {
        for(int i= 1; i <= 20; i++)
        {
            Console.WriteLine("Test1: " + i);
        }
    }
    static void Method2()
    {
        for(int i= 1; i <= 20; i++)
        {
            Console.WriteLine("Test2: " + i);
        }
    }
    static void Method3()
    {
        for(int i= 1; i <= 20; i++)
        {
            Console.WriteLine("Test3: " + i);
        }
    }
    static void Main()
    {
        // Create threads that will execute the methods
        Thread T1 = new Thread(Method1);//Pass method name
        Thread T2 = new Thread(Method2);
        Thread T3 = new Thread(Method3);

        // We do not need to call the methods explicitly because the threads will automatically call them.
        // However, we need to start the threads to begin their execution.
        T1.Start();
        T2.Start();
        T3.Start();
    }
}

/*
CPU share the time: Som time Test1 start some time test2 or sum time test3 like sharing the time:-
Out:-
Test1: 1
Test3: 1
Test2: 1
Test1: 2
Test1: 3
Test1: 4
Test1: 5
Test1: 6
Test1: 7
Test1: 8
Test2: 2
Test2: 3
Test2: 4
Test2: 5
Test2: 6
Test2: 7
Test2: 8
Test2: 9
Test2: 10
Test2: 11
Test3: 2
Test3: 3
Test3: 4
Test3: 5
Test3: 6
Test3: 7
Test3: 8
Test3: 9
Test3: 10
Test3: 11
Test3: 12
Test3: 13
Test3: 14
Test3: 15
Test3: 16
Test3: 17
Test3: 18
Test3: 19
Test3: 20
Test2: 12
Test2: 13
Test2: 14
Test2: 15
Test2: 16
Test2: 17
Test1: 9
Test1: 10
Test1: 11
Test1: 12
Test1: 13
Test1: 14
Test1: 15
Test1: 16
Test1: 17
Test1: 18
Test1: 19
Test1: 20
Test2: 18
Test2: 19
Test2: 20
*/
  • Every time you got diffrence output.

Explanation

  1. Method Definitions:

    • Method1, Method2, and Method3 are simple methods that print out "Test1", "Test2", and "Test3" respectively.
  2. Creating Threads:

    • Thread T1 = new Thread(Method1);: This creates a new thread that will execute Method1.

    • Thread T2 = new Thread(Method2);: This creates a new thread that will execute Method2.

    • Thread T3 = new Thread(Method3);: This creates a new thread that will execute Method3.

  3. Starting Threads:

    • T1.Start();: This starts the execution of T1, which will call Method1.

    • T2.Start();: This starts the execution of T2, which will call Method2.

    • T3.Start();: This starts the execution of T3, which will call Method3.

  4. Stopping the Thread:

    • If you want to stop or terminate a thread, use the Abort() method. For example, to terminate the T1 thread, use T1.Abort();.

Key Points

  • Thread Creation: Threads are created by passing the method to be executed to the Thread constructor.

  • Automatic Method Call: When you start a thread using the Start method, the thread will automatically call the method specified during its creation.

  • Starting Threads: To begin the execution of the threads, you must call the Start method on each thread instance. This does not require you to call the methods directly; the threads handle that automatically.

One more example to better clarify that when something happens with one method, it does not affect another method.

using System;
using System.Threading;

class ThreadProgram
{
    static void Method1()
    {
    for(int i= 1; i <= 20; i++)
    {
        Console.WriteLine("Test1: " + i);
        if(i == 10)
        {
        Console.WriteLine("Thread 2 is going to sleep.");
        Thread.Sleep(1000);
        Console.WriteLine("Thread 2 is wokep now.");
        }
    }
    }
    static void Method2()
    {
    for(int i= 1; i <= 20; i++)
    {
        Console.WriteLine("Test2: " + i);
    }
    }
    static void Method3()
    {
    for(int i= 1; i <= 20; i++)
    {
        Console.WriteLine("Test3: " + i);
    }
    }
    static void Main()
    {
    Thread T1 = new Thread(Method1);
    Thread T2 = new Thread(Method2);
    Thread T3 = new Thread(Method3);
    T1.Start();
    T2.Start();
    T3.Start();
    }
}
/*Out:-
Test2: 1
Test3: 1
Test3: 2
Test3: 3
Test3: 4
Test3: 5
Test3: 6
Test3: 7
Test3: 8
Test3: 9
Test3: 10
Test3: 11
Test3: 12
Test3: 13
Test3: 14
Test1: 1
Test3: 15
Test3: 16
Test2: 2
Test2: 3
Test2: 4
Test2: 5
Test1: 2
Test2: 6
Test2: 7
Test2: 8
Test3: 17
Test3: 18
Test3: 19
Test3: 20
Test1: 3
Test1: 4
Test1: 5
Test1: 6
Test2: 9
Test2: 10
Test2: 11
Test2: 12
Test2: 13
Test2: 14
Test2: 15
Test2: 16
Test2: 17
Test2: 18
Test2: 19
Test2: 20
Test1: 7
Test1: 8
Test1: 9
Test1: 10
Thread 2 is going to sleep.
Thread 2 is wokep now.
Test1: 11
Test1: 12
Test1: 13
Test1: 14
Test1: 15
Test1: 16
Test1: 17
Test1: 18
Test1: 19
Test1: 20
*/
using System;
using System.Threading;

class ThreadProgram
{
    static void Method1()
    {
    for(int i= 1; i <= 10; i++)
    {
        Console.WriteLine("Test1: " + i);
    }
    Console.WriteLine("Test1 thread execute");
    }
    static void Method2()
    {
    for(int i= 1; i <= 10; i++)
    {
        Console.WriteLine("Test2: " + i);
    }
    Console.WriteLine("Test2 thread execute");
    }
    static void Method3()
    {
    for(int i= 1; i <= 10; i++)
    {
        Console.WriteLine("Test3: " + i);
    }
    Console.WriteLine("Test3 thread execute");
    }
    static void Main()
    {
    Thread T1 = new Thread(Method1);
    Thread T2 = new Thread(Method2);
    Thread T3 = new Thread(Method3);
    T1.Start();
    T2.Start();
    T3.Start();
    Console.WriteLine("Main thread execute");
    }
}
/*Out:-
Main thread execute
Test3: 1
Test1: 1
Test1: 2
Test1: 3
Test1: 4
Test1: 5
Test1: 6
Test1: 7
Test1: 8
Test1: 9
Test1: 10
Test1 thread execute
Test2: 1
Test3: 2
Test3: 3
Test2: 2
Test2: 3
Test2: 4
Test2: 5
Test2: 6
Test2: 7
Test2: 8
Test2: 9
Test2: 10
Test2 thread execute
Test3: 4
Test3: 5
Test3: 6
Test3: 7
Test3: 8
Test3: 9
Test3: 10
Test3 thread execute
*/

Execution Flow:

The Main method is executed by the main thread of the application. The main thread executes first, and then the child threads start executing because the main thread's job is done.

Child Threads:

T1, T2, and T3 are started almost simultaneously. The operating system's scheduler manages the execution of these threads.

Constructor of the Thread Class:

In the Thread class, there are four constructors available. Let's take a look at them, focusing on the ParameterizedThreadStart and ThreadStart delegates, and briefly mentioning the other two constructors.

Thread Class Constructors

  1. Thread(ThreadStart start):

    • Initializes a new instance of the Thread class, specifying a delegate that represents the method to be executed.
  2. Thread(ParameterizedThreadStart start):

    • Initializes a new instance of the Thread class, specifying a delegate that represents the method to be executed, and allowing an object to be passed to the method.
  3. Thread(ThreadStart start, int maxStackSize):

    • Initializes a new instance of the Thread class with a specified stack size.
  4. Thread(ParameterizedThreadStart start, int maxStackSize):

    • Initializes a new instance of the Thread class with a specified stack size, allowing an object to be passed to the method.

ThreadStart Delegate

ThreadStart is a delegate, which is a type-safe function pointer that can be used to call a method. It is similar to function pointers in C++ but provides type safety by ensuring that the delegate's signature matches the method's signature.

Why Type-Safe Function Pointer?

A delegate is called a type-safe function pointer because the signature of the delegate must exactly match the signature of the method it calls. This ensures that the correct method is invoked without runtime errors due to signature mismatches. Like in the previous example, the signature of Method1 and the delegate are the same. This way, we ensure that Method1 is the target method.

Example of ThreadStart

Let's discuss ThreadStart in detail:

If you goto the ThreadStart definition, you will see that it is a delegate defined in the System.Threading namespace.

In the previous example, the signature of the ThreadStart delegate and the signature of Method1 are the same: both have a void return type and no parameters.

Explanation of ThreadStart and Thread Initialization:-

When working with delegates, we follow three steps:

  1. Define a delegate (in this case, it's already defined as ThreadStart).

  2. Instantiate the delegate (binding the method to the delegate).

  3. Use the delegate.

Let's go through an example that demonstrates these steps.

Example Using ThreadStart Delegate:

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        static void Method1()
        {
            Console.WriteLine("Hello world");
        }

        static void Main(string[] args)
        {
            // Step 2: Instantiate the delegate (bind the method to the delegate)
            ThreadStart ts = new ThreadStart(Method1); // The Method1 fulfills all criteria (bound to the delegate)

            // Create a new thread and pass the ThreadStart delegate instance
            Thread t = new Thread(ts); // Thread is taking ThreadStart delegate (delegate bound with Thread)

            // Start the thread
            t.Start();
        }
    }
}
// Output: Hello World

Difference Between the Two Examples:

Previous Example:

Thread T1 = new Thread(Method1);
T1.Start();
  • Implicit Delegate Binding: In this example, the Thread class constructor implicitly creates an instance of the ThreadStart delegate and binds it to Method1. The runtime and framework handle this implicitly.

Current Example:

ThreadStart ts = new ThreadStart(Method1);
Thread t = new Thread(ts);
t.Start();
  • Explicit Delegate Binding: In this example, we explicitly create an instance of the ThreadStart delegate and bind it to Method1. We then pass this delegate instance to the Thread constructor.

Clarification:

Thread t = new Thread(Method1); This line implicitly passes the method Method1 as a parameter to the constructor of the Thread class, which expects a ThreadStart delegate. The Common Language Runtime (CLR) and framework handle the creation of the ThreadStart delegate instance internally.


Q :Thread t = new Thread(Method1); In this constructor, we cannot pass a method directly, but here we pass the method directly. How?

Ans : The constructor Thread t = new Thread(Method1); does not directly take a method as a parameter. Instead, it implicitly or internally creates a ThreadStart delegate instance and binds it to the provided method. The CLR and framework handle this conversion internally.


These two code snippets are very much similar: Thread T1 = new Thread(Method1); and ThreadStart ts = new ThreadStart(Method1); Thread t = new Thread(ts); t.Start();

There are multiple ways to initialize and start a thread using the ThreadStart delegate in C#. Let's explore these different methods:

  1. Directly Pass the Method Name: This directly assigns the method Method1 to the ThreadStart delegate. The delegate automatically binds to the method that matches its signature.

  2. Using an Anonymous Method: This uses an anonymous method to bind the ThreadStart delegate to the logic of Method1. It allows you to write the method logic directly within the delegate if needed.

  3. Using a Lambda Expression: This uses a lambda expression to bind the ThreadStart delegate to Method1. Lambda expressions provide a concise way to represent anonymous methods.

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        static void Method1()
        {
            Console.WriteLine("Hello world");
        }

        static void Main(string[] args)
        {
            // Option 1: Directly pass the method name
            // ThreadStart ts = Method1;

            // Option 2: Using an anonymous method
            // ThreadStart ts = delegate { Method1(); };

            // Option 3: Using a lambda expression
            ThreadStart ts = () => Method1();

            // Create a new thread and pass the ThreadStart delegate instance
            Thread t = new Thread(ts);

            // Start the thread
            t.Start();
        }
    }
}
// Output: Hello World
  • Option 1 is the simplest and most direct way to bind a method to a delegate.

  • Option 2 provides flexibility to include logic directly within the delegate.

  • Option 3 offers a concise and modern approach to delegate binding using lambda expressions.

In all the above cases, the method Method1 does not have any parameters. But what happens if Method1 has parameters? In that case, ThreadStart will not work. Instead, we use a parameterized thread class called ParameterizedThreadStart.

ParameterizedThreadStart Delegate

The ParameterizedThreadStart delegate is used to pass parameters to a thread method. This delegate takes an object as a parameter, which can then be cast to the appropriate type within the method.

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        static void Method1(int i)
        {
            Console.WriteLine("Hello world " + i);
        }

        static void Method2(object num)
        {
            int i = Convert.ToInt32(num);
            Console.WriteLine("Hello world " + i);
        }

        static void Main(string[] args)
        {
            // Using ParameterizedThreadStart delegate to specify the method to be executed by the thread
            ParameterizedThreadStart ts = new ParameterizedThreadStart(Method2);

            // Create a new thread and pass the ParameterizedThreadStart delegate instance
            Thread t = new Thread(ts);

            // Start the thread and pass a parameter
            t.Start(50); // Output: Hello world 50

            // However, passing an incorrect type can cause runtime errors
            // The following line will cause a runtime error because Method2 expects an integer convertible object
            t.Start("Hello"); // This will result in a runtime exception (FormatException)

            // To make it type-safe, you can implement validation or use generic methods to ensure the correct type is passed
        }
    }
}

Key Points

  1. ParameterizedThreadStart Delegate:

    • This delegate allows passing a parameter to the thread method.

    • It takes an object as a parameter, which can be cast to the required type within the method.

  2. Example Usage:

    • The Method2 method takes an object parameter and converts it to an int.

    • The Thread class is instantiated with the ParameterizedThreadStart delegate.

    • The Start method of the thread is used to pass the parameter.

  3. Type Safety:

    • Since ParameterizedThreadStart takes an object, it is not type-safe by default.

    • Passing a wrong type, like a string when an integer is expected, will cause a runtime error (e.g., FormatException).

    • To ensure type safety, you can implement validation or use generic methods.

Join Method in Multitherading:

The Join method, available in the Thread class, is used to make the main thread wait until a specific thread finishes executing. This is useful to ensure that a particular thread completes its task before the calling thread continues.

  • Normal Thread Execution Without Join

      using System;
      using System.Threading;
    
      namespace Multithreading
      {
          internal class ThreadProgram
          {
              static void Method1()
              {
                  Console.WriteLine("Method 1 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 1: " + i);
                  }
                  Console.WriteLine("Method 1 thread end");
              }
              static void Method2()
              {
                  Console.WriteLine("Method 2 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 2: " + i);
                  }
                  Console.WriteLine("Method 2 thread end");
              }
              static void Method3()
              {
                  Console.WriteLine("Method 3 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 3: " + i);
                  }
                  Console.WriteLine("Method 3 thread end");
              }
              static void Main()
              {
                  Console.WriteLine("Main thread start");
                  Thread t1 = new Thread(Method1);
                  Thread t2 = new Thread(Method2);
                  Thread t3 = new Thread(Method3);
                  t1.Start();
                  t2.Start();
                  t3.Start();
                  Console.WriteLine("Main thread end");
              }
          }
      }
      /* Possible Output (varies each time):
      Main thread start
      Method 1 thread start
      Method 1: 0
      Method 2 thread start
      Method 2: 0
      Method 2: 1
      Method 2: 2
      Method 2: 3
      Method 2: 4
      Method 2 thread end
      Main thread end
      Method 1: 1
      Method 1: 2
      Method 1: 3
      Method 1: 4
      Method 1 thread end
      Method 3 thread start
      Method 3: 0
      Method 3: 1
      Method 3: 2
      Method 3: 3
      Method 3: 4
      Method 3 thread end
      */
    

    In this example, the main thread starts and then gives control to threads 1, 2, and 3. Then the main thread exits. The problem is that the main thread should exit in the middle of the program. I don't want the main thread to exit until all threads finish. That's why we use the join method.

  • UsingJointo Wait for Thread Completion: To ensure that the main thread waits for the other threads to complete before exiting, we use the Join method:

      using System;
      using System.Threading;
    
      namespace Multithreading
      {
          internal class ThreadProgram
          {
              static void Method1()
              {
                  Console.WriteLine("Method 1 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 1: " + i);
                  }
                  Console.WriteLine("Method 1 thread end");
              }
              static void Method2()
              {
                  Console.WriteLine("Method 2 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 2: " + i);
                  }
                  Console.WriteLine("Method 2 thread end");
              }
              static void Method3()
              {
                  Console.WriteLine("Method 3 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 3: " + i);
                  }
                  Console.WriteLine("Method 3 thread end");
              }
    
              static void Main()
              {
                  Console.WriteLine("Main thread start");
                  Thread t1 = new Thread(Method1);
                  Thread t2 = new Thread(Method2);
                  Thread t3 = new Thread(Method3);
                  t1.Start();
                  t2.Start();
                  t3.Start();
                  t1.Join(); // Wait for t1 to finish
                  t2.Join(); // Wait for t2 to finish
                  t3.Join(); // Wait for t3 to finish
                  Console.WriteLine("Main thread end");
              }
          }
      }
    
      /* Output:
      Main thread start
      Method 1 thread start
      Method 1: 0
      Method 1: 1
      Method 1: 2
      Method 1: 3
      Method 1: 4
      Method 1 thread end
      Method 2 thread start
      Method 2: 0
      Method 2: 1
      Method 2: 2
      Method 2: 3
      Method 2: 4
      Method 2 thread end
      Method 3 thread start
      Method 3: 0
      Method 3: 1
      Method 3: 2
      Method 3: 3
      Method 3: 4
      Method 3 thread end
      Main thread end
      */
    

    Here, the Join method ensures that the main thread does not exit until all the threads (t1, t2, and t3) have completed their execution. Generally, we use the Join method in the main method.

  • JoinMethod with Timeout: The Join method is overloaded and can accept a timeout value, allowing the calling thread to wait for a specified amount of time for the thread to complete:

      using System;
      using System.Threading;
    
      namespace Multithreading
      {
          internal class ThreadProgram
          {
              static void Method1()
              {
                  Console.WriteLine("Method 1 thread start");
                  for (int i = 0; i < 10; i++)
                  {
                      Console.WriteLine("Method 1: " + i);
                      Thread.Sleep(200);
                  }
                  Console.WriteLine("Method 1 thread end");
              }
              static void Method2()
              {
                  Console.WriteLine("Method 2 thread start");
                  for (int i = 0; i < 10; i++)
                  {
                      Console.WriteLine("Method 2: " + i);
                  }
                  Console.WriteLine("Method 2 thread end");
              }
              static void Method3()
              {
                  Console.WriteLine("Method 3 thread start");
                  for (int i = 0; i < 10; i++)
                  {
                      Console.WriteLine("Method 3: " + i);
                  }
                  Console.WriteLine("Method 3 thread end");
              }
    
              static void Main()
              {
                  Console.WriteLine("Main thread start");
                  Thread t1 = new Thread(Method1);
                  Thread t2 = new Thread(Method2);
                  Thread t3 = new Thread(Method3);
                  t1.Start(); t2.Start(); t3.Start();
                  t1.Join(1000); //1000 milliseconds: The main method waits for the t1 thread to finish in 1000 milliseconds. If t1 does not finish in 1000 milliseconds, the main thread will continue.
                  t2.Join(); t3.Join();
                  Console.WriteLine("Main thread end");
              }
          }
      }
      /*Out:-
      Main thread start
      Method 1 thread start
      Method 1: 0
      Method 2 thread start
      Method 2: 0
      Method 2: 1
      Method 2: 2
      Method 2: 3
      Method 2: 4
      Method 2: 5
      Method 2: 6
      Method 2: 7
      Method 2: 8
      Method 2: 9
      Method 2 thread end
      Method 3 thread start
      Method 3: 0
      Method 3: 1
      Method 3: 2
      Method 3: 3
      Method 3: 4
      Method 3: 5
      Method 3: 6
      Method 3: 7
      Method 3: 8
      Method 3: 9
      Method 3 thread end
      Method 1: 1
      Method 1: 2
      Method 1: 3
      Method 1: 4
      Main thread end
      Method 1: 5
      Method 1: 6
      Method 1: 7
      Method 1: 8
      Method 1: 9
      Method 1 thread end
      */
    

    If t1 does not complete within 1000 milliseconds, the main thread will continue its execution. However, since t2 and t3 do not have a timeout specified, the main thread will wait until they finish.

Summary about JOIN method:

  • The Join method is used to make the calling thread wait until the specified thread finishes its execution.

  • Using Join ensures that the main thread does not exit until other threads have completed, which is useful for synchronization.

  • The Join method can also accept a timeout value, allowing the calling thread to wait for a specified amount of time for the thread to complete. If the thread does not complete within this time, the calling thread continues its execution.

ThreadLocking:

In all the above examples, we used static methods. Now, we will use non-static methods:

Context Switching: When you run multiple threads in your code, the operating system shares time among each thread and transfers control between them. This process is called context switching.

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        public void Method1()
        {
            Console.Write("My name is ");
            Thread.Sleep(3000);
            Console.WriteLine("Mritunjay kumar");
        }

        static void Main()
        {

            //Calling Method1 three times using an instance of the class
            ThreadProgram obj = new ThreadProgram();
            //Output 1 come reason:-
            /*
            obj.Method1();
            obj.Method1();
            obj.Method1();
            */

            //Now use thread

            //Calling Method1 three times using threads (In real-world projects, more than one thread may need to access resources like a database)
            //Output 2 come reason:-           
            /*
            Thread t1 = new Thread(obj.Method1);//Bind the non-static method with thread.
            t1.Start();
            */

            //Output 3 come reason:-
            Thread t1 = new Thread(obj.Method1);
            Thread t2 = new Thread(obj.Method1);
            Thread t3 = new Thread(obj.Method1);
            t1.Start();
            t2.Start();
            t3.Start();
        }
    }
}
/*Output 1:-
My name is Mritunjay kumar
My name is Mritunjay kumar
My name is Mritunjay kumar
*/

/*Output 2:-
My name is Mritunjay kumar
*/

/*Output 3:-
My name is My name is My name is Mritunjay kumar
Mritunjay kumar
Mritunjay kumar
*/

In Output 3, the output appears this way because we use three threads. Each thread starts executing and prints the first statement, then they sleep. After sleeping, they print the next statement. Due to context switching controlled by the OS, the output gets mixed.

What happens if this method works with a database? It can cause big problems. So, how do we resolve this? To resolve this problem, we use Thread Locking mechanism.

Handling Context Switching with Thread Locking Mechanism:

Thread locking is a way to make sure that only one thread can access a critical part of the code or a shared resource at a time. This stops race conditions and keeps data consistent by making other threads wait until the lock is released.

using System;
using System.Threading;
namespace Multithreading
{
    internal class ThreadProgram
    {
        public void Method1()
        {
            lock (this)
            {
                Console.Write("My name is ");
                Thread.Sleep(3000);
                Console.WriteLine("Mritunjay kumar");
            }
        }

        static void Main()
        {
            ThreadProgram obj = new ThreadProgram();

            Thread t1 = new Thread(obj.Method1);
            Thread t2 = new Thread(obj.Method1);
            Thread t3 = new Thread(obj.Method1);

            t1.Start();
            t2.Start();
            t3.Start();
        }
    }
}
/*Out:-
My name is Mritunjay kumar
My name is Mritunjay kumar
My name is Mritunjay kumar
*/

Locking:

  • The lock statement ensures that only one thread can enter the critical section of the code at a time.

  • When a thread enters the lock statement, it acquires a lock on lockObj, stopping other threads from entering the locked section until the lock is released.

  • Example: Imagine three people using the same glass to drink water. When the first person drinks, the others wait. Once the first person finishes and leaves, the next person can drink. It means access is given to one thread at a time.

Note'

  • Context Switching: The OS shares CPU time among threads, leading to interleaved execution.

  • Thread Locking: Using lock ensures that only one thread accesses the critical section at a time, preventing issues with shared resources.

  • Join Method: The Join method ensures that the calling thread waits for another thread to complete its execution.

Thread Priority:

What happens if one thread has more work to do compared to another thread? In that case, we can set the thread priority. Let's see how:

When one thread has more work to do compared to another thread, we can set the thread priority to manage CPU resource allocation more efficiently. The Priority property in the Thread class allows us to set the priority of a thread. This priority is defined under the ThreadPriority enum, which has five levels:

  1. Lowest

  2. BelowNormal

  3. Normal

  4. AboveNormal

  5. Highest

By default, every thread runs with a Normal priority. According to the levels, the CPU resources used increase: Lowest uses the least CPU resources, while Highest uses the most.

Example with Same Priority:

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        static long count1, count2;
        static void Method1()
        {
            while (true) 
            {
                count1 += 1;
            }
        }
        static void Method2()
        {
            while (true)
            {
                count2 += 1;
            }
        }

        static void Main()
        {
            Thread t1 = new Thread(Method1);
            Thread t2 = new Thread(Method2);

            t1.Start();
            t2.Start();

            Thread.Sleep(10000); // Main thread sleeps for 10 seconds

            // Terminate the threads
            t1.Abort();
            t2.Abort();

            // Wait until the threads are fully terminated
            t1.Join();
            t2.Join();

            Console.WriteLine("Count1: " + count1);
            Console.WriteLine("Count2: " + count2);
        }
    }
}

/* Output:
Count1: 1170866865
Count2: 1171613038
*/
// Each time you get a different result.

In this example, both methods execute with the same priority (Normal), so the results should be similar, but small differences will occur due to the operating system's scheduling. The OS decides which thread to run at any given time, resulting in varying counts for each run.

Thread priority only sets the order of importance, not the exact amount of CPU time a thread will receive.

Example with diffrence Priority:

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        static long count1, count2;
        static void Method1()
        {
            while (true)
            {
                count1 += 1;
            }
        }
        static void Method2()
        {
            while (true)
            {
                count2 += 1;
            }
        }

        static void Main()
        {
            Thread t1 = new Thread(Method1);
            Thread t2 = new Thread(Method2);

            // Setting the priority
            t1.Priority = ThreadPriority.Lowest;
            t2.Priority = ThreadPriority.Highest;

            t1.Start();
            t2.Start();

            Thread.Sleep(10000); // Main thread sleeps for 10 seconds

            // Terminate the threads
            t1.Abort();
            t2.Abort();

            // Wait until the threads are fully terminated
            t1.Join();
            t2.Join();

            Console.WriteLine("Count1: " + count1);
            Console.WriteLine("Count2: " + count2);
        }
    }
}

/* Output:
Count1: 343970565
Count2: 1349243933
*/

By setting the priority of t1 and t2 threads, the t1 thread uses less CPU resources, and the t2 thread uses more CPU resources. That way, the Count1 value is lower, and the Count2 value is higher.

Difference between single-threaded and multi-threaded model:

using System;
using System.Threading;
using System.Diagnostics; //Use to measurement of the time

namespace Multithreading
{
    internal class ThreadProgram
    {
        static long count1, count2;
        static void Method1()
        {
            for (int i = 0; i <= 10000000; i++) 
            {
                count1 += 1;
            };
        }
        static void Method2()
        {
            for (int i = 0; i <= 10000000; i++)
            {
                count2 += 1;
            };
        }

        static void Main()
        {
            Stopwatch StopWatchByThread = new Stopwatch();
            Stopwatch StopWatchWithoutThread = new Stopwatch();

            Thread t1 = new Thread(Method1);
            Thread t2 = new Thread(Method2);

            StopWatchByThread.Start();
            t1.Start(); t2.Start();
            Console.WriteLine("Stop Watch By Thread: "+ StopWatchByThread.ElapsedMilliseconds + " Milliseconds");

            StopWatchWithoutThread.Start();
            Method1(); Method2();
            Console.WriteLine("Stop Watch Without Thread: " + StopWatchByThread.ElapsedMilliseconds + " Milliseconds");
        }
    }
}

/* Output:
Stop Watch By Thread: 9 Milliseconds
Stop Watch Without Thread: 224 Milliseconds
*/
// Each time you get a different result.

In this example, Method1(); Method2(); uses only the main method thread, while t1.Start(); t2.Start(); uses multiple threads.


Collections:

Definition: A collection is a dynamic array that can automatically resize itself and manage its elements, providing greater flexibility compared to a traditional array.

Arrays vs. Collections:

  • Arrays:

    • Arrays have a fixed size. Once declared, their size cannot be changed.

    • You can resize an array using Array.Resize, but this creates a new array with the new size and destroys the old one.

Example: Resizing an Array

internal class Program
{
    static void Main(string[] args)
    {
        int[] arr = new int[10];
        Array.Resize(ref arr, 12); // This uses an output parameter (ref)
    }
}

In this example, the old array is destroyed, and a new array with a size of 12 is created.

Limitations of Arrays:

  • The size of an array cannot be increased directly.

  • You cannot add a value in the middle of an array if it already contains values.

  • To add a new value, you need to increase the array's size, which involves creating a new array.

  • Similarly, you cannot delete a value in the middle of an array directly.

Collections:

  • Automatically increase its size when new values are added.

  • Insert and delete values in the middle of the collection.

Collections in .NET:

  1. Non-Generic Collections

  2. Generic Collections

STACK:

  • Stack stores values as LIFO (Last In First Out). It has a Push() method to add a value and Pop() & Peek() methods to get values.

  • Stack is a special collection that stores elements in LIFO style. C# has both generic and non-generic Stack classes. It's better to use the generic Stack collection.

  • Stack is useful for storing temporary data in LIFO style, and you might want to delete an element after getting its value.

Creating a stack:

Stack<int> myStack = new Stack<int>();
myStack.Push(1);
myStack.Push(2);
myStack.Push(3);
myStack.Push(4);

foreach (var item in myStack)
    Console.Write(item + ","); //prints 4,3,2,1,

QUEUE:

  • Queue stores values in FIFO style (First In First Out). It keeps the order in which values were added.

  • It provides an Enqueue() method to add values and a Dequeue() method to get values from the collection.

  • Queue is a FIFO (First In First Out) collection.

  • It is part of the System.Collections.Generic namespace.

  • Queue can hold elements of a specified type. It provides compile-time type checking and avoids boxing-unboxing because it is generic.

  • Elements can be added using the Enqueue() method. You cannot use collection-initializer syntax.

  • Elements can be retrieved using the Dequeue() and Peek() methods. It does not support indexing.

Creating a Queue:

Queue<int> callerIds = new Queue<int>();
callerIds.Enqueue(1);
callerIds.Enqueue(2);
callerIds.Enqueue(3);
callerIds.Enqueue(4);

foreach(var id in callerIds)
    Console.Write(id); //prints 1234

We do not use angular brackets <> when creating a non-generic collection. Example:

Generic collection:

ArrayList<int> arl=new ArrayList<int> ();

Non-Generic collection:

ArrayList arl=new ArrayList();

Non-Generic Collections:

  • Collections were introduced in .NET 1.0 as non-generic collections.

  • Examples of non-generic collections include Stack, Queue, LinkedList, SortedList, ArrayList, and Hashtable.

  • These collections are implemented as classes and are defined in the System.Collections namespace.

In traditional programming languages like C++, you need to define and implement Stack, Queue, LinkedList, SortedList, ArrayList, and Hashtable data structures yourself. However, in .NET, they are provided as part of the framework in the form of classes within the System.Collections namespace.

Summary

  • Arrays: Have fixed size, cannot be resized directly, and have limitations in inserting and deleting values.

  • Collections: Provide dynamic resizing, and the ability to insert and delete values easily.

  • Non-Generic Collections: Introduced in .NET 1.0, they include Stack, Queue, LinkedList, SortedList, ArrayList, and Hashtable, all defined in the System.Collections namespace.

Q. Diffrence bitween Array and ArrayList?

Ans:-

ArrayArrayList
Fixed LengthVariable Length
Not possible to insert itemsWe can insert item into the middle
Not posible to delete itemsWe can delete items from the middle

ArrayList:

Using an ArrayList is similar to using an array:

Definition: An ArrayList is a dynamic array that can automatically resize to fit new elements. Unlike traditional arrays, the size of an ArrayList changes as elements are added or removed.

To use ArrayList, you need to import the System.Collections namespace:

Basic Example of UsingArrayList:

using System;
using System.Collections;

namespace Collection
{
   internal class Program
   {
       static void Main(string[] args)
       {
            ArrayList al = new ArrayList(); // Size dynamically increases as needed
            al.Add(100); // Store any type of value in ArrayList at the last position
            Console.WriteLine("Element added: " + al[0]);
        }
   }
}

al.Add(100) stores a value in the first cell. You might wonder how many cells are in the ArrayList. To understand this, we need to know about the property called capacity. The capacity is a property that tells us the number of items that can be stored in a collection.

Understanding Capacity:

The capacity of an ArrayList is the number of elements it can hold before it needs to resize. The initial capacity can grow automatically as you add more elements.

namespace Collection
{
   internal class Program
   {
       static void Main(string[] args)
       {
            ArrayList al = new ArrayList(); // Size dynamically increases as required
            Console.WriteLine("Initial Capacity: " + al.Capacity); // Output: Initial Capacity: 0
            al.Add(100); // Add an element
            Console.WriteLine("Capacity after adding one element: " + al.Capacity); // Output: Capacity after adding one element: 4
        }
   }
}

When the initial capacity is filled, the ArrayList doubles its capacity.

namespace Collection
{
   internal class Program
   {
       static void Main(string[] args)
       {
            ArrayList al = new ArrayList(); // Size dynamically increases as required
            Console.WriteLine("Initial Capacity: " + al.Capacity); // Output: Initial Capacity: 0
            al.Add(100); al.Add(200); al.Add(300); al.Add(400);
            Console.WriteLine("Capacity after adding four elements: " + al.Capacity); // Output: Capacity after adding four elements: 4    
            al.Add(500);
            Console.WriteLine("Capacity after adding fifth element: " + al.Capacity); // Output: Capacity after adding fifth element: 8
        }
   }
}

If the 4 items fill the capacity, it becomes 8. If 8 is filled, the capacity changes to 16. If 16 is filled, the capacity changes to 32. This is how the capacity increases.

The special feature of ArrayList is that it resizes automatically. There is no limit.

Another constructor in ArrayList is the parameterized constructor. You can also specify the size. If you pass the size, it means you can set the initial capacity.

Parameterized Constructor:

You can also initialize an ArrayList with a specified initial capacity:

namespace Collection
{
   internal class Program
   {
       static void Main(string[] args)
       {
            ArrayList al = new ArrayList(5); // Set initial capacity to 5
            Console.WriteLine("Initial Capacity: " + al.Capacity); // Output: Initial Capacity: 5
            al.Add(100); al.Add(200); al.Add(300); al.Add(400); al.Add(500);
            Console.WriteLine("Capacity after adding five elements: " + al.Capacity); // Output: Capacity after adding five elements: 5
            al.Add(600);
            Console.WriteLine("Capacity after adding sixth element: " + al.Capacity); // Output: Capacity after adding sixth element: 10
        }
   }
}

Operations onArrayList:

  • Adding Elements:
al.Add(100); // Adds 100 to the end of the ArrayList
  • Inserting Elements:
al.Insert(3, 350); // Inserts 350 at index 3
  • Removing Elements:
al.Remove(350); // Removes the first occurrence of 350
al.RemoveAt(3); // Removes the element at index 3
  • Printing Elements:
foreach (object o in al)
{
    Console.Write(o + " "); // Prints all elements in the ArrayList
}
Console.WriteLine();

Complete Example:

using System;
using System.Collections;

namespace Collection
{
   internal class Program
   {
       static void Main(string[] args)
       {
            ArrayList al = new ArrayList(5); // Set initial capacity to 5
            Console.WriteLine("Initial Capacity: " + al.Capacity); // Output: 5

            al.Add(100); al.Add(200); al.Add(300); al.Add(400); al.Add(500);
            Console.WriteLine("Capacity after adding five elements: " + al.Capacity); // Output: 5

            al.Add(600);
            Console.WriteLine("Capacity after adding sixth element: " + al.Capacity); // Output: 10

            // Print all elements
            foreach (object o in al)
            {
                Console.Write(o + " "); // Output: 100 200 300 400 500 600
            }
            Console.WriteLine();

            // Insert new item in the middle
            al.Insert(3, 350);
            foreach (object o in al)
            {
                Console.Write(o + " "); // Output: 100 200 300 350 400 500 600
            }
            Console.WriteLine();

            // Remove the item
            al.Remove(350);
            foreach (object o in al)
            {
                Console.Write(o + " "); // Output: 100 200 300 400 500 600
            }
            Console.WriteLine();

            // Remove by index position
            al.RemoveAt(3);
            foreach (object o in al)
            {
                Console.Write(o + " "); // Output: 100 200 300 500 600
            }
            Console.WriteLine();
        }
   }
}

This corrected explanation and examples should help you understand how to use ArrayList in .NET effectively.

Hashtable:

When working with arrays and ArrayList, you access elements using an index. This can be limiting when dealing with more complex data, like storing employee information (name, salary, position). In these cases, managing or remembering indexes can be difficult.

Problem with Arrays and ArrayLists:

  • Arrays and ArrayList use predefined indexes that cannot be changed, making it difficult to remember and manage the position of each element.

  • For example, if you store employee data, it's tough to remember which index corresponds to which information (e.g., name, salary).

Solution: Hashtable

A Hashtable addresses this issue by storing data in key-value pairs. This means you can define keys to make the data more readable and accessible.

Key Features of Hashtable:

  • Dynamic Size: Automatically resizes as needed. Similar to ArrayList, it can resize itself, and you can insert or remove values in the middle.

  • Key-Value Pairs: Allows for storing data with user-defined keys, making it easier to access and manage data.

Example Usage:

using System;
using System.Collections;

namespace Collection
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // Create a Hashtable instance
            Hashtable employeeData = new Hashtable();

            // Add key-value pairs to the Hashtable
            employeeData.Add("FName", "Mritunjay"); // Using the Add method
            employeeData["LName"] = "Kumar"; // Using the indexer
            employeeData["Salary"] = 50000;
            employeeData["Position"] = "Software Developer";

            // Access and print the values using keys
            Console.WriteLine("Name: " + employeeData["FName"] + " " + employeeData["LName"]); // Output: Name: Mritunjay Kumar
            Console.WriteLine("Salary: " + employeeData["Salary"]); // Output: Salary: 50000
            Console.WriteLine("Position: " + employeeData["Position"]); // Output: Position: Software Developer

            // Get all the keys from the Hashtable
            foreach (object key in employeeData.Keys)
            {
                Console.Write(key + " ");
            } // Output: FName LName Salary Position (Order is not same alwase order may vary due to hashing)
        }
    }
}

In .NET, every class by default contains four methods: GetHashCode(), Equals(), GetType(), and ToString().

What is a Hashcode?

  • The GetHashCode() method returns a numeric representation of an object, known as a hash code. For example, if you write Console.WriteLine("Name".GetHashCode());, it might return a numeric value like -694638. This numeric value is called a hash code.

  • Hash codes are consistent for the same value. For instance, calling GetHashCode() on the string "Name" will always return the same hash code.

Hashcode in Hashtable:

  • In a Hashtable, every item contains a key, a value, and a hash code. The key and value are objects, while the hash code is an integer.

  • The Hashtable uses the hash code of the key to quickly locate the corresponding value, making data retrieval very efficient.

Why is Fetching Data Using Hashcode Efficient?

  • Hash codes allow for efficient data retrieval. Instead of searching through an array or ArrayList by index, a Hashtable can use the hash code to directly access the desired value.

  • This is why Hashtable uses hash codes, as they enable fast and efficient data access compared to using indices in arrays or ArrayList.

Advantages of Hashtable:

  • Readability: Keys make the data more understandable (e.g., "Name" instead of an index).

  • Ease of Access: You can directly access data using keys without remembering indices.

  • Flexibility: Easily add, update, or remove key-value pairs as needed.

By using a Hashtable, you can store and manage complex data more efficiently compared to arrays and ArrayList.

Drawback of Non-Generic Collections

One of the main drawbacks of using non-generic collections like ArrayList and Hashtable is that they are not type-safe. This is because these collections store elements as objects, allowing any type of value to be added. For example:

internal class Program
{
    static void Main(string[] args)
    {
        ArrayList al = new ArrayList();

        al.Add(200);
        al.Add("Mritunjay");
        al.Add(true);

        Console.WriteLine();
    }
}

In the above example, the ArrayList accepts all types of values (integer, string, boolean), which can lead to runtime errors and type casting issues.

Problem with Non-Generic Collections:

  • Lack of Type Safety: Since non-generic collections store values as objects, any type of value can be added. This can lead to runtime errors when the wrong type is retrieved or cast.

  • Potential Performance Issues: Storing values as objects requires boxing and unboxing for value types, which can degrade performance.

  • Difficult to Manage: When dealing with large collections of specific types, managing and ensuring the correct type can be cumbersome and error-prone.

Solution: Generic Collections

Generic Collections:

To overcome these limitations, .NET 2.0 introduced generic collections. Generic collections provide both strongly type safety and automatic resizing.

What is a Generic Collection?

A generic collection is a strongly-type-safe collection that allows you to specify the type of elements it can store. This ensures that only the specified type can be added to the collection, preventing runtime errors and improving performance by eliminating the need for boxing and unboxing.

How Type Safety Works in Generic Collections:

In generic collections, type safety is ensured by using type parameters. When you create a generic collection, you specify the type of elements it will store with the syntax List<T>, where T is the type. This ensures the collection can only store elements of that specified type.

Example of Type Safety in a Generic Collection

When you define a generic list with a specific type, the collection is restricted to store only that type of elements. For instance, List<int> can only store integers. Attempting to store a different type will result in a compile-time error.

using System;
using System.Collections.Generic;

namespace CollectionExample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // Creating a generic list of integers
            List<int> intList = new List<int>();

            // Adding integer values to the list
            intList.Add(200);
            intList.Add(300);
            intList.Add(400);

            // This line would cause a compile-time error because the list is type-safe
            // intList.Add("Mritunjay");

            // Displaying the values
            foreach (int value in intList)
            {
                Console.WriteLine(value);
            }
        }
    }
}

In the example above, intList is a list that only accepts integers. If you try to add a string or any other type, the compiler will throw an error.

Using Complex Types in Generic Collections:

Generic collections are not limited to simple types like integers or strings. You can also use complex types, such as custom classes, as type parameters.

using System;
using System.Collections.Generic;

namespace CollectionExample
{
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public double Balance { get; set; }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            // Creating a generic list of Customer objects
            List<Customer> customers = new List<Customer>();

            // Adding Customer objects to the list
            customers.Add(new Customer { Id = 1, Name = "John Doe", Balance = 1000.50 });
            customers.Add(new Customer { Id = 2, Name = "Jane Smith", Balance = 2000.75 });

            // Displaying the customer details
            foreach (Customer customer in customers)
            {
                Console.WriteLine($"Id: {customer.Id}, Name: {customer.Name}, Balance: {customer.Balance}");
            }
        }
    }
}

In the example above, customers is a list that only accepts Customer objects. This ensures type safety and makes the code more readable and maintainable.

We will discuss this line customers.Add(new Customer { Id = 1, Name = "John Doe", Balance = 1000.50 }); later.

System.Collections vs. System.Collections.Generic:

  • System.Collections: Contains non-generic collections like ArrayList and Hashtable. These collections can store any type of objects but lack type safety.

  • System.Collections.Generic: Contains generic collections like List<T>, Dictionary<TKey, TValue>, Queue<T>, and Stack<T>. These collections provide type safety and eliminate the need for type casting.

Using generic collections, you can specify the type of elements the collection should store, ensuring that only that type is allowed. This enhances type safety and reduces the likelihood of runtime errors caused by incorrect type handling.

Using Generic Lists in Code:-

To use a generic list in your code, specify the type parameter when creating an instance of the List class. This ensures that only the specified type can be stored in the list.

using System;
using System.Collections.Generic;

namespace Collection
{
    class GenericList
    {
        static void Main()
        {
            List<int> list = new List<int>();//The bhebhier of this list class is exactly same as the behaviour of ArrayList in collection but diffrence is ArrayList store any type of value but List can store specific type of value

            list.Add(10); list.Add(20); list.Add(30);
            //list.Add(60.5)//Error come

            list.Insert(2, 25);

            list.Remove(2);

            foreach (object o in list)
            {
                Console.Write(o + " ");
            }
        }
    }
}

By using generic collections, you can leverage the benefits of type safety and automatic resizing, making your code more robust and easier to maintain.

Generics:

Earlier, we learned that in the ArrayList, the Add method accepts an Object, but in List<T>, the Add method only accepts a particular type of value.

In C# 2.0, Microsoft introduced generics to address the limitations of collections like ArrayList and methods that handle different data types. Generics improve type safety and performance by allowing you to define classes, methods, and interfaces with a placeholder for the type of data they store or use.

Problems with Non-Generic Code:

using System;

namespace Collection
{
    internal class GenericTest
    {
        public bool Compare(int a, int b)
        {
            return a == b;
        }

        public bool Compare(float a, float b)
        {
            return a == b;
        }

        public bool Compare(object a, object b)
        {
            return a.Equals(b);
        }

        static void Main()
        {
            GenericTest obj = new GenericTest();

            // Compare int
            bool result = obj.Compare(10, 10);
            Console.WriteLine(result); // Output: True

            // Compare float
            result = obj.Compare(10.56f, 10.56f);
            Console.WriteLine(result); // Output: True

            // Compare object
            result = obj.Compare(10, 10);
            Console.WriteLine(result); // Output: True

            // Compare bool
            result = obj.Compare(true, true);
            Console.WriteLine(result); // Output: True

            // Problem: Compare float and double
            result = obj.Compare(10.56f, 10.56);
            Console.WriteLine(result); // Output: False
        }
    }
}

In the code above, there are two main problems we see in result = obj.Compare(10.56f, 10.56);:

  1. Lack of Type Safety: The Compare method that takes object parameters can compare any types without type checking, leading to potential errors at runtime.

  2. Performance Issues: When we pass a value which is a value type (float and double) and the Compare method takes an object, which is a reference type, it internally performs boxing to convert the value type to a reference type. Because of this, the boxing operation is performed internally. If we want to use a float value and a double value, we have to unbox them again. So, every time boxing and unboxing occur when we use this type of function, the performance of our program decreases.

To solve this problem, Microsoft introduced Generics in C# 2.0. With Generics, you can make methods type-safe and avoid using boxing and unboxing.

Example of Generic method:-

using System;

namespace Collection
{
    internal class GenericTest
    {
        public bool Compare<T>(T a, T b)
        {
            return a.Equals(b);
        }

        static void Main()
        {
            GenericTest obj = new GenericTest();

            // Compare float values
            bool result = obj.Compare<float>(10.56f, 10.56f);
            Console.WriteLine(result); // Output: True
        }
    }
}

Explanation:

  1. Generic Method Definition:

     public bool Compare<T>(T a, T b)
     {
         return a.Equals(b);
     }
    
    • The method Compare is defined with a type parameter T. This means the method can accept arguments of any type, as long as both arguments are of the same type.
  2. Calling the Generic Method with Explicit Type Parameter:

     GenericTest obj = new GenericTest();
     bool result = obj.Compare<float>(10.56f, 10.56f);
     Console.WriteLine(result); // Output: True
    
    • Here, the Compare method is explicitly called with float as the type parameter. This ensures that T is set to float .
  3. Additional Examples with Different Types:

    • Compare int values: result =obj.Compare<int>(10, 10);

    • Compare string values: result =obj.Compare<string>("Hello", "Hello");

  4. Type Safety:

    • The generic Compare method ensures type safety. If you try to pass arguments of different types, the compiler will throw an error. For example:

        // This will cause a compile-time error
        bool result = obj.Compare(10.56f, 10.56);
      
    • In this case, one argument is a float and the other is a double. The compiler will not allow this because it expects both arguments to be of the same type.

  5. Avoiding Boxing and Unboxing:

    • Generics eliminate the need for boxing and unboxing. In non-generic collections or methods that use object, value types are boxed (converted to object) when added to the collection or passed as arguments, and unboxed (converted back to the original type) when retrieved. This can negatively impact performance.

    • With generics, the actual type is used, avoiding the overhead of boxing and unboxing, thus improving performance

Using Generics:

Generics in C# allow for the creation of flexible, type-safe methods and classes, eliminating the need for boxing and unboxing, and improving performance. Let's explore how to use generics with methods and classes to perform.

Generic Methods:

Generics ensure type safety, but they don't directly support arithmetic operations because the type T is unknown at compile time. To solve this, we can use the dynamic type, which allows the type to be resolved at runtime.

using System;

namespace Collection
{
    class GenericTest
    {
        public void Add<T>(T a, T b)
        {
            // Console.WriteLine(a + b); // This line would produce an error because at compile time, the types of 'a' and 'b' are unknown. To perform operations like addition, we need to use the 'dynamic' type to resolve the types at runtime.
            dynamic d1 = a; 
            dynamic d2 = b;
            Console.WriteLine(d1 + d2);
        }
        public void Sub<T>(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 - d2);
        }
        public void Mul<T>(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 * d2);
        }
        public void Div<T>(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 / d2);
        }
        static void Main()
        {
            GenericTest obj = new GenericTest();
            obj.Add<int>(10, 20);
            obj.Sub<int>(10, 20);
            obj.Mul<int>(10, 20);
            obj.Div<int>(10, 20);
        }
    }
}

In the above example, dynamic allows the Add, Sub, Mul, and Div methods to work with various data types at runtime, performing automatic type conversion. For instance, if a and b are integers, d1 and d2 will be treated as integers; if they are floats, d1 and d2 will be treated as floats, and so on.

Dynamic: dynamic is a new feature introduced in C# 4.0 that allows declaring a variable as dynamic, meaning its data type is identified at runtime. In C# 3.0, we have var, which is similar to dynamic, but with var, the data type is identified at compile time (var identifies the data type at compile time, while dynamic identifies it at runtime). At runtime, if you pass an int value to a and b, then d1 and d2 become int. If you pass a float value, then d1 and d2 become float. If you pass a decimal value, they automatically convert to decimal. Automatic conversion happens.

Understandingvaranddynamic:-

In C#, the dynamic type was introduced in version 4.0, allowing you to declare variables whose type is determined at runtime. This is different from var, introduced in C# 3.0, which determines the type at compile time.

Key Differences Betweenvaranddynamic:

  • var:

    • Determines the data type at compile time.

    • The compiler figures out the type from the assigned value.

    • Once assigned, the type cannot change.

  • dynamic:

    • Determines the data type at runtime.

    • Allows more flexible code, as the type can change based on the assigned value.

    • Useful when the type is not known until runtime.

Summary:

  • var: Used when the type is known at compile time. The type is inferred from the assigned value and cannot change.

  • dynamic: Used when the type is not known until runtime. It allows the type to change based on the assigned value, providing greater flexibility for dynamic scenarios.

Generic Classes with Dynamic Typing:

We can also use generics at the class level, allowing us to specify the type once when creating an instance of the class. This avoids having to specify the type with each method call.

using System;

namespace Collection
{
    class GenericTest2<T>
    {
        public void Add(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 + d2);
        }

        public void Sub(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 - d2);
        }

        public void Mul(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 * d2);
        }

        public void Div(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 / d2);
        }
    }

    class Program
    {
        static void Main()
        {
            GenericTest2<int> obj = new GenericTest2<int>();
            obj.Add(10, 20);
            obj.Sub(10, 20);
            obj.Mul(10, 20);
            obj.Div(10, 20);
        }
    }
}

The advantage here is that you don't need to specify <T> for each method; you specify at once when defining the class. This way, all methods in the class use the same type, making the code simpler and easier to read.

Summary:

Just as we use generics with collections like List<T>, we can apply the same concept to our own classes. For example:

List<int> list = new List<int>();

We can also create an instance of a generic class:

GenericTest2<int> obj = new GenericTest2<int>();

In this case, because int is specified, all methods in GenericTest2 will operate on integers. If we want to use floats, we can do:

GenericTest2<float> obj = new GenericTest2<float>();

Then all methods will operate on floats. The same applies if we use double:

GenericTest2<double> obj = new GenericTest2<double>();

Whatever type is specified, all methods in the class will operate on that type. This pattern shows how generic collections and classes are implemented in .NET 2.0. The use of generics provides type safety, flexibility, and performance benefits, ensuring that all methods operate on the specified type and eliminating the need for type casting.

Using Dictionaries and Lists in Generic Collections:

In the previous lesson, we learned about the Hashtable, which stores key-value pairs. In generic collections, the Hashtable is replaced with the Dictionary. When creating a Dictionary, it takes two types: the first is KeyType and the second is ValueType. This is denoted as Dictionary<TKey, TValue>. A List takes only one type, which is denoted as List<T>.

Example:

using System;
using System.Collections.Generic;

namespace Collection
{
    class DictionaryTest
    {
        static void Main()
        {
            Dictionary<string, object> dt = new Dictionary<string, object>();
            // 'string' represents the key and 'object' represents the value

            // Insert values
            dt.Add("Name", "Mritunjay Kumar");
            dt.Add("Job", "Manager");
            dt.Add("Salary", 25000.00);
            dt.Add("Age", 23);
            dt.Add("Gender", 'M');

            // Retrieve values
            foreach (string key in dt.Keys)
            {
                Console.WriteLine(key + ": " + dt[key]);
            }
        }
    }
}

Dictionaries store values in a sequence, whereas Hashtables do not store values in a sequence.

In the case of a generic collection, the type of values we want to store in the collections need not be predefined types only (like int, float, char, string, bool, etc.). It can also be some user-defined type.

Example with a User-Defined Type:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }
    }

    class DictionaryTest
    {
        static void Main()
        {
            // In a list, we can do the following:
            List<Customer> list = new List<Customer>();

            // Create instances of Customer
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };

            // Add all instances to the list
            list.Add(c1);
            list.Add(c2);
            list.Add(c3);

            // Or we can store all three values at once
            // list.AddRange(new Customer[] { c1, c2, c3 });

            // Retrieve all values
            foreach (Customer obj in list)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }

            // Output:
            /*
             101 Mritunjay Hyderabad 25000
             102 Sumit Delhi 24000
             103 Rahul Chennai 21000
             */
        }
    }
}

In a list, we can store not only predefined values but also user-defined values.

There are two interfaces generally used in collections:

  • IComparable interface

  • IComparer interface

Example:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }
    }

    class DictionaryTest
    {
        static void Main()
        {
            // Create instances of Customer
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            // Assign all instances to a list
            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };

            // Retrieve all values
            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}
//Out:-
/*
101 Mritunjay Hyderabad 25000
102 Sumit Delhi 24000
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800
*/

To sort the list, we use the Sort() method like that:

using System;
using System.Collections.Generic;

namespace Collection
{
    class GenericList
    {
        static void Main()
        {
            List<int> list = new List<int>();//The bhebhier of this list class is exactly same as the behaviour of ArrayList in collection but diffrence is ArrayList store any type of value but List can store specific type of value

            list.Add(100); list.Add(40); list.Add(50);
            list.Add(90); list.Add(30); list.Add(24);
            foreach (object o in list)
            {
                Console.Write(o + " ");
            }
            Console.WriteLine();

            list.Sort();

            foreach (object o in list)
            {
                Console.Write(o + " ");
            }
            Console.ReadLine();
        }
    }
}
//Out:-
/*
100 40 50 90 30 24
24 30 40 50 90 100
 */

But, if you use the Sort() method with List<Customer>, you will get an error. This happens because Customer is a complex type and the compiler is unsure of how to sort it (by CustId, Name, City, or Balance). To sort the data, we need to write the sorting logic inside the Customer class using the IComparable<Customer> interface and its CompareTo method. And i also want to short by balance or any other value.

Short by balance:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer : IComparable<Customer>
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }

        public int CompareTo(Customer other)
        {
            // Sort based on Balance 
            if (this.Balance > other.Balance)
            {
                return 1; // If this instance's Balance is greater
            }
            else if (this.Balance < other.Balance)
            {
                return -1; // If this instance's Balance is less
            }
            else
            {
                return 0; // If both Balance are equal
            }
        }
    }

    class DictionaryTest
    {
        static void Main()
        {
            // Create instances of Customer
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            // Assign all instances to a list
            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };

            // Sort the list
            cus.Sort();

            // Retrieve all values after sorting
            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}
//Out:-
/*
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800
102 Sumit Delhi 24000
101 Mritunjay Hyderabad 25000
 */

If you want to reverse the sort order, you only need to change the return values in the CompareTo method:

Reverse using CustId:

public int CompareTo(Customer other)
{
    if (this.CustId > other.CustId)
    {
        return -1; // Reverse the comparison
    }
    else if (this.CustId < other.CustId)
    {
        return 1; // Reverse the comparison
    }
    else
    {
        return 0;
    }
}
//Out:-
/*
106 Gudu Uttar Pradesh 23800
105 Mohan Jharkhand 21800
104 Amit Delhi 21500
103 Rahul Chennai 21000
102 Sumit Delhi 24000
101 Mritunjay Hyderabad 25000
*/

But assume you don't have access to the Customer class code and it sorts data based on CustId, but you want to sort based on Balance. In this case, you can create a new class that implements IComparer<Customer> to achieve this. Let's see how:

Example with Custom Sorting:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer : IComparable<Customer>
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }

        public int CompareTo(Customer other)
        {
            // Sorting by CustId
            if (this.CustId > other.CustId)
            {
                return 1;
            }
            else if (this.CustId < other.CustId)
            {
                return -1;
            }
            else
            {
                return 0;
            }
        }
    }

    class BalanceComparer : IComparer<Customer>
    {
        public int Compare(Customer x, Customer y) //These parameters represent the two Customer objects that need to be compared. 
        {
            // Sorting by Balance
            if (x.Balance > y.Balance)
            {
                return 1;
            }
            else if (x.Balance < y.Balance)
            {
                return -1;
            }
            else
            {
                return 0;
            }
        }
    }

    class DictionaryTest
    {
        static void Main()
        {
            // Create instances of Customer
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            // Assign all instances to a list
            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };

            // Retrieve all values sorted by CustId
            Console.WriteLine("Sorted by CustId:");
            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }

            Console.WriteLine();

            BalanceComparer balanceComparer = new BalanceComparer();
            cus.Sort(balanceComparer);

            // Retrieve all values sorted by Balance
            Console.WriteLine("Sorted by Balance:");
            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}
Sorted by CustId:
101 Mritunjay Hyderabad 25000
102 Sumit Delhi 24000
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800

Sorted by Balance:
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800
102 Sumit Delhi 24000
101 Mritunjay Hyderabad 25000

The Sort() method can work in two ways:

  1. Sort() without parameters: This uses the default way of sorting provided in the Customer class, which sorts by CustId because it implements the IComparable interface.

  2. Sort(IComparer<T>) with a custom comparer: This uses a custom way of sorting, like sorting by Balance, provided by an IComparer implementation. In our example, we use the BalanceComparer class for this. IComparer is a parameter.

This makes sorting flexible, allowing you to sort Customer objects by different properties depending on what you need.

Now we have two options: Built-in Sorting and Custom Sorting.

Sort() Method Overloads:

The Sort() method has four overloads:

  1. Sort() without parameters:

    • Uses the default way of sorting provided in the class, such as Customer, which sorts by CustId because it implements the IComparable interface. And this is also suitable for simple types like int, float, double, etc. Alwarady seen.
  2. Sort(Comparison<T> comparison):

    • Takes a Comparison<T> delegate as a parameter. This delegate represents the method that compares two objects of the same type. It's useful for defining custom sorting logic directly in place.
  3. Sort(IComparer<T> comparer):

    • Uses can short custom way, like sorting by Balance, provided by an IComparer<T> implementation. In our example, we use the BalanceComparer class for this. IComparer is a parameter. Alwarady seen.
  4. Sort(int index, int count, IComparer<T> comparer):

    • Sorts a range of elements in the list using the specified comparer. This allows you to specify which portion of the list to sort.

    • Example:

        using System;
        using System.Collections.Generic;
      
        namespace Collection
        {
            public class Customer : IComparable<Customer>
            {
                public int CustId { get; set; }
                public string Name { get; set; }
                public string City { get; set; }
                public double Balance { get; set; }
      
                public int CompareTo(Customer other)
                {
                    // Sorting by CustId
                    if (this.CustId > other.CustId)return 1;
                    else if (this.CustId < other.CustId) return -1;
                    else return 0;
                }
            }
      
            class BalanceComparer : IComparer<Customer>
            {
                public int Compare(Customer x, Customer y) 
                {
                    // Sorting by Balance
                    if (x.Balance > y.Balance) return 1;
                    else if (x.Balance < y.Balance) return -1;
                    else return 0; 
                }
            }
      
            class DictionaryTest
            {
                static void Main()
                {
                    // Create instances of Customer
                    Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
                    Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
                    Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
                    Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
                    Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
                    Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };
      
                    // Assign all instances to a list
                    List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };
      
                    //Create instance
                    BalanceComparer balanceComparer = new BalanceComparer();
                    cus.Sort(1, 4, balanceComparer); //Index start from '0'
      
                    // Retrieve all values sorted by Balance
                    Console.WriteLine("Sorted by Balance:");
                    foreach (Customer obj in cus)
                    {
                        Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
                    }
                }
            }
        }
      

      Out:-

        Sorted by Balance:
        101 Mritunjay Hyderabad 25000
        103 Rahul Chennai 21000
        104 Amit Delhi 21500
        105 Mohan Jharkhand 21800
        102 Sumit Delhi 24000
        106 Gudu Uttar Pradesh 23800
      

      Remaining index numbers 0 and 5 are not included in sorting; the others are included in sorting by Balance.

Sort using Delegate2nd Sort -> Sort(Comparison<T> comparison):

Comparison is a delegate and this method have must same signature return type is integer and take two parameter.

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer : IComparable<Customer>
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }

        public int CompareTo(Customer other)
        {
            // Sorting by CustId
            if (this.CustId > other.CustId)return 1;
            else if (this.CustId < other.CustId) return -1;
            else return 0;
        }
    }

    class DictionaryTest
    {
        //Create method:
        public static int ComparteByName(Customer x, Customer y)
        {
            return x.Name.CompareTo(y.Name);
        }
        static void Main()
        {
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };


            //Pass 'ComparteByName' method in 'Comparison' delegant and also signature is matching both of them 'Comparison' delegant and 'ComparteByName' method
            Comparison<Customer> compDele = new Comparison<Customer>(ComparteByName);

            // Sort the list using the Comparison delegate
            cus.Sort(compDele);
            //Or
            //cus.Sort(CompareByName);

            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}
//Out:-
/*
104 Amit Delhi 21500
106 Gudu Uttar Pradesh 23800
105 Mohan Jharkhand 21800
101 Mritunjay Hyderabad 25000
103 Rahul Chennai 21000
102 Sumit Delhi 24000
*/

When using cus.Sort(CompareByName) and cus.Sort(compDele), both achieve the same result, but in slightly different ways:

  • cus.Sort(CompareByName): Directly passes the CompareByName method to the Sort() method. Since the CompareByName method matches the Comparison<Customer> delegate signature, the Sort() method internally creates a delegate for you.

  • cus.Sort(compDele): Explicitly uses a Comparison<Customer> delegate (compDele) that was previously created using the CompareByName method.

Using an anonymous method shortens the code:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer : IComparable<Customer>
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }

        public int CompareTo(Customer other)
        {
            // Sorting by CustId
            if (this.CustId > other.CustId)return 1;
            else if (this.CustId < other.CustId) return -1;
            else return 0;
        }
    }

    class DictionaryTest
    {
        static void Main()
        {
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };

            // Sort the list using the Comparison delegate
            cus.Sort(delegate (Customer x, Customer y)
            {
                return x.Name.CompareTo(y.Name);
            });

            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}

Using a lambda expression shortens the code cus.Sort((s1, s2) => s1.Name.CompareTo(s2.Name));:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer : IComparable<Customer>
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }

        public int CompareTo(Customer other)
        {
            // Sorting by CustId
            if (this.CustId > other.CustId)return 1;
            else if (this.CustId < other.CustId) return -1;
            else return 0;
        }
    }

    class DictionaryTest
    {
        static void Main()
        {
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };


            // Sort the list using the Comparison delegate
            cus.Sort((s1, s2) => s1.Name.CompareTo(s2.Name));

            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}

The CompareTo method for strings compares two strings to determine their order:

  1. Comparison:

    • Lexicographical Order: Strings are compared character by character based on their Unicode values (similar to alphabetical order).
  2. Return Values:

    • Negative: If the first string comes before the second string.

    • Zero: If both strings are equal.

    • Positive: If the first string comes after the second string.

Example:

string str1 = "apple";
string str2 = "banana";

int result = str1.CompareTo(str2); // result will be negative because "apple" comes before "banana"

Summary: CompareTo for strings checks each character's Unicode value to sort them in alphabetical order.

IEnumerable Interface:

The IEnumerable interface is the parent of all collection types.

  • IEnumerable Interface: This is the base interface for all non-generic collections. It defines a single method, GetEnumerator(), which returns an IEnumerator.

  • ICollection Interface: This inherits from IEnumerable and adds methods for size, enumerators, and synchronization.

  • IList and IDictionary Interfaces: These inherit from ICollection.

    • IList Interface: This represents collections of objects that can be individually accessed by index. Classes like ArrayList are part of this.

    • IDictionary Interface: This represents a collection of key/value pairs. Classes like Hashtable and Dictionary are part of this.

Here's a simplified visual hierarchy:

IEnumerable
 ├── ICollection
 │   ├── IList
 │   │   └── ArrayList
 │   └── IDictionary
 │       ├── Hashtable
 │       └── Dictionary

Inside the IEnumerable interface ICollection class is there, inside the ICollection class IList and IDictionary class ther and all list related class in inside the IList class like ArrayList and inside the IDictionary class all class which take key and value pair like Hashtable , Dictionary are available.

In the previous examples, you were able to extract all values from lists and dictionaries because every collection inherits from the IEnumerable interface. The IEnumerable interface internally has a method called GetEnumerator(). When a class implements IEnumerable, it must also implement the GetEnumerator() method, which is responsible for enabling the foreach loop to iterate through the collection.

If you look at the definition of the List class, you will see that it inherits from IEnumerable<T>. The IEnumerable<T> interface, in turn, inherits from the non-generic IEnumerable. The IEnumerable interface contains the GetEnumerator() method. Without the GetEnumerator() method, the foreach loop would not work. This is why you can use foreach with any collection that implements IEnumerable.

Note's:

  • IEnumerable is the base interface for all collections.

  • ICollection adds size, enumerators, and synchronization methods.

  • IList is for index-based collections.

  • IDictionary is for key/value pair collections.

  • The GetEnumerator() method in IEnumerable enables the foreach loop.

Note's:

When you use a foreach loop, the foreach loop internally calls the GetEnumerator method to get an enumerator. And enumerator is an object that enables iteration over a collection. enumerator provides methods like MoveNext and Reset, and a property called Current. Enumerators come from collections that implement the IEnumerable or IEnumerable<T> interface (Enumerators are used with collections that have IEnumerable or IEnumerable<T>.).

Example Code:

Here I create one class that works like a list:

Without IEnumerable Implementation:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Employee
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }
    }

    //Non-Generic
    public class Orginization
    {
        //Hold the value we use Array:
        List<Employee> Emps = new List<Employee>();
        public void AddEmp(Employee emp)
        {
            Emps.Add(emp);
        }
    }

    class IEnumerableTest
    {
        static void Main()
        {
            Orginization orgEmp = new Orginization();
            orgEmp.AddEmp(new Employee { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 });
            orgEmp.AddEmp(new Employee { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 });
            orgEmp.AddEmp(new Employee { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 });
            orgEmp.AddEmp(new Employee { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 });
            orgEmp.AddEmp(new Employee { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 });
            orgEmp.AddEmp(new Employee { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 });

            foreach (Employee Emp in orgEmp)//Give error hear in 'orgEmp'
                Console.WriteLine(Emp.CustId + " " + Emp.Name + " " + Emp.City + " " + Emp.Balance);
        }
    }
}

The above code will give an error: foreach statement cannot operate on variables of type 'Orginization' because 'Orginization' does not contain a public instance or extension definition for 'GetEnumerator'.

Explain MethodAddEmp:

The method AddEmp in the Orginization class is designed to add an Employee object to the Emps list, which holds all the Employee objects.

public void AddEmp(Employee emp)
{
    Emps.Add(emp);
}
  • (Employee emp): The method takes one parameter, which is an Employee object named emp.

  • Emps.Add(emp);: This line adds the Employee object passed as a parameter (emp) to the Emps list. Emps is a list of Employee objects, declared as List<Employee> Emps = new List<Employee>(); in the Orginization class. The Add method of the List<T> class appends the specified element to the end of the list.

  • Purpose: The AddEmp method provides a way to add new Employee objects to the Orginization's internal list (Emps). By encapsulating the Add functionality within this method,

Explain ListList<Employee> Emps = new List<Employee>(); :

  • List<T>: This is a generic collection class in the System.Collections.Generic namespace. In this case, T is replaced with Employee, so List<Employee> is a list that holds Employee objects.

  • List<Employee> Emps :The variable Emps is declared to hold a reference to a List<Employee> object.

  • new List<Employee>() creates a new, empty list of Employee objects. At this point, Emps is an empty list, ready to have Employee objects added to it. new is a keyword which used to create a new instance of an object. List<Employee>() This is the constructor of the List<T> class not the constructor of the Employee class, which initializes a new instance of the list.

  • The Employee class constructor is not called in new List<Employee>() statement. The Employee class constructor will only be called when you create instances of the Employee class itself, such as when you add new Employee objects to the list. new List<Employee>() is the constructor of the List<T> class that is being called.

  • The Employee class would be a user-defined class. Each instance of the Employee class represents a single employee with properties such as CustId, Name, City, and Balance.

  • new List<Employee>() initializes a new list, and each new Employee { ... } initializes new instances of the Employee class. The Employee class constructor is called when creating new Employee objects, not when initializing the list.

  • When you add Employee instances to this list, the Employee class constructor is called for each new Employee object. For example: Emps.Add(new Employee { CustId = 101, Name = "John Doe", City = "New York", Balance = 1000.0 }); In this line new Employee { CustId = 101, Name = "John Doe", City = "New York", Balance = 1000.0 } calls the Employee class constructor to create a new instance of Employee and initialize it with the provided properties.

WithIEnumerableImplementation:

To fix this, you need to inherit IEnumerable and add the GetEnumerator method in the Orginization class:

using System;
using System.Collections.Generic;
using System.Collections;

namespace Collection
{
    public class Employee
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }
    }

    //Non-Generic
    public class Orginization : IEnumerable
    {
        List<Employee> Emps = new List<Employee>();
        public void AddEmp(Employee emp) 
        {
            Emps.Add(emp);
        }
        public IEnumerator GetEnumerator()
        {
            return Emps.GetEnumerator();//Return type is GetEnumerator
        }
    }//This class work like collection now for employes
    class IEnumerableTest
    {
        static void Main()
        {
            Orginization orgEmp = new Orginization();
            orgEmp.AddEmp(new Employee { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 });
            orgEmp.AddEmp(new Employee { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 });
            orgEmp.AddEmp(new Employee { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 });
            orgEmp.AddEmp(new Employee { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 });
            orgEmp.AddEmp(new Employee { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 });
            orgEmp.AddEmp(new Employee { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 });

            foreach (Employee Emp in orgEmp)
                Console.WriteLine(Emp.CustId + " " + Emp.Name + " " + Emp.City + " " + Emp.Balance);
        }
    }
}
/*Out:-
101 Mritunjay Hyderabad 25000
102 Sumit Delhi 24000
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800
*/

This implementation of Orginization class behave like a collection of Employee objects for employees by implementing the IEnumerable interface. This allows it to be used in a foreach loop.

IEnumerable Implementation: The GetEnumerator() method returns the enumerator of the Emps list, allowing the Orginization class to be used in a foreach loop.

  • The Orginization object orgEmp is created.

  • Several Employee objects are added to orgEmp using the AddEmp method.

  • The foreach loop iterates over orgEmp, printing the details of each Employee.

  • The GetEnumerator() method is used to enable iteration over the collection. It provides the necessary functionality for the foreach loop to iterate through the elements of the Orginization class.

  • Method Signaturepublic IEnumerator GetEnumerator(): This indicates a public method that returns an IEnumerator object. The IEnumerator interface provides the basic mechanisms for iterating over a collection.

    I told you earlier: When you use a foreach loop, the foreach loop internally calls the GetEnumerator method to get an enumerator. And enumerator is an object that enables iteration over a collection. enumerator provides methods like MoveNext and Reset, and a property called Current. Enumerators come from collections that implement the IEnumerable or IEnumerable<T> interface (Enumerators are used with collections that have IEnumerable or IEnumerable<T>.).

  • return Emps.GetEnumerator(); : Emps is a List<Employee>. The List<T> class implements IEnumerable<T>, so it has a GetEnumerator method that returns an enumerator. We also used Emps.GetEnumerator();. The GetEnumerator method of the List<T> class is called, which returns an enumerator object that can iterate through the list of employees.

  • In foreach loop calls orgEmp.GetEnumerator() to get an enumerator. The enumerator is then used to iterate over each Employee object in the Emps list.

Custom Enumerator Implementation:

If you do not want to return the List class GetEnumerator, you can create your own enumerator:

public IEnumerator GetEnumerator()
{
    throw new NotImplementedException();
}

return the GetEnumerator:

public IEnumerator GetEnumerator()
{
    return Emps.GetEnumerator();//Return type is GetEnumerator
}

If you do not want to return the List class GetEnumerator, you can create your own enumerator:

using System;
using System.Collections;
using System.Collections.Generic;

namespace Collection
{
    public class Employee
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }
    }

    public class Orginization : IEnumerable
    {
        List<Employee> Emps = new List<Employee>();

        public void AddEmp(Employee emp)
        {
            Emps.Add(emp);
        }

        public IEnumerator GetEnumerator()
        {
            return new OrginizationEnumerator(this);
        }

        public int Count => Emps.Count;

        public Employee this[int index] => Emps[index];
    }

    public class OrginizationEnumerator : IEnumerator
    {
        Orginization orgColl;
        int currentIndex;
        Employee currentEmployee;

        public OrginizationEnumerator(Orginization org)
        {
            orgColl = org;
            currentIndex = -1;
        }

        public object Current => currentEmployee;

        public bool MoveNext()
        {
            if (++currentIndex >= orgColl.Count)
                return false;
            else
            {
                currentEmployee = orgColl[currentIndex];  //Set the index
                return true;
            }
        }

        public void Reset()
        {
            currentIndex = -1;
        }
    }

    class IEnumerableTest
    {
        static void Main()
        {
            Orginization orgEmp = new Orginization();
            orgEmp.AddEmp(new Employee { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 });
            orgEmp.AddEmp(new Employee { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 });
            orgEmp.AddEmp(new Employee { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 });
            orgEmp.AddEmp(new Employee { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 });
            orgEmp.AddEmp(new Employee { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 });
            orgEmp.AddEmp(new Employee { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 });

            foreach (Employee Emp in orgEmp)
                Console.WriteLine($"{Emp.CustId} {Emp.Name} {Emp.City} {Emp.Balance}");
        }
    }
}
/*Out:-
101 Mritunjay Hyderabad 25000
102 Sumit Delhi 24000
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800
*/

Purpose ofOrginizationEnumeratorclass: The OrginizationEnumerator class is an implementation of the IEnumerator interface. It is designed to iterate over a collection of Employee objects contained in an Orginization class.

Fields:

  • orgColl: Holds a reference to the Orginization instance being enumerated.

  • currentIndex: Keeps track of the current position in the collection. Initialized to -1 to start before the first element.

  • currentEmployee: Holds the current Employee object during iteration.

Constructor:

public OrginizationEnumerator(Orginization org)
{
    orgColl = org;
    currentIndex = -1;
}
  • Initializes orgColl with the given Orginization object.

  • Index start from 0. Sets currentIndex to -1, meaning the enumerator starts before the first element.

Method: MoveNext():

public bool MoveNext()
{
    if (++currentIndex >= orgColl.Count)
        return false;
    else
    {
        currentEmployee = orgColl[currentIndex];
        return true;
    }
}
  • Moves the enumerator to the next element.

  • Increases currentIndex.

  • if (++currentIndex >= orgColl.Count) return false; :If currentIndex is equal to or more than the total number of elements, it returns false, meaning the end of the collection.

  • currentEmployee = orgColl[currentIndex];: it updates currentEmployee with the new element and returns true.

Property: Current: public object Current => currentEmployee; Returns the current Employee object. It is typed as object to match the IEnumerator interface. You can also write like that:

public object Current //Current is use to access the current record. 
{
    get{ return CurrectEmployee; }
}

Method: Reset():

public void Reset()
{
    currentIndex = -1;
}

Resets the enumerator to its initial state, before the first element. This is often used to restart enumeration. Actually, it's not required at that time; you can simply write it like this: public void Reset(){}.

Why We NeedMoveNext(),Reset(), andCurrent:

MoveNext(), Reset(), and Current are part of the IEnumerator interface, which is used for iterating over a collection.

  1. MoveNext() Method:
  • Purpose: Moves the enumerator to the next element in the collection.

  • What It Does: It increments the internal index and checks if it is still within the bounds of the collection. If it is, it updates the Current property to the new element and returns true. If it’s out of bounds (i.e., past the last element), it returns false and the iteration ends.

  • Why We Need It: This method is essential for advancing through the collection. The foreach loop calls MoveNext() to progress to the next item.

  1. Reset() Method:
  • Purpose: Resets the enumerator to its initial position, before the first element.

  • What It Does: It sets the index back to -1 or the starting position. This allows for re-iteration or starting the iteration again from the beginning.

  • Why We Need It: While Reset() is less commonly used in typical iteration scenarios, it can be useful if you need to iterate over the collection again using the same enumerator.

  1. Current Property:
  • Purpose: Gets the current element in the collection.

  • What It Does: It provides access to the element at the current position of the enumerator.

  • Why We Need It: This property returns the element that the enumerator is currently pointing to. During iteration, it allows you to access the item that MoveNext() has moved to.

Purpose ofGetEnumeratorMethod: Allows the Orginization class to be used with foreach loops.

public IEnumerator GetEnumerator()
{
    return new OrginizationEnumerator(this);
}

Returns a new OrginizationEnumerator initialized with the current instance of Orginization. It means, when GetEnumerator() is called, it constructs a new OrginizationEnumerator object. And this enumerator is initialized with the current Orginization instance, allowing it to access and iterate through the Employee objects in that specific Orginization instance.

The foreach loop calls GetEnumerator() to get a new OrginizationEnumerator object each time. Then uses this object to go through the items in the Orginization collection.

  • newKeyword: The new keyword is used to create a new instance of a class or struct. This means it allocates memory for the new object and calls its constructor to set it up. In this case, new is used to create a new OrginizationEnumerator object. This new object will be in charge of going through the items in the Orginization collection.

  • thisKeyword: The this keyword refers to the current instance of the class where it is used. It provides a way to access the current object’s members (like methods and properties). It's often used to pass the current instance to other methods or constructors. Here, this is used to pass the current instance of the Orginization class to the constructor of OrginizationEnumerator. This allows the enumerator to access the collection of employees in the Orginization class.

Indexing and Counting:

Indexing and Counting are features of the Orginization class that enhance its functionality:

CountProperty:public int Count => Emps.Count;

  • Purpose: Provides the number of Employee items in the Orginization.

  • What It Does: Returns the count of items in the Emps list.

  • Why We Need It: This property is useful for determining the number of elements in the collection, especially if you need to perform operations based on the size of the collection.

  •                               public int Count => Emps.Count; //Use to Counting
                                  //Or
                                  public int Count
                                  {
                                      get { return Emps.Count; }
                                  }
    

Indexer (this[int index]): public Employee this[int index] => Emps[index];

  • Purpose: Allows access to Employee objects in the Orginization collection using an index, similar to array indexing.

  • What It Does: Retrieves the Employee at the specified index from the Emps list.

  • Why We Need It: This indexer provides a way to access elements directly by index, making the Orginization class behave like an array or list. This can be useful for direct access to items without needing to iterate through the collection.

      public Employee this[int index] => Emps[index]; //Use to Indexing
      //Or
      public Employee this[int index] 
      {
          get{ return Emps[index]; }
      }
    
    • this: The keyword that indicates this is an indexer, not a regular property. It allows the class to be indexed.

    • [int index]: The parameter list for the indexer. It specifies that the indexer will take an integer parameter, which represents the index of the element to access.

    • Employee: The return type of the indexer. It specifies that the indexer will return an Employee object.

    • Emps[index]: The body of the getter. It returns the Employee object at the specified index from the internal Emps list.

    • UseEmployee emp = orgEmp[0];:

      here's what happens:

      • orgEmp[0]: The indexer is called with the index 0.

      • get { return Emps[0]; }: The getter of the indexer is executed, which returns Emps[0].

      • Emps[0]: This accesses the Emps list (which is a List<Employee>) and retrieves the Employee object at index 0.

When a foreach loop is used with an Organization object, it looks for the GetEnumerator method in the Organization class. The Organization class implements the IEnumerable interface, which requires a GetEnumerator method that returns an IEnumerator. The IEnumerator implementation is provided by the OrganizationEnumerator class, where we define the logic for the Current property and the MoveNext method.

In that example, we have 4 classes:

  1. Employee class

  2. Organization class: This class works like a collection. We define the AddEmp method to add employees, the Count property to return the number of items, the indexer to return an item by index, and the GetEnumerator method to enable the foreach loop. This GetEnumerator method returns an IEnumerator type, which is an interface. Next, we define an OrganizationEnumerator class to implement the IEnumerator interface.

  3. OrganizationEnumerator class: This class inherits from IEnumerator. I define a constructor to access the Organization, and then I implement the Current property and two methods, MoveNext and Reset, although we do not use Reset.

  4. IEnumerableTest class: This is the main class. Here, we use the main method to create an instance of the Organization class called orgEmp. Then, we add employees and use a foreach loop to get the data.

Q. What isIEnumerable,IEnumeratorandGetEnumerator?

Ans:-

IEnumerable: IEnumerable is an interface used by all collection classes. It includes a method called GetEnumerator. Because of GetEnumerator, we can use a foreach loop on the collection. If we want our classes to act like collections, we need to implement the GetEnumerator method.

IEnumerator: An interface that defines methods for iterating over a collection. MoveNext(): Moves to the next item. Reset(): Resets to the position before the first item. Current: Gets the current item in the collection.

GetEnumerator: A method from IEnumerable that returns an IEnumerator. It allows us to use a foreach loop to iteration the collection.

Q. CRUD operations using Collection (Generic Collection)?

  • Create a Product class

  • It has the following properties like Id, name, price

  • Create a collection of Product class

  • Add three products and display

  • We need to do the following operations

    • Add a New Product

    • Update an existing product price

    • Display product by id

    • Display all products

    • Delete a product by taking id

Ans:

Edit this text


LINQ(Language Integrated Query):

LINQ is a query language designed by Microsoft in .NET 3.5. LINQ allows you to write queries on various data sources such as arrays, collections, database tables, datasets, and XML data.

LINQ is available in the System.Linq namespace.

Consider the task of sorting an array. Traditionally, you might use loops and predefined methods to do this. LINQ offers a simpler and more elegant way to handle such tasks.

Here's an example array:

int[] numbers = { 17, 34, 8, 56, 23, 91, 42, 73, 15, 27, 68, 39, 44, 53, 11, 22, 78, 31, 86, 9 };

To sort this array using LINQ, you can use the following syntax. LINQ syntax is similar to SQL, where you select and manipulate data.

SQL Syntax:

SELECT <column_list> FROM <table> [AS <alias>] [<clauses>]

LINQ Syntax:

from <alias> in <collection | array> [<clauses>] select <alias>

Getting All Data from an Array Using LINQ:

var num1 = from i in numbers select i;

var is a keyword introduced in C# 3.0. It declares an implicitly typed local variable, And the data type of var is determined by the value it holds.

num1 is used to capture the array data from from i in numbers select i. num1 is now an array.

Sort the data which is greater than 40:

var numbers1 = from i in numbers where i > 40 select i;

Sort the data which is greater than 40 in ascending order:

var numbers1 = from i in numbers where i > 40 orderby i select i;

Sort the data which is greater than 40 in descending order:

var numbers1 = from i in numbers where i > 40 orderby i descending select i;

Display values:

foreach (var item in numbers1)
    Console.Write(item + " ");

LINQ to SQL:

  • It's a query language that was introduced in the .NET 3.5 framework for working with relational databases, such as SQL Server.

  • LINQ to SQL is not just for querying data, it also lets us perform CRUD operations.

  • CRUD: Create(Insert), Read(Select), Update, Delete**.**

We can also call stored procedures using LINQ to SQL.

Q. There is already a language known as SQL, which we can use to interact with SQL Server with the help of ADO.Net. Then why do we need LINQ?

Ans: SQL is a powerful language used to interact with SQL Server via ADO.NET. However, LINQ (Language Integrated Query) has several benefits over traditional SQL when used in a .NET environment. Let's look at why LINQ is needed and how it can be better than SQL in some cases.

Advantages of LINQ over SQL in ADO.NET:

  • Compile-Time Syntax Checking:

    • SQL in ADO.NET: When you write an SQL query in ADO.NET, it's usually wrapped in double quotes as a string. The .NET compiler doesn't recognize the syntax of this string. The query is sent to the SQL Server, where the database engine validates the syntax. If there's a syntax error, it's caught at runtime, which can increase the load on the database engine.

    • LINQ: LINQ queries are checked for syntax errors at compile time by the .NET compiler. This means errors are caught earlier, reducing runtime issues and lowering the load on the database engine.

  • Type Safety:

    • SQL in ADO.NET: SQL queries in ADO.NET are not type-safe. For example, if your query tries to insert a value into a table with a mismatched data type or too many columns, the database engine will return an error. This error is only caught at runtime, which can lead to inefficiencies and wasted time.

    • LINQ: LINQ is completely type-safe. The .NET compiler makes sure that the types of values in your queries match the database schema. This type checking happens on the client side, making development safer and more efficient. Visual Studio's IntelliSense helps you see the data types of columns, avoiding type mismatches.

  • IntelliSense Support:

    • SQL in ADO.NET: When writing SQL queries in ADO.NET, you don't get IntelliSense support for column names, table names, or data types, which can lead to errors and slower development.

    • LINQ: LINQ offers full IntelliSense support in Visual Studio. This means you get real-time suggestions and feedback on the structure of your query, making development faster and more error-free.

  • Debugging Capabilities:

    • SQL in ADO.NET: Debugging SQL statements is hard because the SQL code runs on the database server. You can't step through SQL code like you can with .NET code.

    • LINQ: LINQ queries are written in C# or VB.NET and run on the client side. This allows you to debug LINQ queries just like any other .NET code, making the debugging process easier.

  • Pure Object-Oriented Code:

    • SQL in ADO.NET: SQL code in ADO.NET often feels separate from the rest of your application. For example, when inserting data into a table, you have to manually join strings, which can lead to SQL injection risks and makes the code harder to maintain.

    • LINQ: LINQ lets you work with data in a fully object-oriented way. Tables become classes, columns become properties, rows are instances of those classes, and stored procedures are methods. This makes the code more consistent, easier to maintain, and in line with modern programming practices.

  • Simplified Code Structure:

    • SQL in ADO.NET: SQL queries can make the codebase a mix of object-oriented and relational code, which can be harder to maintain.

    • LINQ: LINQ allows developers to write queries that follow object-oriented principles. This results in cleaner, more readable, and easier-to-maintain code.

Working with LINQ to SQL:

To work with LINQ to SQL, we first need to convert all the relational objects of the database into object-oriented types. This process is known as ORM (Object-Relational Mapping).

Working with LINQ to SQL: To work with LINQ to SQL, first we need to convert all the relational objects of the database into object oriented types. This process is known as ORM (Object Relational Mapping).

To perform ORM, we use a tool called the OR designer.

Steps to Perform ORM Using LINQ to SQL:

  1. Using the Object-Relational (OR) Designer:

    • ORM with OR Designer: The OR Designer is a tool provided by Visual Studio that helps you perform ORM by visually mapping database tables, views, and stored procedures to corresponding classes, properties, and methods in your .NET application.
  2. Adding a Reference to the System.Data.Linq Assembly:

    • To work with LINQ to SQL, you need to add a reference to the System.Data.Linq.dll assembly in your project. This assembly contains the necessary classes and methods for working with LINQ to SQL.
  3. Configuring the Connection String:

    • You need to write the connection string in the configuration file (typically App.config or Web.config) of your project. This connection string will provide the necessary information for your application to connect to the SQL Server database.

To add the OR Designer, select New Item and choose LINQ to SQL Classes. The extension is .dbml (Database Markup Language). Name the file, ideally matching the database name. Then go to Solution Explorer > References and check System.Data.Linq. If you cannot find the "LINQ to SQL Classes" option, it might be because it is a legacy feature not included by default in newer versions of Visual Studio. To enable it:

  1. Open Visual Studio Installer.

  2. Ensure that ASP.NET and web development is checked under the Workloads tab.

  3. Go to the Individual components tab, search for LINQ to SQL tools, and check it.

  4. Click Modify to install the required components.

  5. After installation, return to Visual Studio, select New Item again, and search for LINQ to SQL Classes.

When you create a .dbml file, it generates two additional files:

  1. <filename>.dbml.layout

  2. <filename>.designer.cs

The designer.cs file is automatically generated by Visual Studio and is not meant to be edited manually. Visual Studio writes the code in this file when you drag and drop database objects onto the OR Designer. You should only view this file, not modify it.

Inside the designer.cs file, a class is generated with the name <filename>DataContext. This class is a partial class that inherits from System.Data.Linq.DataContext.

What does the <filename>DataContext class do?

This class acts as a connection to the database. In ADO.NET, you would use a SqlConnection class to connect to a database, but in LINQ to SQL, the <filename>DataContext class serves this purpose. When you create an instance of this class, it helps establish the connection to the database.

This class needs a connection string, which specifies the database URL. This connection string is usually stored in the App.config file. When you create an instance of the <filename>DataContext class, it reads the connection string from App.config and establishes the connection to the database.

The two panels in the OR Designer in Visual Studio serve specific purposes:

  1. Diagram Panel: This left panel lets you visually design your data model by dragging and dropping tables, views, and relationships. It provides a graphical overview of your database schema.

  2. Properties Panel: This right panel shows detailed information about the selected item in the diagram, such as column names, data types, and relationships. It allows for precise adjustments and configurations.

In my project "LinqToSqlProject," the database name is also "LinqToSqlProject," and I named my .dbml file as DataClasses1.dbml.

Steps:

  1. Drag and Drop Tables:

    • Go to the "Tables" section (under the "Tables" folder) in Server Explorer.

    • Select the table (e.g., Employee) and drag it to the Diagram Panel in the OR Designer.

  2. Connection String in App.config:

    • After performing the above step, Visual Studio automatically adds the connection string to the App.config file. It might look like this:

    • App.config:

        <?xml version="1.0" encoding="utf-8" ?>
        <configuration>
            <configSections>
            </configSections>
            <connectionStrings>
                <add name="LinqToSqlProject.Properties.Settings.LinqToSqlProjectConnectionString"
                    connectionString="Data Source=DESKTOP-HOOMVQE\MSSQLSERVER02;Initial Catalog=LinqToSqlProject;Integrated Security=True;Encrypt=True;TrustServerCertificate=True"
                    providerName="System.Data.SqlClient" />
            </connectionStrings>
            <startup> 
                <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
            </startup>
        </configuration>
      
  3. Generated Code in designer.cs:

    • The DataClasses1.dbml file generates a designer.cs file. This file is not meant for manual editing; Visual Studio manages it automatically when you drag and drop items in the designer.

    • The designer.cs file includes a parameterless constructor in the DataClasses1DataContext class, which reads the connection string:

        public DataClasses1DataContext() : 
            base(global::LinqToSqlProject.Properties.Settings.Default.LinqToSqlProjectConnectionString, mappingSource)
        {
            OnCreated();
        }
      
    • Additionally, a property is created for each table, matching the table name. For example, if your table is named Employee, the code will look like this:

        public System.Data.Linq.Table<Employee> Employees
        {
            get
            {
                return this.GetTable<Employee>();
            }
        }
      
    • A class named Employee is also generated, with fields and properties corresponding to the columns of the Employee table. The fields use an underscore prefix (_):

        public partial class Employee
        {
      
            private System.Nullable<int> _Eno;
      
            private string _Ename;
      
            private string _Job;
      
            private System.Nullable<decimal> _Salary;
      
            private string _Dname;
      
            public Employee()
            {
            }
      
            [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Eno", DbType="Int")]
            public System.Nullable<int> Eno
            {
                get
                {
                    return this._Eno;
                }
                set
                {
                    if ((this._Eno != value))
                    {
                        this._Eno = value;
                    }
                }
            }
      
            [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Ename", DbType="VarChar(50)")]
            public string Ename
            {
                get
                {
                    return this._Ename;
                }
                set
                {
                    if ((this._Ename != value))
                    {
                        this._Ename = value;
                    }
                }
            }
      
            [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Job", DbType="VarChar(50)")]
            public string Job
            {
                get
                {
                    return this._Job;
                }
                set
                {
                    if ((this._Job != value))
                    {
                        this._Job = value;
                    }
                }
            }
      
            [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Salary", DbType="Money")]
            public System.Nullable<decimal> Salary
            {
                get
                {
                    return this._Salary;
                }
                set
                {
                    if ((this._Salary != value))
                    {
                        this._Salary = value;
                    }
                }
            }
      
            [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Dname", DbType="VarChar(50)")]
            public string Dname
            {
                get
                {
                    return this._Dname;
                }
                set
                {
                    if ((this._Dname != value))
                    {
                        this._Dname = value;
                    }
                }
            }
        }
      

Note: The rows or records in the database are represented as instances of the Employee class when the program is running.

  1. Load the data:

    • To load the data, we use the Employees property, which returns the Table.

    • Example get data from database :

      Drag and drop the DataGridView in Form1.cs.

      Page: Form1.cs:

        using System;
        using System.Windows.Forms;
      
        namespace LinqToSqlProject
        {
            public partial class Form1 : Form
            {
                public Form1()
                {
                    InitializeComponent();
                }
      
                private void Form1_Load(object sender, EventArgs e)
                {
                    //Establish the connection
                    DataClasses1DataContext data = new DataClasses1DataContext();
                    //get the property
                    dataGridView1.DataSource = data.Employees;
                }
      
                private void dataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
                {
      
                }
            }
        }
      

      Server Explorer:

      Toolbox:

      DataClasses1.dbml:

      SQL Server:

      Get the data in Form1: when run the Form1

That's how ORM works.


Q. Create the Form to see the employee data one by one?

Ans:- Steps:

  1. Open SSMS:

    Create SQL Server

    Notes: Make sure to check the Trust server certificate option, otherwise you will get an error.

  2. Create databas and add data:

     Create database Company;
    
     CREATE TABLE Employee (
         EmployeeID INT PRIMARY KEY,
         FirstName NVARCHAR(50),
         LastName NVARCHAR(50),
         JobTitle NVARCHAR(100),
         Salary DECIMAL(10, 2),
         Department NVARCHAR(50),
         HireDate DATE
     );
    
     INSERT INTO Employee (EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate) VALUES
     (1, 'John', 'Doe', 'Software Engineer', 75000.00, 'IT', '2022-01-15'),
     (2, 'Jane', 'Smith', 'Data Analyst', 68000.00, 'IT', '2021-03-22'),
     (3, 'Michael', 'Johnson', 'Project Manager', 85000.00, 'Operations', '2020-08-30'),
     (4, 'Emily', 'Davis', 'HR Specialist', 60000.00, 'Human Resources', '2019-11-05'),
     (5, 'David', 'Brown', 'Senior Developer', 90000.00, 'IT', '2018-07-17'),
     (6, 'Linda', 'Wilson', 'Marketing Manager', 78000.00, 'Marketing', '2023-04-12'),
     (7, 'Robert', 'Taylor', 'Accountant', 67000.00, 'Finance', '2021-12-01'),
     (8, 'Mary', 'Lee', 'Customer Support', 52000.00, 'Support', '2020-06-15'),
     (9, 'James', 'Martin', 'DevOps Engineer', 83000.00, 'IT', '2017-05-23'),
     (10, 'Patricia', 'Anderson', 'Business Analyst', 72000.00, 'Operations', '2019-02-14');
    

    Notes: Do not close the server

  3. Create Windows Form App:

    • Give the project name ShowEmployeeFromDatabase.

  4. Open Server Explorer and configure the database:

    • Copy the server name from SSMS, then go to Server Explorer and right-click Data Connections. Select the data source and press next. Enter the server name, then select the database from Select or enter a database name:. Press ok.

      Notes: Make sure to check the Trust server certificate option, otherwise you will get an error.

      If you do not see the database, please restart the system.

  1. ORM setup:

    • Add a new item LINQ to SQL (.dbml file).

    • Go to Server Explorer, open the Tables folder, and drag and drop the Employee table to the left side of Company.dbml. ORM setup is complete.

  2. Crete design:

    • Go to Solution Explorer and open Form1.cs, which is automatically created, or you can choose your own. Next, open Toolbox and then Common Control to create a design like this.

  3. Write the code in Form1.cs:

     using System;
     using System.Collections.Generic;
     using System.Linq;
     using System.Windows.Forms;
    
     namespace ShowEmployeeFromDatabase
     {
         public partial class Form1 : Form
         {
             CompanyDataContext dc; // DataContext instance to connect to the database
             List<Employee> employees; // List to hold Employee data
             int currentRecordIndex = 0; // Variable to track the current record index
    
             public Form1()
             {
                 InitializeComponent();
             }
    
             private void Form1_Load(object sender, EventArgs e)
             {
                 //CompanyDataContext dc = new CompanyDataContext();//Create instance
                 //List<Employee> emp = new List<Employee>();//emp is a list which store only Employee type of data
    
                 // Initialize the DataContext and fetch employee data into the list
                 dc = new CompanyDataContext();
                 employees = dc.Employees.ToList();
    
                 // Show the first record on form load
                 ShowData();
             }
    
             private void ShowData()
             {
                 // Display employee data in the respective text boxes
                 textBox2.Text = employees[currentRecordIndex].EmployeeID.ToString();
                 textBox1.Text = $"{employees[currentRecordIndex].FirstName} {employees[currentRecordIndex].LastName}";
                 textBox3.Text = employees[currentRecordIndex].JobTitle;
                 textBox4.Text = employees[currentRecordIndex].Salary.ToString();
                 textBox5.Text = employees[currentRecordIndex].Department;
                 textBox6.Text = employees[currentRecordIndex].HireDate.ToString("yyyy-MM-dd"); // Format the date for clarity
             }
    
             private void Prev_Click(object sender, EventArgs e)
             {
                 if (currentRecordIndex > 0) 
                 {
                     currentRecordIndex -= 1;
                     ShowData();
                 }
                 else
                 {
                     MessageBox.Show("This is the first record of the table!");
                 }
             }
    
             private void Next_Click(object sender, EventArgs e)
             {
                 if (currentRecordIndex < employees.Count - 1)
                 {
                     currentRecordIndex += 1;
                     ShowData();
                 }
                 else
                 {
                     MessageBox.Show("This is the last record of the table!");
                 }
             }
    
             private void Close_Click(object sender, EventArgs e)
             {
                 this.Close(); // Close the form when the close button is clicked
             }
         }
     }
    

    This code is a Windows Forms application in C# that connects to a database using LINQ to SQL. It displays employee records and allows navigation through them using "Next" and "Previous" buttons.

    Key Points:

    • DataContext (CompanyDataContext): Connects to the database.

    • Employee List (employees): Stores employee records fetched from the database.

    • Record Navigation:

      • currentRecordIndex tracks the current record.

      • ShowData() displays the current record's details in text boxes.

      • "Previous" and "Next" Buttons: Navigate through records.

    • Close Button: Closes the form.

  4. Output:

    If you don't see the output video here, go hear Outpur.

    The form loads employee data on startup and provides a simple interface for viewing and navigating records.


Performing CRUD Operations using LINQ:

  • CRUD: Create, Read, Update, and Delete.

Setup project:

  • Create a Windows application named CRUD_WindowsForm.

  • Configure the database in Server Explorer.

  • Use an existing database and table.

  • Create a DatabaseData.dbml file and add the employee table.

  • Create a Form1.cs file and design it to display the data:

    In the properties, name the Insert button InsertData, the Update button button2, the Delete button button4, and the Close button button3. Name the DataGridView as dataGridView1.

  • Create a Form2Insert.cs file and design it to insert data:

    In this form, name the 7 textBox controls from textBox1 to textBox7. Name the Submit button button1, the Clear button button2, and the Close button button3.

  • Create a Form3Update.cs file and design it to update data:

    In this form, name the 7 textBox controls from textBox1 to textBox7. Name the Submit button button1, the Clear button button2, and the Close button button3. Set the Modifiers property of all textBox controls to internal.

Codeing Part:

Form1.cs:

using System;
using System.Linq;
using System.Windows.Forms;

namespace CRUD_WindowsForm
{
    public partial class Form1 : Form
    {
        DatabaseDataDataContext dataContext;
        public Form1()
        {
            InitializeComponent();
        }

        //Data loader method
        private void LoadData()
        {
            dataContext = new DatabaseDataDataContext();
            dataGridView1.DataSource = dataContext.Employees;
        }

        //Form Load 1 time
        private void Form1_Load(object sender, EventArgs e)
        {
            LoadData();
        }

        //InsertData:-
        private void button1_Click(object sender, EventArgs e)
        {
            Form2Insert fi = new Form2Insert(); //Use form 2 to insert data
            fi.ShowDialog();
            LoadData();
        }

        //UpdateData:-
        private void button2_Click(object sender, EventArgs e)
        {
            if (dataGridView1.SelectedRows.Count > 0) // Check if a row is selected
            {
                DataGridViewRow selectedRow = dataGridView1.SelectedRows[0];

                Form3Update fu = new Form3Update(); // Initialize Form3Update //Use same form "form 2" to update the value by changing the modifier in textBox field private to internal
                fu.textBox1.ReadOnly = true;
                fu.button2.Enabled = false;
                fu.button1.Text = "Update";
                fu.textBox1.Text = selectedRow.Cells[0].Value.ToString() ?? String.Empty; ;
                fu.textBox2.Text = selectedRow.Cells[1].Value.ToString() ?? String.Empty; 
                fu.textBox3.Text = selectedRow.Cells[2].Value.ToString() ?? String.Empty; 
                fu.textBox4.Text = selectedRow.Cells[3].Value.ToString() ?? String.Empty; 
                fu.textBox5.Text = selectedRow.Cells[4].Value.ToString() ?? String.Empty; 
                fu.textBox6.Text = selectedRow.Cells[5].Value.ToString() ?? String.Empty; 
                fu.textBox7.Text = selectedRow.Cells[6].Value.ToString() ?? String.Empty;
                //'dataGridView1' is DataGridView name which is taken from property
                fu.ShowDialog();
                LoadData();
            }
            else
            {
                MessageBox.Show("Please select a row first.", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); // Prompt user to select a row if none is selected
            }
        }

        //Close
        private void button3_Click(object sender, EventArgs e)
        {
            this.Close();
        }


        //Delete
        private void button4_Click(object sender, EventArgs e)
        {
            if (dataGridView1.SelectedRows.Count > 0) 
            {
                if (MessageBox.Show("Are you suore want to delete this data?", "Confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
                {
                    int Eno = Convert.ToInt32(dataGridView1.SelectedRows[0].Cells[0].Value);
                    Employee obj = dataContext.Employees.SingleOrDefault(E => E.EmployeeID == Eno);
                    dataContext.Employees.DeleteOnSubmit(obj);
                    dataContext.SubmitChanges();
                    LoadData();
                }
            }
            else
            {
                MessageBox.Show("Please select a row first for deletion.", "Information",MessageBoxButtons.OK, MessageBoxIcon.Information); 
            }
        }
    }
}

Form2Insert.cs:

using System;
using System.Windows.Forms;

namespace CRUD_WindowsForm
{
    public partial class Form2Insert : Form
    {
        public Form2Insert()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            DatabaseDataDataContext db = new DatabaseDataDataContext();

            Employee eobj = new Employee();

            eobj.EmployeeID = int.Parse(textBox1.Text);
            eobj.FirstName = textBox2.Text;
            eobj.LastName = textBox3.Text;
            eobj.JobTitle = textBox4.Text;
            eobj.Salary = decimal.Parse(textBox5.Text);
            eobj.Department = textBox6.Text;
            eobj.HireDate = DateTime.Now;

            // Assuming you want to insert the new employee into the database
            db.Employees.InsertOnSubmit(eobj);
            db.SubmitChanges();
            MessageBox.Show("Employee added successfully!");
        }

        private void button2_Click(object sender, EventArgs e)
        {
            foreach (Control ctrl in this.Controls)
            {
                if(ctrl is TextBox)
                {
                    TextBox tb = ctrl as TextBox;
                    tb.Clear();
                }
            }
            textBox1.Focus();
        }

        private void button3_Click(object sender, EventArgs e)
        {
            this.Close();
        }
    }
}

Form3Update.cs

using System;
using System.Linq;
using System.Windows.Forms;

namespace CRUD_WindowsForm
{
    public partial class Form3Update : Form
    {
        public Form3Update()
        {
            InitializeComponent();
        }

        //Submit
        private void button1_Click(object sender, EventArgs e)
        {
            DatabaseDataDataContext db = new DatabaseDataDataContext();

            //We do not want to create new record we want to update that why we need the refrence of current record that why we use ' db.Employees.SingleOrDefault(E=>E.EmployeeID == int.Parse(textBox1.Text))'
            Employee eobj = db.Employees.SingleOrDefault(E => E.EmployeeID == int.Parse(textBox1.Text));

            if(eobj != null)
            {
                //If use not modify old value taken
                eobj.FirstName = textBox2.Text;
                eobj.LastName = textBox3.Text;
                eobj.JobTitle = textBox4.Text;
                eobj.Salary = decimal.Parse(textBox5.Text);
                eobj.Department = textBox6.Text;
                eobj.HireDate = DateTime.Now;

                db.SubmitChanges();
                MessageBox.Show("Employee update successfully!");
            }
            else
            {
                MessageBox.Show("Employee not found.");
            }
        }

        //Close
        private void button3_Click(object sender, EventArgs e)
        {
            this.Close();
        }
    }
}

Coding Explanation:

  • LoadData Method:

    The LoadData method is responsible for loading and displaying the employee data on Form1. Here is the method:

      private void LoadData()
      {
          dataContext = new DatabaseDataDataContext(); // Establish a connection to the database
          dataGridView1.DataSource = dataContext.Employees; // Bind the Employees table to the DataGridView
      }
    

    Explanation of the LoadData Method

    1. Connecting to the Database:

      • dataContext = new DatabaseDataDataContext();

      • This line initializes a new instance of DatabaseDataDataContext, which represents the database connection and provides access to the Employees table.

    2. Binding Data to the DataGridView:

      • dataGridView1.DataSource = dataContext.Employees;

      • This line sets the DataSource property of dataGridView1 (the grid displaying data) to the Employees table. This binds the employee data to the grid, displaying it on the form.

    3. dataContext is a variable of type DatabaseDataDataContext, which is a class. It is declared globally in the Form1 class as DatabaseDataDataContext dataContext;. And dataContext inslized by LoadData() method.

  • Close Button Functionality:

    For every Close button on your forms, the code is consistent. Double-click the Close button in the form designer, which will automatically generate an event handler. Inside this event handler, add the following line of code:

      this.Close();
    

    This will close the current form window, when the Close button is clicked.

  • Clear Button Functionality:

    Clears all text boxes in the form and sets the focus back to textBox1.

      private void button2_Click(object sender, EventArgs e)
      {
          foreach (Control ctrl in this.Controls)
          {
              if (ctrl is TextBox)
              {
                  TextBox tb = ctrl as TextBox;
                  tb.Clear();
              }
          }
          textBox1.Focus();
      }
    

    Explanation:

    • Control: In Windows Forms, a Control is a base class for all components that are displayed on a form (e.g., buttons, text boxes, labels, etc.).

    • this.Controls: Represents a collection of all the controls present on the form.

    • if (ctrl is TextBox): This line checks if the current control (ctrl) is a TextBox. is TextBox, the is keyword checks if an object is of a specific type. Here, it checks whether ctrl is a TextBox.

    • TextBox tb = ctrl as TextBox;: This line tries to convert the ctrl object into a TextBox. The as keyword is used for safe casting. If ctrl is a TextBox, it will be converted to a TextBox and assigned to the variable tb. If not, tb will be null.

    • tb.Clear();: This line clears the text inside the TextBox. Clear(): The Clear() method is a TextBox method that removes all text from the text box, making it empty.

  • Delete Button Functionality:

    Deletion of a selected employee record from the database in a Windows Forms application.

      //Delete
      private void button4_Click(object sender, EventArgs e)
      {
          // Check if any row is selected in the DataGridView
          if (dataGridView1.SelectedRows.Count > 0) 
          {
              // Ask the user for confirmation before deleting the selected record
              if (MessageBox.Show("Are you sure you want to delete this data?", "Confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
              {
                  // Get the EmployeeID (assumed to be in the first cell of the selected row) from the selected row
                  int Eno = Convert.ToInt32(dataGridView1.SelectedRows[0].Cells[0].Value);
    
                  // Retrieve the employee object from the database that matches the selected EmployeeID
                  Employee obj = dataContext.Employees.SingleOrDefault(E => E.EmployeeID == Eno);
    
                  // If the employee is found, delete it from the database
                  dataContext.Employees.DeleteOnSubmit(obj);
    
                  // Submit the changes to the database to perform the deletion
                  dataContext.SubmitChanges();
    
                  // Reload the data in the DataGridView to reflect the deletion
                  LoadData();
              }
          }
          else
          {
              // Display a message prompting the user to select a row first if no row is selected
              MessageBox.Show("Please select a row first for deletion.", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); 
          }
      }
    

    Explanation:

    • Retrieving the Employee ID:

        int Eno = Convert.ToInt32(dataGridView1.SelectedRows[0].Cells[0].Value);
      

      This line retrieves the EmployeeID from the first cell (Cells[0]) of the selected row. Eno stores this ID as an integer, which is used to identify the employee to be deleted.

    • Fetching the Employee Object:

        Employee obj = dataContext.Employees.SingleOrDefault(E => E.EmployeeID == Eno);
      

      The code queries the database to find the Employee object that matches the selected EmployeeID using LINQ.

      SingleOrDefault is used to retrieve a single employee record that matches the condition. If no match is found, obj will be null.

    • Deleting the Employee:

        dataContext.Employees.DeleteOnSubmit(obj);
        dataContext.SubmitChanges();
      

      DeleteOnSubmit marks the employee object for deletion in the database.

      SubmitChanges commits the changes to the database, effectively removing the employee record.

  • Insert Button Functionality:

    To implement the insert functionality, double-click the Insert button in the form designer to generate an event handler. Inside the generated button1_Click method, add the following code:

      private void button1_Click(object sender, EventArgs e)
      {
          Form2Insert fi = new Form2Insert(); // Create an instance of Form2Insert to handle data insertion
          fi.ShowDialog(); // Open Form2Insert as a modal dialog
          LoadData(); // Reload the data on Form1 after the insertion is complete
      }
    

    Explanation of the Insert Button Code

    1. Creating an Instance of Form2Insert:

      • Form2Insert fi = new Form2Insert();: This line creates an instance of the Form2Insert class, which is the form where users can enter new employee data.
    2. Opening Form2Insert:

      • fi.ShowDialog();: This opens the Form2Insert form as a modal dialog. A modal dialog means that the user must interact with this form before returning to the main form (Form1). The control flow pauses here until Form2Insert is closed.
    3. Reloading Data on Form1:

      • LoadData();: After the Form2Insert form is closed, control returns to this method. The LoadData() method is then called to refresh the data grid on Form1, reflecting any new data inserted via Form2Insert.
  • Submit button Functionality in Form2Insert.cs:

    This method handles the insertion of a new employee record into the database when the user clicks the "Submit" button in a Windows Forms application.

      private void button1_Click(object sender, EventArgs e)
      {
          //Create new instance
          DatabaseDataDataContext db = new DatabaseDataDataContext();
    
          //Create New Employee Object
          Employee eobj = new Employee();
    
          //Assign Values
          eobj.EmployeeID = int.Parse(textBox1.Text);
          eobj.FirstName = textBox2.Text;
          eobj.LastName = textBox3.Text;
          eobj.JobTitle = textBox4.Text;
          eobj.Salary = decimal.Parse(textBox5.Text);
          eobj.Department = textBox6.Text;
          eobj.HireDate = DateTime.Now;
    
          // Assuming you want to insert the new employee into the database
          db.Employees.InsertOnSubmit(eobj);
          db.SubmitChanges();
          MessageBox.Show("Employee added successfully!");
      }
    
    • DatabaseDataDataContext db = new DatabaseDataDataContext();: Establish a connection to the database. A new instance of the DatabaseDataDataContext class is created, which serves as the connection between the application and the database. This instance, db, allows you to interact with the database tables.

    • Create New Employee Object Employee eobj = new Employee();: A new instance of the Employee class is created. This object, eobj, represents a new record that you want to insert into the Employees table in the database.

    • Insert New Record db.Employees.InsertOnSubmit(eobj);: The InsertOnSubmit method is called on the Employees table (which is a part of the db context). This method marks the eobj object (the new employee) for insertion into the database when the changes are submitted.

    • Save Changes db.SubmitChanges();: The SubmitChanges method is called to save all the changes made to the database through the db context. This commits the insertion of the new Employee record into the Employees table in the database.

    Control return back to Form1.cs-> button1_Click.

  • Submit button Functionality in Form1.cs:

    This method is used to update an existing employee record in the database when the user clicks the "Update" button in the main form of a Windows Forms application. The code opens a new form (Form3Update) where the user can edit the details of the selected employee.

    This method is triggered when the "Update" button (button2) is clicked.

      //UpdateData:-
      private void button2_Click(object sender, EventArgs e)
      {
          if (dataGridView1.SelectedRows.Count > 0) // Check if a row is selected
          {
              DataGridViewRow selectedRow = dataGridView1.SelectedRows[0];
    
              Form3Update fu = new Form3Update(); // Initialize Form3Update //Use same form "form 2" to update the value by changing the modifier in textBox field private to internal
              fu.textBox1.ReadOnly = true;
              fu.button2.Enabled = false;
              fu.button1.Text = "Update";
              //set the data:
              fu.textBox1.Text = selectedRow.Cells[0].Value.ToString() ?? String.Empty; ;
              fu.textBox2.Text = selectedRow.Cells[1].Value.ToString() ?? String.Empty; 
              fu.textBox3.Text = selectedRow.Cells[2].Value.ToString() ?? String.Empty; 
              fu.textBox4.Text = selectedRow.Cells[3].Value.ToString() ?? String.Empty; 
              fu.textBox5.Text = selectedRow.Cells[4].Value.ToString() ?? String.Empty; 
              fu.textBox6.Text = selectedRow.Cells[5].Value.ToString() ?? String.Empty; 
              fu.textBox7.Text = selectedRow.Cells[6].Value.ToString() ?? String.Empty;
              //'dataGridView1' is DataGridView name which is taken from property
              fu.ShowDialog();
              LoadData();
          }
          else
          {
              MessageBox.Show("Please select a row first.", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); // Prompt user to select a row if none is selected
          }
      }
    
    • Form3Update fu = new Form3Update();: A new instance of the Form3Update class (which is a form designed for updating employee details) is created. This form will be used to display the selected employee's data and allow the user to modify it.

    • textBox1.ReadOnly = true;: The first textbox (textBox1) in Form3Update is made read-only, so the user cannot modify the Employee ID. This ensures that the primary key remains unchanged during the update process.

    • button2.Enabled = false;: The second button (button2) in Form3Update is disabled, which might be an additional button not used for the update process.

    • button1.Text = "Update";: The text on the first button (button1) in Form3Update is changed to "Update" to reflect the action that will be performed when clicked.

    • fu.ShowDialog();: This displays the Form3Update form as a modal dialog, meaning that the user must close this form before they can return to the main form (Form1). The user will make changes in this form and submit them.

    • LoadData();: After the user closes the update form, the LoadData() method is called to refresh the DataGridView in the main form, reflecting any changes made to the employee's data.


Calling Stored Procedure using LINQ:

  • When you open the Server Explorer in Visual Studio, under the Data Connections node, you will see a list of available databases. Inside each database connection, you'll find a folder named Stored Procedures. This folder may be empty if you haven't created any stored procedures yet.

Creating a Stored Procedure:

  1. Right-click on the Stored Procedures folder under your database in the Object Explorer.

  2. Select New Stored Procedure.

  3. A new query window will open with a template for creating a stored procedure.

SQL Code to Create a Stored Procedure:

Let's create a stored procedure that returns all records from the Employee table:

CREATE PROCEDURE Employee_Select
AS
SELECT EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate
FROM Employee Order By EmployeeID;

--Or
CREATE PROCEDURE Employee_Select
AS
BEGIN
    SELECT EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate
    FROM Employee
    ORDER BY EmployeeID;
END

The code you provided is SQL code, specifically Transact-SQL (T-SQL), which is used for writing queries and stored procedures in SQL Server.

  • CREATE PROCEDURE: This is a SQL command used to create a stored procedure in a SQL Server database.

  • AS and BEGIN ... END: These keywords define the body of the stored procedure. The first version of this code omits BEGIN ... END, which is optional when i have only one SQL statement, but the second version includes them to clearly define the start and end of the procedure's body.

  • SELECT ... FROM ... ORDER BY: This is a SQL query that selects specific columns (EmployeeID, FirstName, LastName, etc.) from the Employee table and orders the results by EmployeeID.

  • After writing the SQL code, right-click on the dbo.Procedure.sql page, then select Execute or press Ctrl + Shift + E.If any errors occur, they will be displayed at the bottom of the window. If there are no errors, a success message will be shown.

  • There’s no need to save the command because the procedure is created directly on the database server.

  • If you refresh the Server Explorer, you will find the stored procedure listed. In this example, it will be named Employee_Select.

Calling the Stored Procedure using LINQ:

  1. Double-click on the .dbml file in your project.

  2. Drag and drop the Employee_Select stored procedure from the Server Explorer onto the right-hand side of the .dbml design surface.

  • This action will automatically generate a method named Employee_Select() in the DatabaseDataDataContext class, which is defined in the DatabaseData.designer.cs file.

      public ISingleResult<Employee_SelectResult> Employee_Select()
      {
          IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())));
          return ((ISingleResult<Employee_SelectResult>)(result.ReturnValue));
      }
    

  • The method Employee_Select() returns an ISingleResult<Employee_SelectResult>, where Employee_SelectResult is a class generated by LINQ to SQL.

  • The Employee_SelectResult class contains properties corresponding to the columns selected in the stored procedure (EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate).

  • ISingleResult<Employee_SelectResult> is a type in LINQ to SQL that represents the result of executing a stored procedure or a query that returns a sequence of records.

    1. ISingleResult<T>: ISingleResult is an interface in LINQ to SQL, specifically in the System.Data.Linq namespace. It is used to represent the result of a query or stored procedure that returns a collection of records (rows). T is the type of the individual records in that collection.

      ISingleResult is like a table of Employee_SelectResult in the database, similar to Table<Employee>, which is just a table of Employee.

    2. Employee_SelectResult: This is a class generated by LINQ to SQL when you drag and drop a stored procedure into the .dbml design surface. The class Employee_SelectResult contains properties that match the columns returned by the Employee_Select stored procedure. For example, if the stored procedure returns columns like EmployeeID, FirstName, LastName, etc., the Employee_SelectResult class will have properties corresponding to these columns.

    ISingleResult<Employee_SelectResult> represents a collection of Employee_SelectResult objects, where each object corresponds to a row returned by the Employee_Select stored procedure.

    ISingleResult ensures that you can enumerate over the result set, typically using a foreach loop, to access each Employee_SelectResult object in the collection.

Difference Between Employee Class and Employee_SelectResult Class:

  • Employee Class: Represents the entire Employee table. It contains properties for all the columns in the table, so when you use this class, you automatically retrieve all the columns.

  • Employee_SelectResult Class: Represents the result of the Employee_Select stored procedure. This class is similar to the Employee class but is specifically tailored to match the columns returned by the stored procedure. This allows you to specify and retrieve only the columns you need from the Employee table, offering more control over the data you work with.

  • In summary, while the Employee class automatically includes all columns from the Employee table, the Employee_SelectResult class allows you to retrieve only the columns specified in your stored procedure, giving you greater flexibility in managing the data and you can write any type of SQL query like:

      CREATE PROCEDURE Employee_SelectBySalary
          @MinimumSalary DECIMAL(18, 2)
      AS
      BEGIN
          SELECT EmployeeID, FirstName, Salary
          FROM Employee
          WHERE Salary > @MinimumSalary
          ORDER BY Salary DESC;
      END
    

Calling the Employee_Select() method and displaying the results in a DataGridView:

  • Create a new form named Form1SQL with a DataGridView. Double-click the form (not the DataGridView). When you do this, the Form1SQL.cs page will open. Then, import the using System.Data.Linq; namespace. write this code:-

      private void Form1SQL_Load(object sender, EventArgs e)
      {
          DatabaseDataDataContext db = new DatabaseDataDataContext();
          ISingleResult<Employee_SelectResult> tab = db.Employee_Select();
          dataGridView1.DataSource = tab;
      }
    

    In the future, if the Employee_Select() method needs a parameter, we will be able to pass it.

    In the case of dc.Employee_Select, we use a predefined property, but in dc.Employee_Select(), we use a stored procedure to perform this.

    Update Program.cs to Run Form1SQL: Modify the Main Method in Program.cs. Change the Application.Run line to use Form1SQL instead of the default form.

      using System;
      using System.Windows.Forms;
    
      namespace CRUD_WindowsForm
      {
          static class Program
          {
              [STAThread]
              static void Main()
              {
                  Application.EnableVisualStyles();
                  Application.SetCompatibleTextRenderingDefault(false);
                  Application.Run(new Form1SQL());
              }
          }
      }
    

    Summary:

    • Form Creation: You created Form1SQL with a DataGridView and implemented the Form1SQL_Load event to load data.

    • Loading Data: In Form1SQL_Load, you instantiated DatabaseDataDataContext, called Employee_Select(), and set the DataSource of dataGridView1 to the result.

    • Running the Form: You updated Program.cs to run Form1SQL instead of the default form.

    This setup ensures that when Form1SQL is loaded, it will call the Employee_Select() stored procedure, retrieve the data, and display it in the DataGridView.

If you want to retrieve any type of data according to your requirements, you can do so by changing the query of Employee_SelectResult . For example, if I want the data based on the department name:

CREATE PROCEDURE Employee_Select(@Department Varchar(50) = Null)
AS
Begin
if @Department is Null
    SELECT EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate FROM Employee Order By EmployeeID;
Else
    SELECT EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate FROM Employee Where Department = @Department Order By EmployeeID;
End;

/* Old Query:

CREATE PROCEDURE Employee_Select
AS
SELECT EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate
FROM Employee Order By EmployeeID;*/

Follow the same process: delete the current Employee_Select available in the .dbml file, then re-execute the DatabaseData.designer.cs and drag and drop it on the right side of the .dbml file. Now you will see it takes a parameter.

private void Form1SQL_Load(object sender, EventArgs e)
{
      DatabaseDataDataContext db = new DatabaseDataDataContext();
      ISingleResult<Employee_SelectResult> tab = db.Employee_Select();
      dataGridView1.DataSource = tab;
}

Now hear db.Employee_Select(), if you pass null, you get all values. If you pass a department name, you get data for that department. If you don't pass anything, you get an error.

Here is the code for the DatabaseData.designer.cs file:

public ISingleResult<Employee_SelectResult> Employee_Select([global::System.Data.Linq.Mapping.ParameterAttribute(Name="Department", DbType="VarChar(50)")] string department)
{
    IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), department);
    return ((ISingleResult<Employee_SelectResult>)(result.ReturnValue));
}

Now it's taking a parameter.

If the stored procedure is parameterized, then the method becomes parameterized. If the stored procedure is non-parameterized, then the DatabaseData.designer.cs method is also non-parameterized.


How to write a query on a database using SQL:

Create table to understande:

Employee table:

EmployeeIDFirstNameLastNameJobTitleSalaryDepartmentHireDate
1JohnDoeSoftware Engineer75000.00IT2022-01-15
2JaneSmithData Analyst68000.00IT2021-03-22
3MichaelJohnsonProject Manager85000.00Operations2020-08-30
4EmilyDavisHR Specialist60000.00Human Resources2019-11-05
5DavidBrownSenior Developer90000.00IT2018-07-17
6LindaWilsonMarketing Manager78000.00Marketing2023-04-12
7RobertTaylorAccountant67000.00Finance2021-12-01
8MaryLeeCustomer Support52000.00Support2020-06-15
9JamesMartinDevOps Engineer83000.00IT2017-05-23
10PatriciaAndersonBusiness Analyst72000.00Operations2019-02-14

Department table:

CREATE TABLE Department (
    DepartmentID INT PRIMARY KEY,
    DepartmentName VARCHAR(50) NOT NULL,
    Manager VARCHAR(50)
);
INSERT INTO Department (DepartmentID, DepartmentName, Manager) VALUES
(1, 'IT', 'Alice Cooper'),
(2, 'Operations', 'Bob Stevens'),
(3, 'Human Resources', 'Catherine Green'),
(4, 'Marketing', 'Diana Prince'),
(5, 'Finance', 'Edward Norton'),
(6, 'Support', 'Fiona White');
DepartmentIDDepartmentNameManager
1ITAlice Cooper
2OperationsBob Stevens
3Human ResourcesCatherine Green
4MarketingDiana Prince
5FinanceEdward Norton
6SupportFiona White

Both tables are in the same database Company in SQL Server. Configure the database in Server Explorer in Visual Studio. Drag and drop both tables into the .dbml file on the left side. Then, create the Form2SQL.cs file.

Double-click on the form, and a load method will be created:

private void Form2SQL_Load(object sender, EventArgs e){}

Change the Program.cs file code to run Form2SQL().

Application.Run(new Form2SQL());

Writing the SQL query:

  • Syntac:-

Select * | <collist> form <table> as <allas> [<clauses>]
  • Sequence for writing the SQL query:

    Clauses:

    • Where

    • Group By

    • Having

    • Order By

Writing the LINQ query:

  • Syntax:

      From <alias> in <table> [<clauses>] select <alias> | new {<list of columns/collist>}
    
  • Clauses:

    • Where

    • Group By

    • Order By

But you can use Having clauses in LINQ query.

  • Example: Retrive all data from database

      From E in dc.Employees select E;
    

    To strore the data:

      var tabl = From E in dc.Employees select E;
    

Example of LINQ:

  1. Get all IT department employees:

     var tab = from E in dc.Employees where E.Department == "IT" select E;
    
     private void Form2SQL_Load(object sender, EventArgs e)
     {
         DatabaseDataDataContext db = new DatabaseDataDataContext();
         var tab = from E in db.Employees select E;
         dataGridView1.DataSource = tab;
     }
    

  2. Get EmployeeID, FirstName, LastName, JobTitle and Salary of all IT department employees:

     var tab = from E in db.Employees where E.Department == "IT" 
     select new { E.EmployeeID, E.FirstName, E.LastName, E.JobTitle, E.Salary };
    
     private void Form2SQL_Load(object sender, EventArgs e)
     {
         DatabaseDataDataContext db = new DatabaseDataDataContext();
    
         var tab = from E in db.Employees where E.Department == "IT"
         select new { E.EmployeeID, E.FirstName, E.LastName, E.JobTitle, E.Salary};
    
         dataGridView1.DataSource = tab.ToList();
     }
    

  3. Create filter by Department:

    • Add comboBox in form. And add this code in Form Load:

        //Form2SQL.cs
        using System;
        using System.Data;
        using System.Linq;
        using System.Windows.Forms;
      
        namespace CRUD_WindowsForm
        {
            public partial class Form2SQL : Form
            {
                DatabaseDataDataContext db;
                public Form2SQL()
                {
                    InitializeComponent();
                }
      
                //Form load method
                private void Form2SQL_Load(object sender, EventArgs e)
                {
                    db = new DatabaseDataDataContext();
      
                    var dep = from E in db.Employees select new { E.Department };
                    //comboBox1.DataSource = dep; //comboBox1.DataSource = dep; This shows all departments, but the problem is it comes in object form like { Department = "IT" }, and I want it to show only "IT".
                    comboBox1.DataSource = dep.Distinct(); //This shows all departments without duplicates, but the problem is it comes in object form like { Department = "IT" }, and I want it to show only "IT".
                    comboBox1.DisplayMember = "Department"; //This shows all departments in a readable form like "IT"
      
                    dataGridView1.DataSource = from E in db.Employees select E;
      
                }
      
                //Combo box method
                private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
                {
                    dataGridView1.DataSource = from E in db.Employees where E.Department == comboBox1.Text select E; //Filter the data and show in dataGridView1
                }
            }
        }
      

      Explanation:

      • DatabaseDataDataContext db; Database Context (db): The DatabaseDataDataContext object (db) is declared as a class-level variable.

      • Constructor: The Form2SQL constructor initializes the form components but doesn't do anything specific for the database until the form loads.

      • Form Load (Form2SQL_Load) : When the form is loaded, the DatabaseDataDataContext object is initialized db = new DatabaseDataDataContext();, establishing a connection to the database .

      • Initial Data Load (dataGridView1.DataSource = from E in db.Employees select E;) : This line sets the DataGridView (dataGridView1) data source to all rows from the Employees table. The select E query retrieves all columns for each employee .

      • Department Query (dep) var dep = from E in db.Employees select new { E.Department };: This LINQ query retrieves all department names from the Employees table. The query returns an anonymous object with a single property Department for each row.

      • The Distinct() method ensures that each department name appears only once, eliminating duplicates.

      • DisplayMember (comboBox1.DisplayMember = "Department";): The DisplayMember property is set to "Department", which tells the ComboBox to display just the department names, not the full anonymous object { Department = "IT" }. The comment notes an issue with duplicates appearing if Distinct() isn't used correctly.

      Event Handling:

      private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
      {
          dataGridView1.DataSource = from E in db.Employees where E.Department == comboBox1.Text select E;
      }
      
      • ComboBox Selection Change (comboBox1_SelectedIndexChanged):

        • When the user selects a department from the ComboBox, the SelectedIndexChanged event triggers.

        • The DataGridView is then filtered to show only those employees whose Department matches the selected value in comboBox1. The query retrieves all columns (select E) for the matching employees.

    • Output: If you do not see the output, click on this link https://cdn.hashnode.com/res/hashnode/image/upload/v1723745503915/611620fb-4b26-4427-8e4f-b426ffe79266.gif?auto=format,compress&gif-q=60&format=webm.

  4. Get data ordered by Salary:

     dataGridView1.DataSource = from E in db.Employees 
     orderby E.Salary select E;
    
  5. Get data ordered by FirstName in descending order:

     dataGridView1.DataSource = from E in db.Employees 
     orderby E.FirstName descending select E;
    
  6. Get required columns:

     dataGridView1.DataSource = from E in db.Employees 
     select new { E.EmployeeID, E.FirstName, E.Department, E.Salary};
    
  7. Change the column names or alias the names First_Name = E.FirstName:

     dataGridView1.DataSource = from E in db.Employees 
     select new { E.EmployeeID, First_Name = E.FirstName, E.Department, E.Salary};
    
  8. Get the number of employees in each Department:

     //In SQL:
     Select Department, EmployeeCount = count(*) from Emp Group By Department;
     //In LINQ:
     dataGridView1.DataSource = from E in db.Employees
                                group E by E.Department into deptGroup
                                select new
                                {
                                    Department = deptGroup.Key,
                                    EmployeeCount = deptGroup.Count()
                                };
    

    The into keyword allows you to give a name to the grouped result and continue querying it.

    deptGroup is name which represents each group of employees that share the same department.

    deptGroup: After grouping, each deptGroup represents a group of employees in a specific department. It's a collection of all employees who have the same department value.

    The Key property is used to access the value by which the grouping was performed.

    deptGroup.Key: This refers to the department name (e.g., "IT", "HR") that the group is based on.

    Count() is an aggregate function in LINQ. We can use all aggregate functions like Max, Min, Sum, Avg, and Count.

  9. Get the department which have greater than 3 employees :

     //In SQL:
     Select Department, EmployeeCount = count(*) from Emp Group By Department Having Count(*) > 5;
    

    In LINQ: LINQ doesn't have HAVING clauses. So, how do you achieve this? In LINQ, when you use WHERE clauses before GROUP BY, it works like a WHERE clause. But if you use WHERE clauses after GROUP BY, it works like a HAVING clause.

     //In LINQ
     dataGridView1.DataSource = from E in db.Employees
                                group E by E.Department into deptGroup
                                where deptGroup.Count() > 5
                                select new
                                {
                                    Department = deptGroup.Key,
                                    EmployeeCount = deptGroup.Count()
                                };
    
  10. Use multiple clauses:

    //In SQL:
    Select Dept, Count = Count(*) From Employees Where Department = "IT" Group By Dept; 
    //In LINQ:
    dataGridView1.DataSource = from E in db.Employees
                               where E.Department == "IT"
                               group E by E.Department into D
                               select new { Dept = D.Key, Count = D.Count() };
    
    //In SQL:
    Select Dept, Count = Count(*) From Employees Where Department = "IT" Group By Dept Having Count(*)>1; 
    //In LINQ:
    dataGridView1.DataSource = from E in db.Employees
                               where E.Department == "IT"
                               group E by E.Department into D
                               where D.Count() > 1
                               select new { Dept = D.Key, Count = D.Count() };
    

    Arrange this data in decinding order of department number:

    //In SQL:
    Select Dept, Count = Count(*) From Employees Where Department = "IT" Group By Dept Having Count(*)>1 Order By Dept DESC; 
    //In LINQ:
    dataGridView1.DataSource = from E in db.Employees
                               where E.Department == "IT"
                               group E by E.Department into D
                               where D.Count() > 1 
                               orderby D.key descending
                               select new { Dept = D.Key, Count = D.Count() };
    

Thank you for completing the C# programming language course with a focus on LINQ concepts. Your dedication to mastering this powerful tool will undoubtedly enhance your coding efficiency and data manipulation skills. Keep up the great work!


3
Subscribe to my newsletter

Read articles from Mritunjay Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mritunjay Kumar
Mritunjay Kumar