Without changing the source code or generating new derived types, C#’s extension methods offer a potent means of giving already-existing types new capabilities. They first appeared in C# 3.0.
What are Extension Methods in C#?
A unique sort of static method known as an extension method can be called just like an instance method on the extended type. It implies that you can “add” methods to classes, structs, or interfaces that you may not control or that you are unable to change, like third-party libraries or system types (string, IEnumerable).
Why Use Extension Methods?
Using extension techniques is mostly justified for the following reasons:
Extending Sealed Types: They let you add functionality to classes that are sealed, meaning they can’t be inherited.
Enhancing Third-Party Libraries:Without changing the original code, you can add unique functionality to types provided by third parties.
Promoting Fluent Interfaces (Method Chaining): The term “fluent API” refers to the ability of an extension method to chain together numerous method calls when it returns a value of the same type as its input. This results in more understandable and succinct code.
Simplifying Interfaces: By allowing you to specify essential features in an interface and offering convenient methods as add-ons, they facilitate interface implementation.
Handling Special Cases: In order to avoid clogging calling functions, they can “hide” the processing of common checks (such as null handling) or intricate business rules.
How to Declare and Invoke Extension Methods
Declaration: Within a static class, a static method is called an extension method. This term, which indicates the type being extended, comes before its first parameter. This is its defining feature.
Invocation: Even if an extension method is static, it can be called just like an instance method of the extended type. This is just syntactic sugar; the instance-like call is converted to a static method call by the compiler. A less popular option is to call it directly as a static method of the contained class.
In order for an extension function to be discoverable, either its namespace needs to be imported using a using directive, or the static class that contains it needs to be in the same namespace as the code that uses it. Static directives can also be used to make the extension methods from a certain class available since C# 6.0.
Key Characteristics and Rules
Encapsulation: Encapsulation is unbreakable by extension methods. The extended class, like any other external code, can only be accessed by public (and internal within the same assembly) members.
Compiler Priority: If an extension method and a class already have an instance method with the same signature, the compiler will always give the instance method priority.
Dynamic Objects: In dynamic code, extension methods are not supported. At runtime, a RuntimeBinderException will occur if an extension method is attempted to be called on a dynamic object.
Generics: Because extension methods can be generic, they can be used with many kinds of data.
Null References: Unlike instance methods, an extension method calling on a null reference does not raise a NullReferenceException on its own. By simply setting this argument to null inside the extension method, you can manage null checks inside the procedure.
Code Examples
Simple String Extension
This illustration shows how to add a Shorten method to the string type.
using System;
// Define a static class to hold the extension methods
public static class StringExtensions
{
// The 'this' keyword on the first parameter indicates it's an extension method for 'string'
public static string Shorten(this string text, int length)
{
if (string.IsNullOrEmpty(text) || length >= text.Length)
{
return text;
}
return text.Substring(0, length);
}
}
// To make this code runnable as a complete program, you'd typically put it in a Main method:
class Program
{
static void Main(string[] args)
{
string myString = "Hello World!";
// Calls the extension method as if it were an instance method
string shortenedString = myString.Shorten(5);
Console.WriteLine(shortenedString); // Output: Hello
string anotherString = "This is a longer string.";
string veryShort = anotherString.Shorten(3);
Console.WriteLine(veryShort); // Output: Thi
string shortString = "Hi";
string noChange = shortString.Shorten(10); // length is greater than text.Length
Console.WriteLine(noChange); // Output: Hi
string emptyString = "";
string emptyResult = emptyString.Shorten(5);
Console.WriteLine($"Empty string result: '{emptyResult}'"); // Output: Empty string result: ''
}
}
Output
Hello
Thi
Hi
Empty string result: ''
Extension Method with Null Handling
This demonstrates the behaviour of extension methods when null references are used.
using System; // Required for Console.WriteLine
public static class StringNullExtensions
{
public static string EmptyIfNull(this string text)
{
// This line is concise and correct: if 'text' is null, it returns string.Empty;
// otherwise, it returns the value of 'text'.
return text ?? string.Empty; // Uses the null-coalescing operator
}
}
class Program
{
static void Main(string[] args)
{
// Usage examples:
string nullString = null;
string result1 = nullString.EmptyIfNull(); // No NullReferenceException, result1 will be ""
Console.WriteLine($"Result for null string: '{result1}'"); // Output: Result for null string: ''
string nonNullString = "Hello";
string result2 = nonNullString.EmptyIfNull(); // result2 will be "Hello"
Console.WriteLine($"Result for non-null string: '{result2}'"); // Output: Result for non-null string: 'Hello'
string emptyLiteralString = "";
string result3 = emptyLiteralString.EmptyIfNull(); // result3 will be ""
Console.WriteLine($"Result for empty literal string: '{result3}'"); // Output: Result for empty literal string: ''
}
}
Output
Result for null string: ''
Result for non-null string: 'Hello'
Result for empty literal string: ''
Extension Method on an Interface
Extension methods can be used on interfaces to give interface implementers shared implementations of methods they may require.
using System; // Required for Console.WriteLine
// Define an interface
public interface IVehicle
{
int MilesDriven { get; set; }
}
// Define a static class for extension methods
public static class VehicleExtensions
{
// Extends IVehicle, allowing any class implementing IVehicle to use this method
public static int FeetDriven(this IVehicle vehicle)
{
// Interface members are accessible
return vehicle.MilesDriven * 5280;
}
}
// Implement the interface
public class Car : IVehicle
{
public int MilesDriven { get; set; }
}
// Main class to run the example
class Program
{
static void Main(string[] args)
{
// Usage:
Car myCar = new Car { MilesDriven = 10 };
// Called on the Car object, but the extension method works because Car implements IVehicle
int feet = myCar.FeetDriven();
Console.WriteLine($"Car driven {feet} feet."); // Output: Car driven 52800 feet.
// You could also create another class implementing IVehicle and use the same extension method
// public class Bicycle : IVehicle
// {
// public int MilesDriven { get; set; }
// }
// Bicycle myBicycle = new Bicycle { MilesDriven = 2 };
// int bikeFeet = myBicycle.FeetDriven();
// Console.WriteLine($"Bicycle driven {bikeFeet} feet."); // Output: Bicycle driven 10560 feet.
}
}
Output
Car driven 52800 feet.
Generic Extension Method for Collections (LINQ-style)
As an extension method, this example demonstrates how to construct a custom LINQ-like operator, frequently employing yield return for slow evaluation.
using System;
using System.Collections.Generic;
using System.Linq;
public static class LinqBatchExtensions
{
public static IEnumerable<List<T>> Batch<T>(this IEnumerable<T> source, int batchSize)
{
if (batchSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(batchSize), "Batch size must be greater than zero.");
}
List<T> batch = new List<T>();
foreach (T item in source)
{
batch.Add(item);
if (batch.Count == batchSize)
{
yield return batch;
batch = new List<T>();
}
}
if (batch.Count > 0)
yield return batch;
}
}
class Program
{
static void Main(string[] args)
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8 };
var batchedNumbers = numbers.Where(n => n % 2 == 0).Batch(3).ToList();
foreach (var batch in batchedNumbers)
{
Console.WriteLine($"[{string.Join(", ", batch)}]");
}
Console.WriteLine("\n--- Testing with different batch size ---");
List<string> words = new List<string> { "apple", "banana", "cherry", "date", "elderberry", "fig", "grape" };
var batchedWords = words.Batch(2).ToList();
foreach (var batch in batchedWords)
{
Console.WriteLine($"[{string.Join(", ", batch)}]");
}
Console.WriteLine("\n--- Testing with empty list ---");
List<int> emptyList = new List<int>();
var emptyBatches = emptyList.Batch(3).ToList();
Console.WriteLine($"Number of empty batches: {emptyBatches.Count}");
Console.WriteLine("\n--- Testing with batch size 1 ---");
var singleBatches = numbers.Batch(1).ToList();
foreach (var batch in singleBatches)
{
Console.WriteLine($"[{string.Join(", ", batch)}]");
}
try
{
Console.WriteLine("\n--- Testing invalid batch size ---");
numbers.Batch(0).ToList();
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine(ex.Message);
}
}
}
Output
[2, 4, 6]
[8]
--- Testing with different batch size ---
[apple, banana]
[cherry, date]
[elderberry, fig]
[grape]
--- Testing with empty list ---
Number of empty batches: 0
--- Testing with batch size 1 ---
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
--- Testing invalid batch size ---
Batch size must be greater than zero.
Parameter name: batchSize
Consider extension methods as if you were adding a new tool to an already-existing toolbox, even if you weren’t the one who created it. When you reach for a hammer, you use it as though it had always been there, regardless of whether it was part of the original set or you added it later. Extension methods, on the other hand, make your code more expressive and cleaner by allowing you to apply new functionality to existing objects as though they had it always.
You can also read Exploring Keywords In C#: What They Are And How They Work