Extension Methods (part 3)


In my previous 2 posts I talked about some of the benefits of extension methods and then delved into the details of our new binding rules for consuming them in your programs. Today I’m going to talk about some perils to be aware of when defining extension methods.

First, however, I’m going to dig a little into the mechanics used by the compiler for evaluating extension method calls. Essentially, whenever the compiler detects a call to an extension method, it simply translates it into a call to the underlying module method, passing in the call’s receiver as the method’s first argument.

If we take the following simple example:

Delegate Sub DelegateSub()

<Extension()> _ 
Sub Times(ByVal x As Integer, ByVal d As DelegateSub)
For i = 1 To x
End Sub

with the following call site:

Sub SayHello()
End Sub

Sub Main()
Call 3.Times(AddressOf SayHello)
End Sub

The compiler will then emit code that is equivalent to the following:

Sub Main() 
Times(3, AddressOf SayHello)
End Sub

For the most part this transformation is simple and automatic. In fact, we’ve taken great pains to ensure that the experience of calling an extension method on an object is as smooth as possible. Unfortunately, however, with value type extensions things get a little bumpy. To illustrate this, let’s consider the following slightly more complex type:

Structure Vector
Public X As Integer
Public Y As Integer 
Public Z As Integer

    Public Sub Scale(ByVal factor As Integer)
X *= factor
y *= factor
z *= factor
End Sub
End Structure

Here we define a method called “Scale” that mutates the current Vector, multiplying each of its members by the given scaling factor. As an instance method, this works fine. However, let’s suppose we were to try and define “Scale2” as an Extension method:

Module Module1
<Extension()> _
Public Sub Scale2(ByVal [me] As Vector, ByVal factor As Integer)
[Me].X *= factor
[Me].y *= factor
[Me].z *= factor
End Sub
End Module

Calling this method will not have the same results as calling Scale. In particular, if we were to execute the following code:

Sub Main()
Dim v As Vector 
v.X = 1
v.Y = 1
v.Z = 1

    Console.WriteLine(“v.x={0}, v.y={1}, v.z={2}”, v.X, v.Y, v.Z)
    Console.WriteLine(“v.x={0}, v.y={1}, v.z={2}”, v.X, v.Y, v.Z)
End Sub

We would see that the value of v is unchanged after the call to Scale2. The reason for this is because value types have “value semantics”, which means that unless they are passed as “byref” parameters they are copied every time they are passed to a procedure. As a result the extension method ends up mutating a copy of “v” rather than “v” itself. To fix this we need to declare the extension method with its “me” parameter declared “byref”.

<Extension()> _
Public Sub Scale2(ByRef [me] As Vector, ByVal factor As Integer)
[me].X *= factor 
[me].Y *= factor
[me].Z *= factor
End Sub

The “Scale2” extension method will then behave identically to the instance method version.

Unfortunately, however, that doesn’t mean extension methods with “byref” me parameters are without their own issues. This is especially true when defining extension methods on reference types. As an example, consider the following:

Imports System.Runtime.CompilerServices 

Class C1
End Class

Module M1
<Extension()> _
Sub WeirdExtensionMethod(ByRef x As C1, ByVal y As C1)
x = y
End Sub

    Sub Main()
Dim x As New C1

        If (x Is Nothing) Then
Console.WriteLine(“X has magically become null. How did this happen?”)

        End If
End Sub
End Module

Here we define an extension method that when called changes the reference of the variable it was called on. This has the potential of being very alarming to consumers of the method (generally calling a method on object doesn’t cause the variable that holds the reference to suddenly point to another object). With extension methods, unfortunately, it is now possible to write code that does this.

As we started development on the feature we briefly debated fixing both of these issues by requiring all value type extensions to have a byref me parameter and all reference type extensions to have a byval me parameter. However, enforcing this rule also meant we would have to make it illegal to define unconstrained generic extension metho ds like the following:

<Extension()> _
Sub GenericExtensionMethod(Of T)(ByVal x As T)
End Sub

However, we felt that this would unduly hurt interoperability between VB and other languages on the .NET platform that did not employ these restrictions. This was mainly because in the absence of these restrictions it is very natural to define methods like the one shown above. If we prevented these methods from being used in VB would then end up throwing out the vast majority of generic extension methods written in those languages.

Unfortunately, this does mean that extension method authors need to be a little bit more cognizant of the methods they create. On the other hand it also enables VB programmers to use libraries written in other languages by programmers who may not be aware of any of the peculiarities of VB. Given that our design goals were centered on consumption rather than authoring, however, we felt this was a fair exchange.

In any case, stay tuned for my next post, where I will discuss Extension Methods and Late Binding.


Leave a comment

Feedback usabilla icon