Как осуществляется выход из оператора switch
Перейти к содержимому

Как осуществляется выход из оператора switch

Полное руководство по switch-выражениям в Java 12

Старый добрый switch был в Java с первого дня. Мы все используем его и привыкли к нему — особенно к его причудам. (Кого-нибудь еще раздражает break ?) Но теперь все начинает меняться: в Java 12 switch вместо оператора стал выражением:

В switch появилась возможность возвращать результат своей работы, который можно присвоить переменной; вы также можете использовать синтаксис в стиле «лямбда», который позволяет избавиться от сквозного прохода по всем case , в которых нет оператора break .

В этом руководстве я расскажу Вам обо всем, что необходимо знать о switch-выражениях в Java 12.

Предварительный обзор

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

Что бы начать использовать новую версию switch необходимо применить опцию командной строки —enable-preview как во время компиляции, так и во время запуска программ (также необходимо использовать —release 12 при компиляции — примечание переводчика).

Так что имейте ввиду, что switch, как выражение, не имеет на данный момент окончательного варианта синтаксиса в Java 12.

Если у вас возникло желание поиграть со всем этим самим, то вы можете посетить мой демо-проект Java X на гитхабе.

Проблема с операторами в switch

Прежде, чем мы перейдем к обзору нововведений в switch, давайте быстро оценим одну ситуацию. Допустим, мы столкнулись с «ужасным» тернарным булеаном и хотим преобразовать его в обычный булеан. Вот один из способов сделать это:

Согласитесь, что это очень неудобно. Как и многие другие варианты switch, встречающиеся в «природе», представленный выше пример просто вычисляет значение переменной и присваивает его, но реализация обходная (объявляем идентификатор result и используем его позже), повторяющаяся (мои break ‘и всегда результат copy-pasta) и подвержена ошибкам (забыл еще одну ветку? Ой!). Тут явно есть, что улучшить.

Давайте попробуем решить эти проблемы, поместив switch в отдельный метод:

Так намного лучше: отсутствует фиктивная переменная, нет break ‘ов, загромождающих код и сообщений компилятора об отсутствии default (даже если в этом нет необходимости, как в данном случае).

Но, если подумать, то мы не обязаны создавать методы только для того, чтобы обойти неуклюжую особенность языка. И это даже без учёта, что такой рефакторинг не всегда возможен. Нет, нам нужно решение получше!

Представляем switch-выражения!

Как я показал в начале статьи, начиная с Java 12 и выше, вы можете решить вышеуказанную проблему следующим образом:

Я думаю, что это довольно очевидно: если ternartBool равен TRUE , то result ‘у будет присвоено true (иными словами TRUE превращается в true ). FALSE становится false .

Сразу возникают две мысли:

  • switch может иметь результат;
  • что там со стрелками?

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

Выражение или оператор

Возможно, вы удивлены, что switch теперь является выражением. А чем же он был до этого?

До Java 12 switch был оператором — императивной конструкцией, регулирующей поток управления.

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

Разница в том, что if просто выполняет соответствующий блок, тогда как тернарный оператор возвращает какой-то результат:

То же самое для switch: до Java 12, если вы хотели вычислить значение и сохранить результат, то должны были либо присвоить его переменной (а затем break ), либо вернуть из метода, созданного специально для оператора switch .

Теперь же всё выражение оператора switch оценивается (выбирается для выполнения соответствующая ветка), и результат вычислений может быть присвоен переменной.

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

Стрелка или двоеточие

В вводном примере использовался новый синтаксис в лямбда-стиле со стрелкой между меткой и выполняющейся частью. Важно понимать, что для этого не обязательно использовать switch в качестве выражения. Фактически, пример ниже эквивалентен приведенному в начале статьи коду:

Обратите внимание, что теперь вы можете использовать break со значением! Это идеально согласуется с инструкциями switch старого стиля, которые используют break без какого-либо значения. Так в каком случае стрелка означает выражение вместо оператора, для чего она здесь? Просто хипстерский синтаксис?

Исторически сложилось, что метки с двоеточием просто отмечают точку входа в блок операторов. С этого места начинается выполнение всего кода ниже, даже когда встречается другая метка. В switch нам это известно, как сквозной переход к следующему case (fall-through): метка case определяет, куда перепрыгивает поток управления. Для его завершения нужен break или return .

В свою очередь, использование стрелки означает, что будет выполнен только блок справа от нее. И никакого «проваливания».

Подробнее об эволюции switch

Несколько меток на case

До сих пор каждый case содержал только одну метку. Но теперь все изменилось — один case может соответствовать нескольким меткам:

Поведение должно быть очевидным: TRUE и FALSE приводят к одному и тому же результату — вычисляется выражение «sane».

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

Типы за пределами Enum

Все примеры со switch в этой статье используют enum . А как насчет других типов? Выражения и операторы switch также могут работать с String , int , (проверьте документацию) short , byte , char и их обертками. Здесь пока что ничего не изменилось, хотя идеи об использовании таких типов данных, как float и long по прежнему остаются в силе (со второго по последний абзац).

Подробнее о стрелке

Давайте рассмотрим два свойства, характерных для стрелочной формы записи разделителя:

  • отсутствие сквозного перехода к следующему case ;
  • блоки операторов.

Отсутствие сквозного перехода к следующему case

Вот, что говорится в JEP 325 об этом:

Я полностью согласен и приветствую возможность использовать switch без поведения по умолчанию:

Важно усвоить, что это не имеет никакого отношения к тому, используете ли вы switch в качестве выражения или оператора. Решающим фактором тут является стрелка против двоеточия.

Блоки операторов

Как и в случае с лямбдами, стрелка может указывать либо на один оператор (как выше), либо на блок, выделенный фигурными скобками:

Блоки, которые приходится создавать для многострочных операторов имеют дополнительное преимущество (что не требуется при применении двоеточия), которое заключается в том, что для использования одинаковых имен переменных в разных ветках, switch не требует специальной обработки.

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

Подробнее о выражениях switch

И последнее, но не менее важное — особенности использования switch в качестве выражения:

  • множественные выражения;
  • ранний возврат (досрочный return );
  • охват всех значений.

Обратите внимание, что при этом не имеет значения, какая форма используется!

Множественные выражения

Switch-выражения являются множественными выражениями. Это означает, что они не имеют своего собственного типа, но могут быть одним из нескольких типов. Наиболее часто в качестве таких выражений используются лямбда-выражения: s -> s + » » , могут быть Function<String, String> , но также могут быть Function<Serializable, Object> или UnaryOperator<String> .

С помощью switch-выражений тип определяется по взаимодействию между тем, где используется switch и типами его веток. Если switch-выражение назначается типизированной переменной, передается в качестве аргумента или иным образом используется в контексте, где известен точный тип (это называется целевым типом), то все его ветки должны соответствовать этому типу. Вот что мы делали до сих пор:

Как итог — switch присваивается переменной result типа String . Следовательно, String является целевым типом, и все ветки должны возвращать результат типа String .

То же самое происходит и здесь:

А что произойдет сейчас?

(Про использование типа var читайте в нашей прошлой статье 26 рекомендаций по использованию типа var в Java — примечание переводчика)

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

Ранний возврат

Следствием различия между выражением и оператором switch является то, что вы можете использовать return для выхода из оператора switch :

… вы не можете использовать return внутри выражения …

Это имеет смысл независимо от того, используете ли вы стрелку или двоеточие.

Покрытие всех вариантов

Если вы используете switch в качестве оператора, тогда не имеет значения, охвачены все варианты или нет. Конечно, вы можете случайно пропустить case , и код будет работать неправильно, но компилятору все равно — вы, ваша IDE и ваши инструменты анализа кода останетесь с этим наедине.

Switch-выражения усугубляют эту проблему. Куда следует перейти switch , если нужная метка отсутствует? Единственный ответ, который может дать Java — это возвращать null для ссылочных типов и значение по умолчанию для примитивов. Это породило бы множество ошибок в основном коде.

Чтобы предотвратить такой исход, компилятор может помочь вам. Для switch-выражений компилятор будет настаивать, чтобы все возможные варианты были охвачены. Давайте посмотрим на пример, который может привести к ошибке компиляции:

Интересным является следующее решение: добавление ветки default , конечно, исправит ошибку, но это не является единственным решением — еще можно добавить case для FALSE .

Да, компилятор наконец-то сможет определить, охватываются ли все значения enum (исчерпывают ли все варианты), и не установить бесполезные значения по умолчанию! Давайте посидим минуту в безмолвной благодарности.

Хотя, это все же вызывает один вопрос. Что делать, если кто-то возьмет и превратит сумасшедший Bool в кватернионный Boolean, добавив четвертое значение? Если вы перекомпилируете switch-выражение для расширенного Bool, то получите ошибку компиляции (выражение больше не является исчерпывающим). Без перекомпиляции это превратится в проблему во время выполнения. Чтобы отловить эту проблему, компилятор переходит в ветку default , которая ведет себя так же, как та, которую мы использовали до сих пор, вызывая исключение.

В Java 12 охват всех значений без ветки default работает только для enum , но когда switch в будущих версиях Java станет более мощным, он также сможет работать с произвольными типами. Если метки case ‘ов смогут не только проверять равенство, но и проводить сравнения (например _ < 5 -> …) — это позволит охватить все варианты для числовых типов.

Размышление

Из статьи мы узнали, что Java 12 превращает switch в выражение, наделяя его новыми возможностями:

  • теперь один case может соответствовать нескольким меткам;
  • новая стрелочная форма case … -> … следует синтаксису лямбда-выражений:
    • допускаются однострочные операторы или блоки;
    • предотвращается сквозной переход к следующему case ;

    Куда это нас приведет? Во-первых, поскольку это не окончательная версия switch , у вас все еще есть время, чтобы оставить отзыв в списке рассылки Amber, если вы с чем-то не согласны.

    Затем, предполагая, что switch остается таким, каким он является в данный момент, я думаю, что стрелочная форма станет новым вариантом по умолчанию. Без сквозного перехода к следующему case и с лаконичными лямбда-выражениями (это очень естественно иметь case и один оператор в одной строке) switch выглядит намного компактнее и не ухудшает читаемость кода. Я уверен, что буду использовать только двоеточие, если у меня возникнет необходимость в сквозном проходе.

    Оператор switch

    Вторым оператором выбора в C# является оператор switch , который обеспечивает многонаправленное ветвление программы. Следовательно, этот оператор позволяет сделать выбор среди нескольких альтернативных вариантов дальнейшего выполнения программы. Несмотря на то, что многонаправленная проверка может быть организована с помощью последовательного ряда вложенных операторов if , во многих случаях более эффективным оказывается применение оператора switch . Этот оператор действует следующим образом. Значение выражения последовательно сравнивается с константами выбора из заданного списка. Как только будет обнаружено совпадение с одним из условий выбора, выполняется последовательность операторов, связанных с этим условием. Ниже приведена общая форма оператора switch .

    Заданное выражение в операторе switch должно быть целочисленного типа ( char , byte , short или int ), перечислимого или же строкового. (О перечислениях и символьных строках типа string речь пойдет далее в этом курсе.) А выражения других типов, например с плавающей точкой, в операторе switch не допускаются.

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

    Последовательность операторов из ветви default выполняется в том случае, если ни одна из констант выбора не совпадает с заданным выражением. Ветвь default не является обязательной. Если же она отсутствует и выражение не совпадает ни с одним из условий выбора, то никаких действий вообще не выполняется. Если же происходит совпадение с одним из условий выбора, то выполняются операторы, связанные с этим условием, вплоть до оператора break .

    Ниже приведен пример программы, в котором демонстрируется применение оператора switch .

    Результат выполнения этой программы выглядит следующим образом.

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

    В приведенном выше примере оператором switch управляла переменная i типа int . Как пояснялось ранее, для управления оператором switch может быть использовано выражение любого целочисленного типа, включая и char . Ниже приведен пример применения выражения и констант выбора типа char в операторе switch .

    Вот какой результат дает выполнение этой программы.

    Обратите в данном примере внимание на отсутствие ветви default в операторе switch . Напомним, что ветвь default не является обязательной. Когда она не нужна, ее можно просто опустить.

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

    Когда в последовательности операторов отдельной ветви case встречается оператор break , происходит выход не только из этой ветви, но из всего оператора switch , а выполнение программы возобновляется со следующего оператора, находящегося за пределами оператора switch . Последовательность операторов в ветви default также должна быть лишена “провалов”, поэтому она завершается, как правило, оператором break .

    Правило недопущения «провалов» относится к тем особенностям языка C#, которыми он отличается от C, C++ и Java. В этих языках программирования одна ветвь case может переходить (т. е. «проваливаться») в другую. Данное правило установлено в C# для ветвей case по двум причинам.

    Во-первых, оно дает компилятору возможность свободно изменять порядок следования последовательностей операторов из ветвей case для целей оптимизации. Такая реорганизация была бы невозможной, если бы одна ветвь case могла переходить в другую.

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

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

    Ниже приведен результат выполнения этой программы.

    Если значение переменной i в данном примере равно 1, 2 или 3, то выполняется первый оператор, содержащий вызов метода WriteLine() . Такое расположение нескольких меток ветвей case подряд не нарушает правило недопущения «провалов»; поскольку во всех этих ветвях используется одна и та же последовательность операторов.

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

    switch

    Инструкция switch сравнивает выражение со случаями, перечисленными внутри неё , а затем выполняет соответствующие инструкции.

    Синтаксис

    Описание

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

    Сначала программа пытается найти подходящий случай, значение которого равно значению искомого выражения (используется строгое сравнение, ===) и затем выполняет инструкции, соответствующие случаю. Если подходящего случая нет, ищется случай по умолчанию ( default ), который не является обязательным. Если случая по умолчанию нет, выполнение продолжается на инструкции, следующей сразу после switch . По соглашению, случай default описывается последним, но это не является строгим правилом.

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

    Примеры

    Пример: Использование switch

    В этом примере, если expr равно «Bananas», программа находит случай «Bananas» и выполняет соответствующие инструкции. При выполнении инструкции break , выполнение продолжится за пределами switch . Если бы break не было, то выполнились бы инструкции случая «Cherries».

    Пример: Что случится, если не использовать break?

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

    Цепочки case

    Одна операция

    Этот метод использует тот факт, что после case нет прерывания и продолжится выполнение следующего case независимо от того, соответствует ли case предоставленному условию. Подробнее в примере «Что случится, если не использовать break?.

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

    Цепочка операций

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

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

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