Методы расширения и проверка во время компиляции - PullRequest
5 голосов
/ 13 октября 2010

Может быть немного сложно, но мне интересно, почему. В System.Linq.Enumerable.cs из System.Core.dll имеем:

public static int Count<TSource>(this IEnumerable<TSource> source);

В моем коде я делаю что-то злое:

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();
   }
}

Если я раскомментирую CommentedExtensions, я получу ошибку компиляции, говоря, что «этот вызов неоднозначный блабла», как и ожидалось. Но почему я не получил эту ошибку в первый раз? Это также неоднозначно!

EDIT После другого теста я обнаружил, что не получу ошибок компиляции, если методы расширения находятся в разных пространствах имен, даже если они абсолютно одинаковы. Почему это разрешено? Это приносит неоднозначный вызов методов в c #.

EDIT2 Я знаю, что на самом деле два Count различны в IL. На самом деле это звонит

Enumerable.Count(Enumerable.Range(0,10))

и мой злой метод расширения вызывает:

MyExtension.Count(Enumerable.Range(0,10))

поэтому они разные. Но все же я думаю, что это плохой запах. Есть ли у нас "настоящие" методы расширения? что может предотвратить злое поведение?

Ответы [ 4 ]

4 голосов
/ 13 октября 2010

Раздел 7.6.5.2 спецификации языка C # описывает, как компилятор определяет, какие методы расширения находятся в области видимости, и какие методы расширения имеют приоритет над другими:

Поиск C [(метод расширения кандидата)] выполняется следующим образом:

  • Начиная с самого близкого вложения декларации пространства имен, продолжая каждое вложение декларации пространства имен и заканчивая содержащей его единицей компиляции, предпринимаются последовательные попытки найти подходящий набор методов расширения:
    • Если данное пространство имен или единица компиляции напрямую содержит неуниверсальные объявления типов Ci с подходящими методами расширения Mj, то набор этих методов расширения является набором кандидатов
    • Если пространства имен, импортированные с использованием директив пространства имен в данном пространстве имен или модуле компиляции, напрямую содержат неуниверсальные объявления типов Ci с подходящими методами расширения Mj, то набор этих методов расширения является набором кандидатов.

Это означает, что если у вас есть методы расширения в том же пространстве имен, что и код, из которого вы их вызываете, эти методы расширения выбираются. Методы расширения во вложенном пространстве имен будут выбираться среди других импортированных пространств имен.

2 голосов
/ 13 октября 2010

Похоже, что C # сначала просматривает текущее пространство имен

В этом примере IL равен

.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

Если я перемещу метод main в другое пространство имен(XXX) в этом случае компилятор разрешает метод в версии System.Linq

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

Чтобы явно использовать ваш метод в последнем примере, вы используете

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
0 голосов
/ 13 октября 2010

Я думаю, вы пишете два метода с одинаковой перегрузкой, что противоречит принципам ООП.

Если метод расширения находится в той же области пространства имен, что и область вашего использования, то он будет иметь приоритет (так как это самая близкая перегрузка, найденная в месте использования) по сравнению с System.Core.dll, если в обоих пространствах имен существуют одинаковые методы расширения.

Чтобы доказать это, измените имя метода расширения на MyCount1 (...) & MyCount2 (...), тогда он должен работать для вас.

0 голосов
/ 13 октября 2010

Если вы создаете новый класс и добавляете использование в оба пространства имен, а затем используете свой метод, определенный в обоих пространствах имен, ошибка должна появиться снова.

Методы расширения различаются по пространствам имен, а не по статическим классам, в которых они объявлены. Компилятор может знать, какие два метода используются, если два метода расширения определены в одном и том же пространстве имен.

...