As outlined in my
previous post, there are three versioning problems with extension methods:
- The behaviour of your program can change unexpectedly
- The compiler does not warn you about the changes
- Even if the compiler did warn you, your ability to respond is limited
In this post, I’ll outline a suggested solution that fixes all three problems. This will be a lengthy post, so please bear with me (or skip to the conclusion below if you can’t wait ;-)
Introduction
I suggest that:
Extension methods should belong to interfaces. Think of it as defining an "extension contract", a defined set of methods which can be provided as extension methods.
For example, here’s an extension method called
Foo, which belongs to interface
IFooable:
public static class Extender: IFooable
{
public static void Foo(this Thing t)
{
//...
}
}
public interface IFooable
{
void Foo();
}
That’s all standard C# 3, except for the bit in bold, which is new. It means, "The static class Extender provides extension methods
which implement IFooable".
(The definition of interface
IFooable is perfectly ordinary C#, there's nothing new there.)
Details
Every extension method is now associated with a particular “contract for functionality” (the contract represented by the interface). By associating extension methods with defined contracts, we can solve all of the problems noted above.
When the compiler hits this line...
a.Foo()...it identifies all the applicable methods called
Foo [1]. Say it finds an instance method and an extension method, both called
Foo.
- If they both belong to the same interface, the compiler can assume that the instance version is the best choice. The class is, in effect, “overriding” an extension method with a specialized alternative. (In the interests of brevity, I won’t dwell on this point, but please leave a comment below if I haven’t made myself clear.)
- If the methods do not belong to the same interface, the compiler raises an error [2]. The call is ambiguous because the two methods belong to different contracts for functionality.
ExampleLet’s see how this would work in practice. Consider my
previous example of an extension method for strings, called
Contains. If you wrote that method, you’d have to associate it with an interface. You’d write something like this:
public static class StringExtender: IContainable
{
public static bool Contains
(this string source,
string value)
{
//...
}
}
public interface IContainable
{
bool Contains(string value);
}
Now, imagine that Microsoft release their own
Contains method for strings, which is incompatible with yours. The compiler will look at both methods to see which interface they belong to. Microsoft’s Contains may belong to an interface defined by Microsoft
[3] or, since its an instance method, it may not belong to any interface at all. In any case, it does not belong to
your interface. Therefore, the compiler knows that it’s not a substitute for
your Contains.
After detecting that the methods belong to different contracts (interfaces), the compiler raises an error. Fortunately, using interfaces offers us a convenient way to tell the compiler which method we actually want, and thereby resolve the error. Simply include the name of the interface which defines the method you want to call. Or, for instance methods with no interface, use the name of the class. Like this:
string s = "abc";
string t = "a";
//ambiguous
s.Contains(t);
//calls your extension method
s.(IContainable)Contains(t);
//calls Microsoft's instance method
s.(string)Contains(t);
This hypothetical syntax looks a lot like the existing operator for casting
types, except it looks like it's casting
methods – which it is, in a way.
To tie all this back to my
previous post, the current C# spec allows you to force the call to go to the extension method by writing
StringExtender.Contains(s, t).That's like
s.(IContainable)Contains(t). The current C# spec does
not support the opposite,
s.(string)Contains(t), in any way.
Conclusion
There are versioning problems with extension methods. The problems can be solved by making extension methods belong to well-defined contracts for functionally, using standard C# interfaces to define those contracts.
Here is each of the problems I originally stated, and its solution:
Problem: The behaviour of your program can change unexpectedly
Solution: There are no unexpected breakages, because instance methods
only replace extension methods
which belong to the same contract (interface).Problem: The compiler does not warn you about the changes
Solution: The compiler detects any ambiguity, and warns you.
Problem: Even if the compiler did warn you, your ability to respond is limited.
Solution: You can force the call in either direction: to the extension method or to the instance.
Stay tuned for my
next post, in which I’ll outline some groovy side-effects of this proposal... (and also see my later followup
here)
Footnotes
[1] Is there a performance cost, at compile time, in identifying all the possible methods? In the current C# 3 spec, the compiler doesn’t even bother looking for extension methods if an instance one exists. In my suggested approach, it always has to check for extension methods. I would guess that my solution has some compile-time cost, but that the cost would be acceptable on modern hardware.
[2] Or, as I suggested in my previous post, perhaps the compiler could just raise a warning instead, and then go ahead and choose the instance method. That’s not my preferred approach, but it would keep the behaviour in line with Microsoft’s current spec for C# 3 – i.e. the compiler always does what the current spec says, with the difference being it does raise a warning when it knows it's doing something dangerous.
[3] Even if Microsoft gave their interface the same name as yours, the compiler could still tell the difference, just like it does right now for same-named interfaces from different namespaces.