понедельник, 9 февраля 2015 г.

Злые шутки расширяющих методов

 Все вроде бы и знают, но как будто никто не хочет говорить об этом. Во всяком случае мне не попадались статьи, где это разобрано. Ну давайте я расскажу.
  Говорить будем, как вы поняли, о расширяющих методах, которые появились аж в далеком .NET 3.0.

  Сперва заглянем в документацию:
Методы расширения позволяют "добавлять" методы в существующие типы без создания нового производного типа, перекомпиляции или иного изменения исходного типа. Методы расширения представляют собой особую разновидность статического метода, но вызываются так же, как методы экземпляра в расширенном типе. Для клиентского кода, написанного на языках C# и Visual Basic, нет видимого различия между вызовом метода расширения и вызовом методов, фактически определенных в типе.
  То есть сделано все, чтобы итоговый пользователь типа не смог отличить обыкновенный метод от расширяющего.
  Давайте рассмотрим пример:
  1. namespace ExtensionMethods
  2. {
  3.     public class Person
  4.     {
  5.         public string Name { get; set; }
  6.         public string LastName { get; set; }
  7.     }
  8.  
  9.     public static class PersonEnumberableExtension
  10.     {
  11.         public static IEnumerable<Person> Where(this IPersonEnumerable pe, Func<Person, bool> func)
  12.         {
  13.             Console.WriteLine("Extension Where");
  14.  
  15.             foreach (var p in pe)
  16.                 if (func(p))
  17.                     yield return p;
  18.         }
  19.     }
  20.  
  21.     public interface IPersonEnumerable: IEnumerable<Person>, IEnumerable
  22.     {
  23.         IEnumerable<Person> Where(Func<Person, bool> func);
  24.     }
  25.  
  26.     public class PersonIEnumerable : IPersonEnumerable
  27.     {
  28.         private IEnumerable<Person> _persons;
  29.  
  30.         public PersonIEnumerable(IEnumerable<Person> persons)
  31.         {
  32.             _persons = persons;
  33.         }
  34.         public IEnumerator<Person> GetEnumerator()
  35.         {
  36.             return _persons.GetEnumerator();
  37.         }
  38.  
  39.         System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
  40.         {
  41.             return GetEnumerator();
  42.         }
  43.  
  44.         public IEnumerable<Person> Where(Func<Person, bool> func)
  45.         {
  46.             Console.WriteLine("Own Where");
  47.  
  48.             return PersonEnumberableExtension.Where(this, func);
  49.         }
  50.     }
  51.  
  52.     class Program
  53.     {
  54.         public static IEnumerable<Person> GetPersons()
  55.         {
  56.             var list = new List<Person>
  57.             {
  58.                 new Person { Name = "Murad", LastName = "Muradov" },
  59.                 new Person { Name = "Artem", LastName = "Muradov" },
  60.                 new Person { Name = "Mikhail", LastName = "Ivanov" },
  61.                 new Person { Name = "Anton", LastName = "Ivanov" },
  62.                 new Person { Name = "Aleksandr", LastName = "Sergeev" },
  63.                 new Person { Name = "Anton", LastName = "Sergeev" }
  64.             };
  65.  
  66.             return new PersonIEnumerable(list);
  67.         }
  68.  
  69.         static void Main(string[] args)
  70.         {
  71.             var persons = GetPersons().Where(=> p.Name == "Anton" || p.Name == "Murad" || p.Name == "Mikhail");
  72.  
  73.             var one = persons.Where(=> x.LastName == "Ivanov");
  74.  
  75.             foreach (var a in one)
  76.                 Console.WriteLine(a.Name + " " + a.LastName);
  77.  
  78.             Console.ReadKey();
  79.         }
  80.     }
  81. }
  Сколько раз будут выведены "Extension Where" и "Own Where"? Правильный ответ - нисколько. Ни родной метод PersonEnumerable, ни расширяющий метод IPersonEnumerable не будут вызваны, потому что у IEnumerable<T> есть свой вариант Where (стойте, стойте, у интерфейса есть своя реализация?).
  Давайте еще раз заглянем в документацию, чтобы определить, какое место расширяющий метод имеет в порядке выбора метода для вызова:
Методы расширения можно использовать для расширения класса или интерфейса, но не для их переопределения. Метод расширения, имеющий те же имя и сигнатуру, что и интерфейс или метод класса, никогда не вызывается. Во время компиляции методы расширения всегда имеют более низкий приоритет, чем методы экземпляра, определенные в самом типе.
  Но тут имеют ввиду только конкретный тип, методы которого расширяют! То есть, если у IEnumerable<T> есть расширяющий метод, который подходит под сигнатуру, то именно он и будет вызван. Даже несмотря на то, что объект, который скрывается под IEnumerable<T>, имеет свою реализацию метода или свой расширяющий метод. Тут логика "виртуальной таблицы" не работает. Совершенно непохоже на механизмы наследования. Это центральное отличие расширяющих методов от методов "из типа".
  В каких случаях можно ошибиться при вызове расширяющего метода? Когда использование расширяющего метода выглядит, как использования наследования. Например, когда вы пытаетесь вернуть из базы набор сущностей с отложенной фильрацией.
  1. public IEnumerable<Person> GetAll()
  2. {
  3.     return _context.Set<Person>();
  4. }
  В примере используется EF, метод Set<T> возвращает реализацию IQueryable<T>. Программист подразумевал, что позже полученный IEnumerable<T> можно будет отложенно фильтровать при помощи Where. Надеялся, что будет вызвана реализация Where из IQueryable<T>, которая запомнит операцию для дальнейшего ее использования при генерации запроса в базу. Однако, все произойдет по другому пути. IEnumerable<T>.Where в итоге получит все записи из базы, а потом применит к полученному предикаты из Where.
  1. public IEnumerable<Person> GetIvanovs()
  2. {
  3.     var personRepository = new PersonRepository();
  4.     return personRepository.GetAll().Where(=> x.Name == "Ivanov").ToList();
  5. }
  Такие дела. Сахар сахаром (Linq), но нужно всегда поглядывать, какой именно метод ты вызываешь.

4 комментария:

  1. Хм, а какая тут, собственно, проблема? Всё четко, если переменная IEnumerable - то вызывать соответсвующий метод. Если нужны твои вызовы, пиши
    public static PersonIEnumerable GetPersons()
    и будет счастье

    ОтветитьУдалить
    Ответы
    1. Тут не проблема, тут особенность расширяющих методов. IEnumerable.GetEnumerator - вызывается метод типа, который реализовал интерфейс. IEnumerable.Where - всегда вызывается расширяющий метод IEnumerable. В этом плане расширяющие методы совсем непохожи на обыкновенные.
      Кстати реализация расширяющих методов IQueryable опирается на реализацию IQueryProvider. С большой поправкой можно назвать наследованием.

      Удалить
    2. Ну почему непохожи, мне эта ситуация напоминает сокрытие методов, например вот такое

      public class BaseClass
      {
      public void Foo ()
      {
      Console.WriteLine("base");
      }
      }

      public class Inherit : BaseClass
      {
      public new void Foo()
      {
      Console.WriteLine("Inherit");
      }
      }

      void Main()
      {
      BaseClass temp = new Inherit();
      temp.Foo();
      }

      Удалить
    3. Когда скрывают методы, обычно дополняют функционал, а не заменяют. А так да, так тоже можно "сломать наследование".

      Удалить