Extension Methods

Extension Methods #

Extension methods allow you to add methods to existing types without modifying their code. This is particularly useful if you want to extend the functionality of a type to which you do not have access. They also allow you to divide and group the functionality of an object across several classes.

Extension methods must be defined in a static class. The first parameter specifies the type being extended and must be preceded by the this keyword.

public static class StringExtensions
{
    public static int WordCount(this string str)
    {
        return str.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

Although extension methods are defined as static methods, they can be called as if they were instance methods of the extended type.

string greeting = "The quick brown fox jumps over the lazy dog.";
int j = greeting.WordCount();

Extending interfaces is particularly useful, as such methods become available to all types that implement the given interface. This is the principle on which LINQ (Language INtegrated Query) operates - it defines extension methods for the IEnumerable<T> type, which in turn is implemented by all collections. Thanks to this, collections have methods like Where, Select, GroupBy, etc., defined. Since most of these methods also return IEnumerable<T>, they can be chained together in a single sequence of calls (fluent syntax).

List<int> numbers = [5, 10, 3, 8, 1];
IEnumerable<int> query = numbers
                            .Where(n => n % 2 == 0)
                            .OrderBy(n => n)
                            .Select(n => n * n);
foreach (int n in query)
{
    Console.WriteLine(n);
}

Extension Blocks (C# 14) #

Extension blocks allow you to add not only extension methods to a type but also properties, operators, and static members.

using System.Text;

public static class StringExtensions
{
    extension(string s)
    {
        public static string operator *(string str, int count)
        {
            if (count < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(count), "Count cannot be negative.");
            }
            if (count == 0 || string.IsNullOrEmpty(str))
            {
                return string.Empty;
            }

            StringBuilder sb = new StringBuilder(str.Length * count);
            for (int i = 0; i < count; i++)
            {
                sb.Append(str);
            }
            return sb.ToString();
        }

        public bool IsEmpty => s.Length == 0;

        public int WordCount()
        {
            return s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }
}

Usage:

string str = "Quick brown fox jumps over the lazy dog.";
Console.WriteLine(str * 3);
Console.WriteLine(str.IsEmpty);
Console.WriteLine(str.WordCount());

Trivia #

We can extend non-iterable types with a method that returns an iterator.

public static class IntExtensions
{
    public static IEnumerator<int> GetEnumerator(this int i)
    {
        while (i-- > 0)
        {
            yield return i;
        }
    }
}

This allows them to be used in a foreach loop.

foreach (int i in 10)
{
    Console.WriteLine(i);
}

Similarly, we can add the ability for a type to be used with the await operator: await anything.

comments powered by Disqus