Freigeben über


C# Extension Methods: Syntactic Sugar or Useful Tool?

Last week a colleague introduced me to extension methods (C#, VB) in .Net. If you're not familiar with extension methods, they were added in Visual Studio 2008 to provide a means for adding functionality to existing types without creating a new derived type. Extension methods are called as if they were instance methods of the existing type. For example, if I wanted to know how many vowels there were in a string, I could add a VowelCountEx method to the String class like this:

 
    static class ExtensionMethods
    {
        public static int VowelCountEx(this String value)
        {
            char[] vowels = new char[] { 'a', 'e', 'i', 'o', 'u' };
            int count = 0;
            foreach (char ch in value)
            {
                if (vowels.Contains(ch))
                {
                    count++;
                }
            }
            return count;
        }
    }

Now the function is displayed by Intellisense as if it was a member function,

 

and I can then call it like this:

 
    class Program
    {
        static void Main(string[] args)
        {
            string value = "my string";
            int count = value.VowelCountEx();
        }
    }

(By the way, I’m not advocating appending Ex to the names of extension method: I simply want it to be clear that VowelCountEx is an extension method.)

My initial reaction was ‘Sweet!'. However, after thinking about it for a couple of days (no, my brain doesn’t work very fast), I began to wonder if there was anything more to this than hiding the this reference. I could achieve the same functionality like this:

    static class ExtensionMethods    {        public static int VowelCountEx(String value)        {            char[] vowels = new char[] { 'a', 'e', 'i', 'o', 'u' };            int count = 0;            foreach (char ch in value)            {                if (vowels.Contains(ch))                {                    count++;                }            }            return count;        }    }    class Program    {        static void Main(string[] args)        {            string value = "my string";            int count = ExtensionMethods.VowelCountEx(value);        }    }

Other than the method showing up in Intellisense for the type, extension methods hadn't added anything. I was sure there must be something more.  Maybe the compiler was doing something under the hood? So I compiled the following code and opened the resulting executable in ILDasm. (The non-extension method has been renamed to VowelCount to differentiate it from VowelCountEx.)

 
    static void Main(string[] args)
    {
        string value = "my string";
        int count = value.VowelCountEx();
        count = ExtensionMethods.VowelCount(value);
    }

Here's the resulting IL:

 
    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // Code size       22 (0x16)
      .maxstack  1
      .locals init ([0] string 'value',
               [1] int32 count)
      IL_0000:  nop
      IL_0001:  ldstr      "my string"
      IL_0006:  stloc.0
      IL_0007:  ldloc.0
      IL_0008:  call       int32 Test2.ExtensionMethods::VowelCountEx(string)
      IL_000d:  stloc.1
      IL_000e:  ldloc.0
      IL_000f:  call       int32 Test2.ExtensionMethods::VowelCount(string)
      IL_0014:  stloc.1
      IL_0015:  ret
    } // end of method Program::Main

So there really isn't any functional difference between extension methods and normal static methods.

If, then, extension methods are essentially syntactic sugar, should they be used? This comes back to one of the basic principles of clear communication: consistency. If you are going to use extension methods, use them everywhere; if you are going to use normal static methods, do that everywhere. But please, please, please don't mix the two. One of my pet peeves when trying to understand a new code base is inconsistency because it means that I constantly have to either look elsewhere in the code or in the documentation (if any) to discover how to use it.  Mixing extension methods and static methods is certain to cause confusion.

The biggest advantage to extension methods is discoverability: when I type my variable name in an Intellisense-aware editor, the extension methods are added to the list. In contrast, when using a normal static method, I have to remember the name of the class that implements it in order to call it. Another plus is the ability to 'extend' sealed classes and valuetypes. I often find that BCL valuetypes don't have the conversions that I need.  Now, I can add the conversion and have it display in Intellisense.  A third is that they integrate seamlessly with generics. This is not an essential, but it sure comes in handy sometimes.  Finally, my code will no longer be littered with calls to StringHelper.This() and ControlHelper.That().

There are also a couple of things I don't like. Adding the this modifier to the function signature is really confusing if you haven't seen it before. The first time I saw it, I thought it was a syntax error.  Imagine my surprise when it compiled!  However, instead of compiling the code, I could have simply 'fixed' the syntax error.  Then when I compiled I would have seen a syntax error like this:

'Program' does not contain a definition for 'CountVowelsEx' and no extension method 'CountVowelsEx' accepting a first argument of type 'String' could be found (are you missing a using directive or an assembly reference?)

Hmmm...there's nothing wrong with the static method because I fixed the syntax error.  Why won't this #$!$!#$#$ file compile?  A second issue I have with extension methods is that they can pollute Intellisense--thus frustrating their biggest advantage. Since extension methods are inherited by all derived classes, deeply derived classes can end up with so many methods that it's difficult to find the one you're looking for.  However, my biggest beef is that the differences between static and instance methods are no longer obvious in the calling function. Consider the following code:

 
    static void Main(string[] args)
    {
        string value = null;
        int count = value.Length;
        count = value. VowelCountEx();
    }

What happens when value.Length is called? The CLR runtime throws a NullReferenceException immediately: the get_Length function is never called because there is no object through which to reference it. However, when value.VowelCountEx() is called, the code jumps to the function and begins executing. The NullReferenceException isn't thrown until the enumerator for the object is accessed in the foreach statement. If you understand that VowelCountEx is actually a static method, this makes perfect sense; however, there is no way to tell this by examining the code in the calling function. This type of subtle difference causes maintenance developers to tear their hair out because the reason isn't obvious. (If you've ever spent time debugging a problem hidden in someone else's C/C++ macro, you know what I'm talking about.)  A corollary to this is that I never need to add a null check for the calling object in an instance method because the object must be valid; however, I do have to add one to an extension method (if the extended object is a reference type) because there is no such guarantee.

So my answer is yes to both questions: extension methods are syntactic sugar but they can be useful syntactic sugar.  For myself, I'm going to begin using extension methods because I like the discoverability and understand the potential pitfalls. However, I'm going to give myself this advice:

  • Use either extension methods or regular static methods to extend class functionality, but not both.
  • Be aware that a call to an extension method is a call to a static method, not an instance method.
  • Keep the code for extension methods in close proximity to the classes they extend so that I'm reminded to update them when I make changes to the original class. If you want to keep all extension methods in a single class but still achieve proximity, use partial classes.
  • Check for null objects in extension methods that operate on reference types.

If I've missed any pros or cons, please post a comment-discussion is good for the soul.

Cheers,Dan

DISCLAIMER: The information in this weblog is provided "AS IS" with no warranties, and confers no rights. This weblog does not represent the thoughts, intentions, plans or strategies of my employer.