Extension methods and compile-time checking
Maybe a little tricky, but I wonder why. In System.Linq.Enumerable.cs
of System.Core.dll
we have:
public static int Count<TSource>(this IEnumerable<TSource> source);
In m开发者_开发知识库y code I'm doing something evil:
namespace Test
{
public static class Extensions
{
public static int Count<TSource>(this IEnumerable<TSource> source)
{
return -1; //evil code
}
}
//commented temporarily
//public static class CommentedExtensions
//{
// public static int Count<TSource>(this IEnumerable<TSource> source)
// {
// return -2; //another evil code
// }
//}
public static void Main(string[] args)
{
Console.WriteLine(Enumerable.Range(0,10).Count()); // -1, evil code works
Console.Read();
}
}
If I uncomment CommentedExtensions
, I'll get a compile error saying "this call is ambiguous blabla" as expected. But why I didn't get this error at the first time? It's also ambiguous!
EDIT After another test, I found that I won't get compile errors if the extension methods are in different namespaces, even they are completely the same. Why it's allowed? It brings ambiguous call of methods in c#.
EDIT2 I know in fact the two Count
are different in IL. In fact it's calling
Enumerable.Count(Enumerable.Range(0,10))
and my evil extension method is calling:
MyExtension.Count(Enumerable.Range(0,10))
so they are different. But still I think it's a bad smell. Do we have "real" extension methods? which can prevent the evil behavior?
Section 7.6.5.2 of the C# language specification describes how the compiler determines which extension methods are in scope, and which extension methods take precedence over others :
The search for C [(a candidate extension method)] proceeds as follows:
- Starting with the closest enclosing namespace declaration, continuing with each enclosing namespace declaration, and ending with the containing compilation unit, successive attempts are made to find a candidate set of extension methods:
- If the given namespace or compilation unit directly contains non-generic type declarations Ci with eligible extension methods Mj, then the set of those extension methods is the candidate set
- If namespaces imported by using namespace directives in the given namespace or compilation unit directly contain non-generic type declarations Ci with eligible extension methods Mj, then the set of those extension methods is the candidate set.
Meaning that if you have extension methods in the same namespace than the code from which you call them, these extension methods are selected. Extension methods in an enclosing namespace will be choosed over other namespaces that have been imported.
It appears the C# looks in current name space first
In this example the IL is
.method public hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 27 (0x1b)
.maxstack 8
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: ldc.i4.s 10
IL_0004: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
int32)
IL_0009: call int32 Test.Extensions::Count<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
IL_000e: call void [mscorlib]System.Console::WriteLine(int32)
IL_0013: nop
IL_0014: call int32 [mscorlib]System.Console::Read()
IL_0019: pop
IL_001a: ret
} // end of method Program::Main
If I move the main method into the another namespace (XXX) in this case the compiler resolves the method to the System.Linq version
namespace Test
{
public static class Extensions
{
public static int Count<TSource>(this IEnumerable<TSource> source)
{
return -1; //evil code
}
}
}
namespace XXX{
public static class Program
{
public static void Main(string[] args)
{
Console.WriteLine(Enumerable.Range(0, 10).Count()); // -1, evil code works
Console.Read();
}
}
}
.method public hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 27 (0x1b)
.maxstack 8
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: ldc.i4.s 10
IL_0004: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
int32)
IL_0009: call int32 [System.Core]System.Linq.Enumerable::Count<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
IL_000e: call void [mscorlib]System.Console::WriteLine(int32)
IL_0013: nop
IL_0014: call int32 [mscorlib]System.Console::Read()
IL_0019: pop
IL_001a: ret
} // end of method Program::Main
To explicitly use your method in the latter example you use
namespace XXX{
using Test;
public static class Program
{
public static void Main(string[] args)
{
Console.WriteLine(Enumerable.Range(0, 10).Count()); // -1, evil code works
Console.Read();
}
}
}
.method public hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 27 (0x1b)
.maxstack 8
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: ldc.i4.s 10
IL_0004: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
int32)
IL_0009: call int32 Test.Extensions::Count<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
IL_000e: call void [mscorlib]System.Console::WriteLine(int32)
IL_0013: nop
IL_0014: call int32 [mscorlib]System.Console::Read()
IL_0019: pop
IL_001a: ret
} // end of method Program::Main
If you create a new class and add usings to both namespaces and then use your method that is defined in both namespaces, the error should be there again.
Extension methods are distinguish by namespaces, not by the static classes they are declared in. The compiler can know which one two take if two extension methods are defined in the same namespace.
I think, you are writing Two methods with the same overload which is against the principles of OOPs.
If extension method is within the same namespace scope as that of your usage, then it will take precedence(since it is the closest overload found at the place of usage) over the System.Core.dll one, if the same extension methods exists in both namespaces.
To prove this, change your extension method name to MyCount1(...) & MyCount2(...) , then it should work for you.
精彩评论