Extension Methods (part 2)

In my previous post I gave a high level overview of some of the benefits of using Extension Methods in VB 9.0. Today I’m going to delve into some of the details about how to define extension methods and then use them in your programs.

Defining Extension Methods

You can define an extension method by creating a method in a VB module and decorating it with the System.Runtime.CompilerServices.Extension attribute. The type of the methods first parameter specifies the type the method extends, and the remaining arguments indicate the method’s signature. In general, we allow an extension method to be defined on any type that can be represented in a VB signature, with the exception of param arrays, optional arguments, and generic type parameters to (because modules may not be generic). Specifically, this enables you to define extension methods on any of the following types:

  1. Classes (Reference Types)
  2. Structures (Value Types)
  3. Interfaces
  4. Delegates
  5. ByRef / ByVal arguments
  6. Generic method parameters
  7. Arrays

Consuming Extension Methods

In order to consume an extension method, you simply need to bring it into scope. The method will then be visible in intellisence and callable as if it was an instance method. This is a rather simple statement, but it has several interesting implications. In particular, this is a distinct departure from the behavior we implemented in the May 2006 Linq CTP, where we required each type that defined extension methods to be individually imported before its extensions could be used. Although this had some advantages, such as providing fine grained control over extension method visibility, it also suffered from numerous drawbacks.

One big problem was the fact that it was different than C#, which makes extension methods visible by importing the namespace that contains the types that define them. To see why this is an issue, consider the following 2 example C# class libraries:

Example 1:

namespace CSharpExtensionMethodLibrary
{
    static class Extensions1
    {
        public static void ExtensionMethod1(this string x)
        {
        }
    }

    static
class Extensions2
    {
        public static void ExtensionMethod2(this string x)
        {
        }
    }

    static class Extensions3
    {
        public static void ExtensionMethod3(this string x)
        {
        }
    }

    static class Extensions4
    {
        public static void ExtensionMethod4(this string x)
        {
        }
    }

    static class Extensions5
    {
        public static void ExtensionMethod5(this string x)
        {
        }
    }
};

Example 2:


namespace
CSharpExtensionMethodLibrary
{
    static class Extensions
    {
        public static void ExtensionMethod1(this string x)
        {
        }

        public static void ExtensionMethod2(this string x)
        {
        }

        public static void ExtensionMethod3(this string x)
        {
        }

        public static void ExtensionMethod4(this string x)
        {
        }

        public static void ExtensionMethod5(this string x)
        {
        }
    }
};

Here the first example defines an extension method library with the extensions defined in several different static classes all in the same namespace. The second example defines the same extension methods concentrated into a single class. From the point of view of a C# consumer, these libraries are equivalent. To consume them, a C# program simply needs to import their containing namespace. For a VB consumer in the May 2006 Linq CTP, however, these libraries are very different. To consume the first one, a VB programmer would need to add 5 imports statements to his code, where as with the second one he would only need to add one.

Using Example 2 from C#:

using CSharpExtensionMethodLibrary;

class C1
{
   
void Main()
   

       
string x = “”;
       
x.Extension1();
       
x.Extension2();
       
x.Extension3();
       
x.Extension4();
       
x.Extension5();
   
}
}

Using Example 2 from VB in the May 2006 CTP:

Imports CSharpExtensionMethodLibrary.Extension1
Imports CSharpExtensionMethodLibrary.Extension2
Imports CSharpExtensionMethodLibrary.Extension3
Imports CSharpExtensionMethodLibrary.Extension4
Imports CSharpExtensionMethodLibrary.Extension5

Module M1
   
Sub Main()
       
Dim x as String = “”
       
x.Extension1()
       
x.Extension2()
       
x.Extension3()
       
x.Extension4()
   
End Sub
End Module

Obviously this has the potential of causing API usability problems for VB programmers consuming C# extension methods.

However, by restricting our rules to only allow extension methods to be defined in modules and changing the rules for extension method visibility to include “any extension method in scope”, we were able to solve this problem and achieve parity with C# in a way that meshed well with existing VB language concepts. This works because VB modules have the property of “hoisting” there members into their containing namespace, so importing a namespace will also bring the extension methods defined in its types into scope. Therefore, by treating C# static classes as VB modules (for the purposes of extension methods), we can ensure that VB customers have an API usability experience that is comparable to C#.

Using Example2.dll from VB in Orcas:

Imports CSharpExtensionMethodLibrary

Module M1
   
Sub Main()
       
Dim x = “”
       
x.Extension1()
       
x.Extension2()
       
x.Extension3()
       
x.Extension4()
   
End Sub
End Module

However, we still retain the ability to import a module directly, so the fine grained control offered by the May 2006 CTP is available for anyone who needs it. We’ve also introduced shadowing rules that give you some degree of control in resolving conflicts between extension methods. More explicitly, we break extension methods into a series of precedence levels based on the mechanism used to bring them into scope. In the event of a conflict between two methods with identical signatures and different precedence levels, we will pick the one with the higher precedence level. The complete list of these levels is shown below (items with lower numbers have higher precedence).

  1. Extension methods defined inside the current module (if there is one)
  2. Extension methods defined inside types in the current namespace or any of its parents, with child namespaces having higher precedence than parent namespaces
  3. Extension methods defined inside any type imports in the current file
  4. Extension methods defined inside any namespace imports in the current file
  5. Extension methods defined inside any project-level type imports
  6. Extension methods defined inside any project-level namespace imports

In the event that these shadowing rules are not enough to resolve ambiguities you may always bind to a particular method by using the old Whidbey-Style shared method syntax (i.e. StringExtensions.Speak(x) as opposed to x.Speak()).

I think that is enough for today. Fortunately, I’ve only scratched the surface. In my next several posts I will dig into some of the issues you need to be aware of when writing extension method libraries, including:

  1. Extension methods on Value Types
  2. VB Late Binding and Object Extensions
  3. Type Inference rules for Generic Extension Methods
  4. Extension method Versioning Issues