I think I have hit a corner case of extension methods and LINQ.
Today I was declaring some extension methods to make my code more readable.So I created an extension method that gets an object and performs a direct cast:
public static class GeneralExtensions
{
public static T Cast<T>(this object o)
{
return (T)o;
}
}
The intention was to be able to call my direct castings by something like this:
MyObject.CastTo<MyInterface>();
It happens that in the same namespace I have an extension method that has a LINQ expression
using System;
using System.Collections.Generic;
using System.Linq;
public static class EnumExtenstions
{
public static IEnumerable<string> UseLinq(this IEnumerable<object> collection)
{
return (from object value in collection select value.ToString() ).ToList();
}
}
Adding this first extension method to my code base cause the next error
Error CS1936 Could not find an implementation of the query pattern for source type 'object'. 'Select' not found.
Having both extension methods in different namespaces (and not referred), or rename Cast
to something different solves the issue.
I would like to understand a bit more why this is happening. Is it some overlap with LINQ? And if so why my Cast
has priority?
Find the code in .NET Fiddle (Link)
I would like to understand a bit more why this is happening. Is it some overlap with LINQ?
Yes!
When you have a query of the form
from Foo bar in blah select baz
this is rewritten by the compiler into a series of method calls:
blah.Cast<Foo>().Select(bar => baz)
The method calls are resolved just like ordinary method calls. If blah has a member Cast
then it is used; if not, then extension methods are searched.
And if so why my Cast has priority?
The rules for resolving extension methods are a bit tricky but the basic rule is "closest containing class wins". If your extension methods are in a class in the global namespace, and the LINQ extension methods are in a class in the System.Linq namespace, then your extensions are closer. To get to your class the compiler had to go "up" from the current namespace to the global namespace. To get to System.Linq the compiler had to go "up" to the global namespace and then down into System.Linq to find the right class.
In particular, note that the C# compiler does not "backtrack". It does not say "well, the version of Cast that returns object gave me an error when I tried to use Select; is there another version of Cast that works?" The C# compiler simply says that the best possible version of Cast gives an error, and therefore it should not try to find a worse verison of Cast that works. In this particular case that's the behaviour you want, but in many cases you would end up getting an unexpected method called. C# prefers to give an error than to try to guess which method you really meant.
This is an oversimplification of the real rules, which can get complicated when there are multiple "using" directives in multiple nested namespaces. Consult the specification if you need the exact rules.
But better to not go there in the first place. Don't make your own extension methods named Cast, Select, Where, and so on, unless you intend to replicate the LINQ functionality in its entirity.
UPDATE: I just realized that I failed to address a potentially larger problem here: your cast method may not do what you want it to do, regardless of the fact that its naming conflicts with a method of the query pattern.
I note that your cast method is worse than just using a cast operator in a number of ways:
Cast<int>(123)
unnecessarily boxes the int; (int)123
does not.Cast<short>(123)
fails but (short)123
succeeds. There is no conversion from a boxed int to a short.Animal
to Shape
. Cast<Shape>(new Tiger())
fails but (Shape) new Tiger()
succeeds.q
is a nullable int that happens to be null. Cast<string>(q)
succeeds! But (string)q
would fail at compile timeAnd so on. Your cast method has some overlap with the real cast operator but it is by no means a substitute for it. If your intention is to capture the semantics of the cast operator then you need to use dynamic
instead, which starts the compiler again at runtime and does the compile time analysis on the runtime types.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With