Thursday, March 28, 2024 16:32

Table of contents >> Objects > Generic classes

Generic classes

Generic classes, also known as generic data types or simply generics, are classes of unknown type until they are instantiated to some specific type.

Because this concept is a bit harder to explain, I will first exemplify a specific case that will help you better understand it. Let’s consider we have two animal objects, Cat:

and Dog:

and finally, let’s assume that we want to have a class that defines a shelter for the homeless animals – AnimalShelter (yes, i know, it’s a cruel world out there 🙁 ).

To define our animal shelter class, we design it to have a certain number of free cells which will determine how many animals can be hosted in the shelter at a certain time, to have two methods for adding or releasing an animal, and finally, some sort of trick that would only let us host animals of a single kind at any given time, because it would not be a very good idea to host dogs and cats at the same time. The type can be either cats or dogs, and it is unknown and undetermined until we host the first animal in the shelter, at which moment we know for sure that that is the only kind of animal that our shelter can host.

Using only the knowledge we learned so far, we can create our shelter class by using an array of identical objects, to ensure that only a single kind of animal can be hosted at any given moment. So, our objects can be either cats, dogs, or even the generic object type.

Now, let’s construct our shelter for cats.

Analyzing the above code, we see that the shelter capacity – the number of animals that it can host at any given time, is set when the object is created and it has the value of the DefaultPlacesCount constant. Then, we use usedPlaces to keep track of the occupied cells and also to indicate the index of the first free slot on the array.

animal shelter array

Next, we have two methods: Shelter() and Release(int). Shelter will add a new animal in the first free cell in the right side of the array, while Release will remove an animal from a cell of the array, indicated by the int parameter (used it as array index)

animal shelter array release

Then it moves all animals which are having a bigger cell number then the current cell, from which we will release a cat, with a position to the left (steps 2 and 3 are shown in the diagram below).

animal shelter released animal

Released cell at position usedPlaces-1 is marked as free, and a value null is assigned to it. This provides release of the reference to it and respectively allows the system to clean memory (garbage collector), to release the object if it is not used anywhere else in the program at this moment. This prevents from indirect loss of memory (memory leak). Finally, it assigns the number of the last free cell to a usedPlaces field:

generic classes

It is visible that the “removal” of an animal from a cell could be a slow operation, because it requires the transfer of all animals from the next cells with one position left. So far we succeed implementing functionality of the shelter – the class AnimalShelter. When we work with objects of type Cat, everything compiles and executes smoothly:

What happens, however, if we attempt to use an AnimalShelter class for objects of type Dog:

And, as expected, compiler throws an error: The best overloaded method match for ‘AnimalShelter.Shelter(Cat)’ has some invalid arguments. Argument 1: cannot convert from ‘Dog’ to ‘Cat’.

Consequently, if we want to create a shelter for dogs, we will not be able to reuse the class that we already created, although the operations of adding and removing animals from the shelter will be identical. Therefore, we have to literally copy AnimalShelter class and change only the type of the objects, which are handled – Dog. If you remember from few lessons ago, I have said that code re-usability is a very important programming principle, and copy-pasting AnimalShelter classes for each animal is not exactly a good thing.

Ok, but if we decide to make a shelter for other species? How many classes of shelters for the particular type of animals we need to create?

We could use instead of the type Cat, the universal type object, which can take values as Cat, Dog and all other data types we want, but this will create troubles when we need to convert back from the object to the Cat, when creating a shelter for cats and it contains cells of type object, instead of type Cat.

To solve the task efficiently, we have to use a feature of the C# language that allows us to satisfy all required conditions simultaneously, called generic classes, or simply generics (template classes).

We already know from the lesson Methods and functions parameters that when methods and functions need additional information to operate properly, we can offer this information through the use of parameters. When running our program, if we call that method or function, we need to pass arguments to it, which are assigned to their parameters, and then used inside their body.

Like the methods, when we know that the functionality (actions) encapsulated into a class can be applied not only to objects of one, but to many types, and these types are not known at the time of declaring the class, we can use generics (generic types).

It allows us to declare parameters of this class, by indicating an unknown type that the class will work eventually with. Then, when we instantiate our generic class, we replace the unknown with a particular. Subsequently, the newly created object will only work with objects of this particular type that we have assigned at its initialization. The specific type can be any data type that the compiler recognizes, including class, structure, enumeration or even another generic class.

To get a cleaner picture of the nature of the generic types, let’s return to our previous example. As you might guess, the class that describes the animal shelter (AnimalShelter) can operate with different types of animals. Consequently, if we want to create a general solution of the task, during the declaration of class AnimalShelter, we cannot know what type of animals will be sheltered to shelter. This is sufficient indication that we can typify our class, adding to the declaration of the class as a parameter, the unknown type of animals.

Later, when we want to create a cat’s shelter for example, this parameter of the class will pass the name of our type – class Cat. Accordingly, if you create a shelter for dogs, we will pass the type Dog, etc.

Additional Information

Typifying a class (creating a generic class) means to add to the declaration of a class a parameter (replacement) of unknown type, which the class will use during its operation. Subsequently, when the class is instantiated, this parameter is replaced with the name of some specific type.

Formally, the parameterizing of a class is done by adding <T> to the declaration of the class, after its name, where T is the substitute (parameter) of the type, which will be used later:

It should be noticed that the characters ‘<‘ and ‘>’, which surround the substitution T are an obligatory part of the syntax of C# language and must participate in the declaration of generic classes.

So, in our example, the declaration of generic class which describes a shelter for homeless animals should look like as follows:

Now we can imagine that we are creating a template of our class AnimalShelter, which we will specify later, replacing T with a specific type, for instance Cat.

A particular class may have more than one substitute (to be parameterized by more than one type), depending on its needs:

If the class needs several different unknown types, these types should be listed by a comma between the characters ‘<‘ and ‘>’ in the declaration of the class, as each of the substitutes used must be a different identifier (e.g. a different letter) – in the definition they are indicated as T1, T2, …, Tn.

In case we want to create a shelter for animals of a mixed type, one that accommodates both – dogs and cats at same time (though, Tom & Jerry taught us that is never a good idea ^^ ), we should declare the class as follows:

If this were our case, we would use the first parameter T, to indicate objects of type Cat, which our class would operate with, and with U – to indicate objects of type Dog.

So, now that I exemplified the creation of generic classes, I will also show you how to implement and use them, before I go into more complex examples and notions.

This is the way we instantiate a generic class:

Again, similar to T substitution in the declaration of our class, the characters ‘<‘ and ‘>’ surrounding a particular class concrete_type, are required.

In our example, if we want to create two shelters, one for dogs and one for cats, we should use the following code:

In this way, we ensure that the shelter dogsShelter will always contain objects of a type Dog and the variable catsShelter will always operate with objects of type Cat.

Once used during the class declaration, the parameters that are used to indicate the unknown types are visible in the whole body of the class, therefore they can be used to declare the field as each other type:

As you can guess, in our example with the homeless animals shelter we can declare the type of field animalsList, which holds references to objects for the housed animals, instead of a specific type of Cat, with parameter T:

Let’s assume for a moment that when we create an instance of our class (which implies setting a specific type – e.g. Dog) during the execution of the program, the unknown type T will be replaced with the chosen type. If we choose to create a shelter for dogs, we can consider that our field is declared as follows:

So, when we want to initialize a particular field in the constructor of our class, we should do it as usual – creating an array and using substitution of the unknown type – T:

As an unknown type used in the declaration of a generic class is visible in the entire class body, except for field’s declaration, it can be used in a method declaration, namely:

As a parameter in the list of parameters of the method:

– As a result of implementation of the method:

As you already guessed, using our example, we can adapt the methods Shelter() and Release(), respectively:
– As a method of unknown type parameter T:

– And a method, which returns a result of unknown type T:

As we already know, when we create an instance of our class shelter and replace the unknown type with a specific one (e.g. Cat), during the execution of the program, the above methods will have the following form:

– The parameter of method Shelter will be of type Cat:

– The method Release will return a result of type Cat:

Tags: , , , ,

Leave a Reply



Follow the white rabbit