Here we’ll carry on from the first post and dig a little deeper.
Generics allow you to declare type-parameterized code, which you can instantiate with different types. This means you can write the code with “placeholders for types” and then supply the actual types when you create an instance of the class.
- A type is not an object but a template for an object.
- A generic type is not a type but a template for a type.
Generic Class Declaration
Let’s look again at the code example from the last post.
public class Stack<T> { int position; T[] data = new T[100]; public void Push (T obj) => data[position++] = obj; public T Pop() => data[--position]; }
The result is a generic class declaration. The string consisting of the angle brackets with the T means that T is a placeholder for a type. (It doesn’t have to be the letter T – it can be any identifier.) Everywhere throughout the body of the class declaration where T is located, an actual type will need to be substituted by the compiler.
Creating and Using a Generic Class
There are two steps for creating and using your own regular, nongeneric classes:
- declaring the class
- creating instances of the class
Generic classes are not actual classes but templates for classes. You must first construct actual class types from them. You can then create references and instances from these constructed class types.
- Declare a class, using placeholders for some of the types.
- Provide actual types to substitute in for the placeholders. This gives you an actual class definition, with all the “blanks” filled in. This is called a constructed type.
- Create instances of the constructed type.
We have already declared our generic class called Stack in the code above in the first listing. Good. Now we need to tell the compiler what actual types should be substituted for the placeholders (the type parameters). The compiler takes those actual types and creates a constructed type, which is a template from which it creates actual class objects. The syntax for creating the constructed type is shown next and consists of listing the class name and supplying real types between the angle brackets, in place of the type parameters. The real types being substituted for the type parameters are called type arguments.
SomeClass< short, int >
The compiler takes the type arguments and substitutes them for their corresponding type parameters throughout the body of the generic class, producing the constructed type—from which actual class instances are created. Type arguments are the actual types you supply when creating a constructed type, such as int or string.
Creating Variables and Instances
For comparison, let’s look at something we already know.
int myInteger; // declare an integer in the stack myInteger = 5; // give it a value // declare a regular non-generic class somewhere else in code called MyClass MyClass myC = new MyClass(); // type identifier = new type(); var myC2 = new MyClass(); // using var (type inference) produces similar result
Now let’s look at our generic class. It’s the same pattern as our regular non-generic class. In the code below, substitute MyClass in for SomeClass
SomeClass<short,int> mySc1 = new SomeClass<short,int>(); var mySc2 = new SomeClass<short,int>();
As with non-generic classes, the reference and the instance can be created separately, as shown in the figure below. The figure also shows that what is going on in memory is the same as for a nongeneric class. The figure below is from the book Illustrated C# 7: The C# Language Presented Clearly, Concisely, and Visually 5th Edition by Daniel Solis (Author), Cal Schrotenboer (Author) on page 453.
class Program { static void Main() { var first = new SomeClass<short, int >(); // Constructed type