All about Generics

What's in this article?

  • Introduction
  • An overview of generics
  • Why generic class and methods?
  • Creating Generic Class
  • Code Explanation
  • Futures Of Generic Classes
  • Generic Parameters Type
  • Guideline For Naming Type Parameter
  • Constraints On Type Parameter
  • Why Constraints?
  • Inheritance
  • Generic Interface
  • Generic Methods
  • Generic Delegates
  • Default keyword in Generic Code
  • Generic At Runtime

Introduction

In this article some content is taken from the MSDN. Using this article we will learn all about generics in C#.

Overview

Generics are supported by .Net since release of .Net 2.0, in other words Visual Studio 2005. Generics can be used in any kind of .Net application (Console, Windows, Web or Service also). This means that generics
are not only a part of C#, it is also deeply integrated with the Intermediate Language (IL). A .Net predefined class library provides the namespace System.Collection.Generic that provides several new generic based classes. Microsoft recommendeds use of generic classes instead of older non-generic collection classes, such as ArrayList.

Why Generic Classes and Methods?

Generic classes are especially useful for creating algorithms and for reusability, type safety and for the efficiency of your programs that cannot be done by non-generic collection classes. The limitation of non-generic collection classes can be proved by smaller programs that use a non-generic collection class ArrayList. ArrayList can be used without modifyion to store any reference or value type. Actually ArrayList uses the Object class internally to store the value collection. That means any values stored in ArrayList will be implicitly boxed (cast) into an Object type. This mean that if you are storing a value type in an ArrayList then the values must be boxed and when you retrieve a value you must also unbox the value.

A generic class contains operations that are not specific to a particular data type, the most common use of generic classes is with a collection, like linked list, queues, trees, stack, hash table and so on.

We know tht boxing and unboxing causes a loss of performance. So there is the first limitation for using non-generic types of collection classes.
Another drawback is type safety. Since we now know that an ArrayList stores all its values as an Object and all values are type cast into an Object type for storing that value, it is not possible to prevent a user form storing various kinds of values in an ArrayList as in the following:

All-about-Generics-1.jpg

Now, retrieve a value from the ArrayList and print it in the Console using the following code:

All-about-Generics-2.jpg
The code above will throw an exception InvalidCastException {"Specified cast is not valid."}.

Because, while retrieving data from the ArrayList, we use an int type of variable element in the foreach loop whereas the ArrayList has string values.
Ok, let's see another example:

using System;
using
System.Collections;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
System.Threading.Tasks;

namespace
NonGeneric
{
   public class LinkedListNode
    {
        public LinkedListNode(object _Value)
        {
            this.Value = _Value;
        }

          public object Value

        {

            get;

            internal set;

        }

        public LinkedListNode Next

        {

            get;

            internal set;

        }

 

        public LinkedListNode Last

        {

            get;

            internal set;

        }

    }

    public class LinkedList : IEnumerable

    {

        public LinkedListNode First

        {

            get;

            private set;

        }

        public LinkedListNode Last

        {

            get;

            private set;

        }

        public void AddList(object Node)

        {

 

            LinkedListNode NewNode = new LinkedListNode(Node);

            if (First == null)

            {

                First = NewNode;

                Last = First;

            }

 

            else

            {

                Last.Next = NewNode;

                Last = NewNode;

            }

        }

 

        public IEnumerator GetEnumerator()

        {

            LinkedListNode current = First;

            while (current != null)

            {

              yield return current.Value;

                current = current.Next;

            }

        }

    }

 

    class Program

    {

        static void Main(string[] args)

        {

            LinkedList List1 = new LinkedList();

            List1.AddList(2);

            List1.AddList("2");

            foreach (int i in List1)

            {

                Console.WriteLine(i);

            }

        }

    }

}

In the above program you will get the same exception as what I said above. That means that if you create a heterogeneous collection, combining string and int in a single non-generic as an ArrayList class, you are more likely to create a programming error, and this error can be detected only at run time. But you can also avoid this problem in VS 1.0 & 1.1 only by writing your own type specific collection but in such condition that class cannot be reusable for multiple data types and you need to write 2 different programs for different types.


So what we need is we need to write a single program that can be reusable for a heterogeneous type collection (on a per type instance basis) and also make it possible for the compiler to check for type safety.

Note:

Here in the program above, I used the IEnumerator interface, GetEnumerator () Method and yield statement.

1. Enumeration: actually what happened is that whenever you use a foreach statement for navigation in a collection without knowing the number of elements, foreach uses an enumerator. The array or collection implements the IEnumerable interface with the GetEnumerator() method. The GetEnumerator() method returns an enumerator implementing the IEnumerator interface. The interface IEnumerator is used by the foreach statement to iterate through the collection.

Note: the foreach doesn't really need to implement the IEnumerable interface. It's enough to have a method with the name GetEnumerator() that returns an object implementing the IEnumerator interface.

2. Yield statement: the yield return statement returns one element of a collection and moves the position to the next element, the yield breaks the statement to stop the iteration.

So at this point, I hope you now understad why we use generics.

You can also create your own custom type of generic class for your problem that will be type safe and efficient. First what we need here is to eliminate the need for a type cast and also make it possible for the compiler to check for the type. That means the preceding program needs a type parameter that will be possible using generics like LinkedListNode<T> as in the following:


All-about-Generics-3.jpg


Now have a look at a generic collection class example:

  • Creating Generic Class

While creating your own generic class, you need to keep something in mind:

  1. Which types to generalize into the type parameter

        You can use one or more than one type parameter to generalize your program but this can cause a readability problem.
     
  2. If you want to restrict the client from instantiating your generic class then you can use constraints.

       We will discuss constraints in details further in this article.
     
  3. Whether to implement one or more generic interface.

         There may sometimes be a requirement to implement an interface such as IEnumerable<T> that can be used for navigating the data from a list.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace Generic_Implemention

{

    public class LinkedListNode<T>

    {

        public LinkedListNode(T _Value)

        {

            this.Value = _Value;

        }

 

        T Data;

        public T Value

        {

            get

            {

                return Data;

            }

 

            internal set

            {

                Data = value;

            }

        }

        public LinkedListNode<T> Next

        {

            get;

            internal set;

        }

 

        public LinkedListNode<T> Last

        {

            get;

            internal set;

        }

    }

 

    public class LinkedList<T> : IEnumerable<T>

    {

        public LinkedListNode<T> First

        {

            get;

            private set;

        }

 

        public LinkedListNode<T> Last

        {

            get;

            private set;

        }

 

        // As i told you, ArrayList use object class to store value same as i take

 

        //parameter Node of object type.

        public void AddList(T Node)

        {

            LinkedListNode<T> NewNode = new LinkedListNode<T>(Node);

            if (First == null)

            {

                First = NewNode;

                Last = First;

            }

            else

            {

                Last.Next = NewNode;

                Last = NewNode;

            }

        }

 

        public IEnumerator<T> GetEnumerator()

        {

            LinkedListNode<T> current = First;

            while (current != null)

            {

                yield return current.Value;

                current = current.Next;

            }

        }

 

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()

        {

            return GetEnumerator();

        }

    }

 

    class Program

    {

        public static void Main()

        {

            LinkedList<int> List1 = new LinkedList<int>();

            List1.AddList(2);

            List1.AddList(3);

            List1.AddList(5);

            // List1.AddList("4");

            foreach (int i in List1)

            {

                Console.WriteLine(i);

            }

 

            LinkedList<string> List2 = new LinkedList<string>();

            List2.AddList("2");

            List2.AddList("3");

            List2.AddList("5");

            foreach (string i in List2)

            {

                Console.WriteLine(i);

            }

        }

    }

}


The preceding program will compile and execute successfully but if you uncomment // List1.AddList ("4"); from the above program then it will throw a compiler error as not possible with a non-generic class.
In the preceding program, the client can store any type of values, Value or Reference Type, on a per instance basis. Since we store here two types of values, the first one is an int (Value Type) and the second one is string (Reference Type).
But if you want to restrict the user from storing only a value type then you can use constraints. We will discuss this later in this article.

  • Code Explanation

    All-about-Generics-4.jpg

    As we know, with a linked list one element can reference the next element and can also reference the previous element as explained using the image above. So I just created a class LinkedListNode<T>. This class just holds some auto-implemented property, like Value, Prev, Next. Where the Value property assigns a value for the LinkedListNode<T> class and the Prev and Next properties hold the previous and next LinkedListNode<T> object. Every time you call the AddList () method, it generates the object for the LinkedListNode<T> class. And using Conditional constructs it assigns objects to the Next values. You can notice one thing, I never used the Prev property. So you can also assign a value to the Prev property. So here, I think it's enough for you to understand what actually happened with that program.

  • Generic Futures

    Using generics we can create a collection that is type safe at compile time. As we see above if we will use a non-generic collection that uses an Object for storing values. We cannot prevent the user from storing a different type of value (value type or reference type) and it also needs boxing and unboxing that causes a loss of performance. So in such a condition we need to create a generic type of collection that works on a per instance basis.

  •  Generic Parameters Type

    When a client instantiates a variable of generic type, the client must specify a type argument for the generic collection class as in the following:

    All-about-Generics-5.jpg

The type argument of this particular generic collection class can be anything recognized by the compiler. Here in this code above every object (obj1, obj2, and obj3) will be substituted at runtime with a specific type. Actually here what happens internally is, .Net uses JIT to generate specific native code at run time for high efficiency so when we instantiate a generic collection class with a different argument, JIT creates 2 different classes with a specific type since we pass a type argument for the generic class and instantiate it.

  •  Guidelines for type arguments

    Where you can provide any name as a type parameter but for increasing readability and to create a meaningful program Microsoft recommends the following:

  1. If there is a single parameter then you can use T as:

    publicinterface IApp<TApp> { }
    publicclass LinkList<T> { }

  2.  If there is more then one parameter tehn you can use any name but use T as a prefix to indicate you need to pass a type there.

    publicdelegate TOut Con<TIn, TOut>(TIn from);

  •  Constraints On Type Parameter

    First we needed to learn what a constraint is:
    You can also restrict the client from instantiating a generic collection class with only a specific type of parameter. If the client instantiates that generic collection class with a type parameter that is not allowed then that causes a compile time error, this restriction is known as a constraint.

    There are 6 types of constraints that can be use to restrict the user from instantiating a generic class with a specific type parameter.
     

    PropertyDescription
    where T : new() The type argument must have a public parameterless constructor. When used together with other constraints, the new () constraint must be specified last.
    where T : <interface name> Type argument must be or must implement the specified interface. The constraining interface can be generic and multiple interface constraining can be specified to follow multiple inheritances.
    where T : X The type argument supplied for T must be or derive from the argument supplied for X.
    where T : <base class name>Type argument must be derived from the specific base class
    where T: struct The type argument must be a value type. Any value type except null-able can be specified.
    where T : class Type argument must be a reference type

Why Constraints?

As I said, if you want to restrict the user to passing only a limited and specific type parameter then you can use Constraints. So let's look at the following example:

using System;
using
System.Collections;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;

using
System.Threading.Tasks;
namespace Generic_Implemention
{

    public class LinkedListNode<T>

    {

        public LinkedListNode(T _Value)

        {

            this.Value = _Value;

        }

 

        T Data;

 

        public T Value

        {

            get

            {

                return Data;

            }

 

            internal set

            {

                Data = value;

            }

        }

 

        public LinkedListNode<T> Next

        {

            get;

            internal set;

        }

 

        public LinkedListNode<T> Last

        {

            get;

            internal set;

        }

    }

    public class LinkedList<T> : IEnumerable<T> where T : struct

    {

        public LinkedListNode<T> First

        {

            get;

            private set;

        }

 

        public LinkedListNode<T> Last

        {

            get;

            private set;

        }

 

        // As i told you, ArrayList use object class to store value same as i take

 

        //parameter Node of object type.

 

        public void AddList(T Node)

        {

            LinkedListNode<T> NewNode = new LinkedListNode<T>(Node);

            if (First == null)

            {

                First = NewNode;

                Last = First;

            }

            else

            {

                Last.Next = NewNode;

                Last = NewNode;

            }

        }

 

        public IEnumerator<T> GetEnumerator()

        {

            LinkedListNode<T> current = First;

            while (current != null)

            {

                yield return current.Value;

                current = current.Next;

            }

        }

 

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()

        {

            return GetEnumerator();

        }

    }

 

    class Program

    {

        public static void Main()

        {

            LinkedList<int> List1 = new LinkedList<int>();

            List1.AddList(2);

            List1.AddList(3);

            List1.AddList(5);

            // List1.AddList("4");

            foreach (int i in List1)

            {

                Console.WriteLine(i);

            }

            LinkedList<string> List2 = new LinkedList<string>();

            List2.AddList("2");

            List2.AddList("3");

            List2.AddList("5");

            foreach (string i in List2)

            {

                Console.WriteLine(i);

            }

        }

    }

}


In this example you will get a compile time error since you require the client to store only a value type using the public class LinkedList<T> : IEnumerable<T> where T : struct this line.
So, to run the program above successfully, just comment the List2 object from the above program.

You can also apply multiple constraints for the same type parameter as in the following:

class Test<T, U>

where U : struct

where T : Base, new() { }

 

Class Stud_List<T> where T:Stud,Stud_Interface,System.ICompareable<T>, new()


Inheritance

The LinkedList<T> class created earlier implements the interface IEnumerable<T>:

public class LinkedList<T> : IEnumerable<T> where T : struct

{

}


A generic type can implement a generic interface. The same is possible by deriving from a class. A generic class can be derived from a generic base class.

Public class ParentClass<T>

{

}

public class ChildClass<T> : ParentClass<T>

{

}


You can also implement an abstract class as in the following:

Public abstract class ParentClass<T>

{

Public abstract T Add(T x,T y);

}

public class ChildClass<T> : ParentClass<T>

{

Public override int Add(int x,int y)

{

Return x + y;

}

}

Static Member

Static members of a generic class are only shared with single instantiation of the class.
 

Public class First<T>

{

Public static int A;

}

// how to use

First<string>.A=50;

First<int>.A=100;

Console.WriteLine(First<int>.A);

 

// will print 100 because First<T> class // used with two type string and int, two set of static field exist.

Generic Interface

The preference is for generic classes to use a generic interface such as IComparable<T> rather than using non-generic type IComparable, to avoid boxing and unboxing operations for a value type. You can also create your own generic type of interface as follows:
 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

namespace Delegate

{

    interface Imp

    {

        void Swap<T>(ref T First, ref T Sec);

    }

 

    class Program : Imp

    {

        static void Main(string[] args)

        {

            Program p = new Program();

            int First = Convert.ToInt32(Console.ReadLine());

            int Sec = Convert.ToInt32(Console.ReadLine());

            p.Swap<int>(ref First, ref Sec);

            Console.WriteLine("First = {0,5} and Last = {1,5} ", First, Sec);

 

            string S_First = Console.ReadLine();

            string S_Sec = Console.ReadLine();

            p.Swap<string>(ref S_First, ref S_Sec);

            Console.WriteLine("First = {0} and Last = {1} ", S_First, S_Sec);

        }

 

        public void Swap<T>(ref T First, ref T Sec)

        {

            T Temp = First;

            First = Sec;

            Sec = Temp;

        }

    }

}

Generic Methods

Here we will start to learn about generic methods using the following example.

Actually what's required of the user is, he wants to swap a value of value type as an int and there can also be the requirement for swapping the reference type of value as a string too, in such a condition you will create two functions, one for swapping an int and the second for swapping a string as in the following:
 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace NonGeneric_Method

{

 

    class NonGenerics

    {

 

        public static void SwapInt(ref int First, ref int Last)

        {

            int Temp = First;

            First = Last;

            Last = Temp;

        }

 

        public static void SwapString(ref string First, ref string Last)

        {

            string Temp = First;

            First = Last;

            Last = Temp;

        }

 

        public void Caller()

        {

            string FirstName = "Pawan";

            string LastName = "Pandey";

            SwapString(ref FirstName, ref LastName);

            Console.WriteLine(FirstName + " " + LastName);

            int FirstValue = 10;

            int LastValue = 20;

            SwapInt(ref FirstValue, ref LastValue);

            Console.WriteLine(FirstValue + " " + LastValue);

        }

    }

    class Program

    {

        static void Main(string[] args)

        {

            NonGenerics ok = new NonGenerics();

            ok.Caller();

        }

    }

}


So rather than creating two functions you can create a single generic type of function that can be used for a heterogeneous type as in the following:
 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace Generic_Method

{

    class Generics

    {

        public static void Swap<U>(ref U First, ref U Last)

        {

            U Temp = First;

            First = Last;

            Last = Temp;

        }

 

        public void Caller()

        {

            string FirstName = "Pawan";

            string LastName = "Pandey";

            // how you will call Swap function?

            Swap<string>(ref FirstName, ref LastName);

            Console.WriteLine(FirstName + " " + LastName);

            int FirstValue = 20;

            int LastValue = 10;

 

            Swap<int>(ref FirstValue, ref LastValue);

            Console.WriteLine(FirstValue + " " + LastValue);

        }

    }

 

    class Program

    {

        static void Main(string[] args)

        {

            Generics ok = new Generics();

            ok.Caller();

        }

    }

}

Methods declared with a Type Parameter, are known as Generic Methods. If you want you can also call the Swap<T> () function without passing a Type Parameter and the compiler will infer it as in the following:
 

Swap(ref FirstName,ref LastName);


Type Inference

Type inference is the ability to do auto deduction of the type of an expression.

The same rule for type inference applies to static and instance methods. The compiler automatically recognizes the type parameter based on the method argument passed in. Therefore, if there is not any parameter then in such a condition the type inference does not work. The compiler also does not recognize type parameters from a return type. Type inference occurs at compile time before the compiler tries to resolve overloaded method signatures and the compiler includes only those methods on which type inference has succeeded.

If you will declare a Swap function with the same type parameter as the class it will throw a Warning: Type parameter "T" has the same name as the type parameter from outer type "Generic_Method.Generics<T>". So you can provide a different type parameter as follows:

class GenericMethod<T>

{

    // Warning :

    // Type parameter 'T' has the same name as the type parameter from outer type

    void Swap<T>() { }

}

class GenericMethod<T>

{

    //No warning

    void Swap<U>() { }

}


You can also use constraints on a generic method as in the following:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace Generic_Method

{

    class Generics<T>

    {

        public static void Swap<U>(ref U First, ref U Last) where U : struct

        {

            U Temp = First;

            First = Last;

            Last = Temp;

        }

        public void Caller()

        {

            // if you will pass string in Swap parameter as follows there will be an error because you // use constraint where U: struct which mean you can only pass value type of parameters //where string is are of reference type.

 

            // string FirstName = "Pawan";

            // string LastName = "Pandey";

            // Swap<string>(ref FirstName, ref LastName);

 

            // so you must have to pass value type of parameters as following

            int FirstName = 20;

            int LastName = 30;

            Swap<int>(ref FirstName, ref LastName);

 

            Console.WriteLine(FirstName + " " + LastName);

        }

    }

    class Program

    {

        static void Main(string[] args)

        {

            Generics<int> ok = new Generics<int>();

            ok.Caller();

        }

    }

}

A generic method can be overloaded on several type parameters:
 

public void Caller<T>(){}

public void Caller<T, U>(){}

Generic Delegates

Before learning about generic delegate, I think you need to understand delegates. A delegate is like a function pointer in the C/C++ programming language. But there is one difference between a C/C++ programming function pointer and a delegate and that is a delegate is type safe. That's defining with a return type and a parameters type that is not specified in a C/C++ function pointer.

Delegate is a class that can be used to hold a function reference. You can declare them inside a class or outside a class in a namespace. And whenever you pass a function reference for a delegate you must pass those function references to the Delegate that matches the return type and type, number and signature of a parameter with a delegate as you can see in the following example.
 

namespace Delegate

{

    public delegate void fun();

    class Program

    {

        public static string f(string Mesg)

        {

            return Mesg;

        }

 

        static void Main(string[] args)

        {

            fun obj = new fun(f);

        }

    }

}


As I said, a C# delegate is type safe so in the above program you will get an error because you just declare a delegate without a parameter and a return type of void and in the Main function you just pass a reference of a function with the name "f()" and this function's return type and number of parameters does not match with the delegate. So in such a condition the preceding program will throw a compile time error.
So when passing a function reference for a delegate, you must pass those function references that have the same return type, the same number of parameters and the same sequence of parameters however if you have two functions with a different return type and both functions have a single parameter with 2 different types as follows:

public string fun1(string Mesg)

{

return Mesg;

}

 

public int fun2(int Num)

{

return Num;

}


So for this you will create two delegates as follows:
 

public delegate int f1(int Num);

public delegate string f2(string Mesg);


How will you pass the function reference?

f1 obj1 = new f1(fun2);
f2 obj2 = new f2(fun1);

So here in the example above you can see that you need to create a different delegate for each function's type. But our requirement is to create a single delegate for multiple function references that have a different type of parameters and return type so in such a condition you can use a generic delegate as in the following:

 

using System;

class a

{

    public delegate T fun<T>(T Value);

 

    public int f(int x)

    {

        return x * 2;

    }

 

    public string ff(string xx)

    {

        return xx + " this";

    }

 

    public static void Main()

    {

        a ok = new a();

        fun<int> obj = new fun<int>(ok.f);

        Console.WriteLine(obj(20));

 

        fun<string> obj1 = new fun<string>(ok.ff);

        Console.WriteLine(obj1("pawan"));

    }

}


So you can use a single generic delegate for multiple functions on a per instance basis.

Default Keyword In Generic

With a generic class or generic methods there can be one issue when providing a default value for the T (type parameter type) object because the client specifies the type of object dynamically where we don't know the client will provide a reference type or a value type of the type parameter and whether we know the reference type accepts null as the default and the value type accepts 0 (zero). So if you provide zero as a default value and the client uses a reference type for the type parameter tehn there will be a problem. So you can use the default keyword that works differently for where you use a default. Like in the switch... case construct that works whenever there is no case match. But for generics it works as when your type parameter is of reference type; in such a condition it will assign null and if the client passes a value type as the type parameter then the default will assign zero as you will see in the following example:
 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

namespace Delegate

{

    class Program

    {

        public static void Swap<T>(ref T First, ref T Sec)

        {

            T Temp = default(T);

            Temp = First;

            First = Sec;

            Sec = Temp;

        }

 

        static void Main(string[] args)

        {

            int First = Convert.ToInt32(Console.ReadLine());

            int Sec = Convert.ToInt32(Console.ReadLine());

            Swap<int>(ref First, ref Sec);

            Console.WriteLine("First = {0,5} and Last = {1,5} ", First, Sec);

 

            string S_First = Console.ReadLine();

            string S_Sec = Console.ReadLine();

            Swap<string>(ref S_First, ref S_Sec);

            Console.WriteLine("First = {0} and Last = {1} ", S_First, S_Sec);

        }

    }

}


So you can set a breakpoint at the swap() function to check the value of Temp while assigning the default when it's called for swapping a value type and again called for a reference type. It will show 0 (zero) while the first time swap will be called because we pass a value type of parameter over there. And the second type while calling will be a reference type and you will find null in temp after assigning the value using Default.

Generic At Run Time

Whenever you use generics in C# programming language, the .Net runtime generates 2 different classes with a different type based on a value type and reference type.
For example, if you create an instance of a generic class with an int type parameter, the C# runtime will generate a class and appropriately assign an int as a parameter and if you create another instance with the same value type, in other words an int type as in the following example tehn that will reuse the same class for the second instance:

MyClass<int> obj1=new MyClass<int>();

MyClass<int> obj2=new MyClass<int>();


But if you generate another instance with another value type like byte then the runtime will generate another version of the generic.
But generics work differently with reference types, anytime you construct a generic with a reference type the C# runtime generates a class and specifies an object as the parameter and again if you construct a generic again with another reference type then that will use the same class that was generated for the first construction of the generic.

class a { }

class b { }

 

MyClass<a> obj=new MyClass<a>;

MyClass<b> obj=new MyClass<b>;


End Of Article

This is my first article here at C# Corner but I hope you all will get useful and interesting knowledge after reading this article completely.

I hope this article is helpful for you where you got all about generics in the C# programming language.


Similar Articles