Занимательный цикл FOR

от автора

День добрый, уважаемые коллеги.

В данном обзоре будет рассмотрен самый популярный в программировании цикл for в контексте языка C#, что, впрочем, не мешает программистам на других си-подобных языках ознакомиться с этой статьей — вы также найдете для себя кое-что полезное.

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

Содержание

Обзор синтаксиса оператора for

Итак, наш подопытный на сегодня — цикл for.
Согласно MSDN, синтаксис цикла выглядит следующим образом:

for (initializer; condition; iterator)     body 
  • Раздел инициализатора (initializer) устанавливает начальные условия. Оператор в этом разделе выполняется только один раз, перед тем как начнется цикл. Важным моментом здесь является то, что переменные, объявленные в инициализаторе, являются локальными для тела цикла и не могут использоваться по его окончанию.
  • Раздел условия (condition) содержит логическое выражение, вычисляемое для определения того, должен ли цикл остановиться или должен выполнить попытку.
  • Раздел итератора (iterator) определяет, что происходит после каждой итерации тела цикла.
  • Тело цикла (body) выполняется при каждой итерации и может содержать директиву break для выхода из цикла или continue для перехода к следующей итерации.

Более подробно с тем, какие инструкции могут использоваться в инициализаторе и итераторе, вы можете ознакомиться в документации по ссылке выше, мы же перейдем непосредственно к разбору вариантов использования цикла for.

0. Счетчик

Типичное использование цикла for — выполнение некоторой операции n раз, в этом случае цикл выглядит следующим образом:

// счетчик от 0 до n-1 for (int i = 0; i < n; i++) {     // TODO }  // счетчик от n до 1 for (int i = n; i > 0; i--) {     // TODO } 

Этот пример приведен под номером ноль, т.к. все без исключения программисты используют цикл for в данном варианте. Можно только добавить, что вовсе не обязательно изменять значение счетчика на единицу, например, следующий код выводит всем числа, кратным трем, в интервале от 12 до 100 (последняя итерация, разумеется, будет по числу 99):

for (int i = 12; i < 100; i += 3) {     Console.WriteLine(i); } 

1. Цикл while

Цикл while является частным случаем цикла for, если отсутствует инициализатор и итератор:

for (; condition; ) {     // TODO } 

Цикл будет выполняться, пока верно условие. Условие и вовсе можно убрать, тогда получится бесконечный цикл, аналогичный while (true):

for (; ; ) {     // TODO } 

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

2. Объектный итератор

Т.к. «научное» название этого типа цикла мне неизвестно, решил назвать его объектным итератором. Это означает, что на этот раз мы имеем дело не с числовой переменной-счетчиком, а с объектом.

В общем виде цикл выглядит следующим образом:

for (item = initialValue; item != null; item = GetNext(item)) {     // TODO } 

У нас есть некий объект item, который получает начальное значение в инициализаторе или до начала цикла, далее, если объект не пустой, мы выполняет с ним какие-то действия в теле цикла и получаем очередное значение в итераторе. Лучше всего это работает для связей предок-потомок, как например в классе Exception. Под спойлером как раз приведен пример цикла для сбора полного текста ошибки из исключения и всех его дочерних исключений.

Метод GetExceptionTextFull

Код метода GetExceptionTextFull:

/// <summary> /// получить полный текст ошибки /// </summary> /// <param name="ex"> исключение </param> /// <returns></returns> public string GetExceptionTextFull(Exception ex) {     StringBuilder sb = new StringBuilder();     for (string tab = ""; ex != null; ex = ex.InnerException, tab = tab.Length == 0 ? "+---" : "    " + tab)     {         sb.Append(tab);         sb.AppendLine(ex.Message);     }     return sb.ToString(); } 

Использование:

// формируем исключение с двумя вложенными исключениями Exception ex = new Exception("Exception #1", new Exception("Exception #2", new Exception("Exception #3")));  // получаем и печатаем полный текст ошибки string message = GetExceptionTextFull(ex); Console.WriteLine(message); 

Результат:

Exception #1 +---Exception #2     +---Exception #3 

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

for (Type t = type; t != typeof(object); t = t.BaseType) {     // TODO } 

Если вы используете собственные классы, содержащие ссылку на родителя, обход вверх по иерархии очень удобно выполнять циклом for. Например, у вас есть класс Entity, содержащий ссылку на родителя (public Entity Parent { get; set; }).

Используя цикл for можно написать такой вот метод расширения, который для данной вершины вернет родителя указанного типа или null, если родителя, удовлетворяющего условию, нет.

/// <summary> /// получить родительскую сущность /// </summary> /// <typeparam name="T"> тип родительской сущности </typeparam> /// <param name="entity"> сущность </param> /// <returns></returns> public static T GetParentOfType<T>(this Entity entity) where T : Entity {     for (entity = entity.Parent; entity != null && !(entity is T); entity = entity.Parent) ;     return entity as T; } 

3. Итератор по коллекции

Иногда используется на практике как альтернатива foreach, т.к. в отличие от него позволяет получить индекс элемента и, возможно, изменить коллекцию (во время итерации с помощью foreach коллекцию менять нельзя):

for (int i = 0, count = list.Count; i < count; i++) {     var item = list[i];     // TODO } 

Также стоит помнить, что хотя for не дает прироста производительности, foreach создает дополнительные объекты (Enumerator), которые впоследствии вынужден собирать сборщик мусора. Поэтому в некоторых редких случаях использование for вместо foreach может понизить затраты ресурсов системы. Кто-то, вероятно, скажет, что C# не предназначен для высоконагруженных систем, где подобная оптимизация может стать актуальной и будет прав, пока не начнет писать игры на C#.

X. Прочие интересные циклы

В данном разделе будут рассмотрены просто интересные циклы, которые я откопал в своем коде.

1. Построение всех возможных комбинаций элементов списка.

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

Количество комбинаций равно 2 ^ N, где N — число комбинируемых элементов. Идея основана на том, что биты чисел от 0 до N-1 определяют, будет ли включен конкретный элемент исходного набора в результат или нет. Например, для набора букв A, B и C, комбинация под номером 5 (двоичный код 101) включает элементы A и C, а комбинация №7 (код 111) включает все элементы. Вложенный цикл ведет итерацию сразу по двум переменным: j — номер элемента и k — это номер комбинации, сдвигаемый побитово вправо (k >>= 1). Если младший бит числа k равен единице ((k & 1) != 0), то элемент с номером j включается в результат.

/// <summary> /// построить все возможные комбинации /// </summary> /// <typeparam name="T"> тип элемента </typeparam> /// <param name="data"> список объектов </param> /// <returns></returns> public static List<List<T>> MakeCombinations<T>(IEnumerable<T> data) {     if (data == null) return null;      // конвертируем в массив     T[] array = data.ToArray();      // считаем число комбинаций и готовим результирующий список     int count = 1 << array.Length;     List<List<T>> res = new List<List<T>>(count);      // перечисляем все комбинации     for (int i = 0; i < count; i++)     {         List<T> list = new List<T>();         for (int j = 0, k = i; k > 0; j++, k >>= 1)         {             if ((k & 1) != 0) list.Add(array[j]);         }         res.Add(list);     }     return res; } 

Использование:

// формируем список исходных значений - набор букв от A до D var data = Enumerable.Range('A', 'D' - 'A' + 1).Select(e => ((char)e).ToString()).ToList();  // печатаем все возможные перестановки (2 ^ 4 = 16) foreach (var list in MakeCombinations(data).OrderBy(e => e.Count)) {     Console.WriteLine(string.Join(" + ", list.ToArray())); } 

Результат:

A B C D A + B A + C B + C A + D B + D C + D A + B + C A + B + D A + C + D B + C + D A + B + C + D 

2. Подсчет суммы первых N чисел Фибоначчи.

Это, конечно, не пример из кода промышленной системы, да и сама задача решается несколько иным способом (сумма первых N чисел Фибоначчи равна f(N + 2) — 1), но у решения «в лоб» достаточно интересный код, а цикл, используемый в нем примечателен отсутствием тела.

// начальные условия int n = 10; int sum = 0;  // суммирование for (int a = 0, b = 1, i = 0; i++ < n; sum += b, b = a + (a = b)) ;  // вывод результата Console.WriteLine("Сумма первых {0} чисел Фибоначчи равна {1}", n, sum); 

Также прошу обратить внимание на оператор b = a + (a = b), который одновременно вычисляет следующее число Фибоначчи (b) и сохраняет предыдущее значение b в a. Если изменить последовательность слагаемых, то результат будет иной, т.к. сперва выполнится присваивание, а только затем суммирование. В целом, следует избегать таких выражений, если вы не до конца уверены в своем знании арифметики C, с другой стороны, вы и не узнаете ее, пока не опробуете сами.

Заключение

Целью данной статьи было раскрытие скрытых возможностей ключевого цикла в программировании, о которых многие, возможно, и не задумывались ранее, или просто не применяли, опасаясь «трудночитаемости кода». Бред это все, товарищи. Копируя куски кода стандартных решений, вы не добьетесь успехов в программировании и лишь пополните ряды шаблонных программистов, не способных решить задачи, выходящие за рамки примеров из учебников. В общем, экспериментируйте. Начните с циклов, затем перейдите к linq, потом к рефлексии. Постепенно, шаг за шалом, вы получите необходимые навыки и сможете создавать сложные приложения, и, что самое главное, вам будет интересно их создавать, следовательно вы будете получать удовольствие от своей работы.

Другие мои публикации

  1. Локализация проектов на .NET с интерпретатором функций
  2. Заполнение текстовых шаблонов данными на основе модели. Реализация на .NET с использованием динамических функций в байт-коде (IL)

ссылка на оригинал статьи http://habrahabr.ru/post/260521/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *