среда, 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.

    четверг, 20 мая 2010 г.

    Мониторинг утверждений в период выполнения

    Это последняя статья из цикла статей о теоретических аспектах проектирования по контракту, в которой рассматриваются уровни мониторинга утверждений во время выполнения, а также роль контрактов в создании документации.

    Мониторинг утверждений в период выполнения

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

    Перечень подобных настроек определяется используемым языком или библиотекой, а также количеством поддерживаемых утверждений (как уже говорилось ранее, не все реализации принципов проектирования по контракту поддерживают инварианты циклов). Если рассматривать полный перечень утверждений, изначально предложенный Бертраном Мейером в [Meyer2005], то можно выделить следующие уровни мониторинга:

    no – во время выполнения нет проверок никаких утверждений. В этом случае утверждения играют роль комментариев;

    require – проверка только предусловий на входе методов;

    ensure – проверка постусловий на выходе методов;

    invariant – проверка выполнимости инвариантов на входе и выходе всех экспортируемых методов;

    loop – проверка выполнимости инвариантов циклов;

    check – проверка инструкций утверждений;

    all – выполнение всех проверок (в языке Eiffel этот уровень мониторинга эквивалентен уровню check);

    Каждый последующий уровень автоматически влечет за собой выполнение всех предыдущих. Именно поэтому check и all в языке Eiffel определяют одинаковые уровни мониторинга.

    Результат нарушения утверждения в период выполнения также может изменяться в зависимости от параметров компиляции и может различаться в зависимости от реализации. Так, в языке Eiffel нарушение утверждения в период выполнения всегда приводит к генерации исключения, а в библиотеке Code Contracts разработчик может выбрать требуемое поведение; это может быть либо генерация исключения, либо нарушение стандартного утверждения (assertion), что приведет остановке выполнения программы и выдаче пользователю стандартного диалогового окна.

    Оптимальный уровень утверждений

    Подобная гибкость автоматически приводит к еще одному вопросу: какой уровень мониторинга утверждений во время выполнения является оптимальным? Ответ на него выбирается как компромисс между уровнем доверия к качеству кода и последствиями необнаруженных ошибок в период выполнения.

    Существует два простых крайних случая. Во время отладки системы мониторинг утверждений должен быть включен на максимальном уровне, а для систем с высокой степенью доверия к коду, критичных ко времени выполнения мониторинг утверждений может быть отключен полностью. Если первый случай особых вопросов не вызывает, то второй совет звучит весьма неоднозначно. Хотя в настоящее время широко развиваются системы статистического анализа кода, которые благодаря механизму утверждений способны выявить значительное количество ошибок, формальных доказательств корректности кода все еще не существует, поэтому говорить о “полном” доверии к коду вряд ли возможно. Здесь очень уместно высказывания Тони Хоара:

    Абсурдно выполнять проверку в период отладки, когда не требуется доверие к получаемым результатам, и отключать ее в рабочем состоянии, когда ошибочный результат может стоить дорого или вообще катастрофичен. Что бы вы подумали о любителе плавания, который надевает спас-жилет во время тренировок на берегу и снимает его, бросаясь в море? [Hoare1973]

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

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

    Контракты и документация

    Помимо явных преимуществ использования контрактов для создания надежного и расширяемого программного обеспечения, утверждения обладают еще одним важным преимуществом – самодокументированием.

    Как уже говорилось ранее, обычно, если спецификация и существует, то является отдельным документом, что является нарушением принципа “Нет избыточности” [Meyer2005] или принципа DRY (Don’t Repeat Yourself) [Hunt2002]. Подобное дублирование информации пагубно по той простой причине, что проблемы рассогласования дублируемой информации является лишь вопросом времени. Как бы ни старалась команда разработчиков (или даже выделенный человек) поддерживать информацию в различных источниках в согласованном состоянии, рано или поздно наступит момент, когда рассогласование информации все же произойдет.

    Поскольку проектирование по контракту предполагает содержание элементов спецификации в самом исходном коде, мы получаем единый источник информации о программной системе, вероятность рассогласования которого минимальна.

    В случае проектирования по контракту, предусловия, постусловия и инварианты классов обеспечивают потенциальных клиентов модуля всей необходимой информацией о предполагаемых службах, выраженных в соответствующей и точной форме. “Никакое количество описательной документации не может заменить множества аккуратно выраженных утверждений, являющихся частью самого ПО”.

    Выводы

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

    Литература

    1. [Meyer2005] Мейер Б. Объектно-ориентированное конструирование программных систем. М.: Русская редакция, 2005
    2. [Meyer2009] Meyer B. Touch of Class. Learning to Program Well with Objects and Contracts. London: 2009
    3. [Hoare1981] C.A.R. Hoare: The Emperor’s Old Clothes (1980 Turing Award lecture), in Communications of the ACM, vol. 24, no. 2, February 1981, pages 75-83.
    4. [Hunt2000] Хант Э., Томас Д. Программист-прагматик. Путь от подмастерья к мастеру. М.: Лори, 2007
    5. [McConnell] Макконнелл С. Совершенный код. 2-е издание. СПб.: Питер, 2005
    6. [Howard] Ховард М., Лебланк Д. Защищенный код. 2-е издание. М.: Русская редакция, 2005
    7. [Maguire] Maguire, Steve. Writing Solid Code. Redmond, WA: Microsoft Press, 1993
    8. [Coplien1992] Джеймс Коплиен. Программирование на С++. Питер, 2005
    9. [Hoare1973] C.A.R. Hoare: Hints on Programming Language Design, Stanford University Artificial Intelligence

    понедельник, 17 мая 2010 г.

    Проектирование по контракту. Наследование

    Рассмотренные ранее механизмы взаимодействия между двумя программными элементами моделируют взаимоотношение “использует” (HAS-A relationship) или взаимоотношение типа “клиент-поставщик”. Механизмы наследования, играющие ключевую роль в объектном подходе с точки зрения расширяемости и повторного использования, моделируют взаимоотношение “является” (IS-A relationship) и добавляют взаимоотношения между базовым классом и его потомком.

     Figure4
    Рисунок 4 – Взаимоотношения между объектами

    Опытные разработчики знают, что чрезмерное использование наследования – это один из ключевых факторов, который приводит к созданию сложного в понимании и сопровождении кода. Связано это, прежде всего с тем, что отношение «является» обладает более сильной связью, чем отношение “использует”, и каждый раз, при анализе кода производного класса нужно анализировать поведение всех его базовых классов и четко понимать «контракт», который базовые классы предоставляют своим наследникам. Использование утверждений совместно с наследованием не только упрощает создание производных классов, но и гарантирует клиентам базового класса согласованное поведение при использовании динамического связывания и полиморфизма.

    Инварианты

    Инварианты определяют глобальные свойства некоторого класса, которые должны соблюдаться после его создания на протяжении всего времени жизни. Поскольку производный класс также “является” базовым классом, то все, что характерно для базового класса, характерно и для его потомков.

    ПРИМЕЧАНИЕ
    Правило родительских инвариантов: инварианты всех родителей применимы и к самому классу

    Инварианты базовых классов добавляются к текущему классу и соединяются логической операцией and. Если базовый класс не содержит явного инварианта, то его роль играет инвариант True. По индукции в классе действуют инварианты всех его потомков, как прямых, так и косвенных.

    Предусловия и постусловия

    Полиморфизм и динамическое связывание добавляет некоторые особенности при работе с предусловиями и постусловиями.

    Давайте рассмотрим гипотетический пример. Предположим у нас есть класс С, который содержит ссылку на класс B. Вследствие динамического связывания, в период выполнения вместо объекта класса B может быть использован объект класса D или любого другого наследника класса B.

    Предположим, что в классе B определена открытая функция int Foo(int x), с предусловием x > 5 (pre_b) и постусловием Result > 0 (post_b):

    class B {

      public virtual int Foo(int x) {

          Contract.Requires(x > 5, "x > 5");

          Contract.Ensures(Contract.Result<int>() > 0,

                           "Result > 0");

          // Реализация метода

      }

    }

    Тогда использование объекта класса B внутри класса C может выглядеть следующим образом:

    class C {

      //...

      B b = GetFromSomewhere();

      int x = GetX();

      if (x > 5) { //Проверяем предусловие pre_b

        int result = b.Foo(x);

        Contract.Assert(result > 0); // Проверяем постусловие post_b

      }

    }

    Благодаря проверке предусловия, класс С выполняет свою часть контракта и может рассчитывать на выполнение контракта классом B (или одним из го потомков). Согласно принципу подстановки Лисков [Coplien1992] поведение приведенного фрагмента кода не должно измениться, если во время выполнения динамическим типом объекта B будет один из наследников класса B.

    Но что, если при переопределении функции Foo один из наследников класса B захочет изменить предусловия и постусловия?

    Figure5  
    Рисунок 5 – Некорректное переопределение предусловия и постусловия

    Давайте предположим, что функция int Foo(int x) в класс D начинает требовать больше (содержит более сильное предусловие вида: x > 10), и гарантировать меньше (содержит более слабое постусловие вида: x > -5):

    class D : B {

        public override int Foo(int x) {

            Contract.Requires(x > 10, "x > 10");

            Contract.Ensures(Contract.Result<int>() > -5,

                             "Result > -5");

            return -1;

        }

    }

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

    Отсюда можно сделать вывод, что при переопределении методов предусловие может заменяться лишь равным ему или более слабым (требовать меньше), а постусловие – лишь равным ему или более сильным (гарантировать больше). Новый вариант метода не должен отвергать вызовы, допустимые в оригинале, и должен, как минимум, представлять гарантии, эквивалентные гарантиям исходного варианта. Он вправе, хоть и не обязан, допускать большее число вызовов или давать более сильные гарантии [Meyer2005].

    class D : B {

        public override int Foo(int x) {

            Contract.Requires(x > 0, "x > 0");

            Contract.Ensures(Contract.Result<int>() > 10,

                             "Result > 10");

            return 25;

        }

    }

    Ковариантность и контравариантность

    Хорошим примером изменения гарантий методов при переопределении является возможность многих языков программирования изменять тип возвращаемого значения при переопределении с базового на производный. Это понятие называется «ковариантность по типу возвращаемого значения» (return type covariance) и выглядит следующим образом:

    class B1 {};

    class D1 : public B1 {};

    class B2 {

    public:

      virtual B1 Foo();

    };

    class D2 : public B2 {

    public:

      virtual D1 Foo();

    };


    Figure6  
    Рисунок 6 – Ковариантность по типу возвращаемого значения

    Эта  возможность (которая поддерживается в C++, Java, D и других языках) полностью согласуется с принципами проектирования по контракту и, в частности, с принципом усиления постусловия, поскольку можно считать, что для типов D1 и B1 выполняется соотношение typeof(D1) < typeof(B1).


    Figure7  
    Рисунок 7 – Отношение порядка между объектами иерархии наследования

    Аналогичным примером может служить ковариантность по типу возвращаемого значения и контрвариантность по типу принимаемого значения обобщенных интерфейсов и делегатов в C# 4.0. И хотя в этом случае речь идет не о наследовании, а о совместимости по присваиванию, можно говорить о тех же самых принципах и правилах, что и при переопределении утверждений классами потомками. Так, например, делегату d1 с предусловием pre1 и постусловием post1 может быть присвоен делегат d2 с предусловием pre2, равном pre1 или более слабым, и постусловием post2, равным post1 или более сильным:

    void Foo(object obj) {}

    string Boo() {returnBoo”;}

     

    //...

    // Контравариантность аргументов:

    // предусловие делегата Action<object> и, соответственно

    // метода Foo, слабее предусловия делегата

    // Action<string>, поскольку typeof(object) < typeof(string)

    Action<string> action = Foo;

    // что аналогично следующему коду:

    Action<string> action = new Action<object>(Foo);

     

    // Ковариантность возвращаемого значения:

    // постусловие делегата Func<string> и, соответственно

    // метода Boo, сильнее постусловия делегата

    // Func<object>, поскольку typeof(string) > typeof(object)

    Func<object> func = Boo;

    // что аналогично следующему коду:

    Func<object> func = new Func<string>(Boo);