Introduction
With the introduction of LINQ in .NET 3.0, Microsoft added the ability to extend object functionality directly, without having to sub-class. This article will cover the basics of how to write extension methods. The techniques demonstrated require either Visual Studio 2008 or latter, or an earlier version of Visual Studio with the .NET 3.0 Framework.
Extension Methods
Writing an extension method is pretty easy. In Visual Basic, you write a method within a module, tack on the <Extension()>
attribute from the System.Runtime.CompilerServices
namespace, and have the first parameter set to the type of object that will be extended. In C# you create a static
class, add your method as a static
member, and have the first parameter set to the type of object that will be extended, using the this
modifier. Now when IntelliSense pops up on objects of that type, your extension method will appear with the "extension" icon.
There are a number of limitations to be aware of. You can only write extension methods, not properties or events. Extension methods can overload existing methods, but not override them. And if your extension methods are in a different namespace from the calling code, you must use ;the Imports
(VB) or using
(C#) statement to bring your extensions into scope.
Extending Strings: The Basics
What got me started thinking about extensions was the String
object: specifically, the lack of a case-insensitive Replace
method. I am adding merge functionality to an in-house application, and will have a base document containing tokens; these tokens will then be swapped out for data taken out of our database. Because these documents are written by users, I have little control over the casing of the tokens.
I found the beginning of a solution in the Microsoft.VisualBasic
namespace. That library has a Strings
toolbox, with methods that have historically been available in VB. The Strings.Replace
method allows the coder to select either a binary (case sensitive) or text (case insensitive) search.
The first problem was that Strings.Replace
is a toolbox method rather than an intrinsic one, which can make it awkward to use. It has a lot of parameters, most of which are not needed in most cases, so it has a different syntax than the standard Replace
method. Lastly, a lot of C# coders do not like using the Visual Basic library. Writing an extension method lets you hide these issues. This is how it would look in VB:
Namespace MyExtensions
<HideModuleName()> _
Public Module ExtensionModule
<System.Runtime.CompilerServices.Extension()> _
Public Function MyReplace(ByVal Str As String, _
ByVal Find As String, _
ByVal Replacement As String) As String
Return Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Text)
End Function
End Module
End Namespace
And in C#:
namespace MyExtensions
{
public static class ExtensionMethods
{
public static String MyReplace(this String Str,
String Find,
String Replacement)
{
return Microsoft.VisualBasic.Strings.Replace(Str,
Find,
Replacement,
1,
-1,
CompareMethod.Text);
}
}
}
You will note that the first parameter holds the value of the object itself, in this case a String
, which allows you to use it in your method.
If you are importing or using the MyExtensions
namespace, IntelliSense will now show the method MyReplace
on all String
objects. In C#, the type string
translates to the .NET String
object, so your extension will be available from either type.
Dim S As String = "ABCDEFGHI"
Dim S1 = S.MyReplace("hi", "yz")
This code creates a String
object named S
and gives it a value. A second String
object, S1
, is created and assigned a value using the extension method. Because MyReplace
performs a case insensitive replace, S1
will be assigned a value of "ABCDEFGyz".
You may have noticed that the above examples use value types for the first parameters. This is not a requirement, at least not in VB: by using a reference parameter, you can create extension methods that modify the calling object itself. Consider this method:
Namespace MyExtensions
<HideModuleName()> _
Public Module ExtensionModule
<System.Runtime.CompilerServices.Extension()> _
Public Sub MyReplaceRef(ByRef Str As String, _
ByVal Find As String, _
ByVal Replacement As String)
Str = Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Text)
End Sub
End Module
End Namespace
Because the parameter Str
is passed by reference, you can reassign its value. With this method, you can now do something like this:
Dim S As String = "ABCDEFGHI"
S.MyReplaceRef("hi", "yz")
After this code is run, S
will have a value of "ABCDEFGyz".
I have not been able to write similar functionality in C#; the compiler does not like it when I pass the first parameter in as a pointer. Then again, I code VB for a living so I might be missing something obvious. If anyone knows how to do this, please let me know.
Extending Strings: Overloading
The above examples use distinct names for the extension methods. But what if I just wanted to call it Replace
?
You can write extensions that overload existing methods, provided that your extensions have a different effective signature; do not count the first parameter which gives the object being extended. If you were to simply change MyReplace
to Replace
, your extension will not appear because its effective signature is String String
, the same as one of the two built-in String.Replace
methods. To make it available, you must change the signature. We can do this easily by adding a boolean parameter to show whether or not your replace will be case sensitive. This is what the revised method looks like:
Namespace MyExtensions
<HideModuleName()> _
Public Module ExtensionModule
<System.Runtime.CompilerServices.Extension()> _
Public Function Replace(ByVal Str As String, _
ByVal Find As String, _
ByVal Replacement As String, _
ByVal CaseSensitive as Boolean) As String
If CaseSensitive Then
Return Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Binary)
Else
Return Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Text)
End If
End Function
End Module
End Namespace
And in C#:
namespace MyExtensions
{
public static class ExtensionMethods
{
public static String Replace(this String Str,
String Find,
String Replacement,
bool CaseSensitive)
{
if (CaseSensitive)
return Microsoft.VisualBasic.Strings.Replace(Str,
Find,
Replacement,
1,
-1,
CompareMethod.Binary);
else
return Microsoft.VisualBasic.Strings.Replace(Str,
Find,
Replacement,
1,
-1,
CompareMethod.Text);
}
}
}
Now, IntelliSense will show that there are three forms of String.Replace
; the third one will have the three parameter signature and be flagged as an extension.
In Visual Basic, it is possible to write extension methods with optional parameters (C# does not support optional parameters.)
Namespace MyExtensions
<HideModuleName()> _
Public Module ExtensionModule
<System.Runtime.CompilerServices.Extension()> _
Public Function Replace(ByVal Str As String, _
ByVal Find As String, _
ByVal Replacement As String, _
Optional ByVal CaseSensitive as Boolean = False) As String
If CaseSensitive Then
Return Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Binary)
Else
Return Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Text)
End If
End Function
End Module
End Namespace
This will compile just fine and appear through IntelliSense. You will run into trouble if you try to use it, however.
Dim S As String = "ABCDEFGHI"
Dim S1 = S.Replace("hi", "yz")
The problem is that the compiler checks the built-in methods first. If, and only if, none of the built-in methods match will it check for extensions.
In the above example, Replace
is being called with a String String
signature. Because this matches the signature of one of the built-in String.Replace
methods, the compiler will use the build-in method and not your code. The moral is that if you are going to write extension methods with optional parameters, you should make sure that the required parameters make your methods' signatures distinct.
Beyond Strings
Of course, the techniques outlined above are not limited to just String
objects. You could, say, write a DateTime
extension which returns a frequently used text format:
<System.Runtime.CompilerServices.Extension()> _
Public Function ToStringAnsi(ByVal DT As DateTime) As String
Return DT.ToString("yyyy-MM-dd")
End Function
public static String ToStringAnsi(this DateTime DT)
{
return DT.ToString("yyyy-MM-dd");
}
Or maybe you often need the inverse of a decimal variable:
<System.Runtime.CompilerServices.Extension()> _
Public Function Invert(ByVal D As Decimal) As Decimal
Return 1 / D
End Function
public static Decimal Invert(this Decimal D)
{
return (1 / D);
}
Extending Generic Types
Any object can be extended, even generic objects. Suppose you wanted to create a Maximum
method on a generic List
that returns the item in the list with the maximum value. You start by declaring the extension itself to be generic, which brings your strong type into the picture. You can also apply constraints just as with any other generic declaration. Then, use the type to set up your other parameters and code away.
<System.Runtime.CompilerServices.Extension()> _
Public Function Maximum(Of T As IComparable)(ByVal L As List(Of T), _
ByVal Comparer As IComparer(Of T)) As T
If L Is Nothing Then Return Nothing
Dim Max As T = L(0)
For Each Item As T In L
If Comparer.Compare(Item, Max) > 0 Then Max = Item
Next
Return Max
End Function
public static T Maximum<T>(this List<T> L, IComparer<T> Comparer)
where T: IComparable
{
if (L == null) return default(T);
T Max = L[0];
foreach (T Item in L)
{
if (Comparer.Compare(Item, Max) > 0) Max = Item;
}
return Max;
}
In these code examples, note that T
is required to implement IComparable
; this will automate a lot of your safety checking. Because the extension is declared generic, you can pass T
to the rest of your method, including the declarations of L
and Comparer
. The code verifies that there is, in fact, a valid List
, makes a note of the first item in the list, and then calls Comparer.Compare
on each item in the list, updating the temporary storage if a greater value is found. After iterating through the list, the temporary storage holds the maximum value, which is returned.
Public Sub Test()
Dim L As New List(Of String)
L.Add("Alpha")
L.Add("Beta")
L.Add("Gamma")
L.Add("Delta")
System.Diagnostics.Debug.WriteLine(L.Maximum(StringComparer.CurrentCulture))
End Sub
public void Test()
{
List<String> L = new List<String>();
L.Add("Alpha");
L.Add("Beta");
L.Add("Gamma");
L.Add("Delta");
System.Diagnostics.Debug.WriteLine(L.Maximum(StringComparer.CurrentCulture));
}
StringComparer
provides several comparison methods for String
objects; CurrentCulture
performs a case sensitive string comparison based on the workstation's cultural settings. Assuming an English based setting, these examples will return "Gamma".
There an odd difference between the VB and C# versions that should be noted. The VB method can return Nothing
; generics in C# apparently cannot return null
, so if L == null
I have the method return the default value of T
.
You can also extend objects that have two or more generic parameters, such as Dictionary(Of T1, T2)
.
Extending Interfaces
Strictly speaking, the objects you extend do not have to be objects at all: you can extend interfaces, too. Suppose that you want a Maximum
method on all objects that implement ICollection
and not just on List
objects. You can do this very easily by replacing List
with ICollection
, like so:
<System.Runtime.CompilerServices.Extension()> _
Public Function Maximum(Of T As IComparable)(ByVal IC as ICollection(Of T), _
ByVal Comparer As IComparer(Of T)) As T
If IC Is Nothing Then Return Nothing
Dim Max As T = IC(0)
For Each Item As T In IC
If Comparer.Compare(Item, Max) > 0 Then Max = Item
Next
Return Max
End Function
public static T Maximum<T>(this ICollection<T> IC, IComparer<T> Comparer)
where T: IComparable
{
if (L == null) return default(T);
T Max = default(T);
foreach (T Item in IC)
{
if (Comparer.Compare(Item, Max) > 0) Max = Item;
}
return Max;
}
Now, Maximum
is available on lists, dictionaries, arrays and any other object that implements ICollection
, even when these objects are created intrinsically.
Here again, C# imposes a constraint not found in VB. In Visual Basic, I can set Max
to the first item in the collection; C# does not allow this, so Max
is initialized with default(T)
.
There is no requirement that your interfaces be generic, of course.
Best Practices
Extension methods are a very powerful tool, and with great power comes great responsibility. In 2007, the Visual Basic Team at Microsoft published a blog article titled Extension Methods Best Practices, available here. If you are going to be doing a lot with this technique, it might be good to review these suggestions.
History
- 20th March, 2009: Initial post
- 31st March, 2009: Cleaned up and clarified the text and fixed some formatting issues.
- 1st April, 2009: Some minor corrections.