84. Generic methods#

Generics aren’t confined to just classes and interfaces; methods can be generic as well. Even if their containing type isn’t generic. Through the use of generic methods, we can ensure greater type safety without compromising on performance.

A generic method is defined similarly to a generic type, in the sense that we specify a list of type parameters in angle brackets <...>. However, the type parameters are defined on the method itself, not on the class.

Key point

Generic methods can utilize their own type parameters, independent of their enclosing class’s constraints. This means a regular class, without any generic type parameters of its own, can still contain methods that operate generically. It also means that within a generic class, a method can introduce its own distinct type parameters, separate from those of the class.

../_images/cover-generic-methods.jpg

Fig. 84.1 Just like how a wrapping station can encase any, regardless of the specifics of the gift, inside a standard box, generic methods wrap around any type, without needing to know its specifics.#

Imagine that you want a method that duplicates every element in a list. Do we have to create separate methods for lists of int, string, double, and so on? No, because this is a prime example of when the of thing that the method deals with doesn’t matter. As long as we get passed a list it doesn’t matter what the list contains. We can utilize generics to write a single solution that works for all types. For the sake of simplicity, let’s write it as a local function.

List<T> duplicateEachItem<T>(List<T> list)
{
    List<T> duplicatedList = new List<T>();
    foreach (T item in list)
    {
        duplicatedList.Add(item);
        duplicatedList.Add(item);
    }
    return duplicatedList;
}

We can now use the generic local function to duplicate lists.

List<int> numbers = new List<int> { 1, 2, 3 };
List<int> duplicatedNumbers = duplicateEachItem(numbers);

Console.WriteLine(String.Join(", ", duplicatedNumbers));
1, 1, 2, 2, 3, 3

When we call DuplicateEachItem in the code above, the compiler infers the type argument for T based on the arguments we pass to the method. The inferred type argument in this case is int. However, you can also specify the type argument explicitly, like in the code below. This might be useful if the compiler misunderstands what we’re trying to do.

duplicateEachItem<int>(numbers); // Explicit type argument.

Note

Generic methods make your code versatile without the need for overloading methods for different types.

Let’s take another example. Say that we’ve got two lists and that we want to combine them into one by taking one item from each list at a time. This operation is, in programming, commonly called a ‘zip’ and can be defined in various ways. Let’s implement it as a generic method using local functions.

List<T> zip<T>(List<T> list1, List<T> list2)
{
    List<T> result = new List<T>();
    int count = Math.Max(list1.Count, list2.Count);

    for (int i = 0; i < count; i++)
    {
        if (i < list1.Count) result.Add(list1[i]);
        if (i < list2.Count) result.Add(list2[i]);
    }

    return result;
}

Warning

Always be cautious with generic methods and ensure that the operations within the method are universally applicable to all potential types.

Let’s try out our generic zipping method.

List<int> listA = new List<int> { 1, 2, 3 };
List<int> listB = new List<int> { 10, 20, 30, 40, 50 };

List<int> zipped = zip(listA, listB);

Console.WriteLine(String.Join(", ", zipped));
1, 10, 2, 20, 3, 30, 40, 50

See also

C# offers a zip method in the System.Linq namespace, which pairs elements from two collections into a tuple or another type. Our implementation here provides a different behavior.

Overloading generic methods is entirely possible. It allows you to offer specialized behavior for certain types while retaining the flexibility of generics for others. However, when overloading generic methods, it’s vital to tread carefully. The compiler chooses which method to invoke based on the method signature. When working with generic methods, type inference plays a crucial role in this decision-making process, which can lead to unexpected behavior if the method signatures are too similar or if type inference leads to ambiguity.

Warning

It’s possible to overload generic methods, but be careful since it’s easy to end up with surprising behavior.

public void Process<T>(T input)
    => Console.WriteLine($"Generic method called.");

public void Process(int number)
    => Console.WriteLine($"Overloaded method called.");
Process(10);      // Invokes the non-generic method.
Process<int>(10); // Invokes the generic method.
Overloaded method called.
Generic method called.

In the above example, when we explicitly use type inference by providing , the generic method is invoked. Without it, the compiler sees the specialized overload for int as a better match and selects it. This demonstrates the necessity of being clear and explicit in both definition and invocation to avoid confusion.

Lastly, extension methods, which we’ve covered in another chapter, can also be generic. This approach has been employed extensively in .NET, especially for defining useful methods on collections. By using generic extension methods, you can add new functionalities to existing types without actually modifying them. For example, you might want to add the duplicateEachItem method directly to the List<T> type to allow for a more intuitive method call:

using System.Collections.Generic;

public static class ListExtensions
{
    public static List<T> DuplicateEachItem<T>(this List<T> list)
    {
        List<T> duplicatedList = new List<T>();
        foreach (T item in list)
        {
            duplicatedList.Add(item);
            duplicatedList.Add(item);
        }
        return duplicatedList;
    }
}

This allows for a more fluent usage:

List<int> numbers = new List<int> { 1, 2, 3 };

// Calling DuplicateEachItem on the instance.
List<int> duplicatedNumbers = numbers.DuplicateEachItem();

Console.WriteLine(String.Join(", ", duplicatedNumbers));
1, 1, 2, 2, 3, 3

Notice how we’re calling DuplicateEachItem on an instance of List<int> rather than passing the list to the method. It’s now a generic instance method rather than a generic local function.

In conclusion, generic methods are a cornerstone of the C# language, that make it possible to eliminate duplicated code without sacrificing static type safety.