Labeled break and continue в C# 15 — разбор плохого примера и поиск реального кейса

от автора

Всем привет. В последнее время в одной профессиональной соцсети я все чаще стал натыкаться на посты, связанные с dotnet C# тематикой. К сожалению, эти посты в большинстве своем не содержат полезной информации. Скорее всего они создаются для охвата аудитории с целью привлечения трафика на сторонние платформы по продаже курсов для разработчиков. По-моему, этот способ называется «воронка продаж» (поправьте, если я ошибаюсь). Как правило, эти посты затрагивают какую-то не очень сложную тему и содержат примеры кода. Недавно мне попался очередной пост, в котором автор пытался показать пример использования новой фичи labeled break and continue. Это новая фича, которую добавили в C# 15 (dotnet 11). На момент написания она была принята в Working Set, но финального релиза ещё не было. Ниже код, похожий на оригинал из поста. Он разделен на 2 секции: «как делали раньше» и «как сделать используя новый подход»:

Стандартный способ:

string foundValue = null;bool shouldBreak = false;for (int x = 0; x < xMax; x++){    for (int y = 0; y < yMax; y++)    {        foundValue = GetValue(x, y);        if (foundValue == targetValue)        {            shouldBreak = true;            break;        }    }    if (shouldBreak)    {        break;    }}ProcessValue(foundValue);

Новый способ:

outer: for (int x = 0; x < xMax; x++){    for (int y = 0; y < yMax; y++)    {        if (ShouldSkipRest(x, y))        {            continue outer;        }        if (ShouldExitAll(x, y))        {            break outer;        }    }}

Выскажу сугубо личное мнение: оба варианта написаны плохо. Разработчики программного обеспечения должны стремиться писать читаемый код. Так же нужно заботиться об удобстве его отладки, но это уже вторично. В 2017 года «Sonar Source» предложила метрику Cognitive Complexity, которая помогает измерить непреднамеренную сложность написанного кода. Я часто использую ее. У меня установлены соответствующие расширения во всех IDE. Теперь вернемся к примерам из поста.

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

Вот как бы я реализовал этот код:

public void Process(){    string? foundValue = null;    for (int x = 0; x < xMax; x++)    {        if (TryGetValue(x, out foundValue))        {            ProcessValue(foundValue);            return;        }    }}private bool TryGetValue(int x, out string? outputValue){    outputValue = null;    for (int y = 0; y < yMax; y++)    {        var currentValue = GetValue(x, y);        if (currentValue == targetValue) // targetValue is a class-level constant        {            outputValue = currentValue;            return true;        }    }    return false;}

Такой код уже лучше читается. В явном виде отсутствуют вложенные циклы for. Удобнее дебажить код для внешнего цикла for по переменной x. Так же можно уменьшить «Cognitive Complexity» если инвертировать оператор выбора if (TryGetValue(x, out foundValue)). Часто IDE предлагает такой вариант рефакторинга.

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

Помимо проблем с читаемостью, первый пример содержит скрытый баг: ProcessValue(foundValue) вызывается безусловно, даже если значение так и не было найдено. Но ещё важнее другое: оба примера решают разные задачи — первый ищет конкретное значение, второй использует абстрактные методы-заглушки. Это не «старый и новый способ» — это два несвязанных куска кода, и сравнивать их как эквиваленты вообще некорректно.

В общем у меня сложилось впечатление, что фича labeled break and continue имеет сомнительную пользу для разработчиков. В то же время я подумал, что люди, проектирующие C#, далеко не глупые и вряд ли добавили бы сомнительную фичу. И я решил поискать историю появления этой фичи. Оказалось, что сообщество запрашивало ее довольно давно. Один из первых запросов датируется октябрем 2015 года First mention. Вот еще пример обсуждения из 2018 года GitHub discussion. Однако, ни одна из дискуссий не привела к разработке этой фичи. Основная причина отказа заключалась в том, что среди членов LDM (Language Design Meeting) не было человека, который бы взял на себя ответственность за разработку. По-моему, такие люди называются «champion». Однако, все изменилось 14 декабря 2025 года. Предложение было оформлено и внесено в официальный трекер Cyrus Najmabadi — членом команды C# Language Design. Ссылка Official feature link. На данный момент фича все еще в статусе Open. Посмотрим, к чему в итоге приведет ее разработка и в каком виде она предстанет перед нами в релизе.

Однако, все еще остается вопрос — зачем это нужно? Вероятнее всего, наиболее подходящий кейс для этой фичи — это ситуация, когда внутри цикла находится switch-оператор, и из его case нужно выйти не из switch, а из самого цикла. Без labeled break and continue это невозможно сделать без goto или других костылей вроде вложенных функций или возвращаемых tuple. Пример такого кода может выглядеть следующим образом:

outer: foreach (var item in collection){    switch (item.Type)    {        case ItemType.StopAll:            break outer; // без labeled break нужно вызывать goto        case ItemType.SkipGroup:            continue outer; // без labeled break нужно вызывать goto    }}

Более подробную информацию о всех возможных кейсах можно посмотреть на официальной странице Use cases.

Спасибо всем, кто дочитал до конца мой пост. Буду благодарен за обратную связь.

ссылка на оригинал статьи https://habr.com/ru/articles/1025794/