Generics are available since NET version 2.0 as a part of CLR types. Those might be classes, structures, interfaces, and methods which type specifications are declared and initialized in consumer code. One of the benefits of generics is increased code reusability and type safety. NET Framework has number of generic classes in System.Collections.ObjectModels and System.Collections.Generic namespaces like SortedDictionary, and SortedList, to mention few, which are type safety.
Generic sort and equity interfaces and delegate types are located in System.
Generic classes
Generic class might use type parameter defining what objects it stores. Type parameters appear as type of its fields and methods.
public class SimpleGenericClass<T>
{
public T val;
}
Note the bracket (“<T>”) after the class name (“SimpleGenericClass”). This (“<T>”) indicates that the class is generic. The letter “T” is the type placeholder and is replaced with actual type when a new instance is created. These types of parameters (without constraints which are described below) are called unbounded type.
Such a declaration is knows as a generic type definition and acts as a template.
Looking in the body of the class we see the type “T” of the member variable “val”. It means that actual type of the variable will be specified during class construction as shown below:
SimpleGenericClass<string> s = new SimpleGenericClass<string>(); // desired type - string in this case - is between brackets “<>”
s.val = “This is a string.”; // value is a string type
// s.val.GetType() will return System.String
SimpleGenericClass<int> s = new SimpleGenericClass<int>(); // type is int
s.val = “15”; // value is an int type
// s.val.GetType() will return System.Int32
Generic or unbounded types give only the lowest minimum of features to the object to exist. It could use only these features supplied by System.Object. This is why developers have to consider following list of limitations which might face using unbounded type parameters:
Do not support != and == because there is no guarantee that supplied type argument will support them.
Let’s suppose we have a class with a generic method as shown below
public class Test<T>
{
public bool Find<T>(T a, T b)
{
return (a == b);
}
}
This is because compilation depends on the types supplied and during compilation it is not clear whether this action (== equation) will be supported by passed types or whether supplied types will be comparable.
Value types do not provide an overload for == by default nevertheless most of the value types provided by the framework provide their own overload.
Can be converted to and from System.Object, or explicitly to any interface type
This means unconstrained type parameters are assumed to be of type System.Object. So less constraints applied, less resources are available. Using constraints for type parameters is recommended because it will make accessible all operations and method available to constraining type and those in inheritance hierarchy.
They are null comparable. For arguments of value type will return always false (because value type must be initialized before they are used and always will have a value).
Constraints
Generic parameters might be limited by constraints. They restrict acceptable types during object instantiation. Passing an object that does not match to allowed types will raise a compile-time error. Type constraints are defined with where contextual keyword. The syntax is:
public class SimpleGenericClass<T> where T: struct
{
}
struct – restrict to value types, except Nullable.
class – restrict to reference type (any class, interface, delegate, or array type)
new() – require public parameterless constructor. It guarantees that type will have a public constructor without argument. This must be last in constraint list.
<baseclass> – type must be or derive from specified base class
<interface> – type must implement specified interface. Interface can be generic. Multiple interface constraints also can be specified
U – type must derive from the U argument, also known as naked type constraint.
Multiple constraints are acceptable as well as generic type constraints
public class SimpleGenericClass<T> where T: System.IComparable<T>, new()
{
}
Without constraints only System.Object methods are supported. Applying constraints increase operators and methods to call to all types of inheritance hierarchy.
Generic methods
A Generic method’s type parameter is used either for its return value or formal parameters. They are known as generic type parameters or simply type parameters in method definition. A method is generic only if it has its own list of type parameters as in following declaration:
class StandardClass
{
T test<T>(T args) {. . .} // method is generic
}
To define generic methods, the type parameter syntax is added immediately after the method name. Without the type parameter, method is not generic nevertheless it is declared either into generic type or its formal parameters are generic:
class Generic<T>
{
T test(T args) {. . .} // method is not generic
}
Overloaded methods which are often used to perform similar operations on different types of data might be coded as generic methods, which will produce more compact code.
How Generics works
Compilation converts code into MSIL (Microsoft intermediate language) and creates a portable executable file (PE) together with metadata as generics information, attributes etc. Metadata is used during runtime for different purposes like application control, data processing, etc. For generic types, metadata identifies existence of type parameters and if found, the runtime creates a specialized generic type. When this specialized type is first constructed, runtime uses supplied parameter types and replaces appropriate code in MSIL.
Depending on supplied parameters, value or reference type, MSIL is differently used.
For value type parameters runtime creates specialized type for each unique parameter. For example, if there is a list of integer declared as in code below a specialized version of integer list class will be created.
Because supplied parameters in both cases are integers, runtime will reuse specialized List class it has generated when int parameter has been supplied. So each instantiated integer List class will share the single specialize instance of List code that the runtime has produced.
List<int> listOne = new List<int>();
List<double> listTwo = new List<double>();
When another value type - let’s say double as in code above - is passed for creating a List object, runtime will create another version of specialized type replacing appropriate place in MSIL code to suit supplied double type.
When parameter is reference type runtime creates specialized generic type and replaces given parameter in MSIL with object reference. For each instance that has reference type as parameter, runtime reuses previously created version of specialized generic type. This always happens nevertheless what the type is as long as it is reference type.
For example if we have two classes (reference types) Order and Customer and we have following code:
class Customer {}
class Order {}
List<Customer> customer;
List<Order> orders = new List<Order>();
customer = new List<Customer>();
In line 3, the runtime will create specialized type with reference to Customer object without storing any data. In line 4, runtime will reuse the created specialized version of List class to create the new instance instead of creating new one as with value types. Reference is change to Order type.
In last line new instance is created using the same specialized type but with reference to Customer object.
This means that the C# implementation of generics reduces the code by reducing created by the compiler specialized classes to one.
Microsoft recommends using System.Collections.Generic classes instead of their System.Collections counterparts (like ArrayList). Using Generics will improve performance also because this avoids boxing/unboxing for any types inherited from System.Object and their virtual methods like ToString(). Anywhere, whenever possible, arguments should be generics instead of object type. For same reason is recommended to override ToString, Equals, and GetHash virtual members.
Conclusion
Generics help to write more reusable and compact code, reduce overloading. Replacing non-generic classes with their generic counterparts is recommended.
Comments (0)