среда, 16 июня 2010 г.

Итераторы в языке C#. Часть 3

  • И напоследок... блок finally

    Последним моментом, о котором нужно обязательно сказать при рассмотрении итераторов в C# являются проблемы,  связанные с блоком finally внутри блока итераторов. Давайте в наш последний пример добавим блок try/finally и даже не  глядя на сгенерированный код, подумаем о его поведении и возможных последствиях:

    public static IEnumerable<int> GetNumbers()
    {
        try
        {
            yield return 7; // 1
            // 2: обработка первого элемента внешним кодом
            yield return 42; // 3
            // 4: обработка второго элемента внешним кодом
        }
        finally
        {
            Console.WriteLine("Внутри блока finally метода GetNumbers");
        }
    }

    Как уже было сказано ранее, блок итератора не выполняется последовательно, а разворачивается в конечный автомат, реализация которого  находится в методе MoveNext. Очевидно, блок finally должен выполняться не после каждого вызова метода MoveNext, а только  один раз на полную итерацию последовательности, поскольку в противном случае мы можем, например, освободить ресурсы, которые  потребуются при следующей итерации цикла. А раз так, то кто сможет гарантировать, что пользователь захочет пройти последовательность целиком? Почему, получив итератор из метода GetNumbers, пользователь обязательно должен вызвать MoveNext более одного раза?

    Но несмотря на то, что компилятор не может гарантировать вызов блока finally итератора, он делает все возможное, чтобы свести такую вероятность к минимуму. Давайте рассмотрим сгенерированный код:

    private sealed class GetNumbersIterator : IEnumerable<int>, IEnumerable,
            IEnumerator<int>, IEnumerator, IDisposable
    {

        // Остальные методы остаются такими же, поэтому пропущены
        private void m__Finally3()
        {
            this.__state = -1; /*after*/
            Console.WriteLine("Внутри блока finally метода GetNumbers");
        }

        private bool MoveNext()
        {
            try
            {
                switch (this.__state)
                {
                    case 0: /*before*/
                        this.__state = -1; /*running*/
                        this.__state = 1; /*running; can finilize*/
                        this.__current = 7;
                        this.__state = 2; /*suspended*/
                        return true;

                    case 2: /*suspended*/
                        this.__state = 1; /*running; can finilize*/
                        this.__current = 42;
                        this.__state = 3; /*suspended*/
                        return true;

                    case 3: /*suspended*/
                        this.__state = 1; /*running*/
                        // Нормальное завершение блока итератора
                        this.m__Finally3();
                        break;
                }
                return false;
            }
            fault
            {
                // Возникло исключение в блоке try
                this.System.IDisposable.Dispose();
            }
        }

        void IDisposable.Dispose()
        {
            switch (this.__state)
            {
                case 1:
                case 2:
                case 3:
                    try
                    {
                    }
                    finally
                    {
                        // Явный вызов метода Dispose
                        this.m__Finally3();
                    }
                    break;
            }
        }
    }

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

    1) после нормального завершения итерирования коллекции (либо после вызова yield break, либо после обыкновенного завершения блока итератора);

    2) в случае генерации исключения в блоке итератора (вы могли обратить внимание на ключевое слово fault вместо finally; это не ошибка, такого ключевого слова нет в языке C#, но такая конструкция существует языке IL, которая означает, что этот фрагмент кода будет выполнен только в случае генерации исключения, после чего исключение будет проброшено далее по стеку);

    3) в случае вызова метода Dispose.

    На последнем случае давайте остановимся подробнее и попытаемся понять для чего вообще итератор реализует интерфейс IDisposalbe. Итак, что произойдет, если исключение произойдет не в нашем коде, а в коде пользователя, при обработке первого элемента коллекции? Давайте снова вернемся к нашему последнему примеру:

    public static IEnumerable<int> GetNumbers()
    {
        try
        {
            yield return 7; // 1
            // 2: обработка первого элемента внешним кодом
            yield return 42; // 3
            // 4: обработка второго элемента внешним кодом
        }
        finally
        {
            Console.WriteLine("Внутри блока finally метода GetNumbers");
        }
    }

    Большинство разработчиков вправе предполагать, что если между строками 1 и 3 (т.е. при обработке первого элемента коллекции пользовательским кодом) произойдет исключение, то блок finally должен быть выполнен точно также, как и при возникновении исключения непосредственно в блоке итератора, например, в строке 1. Такое поведение становится более очевидным, если вместо возвращения двух магических чисел, блок итератора будет выполнять более осмысленную работу, например открывать файл и возвращать строки по одной:

    public static IEnumerable<string> ReadFile(string filename)
    {
        using (TextReader reader = File.OpenText(filename))
        {
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                yield return line;
            }
        }
    }

    В этом фрагменте блок try/finally генерирует за нас компилятор, но это никак не влияет на поведение. Давайте зададим предыдущий вопрос еще раз: вправе ли разработчик рассчитывать на корректное освобождение ресурсов, если пользовательский код сгенерирует исключение?

    К сожалению ответ на этот вопрос будет утвердительным в том случае, если пользователь будет следовать общепринятым идиомам использования итераторов: воспользуется оператором foreach, либо реализует аналогичную функциональность самостоятельно:

    foreach (var s in ReadFile(filename))
    {
        // Обрабатываем очередную строку,
        // при этом при обработке может возникнуть исключение
        Console.WriteLine(s);
    }

    Компилятор преобразовывает оператор foreach следующим образом:

    IEnumerator<string> enumerator = ReadFile(filename).GetEnumerator();
    try
    {
        while (enumerator.MoveNext())
        {
            var s = enumerator.Current;
            // Обрабатываем очередную строку,
            // при этом при обработке может возникнуть исключение
            Console.WriteLine(s);
        }
    }
    finally
    {
        IDisposable disposable = enumerator as System.IDisposable;
        if (disposable != null) disposable.Dispose();
    }

    В таком случае, если при обработке элемента коллекции возникнет исключение, то автоматически будет вызван метод Dispose итератора, что приведет к вызову блока finally блока итератора.

    Заключение

    Подобные знания внутренностей языка программирования могут показаться излишним, и в большинстве случаев, скорее всего, так оно и есть. Но итераторы играют весьма важную роль в языке программирования C#, так, например, большая часть особенностей использования LINQ 2 Objects основана на итераторах и на их «ленивом» выполнении. И это касается не только разработки и применения некоторых библиотек, это также касается  реализации многих повседневных задач. В большинстве случаев прикладной программист может найти другие пути решения и не использовать блоки итераторов, но существует ряд задач, которые очень просто и элегантно решаются именно с их помощью (например, работа с деревьями), поэтому если вы все же столкнетесь с необходимостью применения этого инструмента, желательно обладать достаточным опытом и знаниями, чтобы случайно не прострелить себе ногу.

    Литература

    1. Гамма Э. и др. Приемы объектно-ориентированного проектирования. Паттерны проектирования. Питер, 2007
    2. Skeet J. C# In Depth: What you need to master C# 2 and 3. Manning Publications, 2008
    3. Hejlsberg A. et al. The C# Programming Language. 3rd Edition. A-W Professional, 2008
    4. Skeet J. Iterators, iterator blocks and data pipelines. http://csharpindepth.com/Articles/Chapter11/StreamingAndIterators.aspx
    5. Chen R. The implementation of iterators in C# and its consequences. Parts 1 - 4

    воскресенье, 13 июня 2010 г.

    Итераторы в языке C#. Часть 2

  • В прошлый раз мы начали говорить об итераторах в языке C# и рассмотрели пример создания итератора своими руками. Решение оказалось не очень сложным, но достаточно “многословным”, поэтому в этот раз будет рассмотрено решение той же задачи (итерирование собственной простейшей коллекции) с помощью блока итераторов.

    Блоки итераторов (Iterator Blocks)

    Начиная с версии 2.0 в языке C# появилась возможность реализации итераторов с помощью «блока итераторов» (iterator block), в результате, наш предыдущий пример может быть переписан следующим образом:

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

    Впечатляет! Вместо 40 строк кода, мы получили всего 4, причем две из них это фигурные скобки. Но прежде чем делать какие-то выводы, стоит взглянуть на то, во что преобразует этот код компилятор:

    class CustomContainer
    {
        // Остальной код аналогичен
        public IEnumerator<int> GetEnumerator()
        {
            __GetEnumeratorIterator iterator =
               new __GetEnumeratorIterator(0); /*state = “before”*/
            iterator.__this = this;
            return iterator;
        }

        [CompilerGenerated]
        private sealed class __GetEnumeratorIterator : IEnumerator<int>, IEnumerator, IDisposable
        {
            // Fields
            private int __state;
            private int __current;
            public CustomContainer __this;
            public int __i;

            // Methods
            [DebuggerHidden]
            public __GetEnumeratorIterator(int __state)
            {
                this.__state = __state;
            }

            bool IEnumerator.MoveNext()
            {
                switch (this.__state)
                {
                    case 0: /*before*/
                        this.__state = -1; /*running*/
                        this.__i = 0;
                        while (__i < this.__this.list.Count)
                        {
                            this.__current = this.__this.list[__i];
                            __state = 1; /*suspended*/
                            return true;
                        Label_0056:
                            __state = -1; /*running*/
                            __i++;
                        }
                        break;

                    case 1:
                        goto Label_0056;
                }
                return false;
            }

            [DebuggerHidden]
            void IEnumerator.Reset()
            {
                throw new NotSupportedException();
            }

            void IDisposable.Dispose() { }

            // Properties
            int IEnumerator<int>.Current
            {
                [DebuggerHidden]
                get { return this.__current; }
            }

            object IEnumerator.Current
            {
                [DebuggerHidden]
                get { return this.__current; }
            }

        }
    }

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

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

    ПРИМЕЧАНИЕ
    Конечно же имена вложенного класса и всех его переменных и методов не являются такими "благозвучными". Для  устранения конфликта имен компилятор генерирует имена, которые являются некорректными с точки зрения языка C#, например, реальное имя сгенерированного класса может быть таким: <GetEnumerator>d__0.

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

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

    Figure3
    Рисунок 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 7
    Result=True; Current=7
    Снова вызываем MoveNext()...
                    Сразу после yield return 7
                    Сразу перед yield return 42
    Result=True; Current=42
    Снова вызываем MoveNext()...
                    Сразу после yield return 42
    Result=False (stopping)

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

    private bool MoveNext()
    {
        switch (this.__state)
        {
            case 0: /*before*/
                this.__state = -1; /*running*/
                Console.WriteLine(Test.padding + "Первая строка метода GetNumbers()"); // 1
                Console.WriteLine(Test.padding + "Сразу перед yield return 7"); // 2
                this.__current = 7; // 3
                this.__state = 1; /*state: “suspended”; substat: “after first yield return”*/
                return true;

            case 1: /*state: “suspended”; substat: “after first yield return”*/
                this.__state = -1; /*running*/
                Console.WriteLine(Test.padding + "Сразу после yield return 7"); // 4
                Console.WriteLine(Test.padding + "Сразу перед yield return 42"); // 5
                this.__current = 42; // 6
                this.__state = 2; /*state: “suspended”; substate: “after second yield return”*/
                return true;

            case 2: /*state: “suspended”; substate: “after second yield return”*/
                this.__state = -1; /*after*/
                Console.WriteLine(Test.padding + "Сразу после yield return 42"); //7
                break;
        }
        return false;
    }

    Поскольку весь код метода GetEnumerator расположен в методе MoveNext сгенерированного класса, то этот код не вызовется сразу после создания объекта итератора, а лишь после вызова метода MoveNext. При этом даже при вызове метода MoveNext этот код не будет вызван целиком, как мы привыкли думать о коде обычного метода, вместо этого он будет вызываться по частям.

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

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

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

    При генерации конечного автомата в сгенерированном коде нет различий между состояниями before, running и after (каждому из них соответствует состояние, равное -1), поскольку поведение кода в эти моменты времени является одинаковым (согласно спецификации, попытка обращения к свойству Current приводит к неопределенному поведению). Состояние конечного автомата suspended содержит множество «подсостояний», которое определяется количеством ключевых слов yield return блока итераторов; это было бы явно видно при использовании дополнительной переменной состояния, однако в данной реализации это делается за счет того, что состоянию suspended соответствует не одно значение, а множество положительных значений переменной __state: в нашем случае 1 (after first yield return) и 2 (after second yield return).

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

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

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

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

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

    private static IEnumerable<int> GetNumbers()
    {
        return new GetNumbersIterator(-2); /*state: before GetEnumerator called*/
    }

    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: /*before*/
                    this.__state = -1; /*running*/
                    this.__current = 7;
                    this.__state = 1; /*suspended*/
                    return true;

                case 1: /*suspended*/
                    this.__state = -1; /*running*/
                    this.__current = 42;
                    this.__state = 2; /*suspended*/
                    return true;

                case 2: /*suspended*/
                    this.__state = -1; /*after*/
                    break;
            }
            return false;
        }

        IEnumerator<int> IEnumerable<int>.GetEnumerator()
        {
            if ((Thread.CurrentThread.ManagedThreadId == this.l__initialThreadId)
                && (this.__state == -2)) /*__state == "before GetEnumerator called"*/
            {
                this.__state = 0; /*before*/
                return this;
            }
            return new GetNumbersIterator(0); /*state == "before"*/
        }
        // Остальной код опущен
    }

    Теперь становится понятным причина, по которой первоначальное состояние итератора передается в конструкторе,  в этом случае у нас может быть два первоначальных состояния итератора: -2 (before GetEnumerator called) и  0 (before). Это объясняется тем, что наш итератор играет две роли: роль итератора и роль итерируемой коллекции, а раз  так, то нам нужно как-то отличать, что мы создаем. Поскольку внутри метода GetNumbers мы создаем коллекцию, мы  передаем начального состояния, равного -2, а при создании  другого экземпляра итератора, мы указываем начальное состояние, равное 0. Затем в методе GetEnumerator отслеживается, является ли это обращение к нему первым (в этом случае возвращается this), либо нет (в таком случае возвращается новый итератор).

    В следующий раз: блок finally или как не отстрелить себе ногу с помощью блока итераторов

    пятница, 11 июня 2010 г.

    Итераторы в языке C#. Часть 1

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

    Общий вид шаблона проектирования «Итератор» следующий:

    1  
    Рисунок 1 – Общий вид шаблона проектирования «Итератор»

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

    Создание итератора своими руками

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

    ПРИМЕЧАНИЕ
    Стандартные идиомы именования, применяемые в языке C# и платформе .NET несколько отличаются от стандартных идиом именования, применяемых в реализации этого шаблона проектирования в других языках и средах. Возможно более уместными названиями были бы такие названия, как Iterable и Iterator, но поскольку можно с увернностью сказать, что менять эти названия никто не станет, вы просто должны понимать, что за интерфейсами IEnumerable и IEnumerator скрывается именно «итерируемая коллекция» и «итератор».

    Давайте начнем с менее распространенного варианта, который основан не на реализации интерфейсов IEnumerable или IEnumerable<T>, а на соответствии кода приведенному выше шаблону (реализация итератора с помощью одного из интерфейсов IEnumerable является аналогичной, просто хочется подчеркнуть, что реализация интерфейса IEnumerable не является обязательной):

    ПРИМЕЧАНИЕ
    Необходимость в сопоставлении с шаблоном (“match the pattern”) потребовалось разработчикам языка C# 1.0 для того, чтобы реализовать типизированные коллекции без использования обобщений (который на тот момент еще не было). Интерфейс IEnumerable возвращает object, а это значит, что было бы невозможно реализовать эффективный итератор по типизированной коллекции целых чисел, поскольку каждый раз при получении элемента коллекции происходила бы упаковка и распаковка текущего элемента.

    class CustomContainer
    {
        public int this[int idx] { ... }
        public int Count { ... }
        public void Add(int value) { ... }

        public CustomIterator GetEnumerator()
        {
            return new CustomIterator(this);
        }
       
        public struct CustomIterator
        {
            internal CustomIterator(CustomContainer container)
            {
                this.container = container;
                currentIndex = -1;
            }

            public int Current
            {
                get 
                {
                    if (currentIndex == -1 ||
                        currentIndex == container.Count)
                    {
                        throw new InvalidOperationException();
                    }
                    return container[currentIndex];
                }
            }

            public bool MoveNext()
            {
                if (currentIndex != container.Count)
                {
                    currentIndex++;
                }
                return currentIndex < container.Count;
            }
           
           // При реализации итератора без интерфейса IEnumerator
           // этого метода может и не быть
            public void Reset()
            {
                currentIndex = -1;
            }

            private readonly CustomContainer container;
            private int currentIndex;
    }

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

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

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

    ПРИМЕЧАНИЕ
    Хотя, как мы увидим позднее, код, генерируемый компилятором не всегда соблюдает подобные принципы. Так, например, если метод «блок итератора» возвращает IEnumerable или IEnumerable<T>, то компилятор сгенерирует класс, который будет одновременно и «коллекцией» и итератором.

    Figure2  
    Рисунок 2 – Контейнер с двумя объектами итераторами

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

    Хотя приведенная выше реализация не является слишком сложной с технической точки зрения, но она достаточно объемная (при учете, что эта реализация не отслеживает изменение коллекции), да и допустить off-by-one ошибки (*) очень просто. Поэтому не удивительно, что далеко не все пользовательские коллекции в C# 1.0 поддерживали этот шаблон проектирования, многие из них просто предоставляли специфический интерфейс доступа к своему содержимому. Также не должно быть удивительным то, что разработчики языка C# упростили этот процесс в будущих версиях языка, в частности, путем введения «блока итераторов» (iterator block), и ключевых слов yield return и yield break.

    В следующий раз: решение этой же задачи с помощью блока итераторов (Iterator Blocks).

    -------------------------------
    (*) Это стандартное название ошибки завышения или занижения на единицу индекса массива (http://en.wikipedia.org/wiki/Off-by-one_error), но я не знаю человеческого и при этом звучного названия этой ошибки на русском языке; вариант, вида «ошибка занижения или завышения на единицу» вообще нельзя назвать

    четверг, 3 июня 2010 г.

    Пять принципов чистых тестов (F.I.R.S.T. Principles)

    Многим разработчикам известны принципы проектирования, которые благодаря Роберту Мартину получили звучное название  S.O.L.I.D. Многим из этих принципов уже не один десяток лет (принцип подстановки Лисков впервые был озвучен более двадцати лет назад), они были опробованы на миллионах строк кода, тысячами разработчиками (при этом добрая половина из них применяла эти принципы даже не имея понятия об этом:) ). Конечно, слепое следование любым принципам никогда ни к чему хорошему не приводило и приводить не будет, но тем не менее в них описаны разумные вещи, о которых нужно как минимум знать, да и понимать их тоже будет совсем не лишним.

    Помимо принципов проектирования существуют и другие, незаслуженно менее известные принципы, которые положены в основу написания качественных тестов. На каждом шагу говорится о качестве кода, продуманности дизайна или архитектуры, но при этом довольно слабо уделяется внимание читабельности и сопровождаемости тестов. А ведь объем кода в тестах по хорошему может быть (а точнее должен быть) не меньшим, чем объем кода; возможно именно из-за некачественного кода модульных тестов они так редко поддерживаются в актуальном состоянии, что очень быстро приводит к тому, что они устаревают, становятся неактуальными и вообще остаются на “обочине” процесса разработки.

    Принципы написания качественных тестов придуманы не на пустом месте. Большинство опытных разработчиках знают о них точно так же, как и о принципах проектирования без помощи “Дядюшки” Боба, но как и в случае с принципами проектирования именно Боб Мартин объединил пять принципов тестирования, в результате чего получилось звучное название Б.Н.П.О.С. (или F.I.R.S.T., если вам звучность русскоязычного названия не по душе): Fast (Быстрота), Independent (Независимость), Repeatable (Повторяемость), Self-Validating (Очевидность) (*), Timely (Своевременность)).

    Итак, каждый модульный тест должен обладать следующими характеристиками:

    Быстрота (Fast). Тесты должны выполняться быстро. Все мы знаем, что разработчики люди, а люди ленивы, поскольку эти выражения являются “транзитивными”, то можно сделать вывод, что люди тоже ленивы. А ленивый человек не захочет запускать тесты при каждом изменении кода, если они будут долго выполняться.

    Независимость (Independent). Результаты выполнения одного теста не должны быть входными данными для другого. Все тесты должны выполняться в произвольном порядке, поскольку в противном случае при сбое одного теста каскадно “накроется” выполнение целой группы тестов.

    Повторяемость (Repeatable). Тесты должны давать одинаковые результаты не зависимо от среды выполнения. Результаты не должны зависеть от того, выполняются ли они на вашем локальном компьютере, на компьютере соседа или же на билд-сервере. В противном случае найти концы с концами будет весьма не просто.

    Очевидность (Self-Validating). Результатом выполнения теста должно быть булево значение. Тест либо прошел, либо не прошел и это должно быть легко понятно любому разработчику.  Не нужно заставлять людей читать логи только для того, чтобы определить прошел тест успешно или нет.

    Своевременность (Timely). Тесты должны создаваться своевременно. Несвоевременность написания тестов является главной причиной того, что они откладываются на потом, а это “потом” так никогда и не наступает. Даже если вы и не будете писать тесты перед кодом (хотя этот вариант уже доказал свою жизнеспособность) их нужно писать как минимум параллельно с кодом.

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

    (*) Пять этих принципов опубликованы в книге “Clean Code”, поэтому русскоязычный вариант перевода взят из русскоязычного издания этой книги. Можно долго спорить о корректности перевода Self-Validating, как “Очевидность”, но, как говорится, что написано пером… И хотя вариант типа “Самодостоверность” выглядит более корректным, с точки зрения семантики вариант “Очевидность” кажется не таким уж и плохим.

    среда, 2 июня 2010 г.

    Распаковка (unboxing) и InvalidCastExcpetion

    Несмотря на то, что упаковка и распаковка (boxing/unboxing) стала встречаться значительно реже в повседневной практике разработчика после появления обобщенных (generic) коллекций в C# 2.0, эта тема все еще остается одной из самых коварных и малопонятных для многих, поскольку поведение во время выполнения далеко не всегда является интуитивно понятным и ожидаемым с их точки зрения.

    Классическим примером ошибок, связанных с упаковкой/распаковкой является изменения не того экземпляра значимого типа (value type), когда в результате выполнения некоторого кода изменяется не требуемый объект, а всего лишь его копия (именно это и является причиной того, что изменяемые (mutable) структуры являются главным вселенским злом). Другим примером является неочевидное для многих поведение, когда при распаковке объекта одного типа в переменную другого типа генерируется исключение InvalidCastException.