Generics - Eishaaya/GMRDataStructuresTest GitHub Wiki

What does Generic mean in Computer Science / C#?

Generics give a class or method the ability to be free from a particular type until it is used in the program. The specifications of the method or class can be written with a “generic” data type so that when the compiler encounters the method or class, it generates code to handle the particular data type that was specified by the caller. Generics are extremely important when implementing data structures as it allows for maximum code reusability without the constraints of a particular type. You can also create generic classes with constraints on particular data types. For example, a set of methods can be restricted to integers only. This allows your methods or classes to be used however the user needs them to be, without regard to adding new constructors or methodologies.

In C#, you can create generic classes, methods, interfaces, events, and delegates. This curriculum will only go over generic classes and methods.

To implement a generic class, a type parameter must be defined:

//Note: T is the standard for declaring generic types in C#
class GenericList<T>
{
    //...
}

//Note: the value within <> denotes the "name" of the generic variable name that one would like use
//Note: using <value> will make value a custom type 
class CustomTypeGenericList<CustomType>(CustomType item)
{

}

The type parameter can be named anything, although the .NET convention is T.

Classes can also have multiple type parameters:

//Note: Using C# standards, make the variables make sense, Type U follows T if they are unrelated
class GenericList<T, U>
{
    //...
}

//Note: Example where the generic item is explicitly defined -- Should not be done like this
class CustomTypeGenericList<CustomTypeBanana, CustomTypeApple>
{
    //...
}

A generic class can be instantiated as follows:

static void Main(string[] args)
{
    GenericList<int, string> genericList = new GenericList<int, string>();
}

Methods can be defined generically as well:

public static void TestSum()
{
    int[] intArray = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    string[] stringArray = { "Data Structures", "Is Fun" };

    //Print string and integer arrays using the same function with no casting
    Print(intArray);
    Print(stringArray);
}

static void Print<T>(T[] array)
{
    foreach(T item in array)
    {
        Console.WriteLine(item);
    }
}

Suppose Print is rewritten without a generic type:

//Note: 2 Functions are needed and overload each other
static void Print(int[] array)
{
    for(int i = 0; i < array.Length; i++)
    {
        Console.WriteLine(array[i]);
    }
}

static void Print(string[] array)
{
    for(int i = 0; i < array.Length; i++)
    {
        Console.WriteLine(array[i]);
    }
}

There would need to be a print method for each input array type. Instead a generic print method is usable by any type of array.

While this may seem to be a only a code saving practice, there is also a very small but extremely useful component to generics, compile time errors. One of the biggest reasons to use generics is to move annoying and hard to find run-time errors to compile time issues. The example below explains the issue.

 class Program
    {
        /// <summary>
        /// A Function that compares 2 items
        /// </summary>
        /// <typeparam name="T">Generic Type</typeparam>
        /// <param name="thing1">first item</param>
        /// <param name="thing2">second item</param>
        /// <returns>Which item is bigger</returns>
        static int CompareTwoThings<T>(T thing1, T thing2) where T : IComparable
        {
            return thing1.CompareTo(thing2);
        }

        /// <summary>
        /// A Function that compares 2 items
        /// </summary>
        /// <param name="thing1">first item</param>
        /// <param name="thing2">second item</param>
        /// <returns>Which item is bigger</returns>
        static int CompareTwoObjects(object thing1, object thing2)
        {
            return ((IComparable)thing1).CompareTo(thing2);
        }

        static void Main(string[] args)
        {
            //This does not cause an error during compile time because they are both comparable
            int result1 = CompareTwoThings(2, 5);

            //This causes an error during compile time because they are NOT both comparable
            int invalid = CompareTwoThings(new Random(), new Random());

            //This does NOT cause an error during compile time because it is possible for the objects coming in to be comparable
            int result2 = CompareTwoObjects(new Random(), new Random());

            //An ArrayList is a container data structure that holds any object, it is dangerous to use because the types may not be relatable to each other
            ArrayList myList = new ArrayList();
            myList.Add(5);
            myList.Add("Hello");
        }
    }

Essentially, everything in C# is an object since the Object class is the root of the type hierarchy. Therefore, accepting an object as a type will allow the same value insertion as generics with one key exception, the compiler does not type check the variables being passed in with the actual functionality in the class. In this way, the line that was mentioned to fail would fail during run time, because it is trying to add 2 Random Class instances while the generic method noted that the type was not valid. This is extremely important to know because as one's code grows more complex, it will become harder and harder to fix run-time errors.

All of the data structures encountered later on will be implemented with generics. As you will see, this approach is especially useful with collection classes.


<-- Previous | Next -->

⚠️ **GitHub.com Fallback** ⚠️