|
||||||||||
АвтоАвтоматизацияАрхитектураАстрономияАудитБиологияБухгалтерияВоенное делоГенетикаГеографияГеологияГосударствоДомДругоеЖурналистика и СМИИзобретательствоИностранные языкиИнформатикаИскусствоИсторияКомпьютерыКулинарияКультураЛексикологияЛитератураЛогикаМаркетингМатематикаМашиностроениеМедицинаМенеджментМеталлы и СваркаМеханикаМузыкаНаселениеОбразованиеОхрана безопасности жизниОхрана ТрудаПедагогикаПолитикаПравоПриборостроениеПрограммированиеПроизводствоПромышленностьПсихологияРадиоРегилияСвязьСоциологияСпортСтандартизацияСтроительствоТехнологииТорговляТуризмФизикаФизиологияФилософияФинансыХимияХозяйствоЦеннообразованиеЧерчениеЭкологияЭконометрикаЭкономикаЭлектроникаЮриспунденкция |
Несколько слов о вложенных делегатахВы знаете, что делегаты могут быть вложены в тип класса, что должно означать тесную ассоциацию между этими двумя ссылочными типами. Если тип-контейнер при этом оказывается обобщенным, вложенный делегат может использовать в своем определении параметры типа.
Понятие итератора в языке 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:
Очевидно, что процесс итерирования физически не связан с самой коллекцией, но еще более важным фактором является то, что можно использовать более одного объекта-итератора для разных, независимых операций перебора элементов, именно поэтому в нашей реализации метод GetEnumerator всегда возвращает новый объект. Согласно идиоме, принятой в.NET, объект-итератор после создания должен указывать на элемент, предшествующий первому элементу коллекции (в нашем случае это означает, что текущий индекс должен равняться -1), и должен указывать на первый элемент коллекции после первого вызова MoveNext. Метод MoveNext должен возвращать true, если перемещение на следующий элемент коллекции выполнено успешно, в противном случае (если мы уже прошли всю коллекцию), этот метод должен возвращать false (при этом объект-итератор должен указывать на элемент, расположенный за последним элементом коллекции). Метод Reset должен возвращать объект-итератор в первоначальное состояние, а обращение к текущему элементу (к свойству Current) в случае, если объект-итератор указывает на некорректный элемент, должно приводить к генерации исключения InvalidOperationException. Объект-итератор также должен позаботиться о том, чтобы после его создания коллекция не была изменена, и, в случае обращения к текущему элементу после изменения коллекции, также должно генерироваться исключение InvalidOperationException. Начиная с версии 2.0, в языке C# появилась возможность реализации паттерна «Итератор» с помощью новой возможности языка «Итераторы» (Iterators).
Блок итератора преобразовывается во вложенный 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. Конечный автомат состояний итератора Предыдущий пример достаточно показателен, но для рассмотрения внутреннего устройства сгенерированного кода давайте все же рассмотрим еще более простой код:
Результат выполнения этого кода:
Метод MoveNext сгенерированного класса:
Поскольку весь код метода GetEnumerator расположен в методе MoveNext сгенерированного класса, то этот код вызовется не сразу после создания объекта итератора, а лишь после вызова метода MoveNext. При этом даже при вызове метода MoveNext этот код не будет вызван целиком, как мы привыкли думать о коде обычного метода, вместо этого он будет вызываться по частям. При первом вызове метода MoveNext, будет выполнена часть кода, с начала метода до первого оператора yield return (будут выполнены строки 1 и 2). После чего в текущее значение итератора будет сохранено значение 7, текущее состояние итератора будет сохранено путем установки значения __state в 1 (состояние: «приостановлен после первого yield return»), а метод MoveNext вернет true (что скажет вызывающему коду о том, что получен следующий элемент коллекции).
При следующем вызове метода 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>, и посмотрим на код, генерируемый компилятором:
Код, сгенерированный компилятором:
Теперь становится понятным причина, по которой первоначальное состояние итератора передается в конструкторе, в этом случае у нас может быть два первоначальных состояния итератора: -2 ("до вызова GetEnumerator") и 0 (before). Метод GetEnumerator отслеживает, состояние -2, возвращая this. В противном случае создается дополнительная копия. Поиск по сайту: |
Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Студалл.Орг (0.005 сек.) |