Продолжение выполнения внешнего цикла
Когда у вас есть вложенный цикл, иногда возникает желание перейти к следующей итерации внешнего цикла, а не внутреннего. Например, у нас есть последовательность критериев и последовательность элементов, и мы хотим определить, отвечает ли какой-либо из элементов всем критериям:
match = null;
foreach(var item in items)
{
foreach(var criterion in criteria)
{
if (!criterion.IsMetBy(item))
{
// В дальнейших проверках нет никакого смысла, этот элемент не подходит.
// Мы хотим перейти к следующей итерации внешнего цикла. Но как?
}
}
match = item;
break;
}
Существует несколько способов добиться этого. Ниже представлены несколько таких способов в порядке увеличения «изящности»:
Метод №1 (ужасный): В цикле, оператор “continue” по-сути является оператором “goto”, который передает управление вниз цикла, непосредственно перед проверкой условия цикла и началом выполнения следующей итерации. (Очевидно, что когда вы произносите “goto” в виде “continue”, мысли толпы типа «любые применения операторов “goto” является злом на все времена» не будут вас досаждать.) Вы, конечно, можете использовать оператор goto явно:
match = null;
foreach(var item in items)
{
foreach(var criterion in criteria)
{
if (!criterion.IsMetBy(item))
{
goto OUTERCONTINUE;
}
}
match = item;
break;
OUTERCONTINUE:
;
}
Метод №2 (лучше): Когда я вижу вложенный цикл, я практически всегда рассматриваю возможность преобразования внутреннего цикла в отдельный метод.
match = null;
foreach(var item in items)
{
if (MeetsAllCriteria(item, critiera))
{
match = item;
break;
}
}
где тело метода MeetsAllCriteria достаточно очевидно:
foreach(var criterion in criteria)
if (!criterion.IsMetBy(item))
return false;
return true;
Метод №3 (потрясающий): второй пример показывает «механизм» работы кода, но скрывает его назначение. Цель кода – дать ответ на вопрос: «какой элемент (если такой вообще есть) первым отвечает всем критериям?» Если значение кода соответствует этому вопросу, тогда следует написать код, из которого это следовало бы явно:
var matches = from item in items
where criteria.All(
criterion=>criterion.IsMetBy(item))
select item;
match = matches.FirstOrDefault();
Т.е. найти в items элементы, отвечающие всем критериям, и если такие элементы есть, получить первый из них. Этот код теперь говорит о том, что он делает и, кроме того, из-за отсутствия циклов необходимость в операторе “continue” отпадает вовсе!
В LINQ мне больше всего нравится то, что он позволяет совершенно по-иному думать о решаемой задаче. Сегодня, когда я пишу цикл, я останавливаюсь и прежде чем набрать “for” я думаю о следующих двух вещах:
* Описал ли я где-то то, что делает этот код семантически, или код подчеркивает используемые механизмы ценой утаивания семантики?
* Существует ли способ изобразить решение задачи, как результат запроса к некоторому набору данных, а не как результат нескольких шагов в процедурном стиле?