Следование шаблону

Недавно я получил следующий вопрос:

Во время семантического анализа цикла foreach в языке C # используется подход на основе шаблона ( pattern - basedapproach ). Аналогично дело обстоит и с LINQ . А почему другие возможности языка, как например конструкция « using » также не использует этот подход?

Отличный вопрос!

Прежде всего, давайте рассмотрим, что же мы понимаем под «подходом на основе шаблона»?

С первого взгляда может показаться, что для использования конструкции foreach итерируемая коллекция должна реализовывать интерфейс IEnumerable или IEnumerable<T>. Но, оказывается, в действительности это не так. На самом деле требуется, чтобы коллекция содержала открытый метод GetEnumerator, который должен возвращать некоторый тип с публичным свойством с именем Current, и открытый метод MoveNext, возвращающий bool. Если компилятор видит выполнение всех этих требований, то он генерирует код, вызывающий эти методы. И только если коллекция не выполняет эти требования, он проверяет наличие интерфейсов IEnumerable и IEnumerable<T>.

По сути, в данном случае язык C# поддерживает строгую форму «утиной типизации» (duck typing). Т.е. если на этапе компиляции известно, что объект содержит метод Quack(), тогда мы считаем этот тип уткой, даже если он не реализует интерфейс IDuck. Существует еще несколько мест в языке C#, где мы используем подобное «сопоставление с образцом»; при наличии необходимых методов, конкретный тип нам не важен.

А почему вообще мы используем такой подход для цикла foreach?

По двум причинам. Во-первых, представьте себе мир без обобщенных типов: мир C# 1.0. Предположим, что у вас есть только что реализованная коллекция из десяти квадратов целых чисел:

 class MyIntegers : IEnumerable
{
  private class MyEnumerator : IEnumerator
  {
    private int index = 0;
    public object Current { return index * index; }
    public bool MoveNext()
    {
      if (index > 10) return false;
      ++index;
      return true;
    }
  }
  public IEnumerator GetEnumerator() { return new MyEnumerator(); }
}

При переборе всех элементов этой коллекции в цикле foreach, происходит упаковка каждого значения коллекции. Как можно этого избежать? Например, так:

 class MyIntegers : IEnumerable
{
  public class MyEnumerator : IEnumerator
  {
    private int index = 0;
    object IEnumerator.Current { return this.Current; }
    int Current { return index * index; }
    public bool MoveNext()
    {
      if (index > 10) return false;
      ++index;
      return true;
    }
  }
  public MyEnumerator GetEnumerator() { return new MyEnumerator(); }
  IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); }
}

Теперь цикл foreach видит открытый метод GetEnumerator, который возвращает MyEnumerator, который, в свою очередь, содержит открытое свойство Current, возвращающее int. Всё, никакой упаковки.

Во-вторых, используя этот подход «MyEnumerator» может быть изменяемой структурой. Да, изменяемые структуры являются плохой практикой программирования, но в данном конкретном случае ничего плохого в этом нет, поскольку цикл «foreach» никогда не будет возвращать сырой нумератор для потенциально неправильного использования. Правда иногда может возникнуть необходимость ослабить нагрузку на сборщик мусора, и использование структуры в качестве нумератора может быть небольшой, но ощутимой выгодой. (Как обычно, не делайте что-либо структурами «только потому, что они быстрее», пока вы не получите явных доказательств того, что это дает ощутимую и важную разницу в производительности.)

Итак, мы использовали подход на основе шаблона для цикла «foreach», во-первых, поскольку мы хотели избежать упаковки в C# 1.0, и, во-вторых, поскольку хотели добиться пусть и небольшого, но важного увеличения производительности за счет отсутствия создания нумератора ссылочного типа.

А как на счет LINQ? Мы также используем этот подход в LINQ. Когда вы пишите «fromcincustomerswhere ... », мы не требуем наличия метода «Where» у объекта «customers», и даже не требуем, чтобы он реализовывал интерфейс IEnumerable. Мы вообще не налагаем каких-либо требований относительно типа; мы, скорее, требуем, чтобы во время разрешения перегрузки был найден экземплярный метод или метод расширения с именем « Where », работающий с паттерном LINQ.

И делаем мы так, опять же, по двум причинам. Во-первых, LINQ был придуман очень поздно; LINQ бы не добился успеха, если бы мы потребовали от каждого, кто хотел бы использовать свой источник данных совместно с LINQ выпускать новую версию источника, с методами Where и Select. Это должна была быть возможность, которая бы работала «на лету» без какого-либо участия со стороны разработчика источника данных.

Второй более высокой причиной было то, что LINQ должен был внести паттерн «монада» в язык C#. LINQ проектировался для использования с монадой «последовательность», но если вы достаточно сумасшедший, то можете использовать синтаксис запросов (query comprehension) с любыми монадами, как показал Вес в своей замечательной статье. Люди иногда спрашивают о том, почему в языке C# нет интерфейса «IMonad», который бы указывал, что тип может использоваться в качестве монады. Короткий ответ заключается в том, что концепция вида «я тип, который придерживается правил поведения монады» является слишком общей, чтобы с ней могла справиться система типов .NET; монада является типом более высокого уровня по сравнению с интерфейсом. Поэтому, вместо того, чтобы закрепить их в системе типов, мы решили закрепить их в компиляторе. (*)

Теперь мы подходим к исходному вопросу: если мы используем подход на основе шаблона в нескольких местах языка C#, то почему бы не использовать его везде? Почему объект должен реализовывать интерфейс IDisposable для использования его в блоке «using»? Почему нельзя просто потребовать наличие открытого метода Dispose?

Ответ заключается в том, что нам нравится основываться на системы типов; и мы делаем это везде, где только можем. Типы является первым и лучшим способом решения проблем в статически-типизированном языке программирования, и мы хотим максимально этого придерживаться. В случае с блоком «using» не видны явные причины, почему объект не должен реализовывать специальный интерфейс. Как мы уже видели, можно избежать упаковки при очистке ресурсов, и даже, если бы этого было сделать нельзя, то затраты на упаковку, при очистке десятка дескрипторов, является ничтожно малыми, по сравнению с затратами на упаковку миллионов целых чисел при переборе элементов коллекции. Размышления, применимые к LINQ, к «using» не применимы – мы не хотим, чтобы вы расширяли поведение чужих объектов при очистке ресурсов, даже если мы хотим, чтобы вы расширяли сортировку, поиск и группировку чужих объектов. Без убедительных аргументов за использования подхода на основе шаблона и без убедительных аргументов против подхода, на основе интерфейса, мы остановились на использовании интерфейса.

Однако, поскольку язык C# пополняется всё более и более высокоуровневыми конструкциями, мы считаем использование подхода на основе шаблона оправданным. Например, новые возможности «async/await», для определения того, как будет осуществляться ожидание определенной задачи, будет использовать подход на основе шаблона, аналогичный LINQ-у. Подход на основе шаблона позволяет большее участие третьих сторон и позволяет проектировщикам языка встроить в язык шаблоны проектирования, которые являются слишком сложными для существующей системы типов. Именно поэтому я ожидаю всё большего и большего количества подобных шаблонов в будущем.

-----------------------------

(*) Подробное объяснение того, почему система типов .NET является недостаточной для представления типов высшего порядка уходит далеко за рамки этого блога. Если вам интересна эта тема, то советую обратить внимание на этот вопрос на StackOverflow. Более подробная информация о том, насколько система обобщенных типов .NET недостаточно для представления нужных шаблонов, содержится в моей недавней статье по этой теме. Если вам нужно еще более простое введение о монадах, чем статья Веса, то посмотрите следующий вопрос на StackOverflow.

Оригинал статьи