C# Generics
What are
Generics?
Generics allow us to write classes, structures, methods,
interfaces or delegate in such a way that they operate on the type of data
specified in the parameter <T>.
So for example, we write a general code for a class and it can perform that
functionality on different types of data mentioned in the type parameter.
Can’t we do
the same with object class?
Well not in all cases. Generics allow us to write a general
code which will compile as if it were written on the type we specified in
parameter. Also, if we try to get this functionality with object class, there
is a lot of burden in doing so, such as downcasting
every time we need to operate on specific type, boxing & unboxing can
be an overhead on processing.
Why we need Generics?
- Generics provide type safety, so no explicit downcasting is required and there is no chance of type mismatch at runtime causing a runtime exception (as we know, for example that downcasting an object into an int will be done at runtime) because we will be able to know the type at compile time.
- There is no overhead of boxing and unboxing when dealing with primitive types that we would have, if we tried to get this functionality with object class.
- Often, we are overloading a method or creating several version of a class that implement same algorithm but on different data types e.g., in case of data structures. In such condition we can have one generic code that implements the logic. Remember that generic methods can also be overloaded just like other method as long as their method signature is different.
Generic Class
Syntax
To create a generic class, we need
to specify generic parameter type list that it uses within <> after the class name, you can give any name to the type
parameter but generally <T> or <T, V> is used. Then you can
write code for that class and implement that logic with T or V considering them
as if they are type like an int or double.
Example 1: Creating a simple Generic class
namespace HelloWorld
{
class GenericClass<T>
{
T typeVariable;
public GenericClass(T t)
{
this.typeVariable
= t;
}
public void DisplayType()
{
Console.WriteLine("Type of T is " + typeof(T) );
}
public T GetMyVariable()
{
return typeVariable;
}
}
class Demo
{
static void Main(string[] args)
{
GenericClass<int> myIntObj = new GenericClass<int>(2);
GenericClass<double> myDoubleObj = new GenericClass<double>(2.2);
GenericClass<string> myStringObj = new GenericClass<string>("Hello World!");
GenericClass<Person>
myPersonObj = new GenericClass<Person>(new Person("Imran Aftab"));
int
a = myIntObj.GetMyVariable();
double b = myDoubleObj.GetMyVariable();
string c = myStringObj.GetMyVariable();
Person d = myPersonObj.GetMyVariable();
myIntObj.DisplayType();
myDoubleObj.DisplayType();
myStringObj.DisplayType();
myPersonObj.DisplayType();
Console.ReadKey();
}
class Person
{
public string Name { get; set; }
public Person(string name)
{
this.Name = name;
}
}
}
}
OUTPUT:
Type of T is System.Int32
Type of T is System.Double
Type of T is System.String
Type of T is HelloWorld.Demo+Person
The example above shows the power of using generics and; how
generics gives flexibility and reusability to our code.
First, we created a generic class indicated by <> where the class name is GenericClass and type parameter is T. T
is now a generic type which will be replaced at compile type with the type mentioned
in the type parameter when creating an instance of it. Next,
in this class we have a constructor and two instance methods name DisplayType and GetMyVariable. DisplayType
returns the type specified within angle brackets and GetMyVariable returns the
variable of type T.
In the Demo class,
we have an inner class Person which
is accessible only through Demo class only. In Person class, we only have one property named Name and a constructor which gives initializes Name property.
In the main method, we can see the magic of using a generic
class that we don’t need to downcast our typeVariable
when type T is returned from GetMyVariable method. Also, we can see
from the results that the type of typeVariable
changes to whatever type is mentioned within <> while creating an instance of it and DisplayType method shows the type mention in parameters of class
instead of type T.
Generic Method
So far, we have created a generic class which uses a
parameterized type to create T’s
type field named typeVariable in the
GenericClass class. Let’s see how we
can create a method in a similar way.
Syntax
Creating a generic method is similar to creating a generic
class. Just put generic type <T>
after the method name and use that type in your method.
Modifier Return-type Method-Name<T>(Parameter-List)
{
//Statements;
}
Example 2: Creating a simple Generic method in non-generic class
namespace HelloWorld
{
class GenericMethod
{
public static void MyMethod<T>(T[] col)
{
for
(int i = 1; i <= col.Length;
i++)
{
Console.WriteLine("item " + i + " is " + col[i-1]);
}
Console.WriteLine();
}
}
public class Demo
{
static void Main(string[] args)
{
int[]
a = { 1,2,3,4,5};
double[] b = { 6.5,7.4,8.3,9.2,10.1};
string[] c = { "Hello",
"World!"};
Person[] d = {
new Person("Imran"),
new Person("Usman")
};
GenericMethod.MyMethod<int>(a);
GenericMethod.MyMethod<double>(b);
GenericMethod.MyMethod<string>(c);
GenericMethod.MyMethod<Person>(d);
Console.ReadKey();
}
private class Person
{
public string Name { get; set; }
public Person(string name)
{
this.Name = name;
}
public override string ToString()
{
return Name;
}
}
}
}
OUTPUT
item 1
is 1
item 2
is 2
item 3
is 3
item 4
is 4
item 5
is 5
item 1
is 6.5
item 2
is 7.4
item 3
is 8.3
item 4
is 9.2
item 5
is 10.1
item 1
is Hello
item 2
is World!
item 1
is Imran
item 2
is Usman
Generic
Constrained Type
Generic type parameter allows us the ability to operate on
any type but often times we want to operate on a type which has some relation
with the generic type so that we can operate on it. For example, if we want to
operate on classes which are stream types then we need to ensure that the type
parameter is a derived class of stream.
In these situation, we put a constraint on the parameter type
of generics. For example, if we want to ensure the type is a collection, we
would specify that it implements IEnumerable and so forth.
Generally, we can put following constraints on a parametric
type of generic.
- 1. Base class constraint
- 2. Interface constraint
- 3. Constructor constraint
- 4. Reference type constraint
- 5. Value type constraint
Above mentioned constraints are self-explanatory but the
constructor constraint might be a bit tricky because it only allows
parameter-less constructor to be the constraint on the type parameter even if
other constructors are available. So, either we should use the default
constructor or you must define a parameter-less constructor.
Base class
constraint
As the name implies, the base class constraint requires a
type parameter to be either the base class itself or a class which is derived
from that class. This allow the type parameter to use properties or methods
available in base class. Let’s see an example:
Example 3: Creating a Generic class with base class constraint
namespace HelloWorld
{
public class Person
{
string name;
string cnic;
public Person(string name,string cnic)
{
this.name
= name;
this.cnic
= cnic;
}
public virtual string getPersonData()
{
return "Name: " + this.name + "\n" + "CNIC: " + this.cnic;
}
}
public class Student : Person
{
string studentId;
public Student(string name, string cnic,string studentId) : base(name, cnic)
{
this.studentId
= studentId;
}
public override string getPersonData()
{
return base.getPersonData() + "\n" + "Student Id: " + this.studentId;
}
}
public class GenericConstraint<T> where T : Person
{
T ob;
public GenericConstraint(T obj)
{
this.ob
= obj;
}
public string getPersonData()
{
return ob.getPersonData();
}
}
class Demo
{
static void Main(string[] args)
{
Student imran = new Student("Imran Aftab", "12345", "CIIT/ABC/LHR");
GenericConstraint<Student>
s = new GenericConstraint<Student>(imran);//Student is a Person
(Polymorphism)
string data = s.getPersonData();//applies method overriding (Polymorphism)
Console.WriteLine(data);
Console.ReadKey();
}
}
}
In the above example, we created
four classes. First class is the Person
class which has two fields (name &
cnic), a constructor with two
parameters to initialize fields and a virtual method getPersonData that displays values of fields (which can be
overridden by a class derived from Person).
The second class is Student which inherits Person class adds one more field studentId, a constructor to initialize
fields of Student and overrides the getPersonData
method to return those field as well as the new field studentId of this class.
The third class is the generic
class GenericConstraint which shows
how base class constraint work. To put a base class constraint, we need to put
a where keyword after that we
specify which type parameter must have which class. In this case, type
parameter T must inherit Person or either itself should be a Person. This gives us the ability to
access type parameter T’s member
which in this case are the public
members of Person class. In this
generic class (GenericConstraint),
we created an instance of T type
(which is Person), a constructor to
create object of T type and a method getPersonData
which calls T’s instance method getPersonData and returns a string.
Finally, the fourth class is a Demo class, where we create an instance
of Student (Polymorphism). After that we create an instance of GenericConstraint class and passes it
the instance of Student. On GenericConstraint instance we call getPersonData method which calls
derived class’s (Student) getPersonData method instead of base
class (Person) method (Polymorphism).
Interface
constraint
Interface constraint is almost identical to base class
constraint, the only thing that is different is that instead of base class the
type parameter must implement that interface. Let’s quickly look at an example
which is self-explanatory.
Example 4: Creating a Generic class with interface constraint
namespace HelloWorld
{
public interface IMakeSound
{
string Sound { get; set; }
void makeSound();
}
public class Animal : IMakeSound
{
//Implementing
Interface Properties
public string Name { get; set; }
public string Sound{ get; set; }
//Constructor
public Animal(string name,string sound)
{
this.Name
= name;
this.Sound
= sound;
}
//Interface Method
public void makeSound()
{
Console.WriteLine(Name + " makes a sound like \"" + Sound + "\"");
}
}
public class InterfaceConstraint<T> where T:IMakeSound
{
T ob;
public InterfaceConstraint(T o)
{
ob = o;
}
public void Say()
{
ob.makeSound();
}
}
class Demo
{
static void Main(string[] args)
{
Animal dog = new Animal("Dog", "Woof Woof!");
InterfaceConstraint<IMakeSound> soundMakerA = new
InterfaceConstraint<IMakeSound>(dog );
soundMakerA.Say();
Console.WriteLine();
Animal cat = new Animal("Cat", "Meow Meow!");
InterfaceConstraint<IMakeSound> soundMakerB = new
InterfaceConstraint<IMakeSound>(cat);
soundMakerB.Say();
Console.ReadKey();
}
}
}
OUTPUT
Dog makes a
sound like "Woof Woof!"
Cat makes a
sound like "Meow Meow!"
I think this example is self-explanatory since nothing has
changed except instead of base class we have an interface and the generic class
type parameter is implementing that interface.
Constructor
Constraint
As I said earlier that constructor constraint has the ability
to put the constraint on type parameter to must have a parameter-less constructor or default
constructor and you cannot constraint it to have particular constructor
even if other constructors are available. This is done by using new() keyword after the where keyword.
The rest is quite similar to the previous ones.
namespace HelloWorld
{
class Product
{
string name;
static long id;
public Product()
{
id = 10000001;
name = "N/A";
}
public override string ToString()
{
return "Product
Name: "
+ name + "\n"+"id: " +id;
}
}
class ConstructorConstraint<T> where T : new()
{
T ob;
public ConstructorConstraint()
{
ob = new T();
}
public T getType()
{
return ob;
}
}
class Demo
{
static void Main(string[] args)
{
ConstructorConstraint<Product> mangos = new
ConstructorConstraint<Product>();
Console.WriteLine(mangos.getType());
Console.ReadKey();
}
}
}
OUTPUT
OUTPUT
Product
Name: N/A
id: 10000001
Reference
Type Constraint
Reference type as its name implies restrict the type
parameter to be a reference type only. This is done by using class keyword when defining constraint
after where keyword.
namespace HelloWorld
{
class ReferenceConstraint<T> where T : class
{
T ob;
public ReferenceConstraint()
{
ob = null;
}
}
public class Demo
{
ReferenceConstraint<Object> ob = new ReferenceConstraint<Object>();
//ReferenceConstraint<int>
ob1 = new ReferenceConstraint<int>(); compile time error
}
}
Value Type
Constraint
Value type as its name implies restrict the type parameter to
be a value type only. This is done by using struct keyword when defining constraint after where keyword (as we know struct is a value type).
namespace HelloWorld
{
class ValueTypeConstraint<T> where T : struct
{
T ob;
public ValueTypeConstraint(T ob)
{
//ob = null; error
this.ob
= ob;
}
}
class Test
{
ValueTypeConstraint<int> o = new ValueTypeConstraint<int>(10);
//ValueTypeConstraint<Object>
o = new ValueTypeConstraint<Object>(new Object()); compilation error
}
}
Conclusion
Generics provide versatility to how
C# language solve a problem by allow one code with a parametrized type to act
like if it were written for that type at
compile time. Generics may seem a bit sophisticated at first but becomes
easy once you understand logic behind it.
Comments
Post a Comment