Студопедия
Случайная страница | ТОМ-1 | ТОМ-2 | ТОМ-3
АрхитектураБиологияГеографияДругоеИностранные языки
ИнформатикаИсторияКультураЛитератураМатематика
МедицинаМеханикаОбразованиеОхрана трудаПедагогика
ПолитикаПравоПрограммированиеПсихологияРелигия
СоциологияСпортСтроительствоФизикаФилософия
ФинансыХимияЭкологияЭкономикаЭлектроника

Несколько слов о вложенных делегатах

Потоки ввода-вывода | Автоматическое управление памятью ссылочных данных | Упаковка и разупаковка данных | Типы данных со значением null | Конструкторы и деструкторы | Частично определяемые классы и их назначение | Механизм вызова событий | Создание пользовательских обобщенных коллекций | Создание обобщенных интерфейсов | Установка ограничений на параметры обобщенных классов |


Читайте также:
  1. А у меня снова было несколько секунд на то, чтобы вскочить на ноги и рвануть бегом по лысой проплешине в траве, обильно политой свежей кровищей.
  2. Б. Составьте несколько ситуаций и объясните, в каком смысле употреблены в них существительные.
  3. В следующих пунктах выберите один или несколько правильных ответов.
  4. В следующих пунктах выберите один или несколько правильных ответов.
  5. В) сведения об авторе (или об авторах, если их несколько).
  6. Вот оно как… Стало быть, не холм, а подземный бункер типа норы Сидоровича, только больше в несколько раз.
  7. Вступление Несколько слов о том, зачем я написала эту книгу

Вы знаете, что делегаты могут быть вложены в тип класса, что должно означать тесную ассоциацию между этими двумя ссылочными типами. Если тип-контейнер при этом оказывается обобщенным, вложенный делегат может использовать в своем определении параметры типа.

// Вложенные обобщенные делегаты могут иметь доступ к параметрам // обобщенного типа-контейнера. public class MyList<T>{ private List<T> listOfData = new List<T>(); public delegate void ListDelegate(T arg); }

 

25. Понятие итератора в языке C#. Оператор foreach. Оператор yield.

Понятие итератора в языке C#

Итератор – объект, абстрагирующий за единым интерфейсом доступ к элементам коллекции. Итератор иногда также называют курсором, особенно если речь идет о базе данных. В Обероне он называется также бегуно́к и представлен как тип данных. В простейшем случае итератором в низкоуровневых языках является указатель.

Использование итераторов в обобщённом программировании позволяет реализовать универсальные алгоритмы работы с контейнерами.

Итераторы в.NET Framework называются 'перечислителями' (enumerators) и представлены интерфейсом IEnumerator. IEnumerator реализует метод MoveNext(), который переходит к следующему элементу и указывает, достигнут ли конец коллекции; свойство Current служит для получения значения указываемого элемента; дополнительный метод Reset() возвращает перечислитель на его исходную позицию. Перечислитель первоначально указывает на специальное значение перед первым элементом, поэтому вызов MoveNext() необходим для начала итерации.

Шаблон проектирования «Итератор» предназначен для последовательного доступа ко всем элементам коллекции (агрегата), не раскрывая ее внутренней структуры. Это один из классических шаблонов проектирования, описанный в знаменитой книге «банды четырех», который подтвердил свою эффективность и жизнеспособность за длительный период применения. Важность и особенности реализации этого шаблона сильно зависят от конкретного языка программирования, но в том или ином виде, он присутствует в большинстве современных языках и библиотеках.

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

Однако терминология, принятая компанией Майкрософт в языке программирования C# и платформе.Net, несколько отличается от общепринятой. Так, в качестве основного инструмента реализации паттерна проектирования «Однонаправленный итератор» используются интерфейсы IEnumerable (простраство имен System.Collections) и обобщенная версия этого же интерфейса IEnumerable<T> (пространство имен System.Collections.Generics). С другой стороны, начиная со второй версии в языке программирования C# появилась новая возможность языка, под названием «итератор». Эта возможность реализуется в языке программирования C# с помощью блока итераторов (Iterator Block), однако на самом деле эта возможность может быть использована как для реализации паттерна проектирования «Итератор», так и паттерна проектирования «Генератор». Поэтому далее в статье, если не сказано обратное, под термином «итератор» будет подразумеваться именно возможность языка программирования, а не паттерн проектирования.

Для реализации паттерна «Однонаправленный итератор» на языке C# нужно выполнить одно из двух условий. Во-первых, можно просто реализовать интерфейс IEnumerable или его «обобщенный» вариант – IEnumerable<T>. Во-вторых, коллекция может просто содержать метод GetEnumerable, который, возвращает объект, содержащий свойство Current и метод MoveNext.

Использование паттерна «Итератор» в языке C# всегда было простым и удобным; оператор foreach упрощает работу с итераторами, самостоятельно вызывая MoveNext до тех пор, пока эта функция не вернет false:

var container = GetCustomContainer(); foreach (var i in container) Console.WriteLine("{0} ", i);

Очевидно, что процесс итерирования физически не связан с самой коллекцией, но еще более важным фактором является то, что можно использовать более одного объекта-итератора для разных, независимых операций перебора элементов, именно поэтому в нашей реализации метод GetEnumerator всегда возвращает новый объект.

Согласно идиоме, принятой в.NET, объект-итератор после создания должен указывать на элемент, предшествующий первому элементу коллекции (в нашем случае это означает, что текущий индекс должен равняться -1), и должен указывать на первый элемент коллекции после первого вызова MoveNext. Метод MoveNext должен возвращать true, если перемещение на следующий элемент коллекции выполнено успешно, в противном случае (если мы уже прошли всю коллекцию), этот метод должен возвращать false (при этом объект-итератор должен указывать на элемент, расположенный за последним элементом коллекции). Метод Reset должен возвращать объект-итератор в первоначальное состояние, а обращение к текущему элементу (к свойству Current) в случае, если объект-итератор указывает на некорректный элемент, должно приводить к генерации исключения InvalidOperationException. Объект-итератор также должен позаботиться о том, чтобы после его создания коллекция не была изменена, и, в случае обращения к текущему элементу после изменения коллекции, также должно генерироваться исключение InvalidOperationException.

Начиная с версии 2.0, в языке C# появилась возможность реализации паттерна «Итератор» с помощью новой возможности языка «Итераторы» (Iterators).

public class CustomContainer{ // Остальной код аналогичен public IEnumerator<int> GetEnumerator() { for (int i = 0; i < list.Count; i++) yield return list[i]; }}

Блок итератора преобразовывается во вложенный private-класс, реализующий интерфейсы IEnumerator, IEnumerator<T> и IDisposable, причем, если ваш метод будет возвращать интерфейс IEnumerator (т.е. необобщенный интерфейс), то в любом случае будут реализованы все три интерфейса, при этом обобщенным интерфейсом будет IEnumerator<object>. В случае возврата интерфейса IEnumerable (или IEnumerable<T>), к этим трем интерфейсам добавятся еще два: IEnumerable и IEnumerable<T>.

Автоматически сгенерированный класс содержит несколько обязательных и несколько необязательных дополнительных полей. Каждый сгенерированный класс реализует конечный автомат, который отслеживает текущее состояние объекта-итератора и переходит к выполнению очередного блока кода внутри блока итератора после вызова метода MoveNext. Данный класс содержит поле __state (состояние конечного автомата), ссылку на внешний класс (__this), а также поле __current, тип которого соответствует типу элемента, возвращаемого объектом-итератором. Необязательными полями являются поля, соответствующие локальным переменным метода GetEnumerator (в данном случае __i), а также все параметры этого метода (поскольку в данном примере метод GetEnumerator не содержит параметров, то соответствующих полей нет).

Большинство сгенерированных методов достаточно просты. Метод GetEnumerator каждый раз просто создает экземпляр итератора и в параметре конструктора передает целочисленное значение, которое является начальным значением состояния (важность этого решения будет понятна при рассмотрении классов, реализующих IEnumerator), а также устанавливает свойство __this, давая возможность итератору получить доступ к самому контейнеру и всему его содержимому; свойство Current возвращает текущее значение итератора (переменную __current), метод Reset не реализован (причем это не особенность реализации, об этом явно сказано в спецификации языка C#), метод Dispose является пустым (позднее я приведу пример, когда это не так), а вся основная работа делается методом MoveNext.

Именно метод MoveNext содержит основной код, который до этого находился в методе GetEnumerator, а также именно в нем находится реализация конечного автомата, отвечающего за изменение текущего значения, возвращаемого итератором. Конечный автомат содержит некоторое количество "предустановленных" состояний (которые описаны в спецификации языка C#), а также ряд дополнительных состояний, количество которых зависит от реализации (точнее, от количества операторов yield return).

Рисунок 3. Конечный автомат состояний итератора

Предыдущий пример достаточно показателен, но для рассмотрения внутреннего устройства сгенерированного кода давайте все же рассмотрим еще более простой код:

static IEnumerator<int> GetNumbers(){ string padding = "\t\t"; Console.WriteLine(padding + "Первая строка метода GetNumbers()"); // 1 Console.WriteLine(padding + "Сразу перед yield return 7"); // 2 yield return 7; // 3 Console.WriteLine(padding + "Сразу после yield return 7"); // 4 Console.WriteLine(padding + "Сразу перед yield return 42"); // 5 yield return 42; // 6 Console.WriteLine(padding + "Сразу после yield return 42"); //7} public static void Main(){ Console.WriteLine("Вызываем GetNumbers()"); IEnumerator<int> iterator = GetNumbers(); Console.WriteLine("Вызываем MoveNext()..."); // Прежде чем обратиться к первому элементу коллекции // нужно вызвать метод MoveNext bool more = iterator.MoveNext(); Console.WriteLine("Result={0}; Current={1}", more, iterator.Current); Console.WriteLine("Снова вызываем MoveNext()..."); more = iterator.MoveNext(); Console.WriteLine("Result={0}; Current={1}", more, iterator.Current); Console.WriteLine("Снова вызываем MoveNext()..."); more = iterator.MoveNext(); Console.WriteLine("Result={0} (stopping)", more); }

Результат выполнения этого кода:

Вызываем GetNumbers()Вызываем MoveNext()... Первая строка метода GetNumbers() Сразу перед yield return 7Result=True; Current=7Снова вызываем MoveNext()... Сразу после yield return 7 Сразу перед yield return 42Result=True; Current=42Снова вызываем MoveNext()... Сразу после yield return 42Result=False (stopping)

Метод MoveNext сгенерированного класса:

private bool MoveNext(){ switch (this.__state) { case 0: /*состояние: "до"*/ this.__state = -1; /*состояние: "выполняется"*/ Console.WriteLine(Test.padding + "Первая строка метода GetNumbers()"); // 1 Console.WriteLine(Test.padding + "Сразу перед yield return 7"); // 2 this.__current = 7; // 3 this.__state = 1; /*состояние: "приостановлен после первого yield return"*/ return true; case 1: /*состояние: "приостановлен после первого yield return"*/ this.__state = -1; /* состояние: "выполняется"*/ Console.WriteLine(Test.padding + "Сразу после yield return 7"); // 4 Console.WriteLine(Test.padding + "Сразу перед yield return 42"); // 5 this.__current = 42; // 6 this.__state = 2; /*состояние: "приостановлен после второго yield return"*/ return true; case 2: /*состояние: "приостановлен после второго yield return"*/ this.__state = -1; /*состояние: "после"*/ Console.WriteLine(Test.padding + "Сразу после yield return 42"); //7 break; } return false;}

Поскольку весь код метода GetEnumerator расположен в методе MoveNext сгенерированного класса, то этот код вызовется не сразу после создания объекта итератора, а лишь после вызова метода MoveNext. При этом даже при вызове метода MoveNext этот код не будет вызван целиком, как мы привыкли думать о коде обычного метода, вместо этого он будет вызываться по частям. При первом вызове метода MoveNext, будет выполнена часть кода, с начала метода до первого оператора yield return (будут выполнены строки 1 и 2). После чего в текущее значение итератора будет сохранено значение 7, текущее состояние итератора будет сохранено путем установки значения __state в 1 (состояние: «приостановлен после первого yield return»), а метод MoveNext вернет true (что скажет вызывающему коду о том, что получен следующий элемент коллекции).

ПРИМЕЧАНИЕ Можно сказать, что каждый блок кода, между операторами yield return выполняется отложенно (lazily) только после очередного вызова метода MoveNext. Именно на этом основана ленивость библиотеки LINQ, которая построена в виде методов расширения интерфейсов IEnumerable и IEnumerable<T>.

При следующем вызове метода MoveNext выполнение будет продолжено сразу же после предыдущего оператора yield return (выполнятся строки 4 и 5), текущее значение итератора станет равным 42, а текущее состояние итератора станет равным 2 (состояние: " приостановлен после второго yield return ") и, опять же, метод MoveNext вернет true.

Следующий вызов метода MoveNext «продолжит» выполнение со строки 7, после чего состояние итератора станет равным -1 (состояние: " после "), а метод MoveNext вернет false, что скажет вызывающему коду о том, что перебор завершен.

При генерации конечного автомата в сгенерированном коде нет различий между состояниями before, running и after (каждому из них соответствует состояние, равное -1), поскольку поведение кода в эти моменты времени является одинаковым (согласно спецификации, попытка обращения к свойству Current приводит к неопределенному поведению). Для каждого yield return вводится отдельное состояние, которому соответствует уникальное целочисленное значение.

Интерфейсы IEnumerable и IEnumerable<T>

При возвращении интерфейса IEnumerable или IEnumerable<T> компилятор генерирует код, очень похожий на рассмотренный ранее, но с некоторыми модификациями. Главной особенностью в этом случае является то, что сгенерированный класс, помимо реализации интерфейсов IEnumerable и IEnumerable<T>, также реализует интерфейсы IEnumerator, IEnumerator<T> и IDisposable. Такое решение принято, по всей видимости, в целях экономии памяти и времени – ведь каждое создание объекта приводит к выделению памяти в управляемой куче, что далеко не бесплатно. В результате в одном классе реализуется и интерфейсы, возвращающие итератор, и сам объект-итератор. Но поскольку возможность независимых проходов по коллекции все равно необходима, разработчики пошли на следующий шаг: при первом вызове метода GetEnumerator возвращается тот же объект, а при последующих вызовах (эта проверка является потокобезопасной) возвращается новый объект, содержащий первоначальное состояние параметров. В связи с этим появляется новое состояние (-2), которое можно назвать " до вызова GetEnumerator ", а также появляются поля, содержащие первоначальные значения параметров (поскольку эти параметры могут изменяться после создания enumerable-объекта).

Давайте изменим предыдущий пример таким образом, чтобы функция GetNumbers возвращала IEnumerable<int>, и посмотрим на код, генерируемый компилятором:

 

static IEnumerable<int> GetNumbers(){ yield return 7; yield return 42;}

Код, сгенерированный компилятором:

private static IEnumerable<int> GetNumbers(){ return new GetNumbersIterator(-2); /*состояние: "до вызоваGetEnumerator"*/} private sealed class GetNumbersIterator: IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable{ // Fields private int __state; private int __current; private int l__initialThreadId; // Methods public GetNumbersIterator(int __state) { this.__state = __state; this.l__initialThreadId = Thread.CurrentThread.ManagedThreadId; } bool IEnumerator.MoveNext() { switch (this.__state) { case 0: /*состояние: "до"*/ this.__state = -1; /*состояние: "выполняется" */ this.__current = 7; this.__state = 1; /*состояние: "приостановлен" */ return true; case 1: /*состояние: "приостановлен" */ this.__state = -1; /*состояние: "выполняется"*/ this.__current = 42; this.__state = 2; /*состояние: "приостановлен"*/ return true; case 2: /*состояние: "приостановлен"*/ this.__state = -1; /*состояние: "после"*/ break; } return false; } IEnumerator<int> IEnumerable<int>.GetEnumerator() { if ((Thread.CurrentThread.ManagedThreadId == this.l__initialThreadId) && (this.__state == -2)) /*"до вызова GetEnumerator"*/ { this.__state = 0; /*"до" */ return this; } return new GetNumbersIterator(0); /* "после"*/ } // Остальной код опущен}

Теперь становится понятным причина, по которой первоначальное состояние итератора передается в конструкторе, в этом случае у нас может быть два первоначальных состояния итератора: -2 ("до вызова GetEnumerator") и 0 (before). Метод GetEnumerator отслеживает, состояние -2, возвращая this. В противном случае создается дополнительная копия.


Дата добавления: 2015-11-16; просмотров: 55 | Нарушение авторских прав


<== предыдущая страница | следующая страница ==>
Обобщенные делегаты| И напоследок... блок finally

mybiblioteka.su - 2015-2024 год. (0.009 сек.)