Chociaż możliwe jest połączenie wielu instrukcji if-else razem, jest to trudne do odczytania i nieefektywne. Rozważmy następujący program:
#include <iostream>
void printDigitName(int x)
{
if (x == 1)
std::cout << "One";
else if (x == 2)
std::cout << "Two";
else if (x == 3)
std::cout << "Three";
else
std::cout << "Unknown";
}
int main()
{
printDigitName(2);
std::cout << '\n';
return 0;
}Zmienna x w printDigitName() będzie oceniany maksymalnie trzy razy w zależności od przekazanej wartości (która jest nieefektywna), a czytelnik musi mieć pewność, że x jest on oceniany za każdym razem (a nie jakaś inna zmienna).
Ponieważ testowanie zmiennej lub wyrażenia pod kątem równości względem zestawu różnych wartości jest powszechne, C++ udostępnia alternatywną instrukcję warunkową zwaną instrukcją switch, która jest wyspecjalizowana w tym celu. Oto ten sam program, co powyżej, wykorzystujący przełącznik:
#include <iostream>
void printDigitName(int x)
{
switch (x)
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}
int main()
{
printDigitName(2);
std::cout << '\n';
return 0;
}Idea instrukcji switch jest prosta: wyrażenie (czasami nazywane warunkiem) jest oceniane w celu wygenerowania wartości.
Następnie zachodzi jedna z poniższych sytuacji:
- Jeśli wartość wyrażenia jest równa wartości po którejkolwiek z etykiet przypadków, instrukcje po dopasowaniu case-label.
- Jeśli nie można znaleźć pasującej wartości i istnieje domyślna etykieta, wykonywane są instrukcje znajdujące się po domyślnej etykiecie.
- Jeśli nie można znaleźć pasującej wartości i nie ma domyślnej etykiety, przełącznik jest pomijany.
Przyjrzyjmy się każdemu z tych pojęć bardziej szczegółowo.
Uruchamianie przełącznika
Rozpoczynamy instrukcję switch za pomocą słowa kluczowego switch , po którym następują nawiasy z wyrażeniem warunkowym, które chcemy wewnątrz ocenić. Często wyrażenie jest tylko pojedynczą zmienną, ale może to być dowolne prawidłowe wyrażenie.
Warunek w przełączniku musi być typu całkowitego (zobacz lekcję 4.1 -- Wprowadzenie do podstawowych typów danych jeśli potrzebujesz przypomnienia, które typy podstawowe są uważane za typy całkowite) lub typu wyliczeniowego (omówionego w przyszłych lekcjach 13.2 — Wyliczenia bez zakresu i 13.6 -- Wyliczenia o ograniczonym zakresie (klasy wyliczeniowe)) lub można go przekształcić w jeden. Nie można tutaj używać wyrażeń, które wyznaczają typy zmiennoprzecinkowe, łańcuchy znaków i większość innych typów niecałkowych.
Dla zaawansowanych czytelników
Dlaczego typ przełącznika pozwala tylko na typy całkowite (lub wyliczeniowe)? Odpowiedź jest taka, że instrukcje switch zostały zaprojektowane tak, aby były wysoce zoptymalizowane. Historycznie rzecz biorąc, najpowszechniejszym sposobem implementacji instrukcji switch przez kompilatory jest tabele przeskoków -- a tabele przeskoków działają tylko z wartościami całkowitymi.
Dla tych z Was, którzy już zaznajomili się z tablicami, tabela przeskoków działa podobnie jak tablica, wartość całkowita jest używana jako indeks tablicy, aby „przeskoczyć” bezpośrednio do wyniku. Może to być znacznie bardziej wydajne niż wykonywanie serii porównań sekwencyjnych.
Oczywiście kompilatory nie muszą implementować przełączników przy użyciu tabel skoków, a czasami nie. Technicznie nie ma powodu, dla którego C++ nie mógłby złagodzić ograniczenia, aby można było używać również innych typów, po prostu jeszcze tego nie zrobili (od C++ 23).
Po wyrażeniu warunkowym deklarujemy blok. Wewnątrz bloku używamy etykiet do definiowania wszystkich wartości, które chcemy przetestować pod kątem równości. W instrukcjach switch używane są dwa rodzaje etykiet, które omówimy później.
Etykiety przypadków
Pierwszy rodzaj etykiet to etykieta case, która jest deklarowana przy użyciu słowa kluczowego case i po której następuje wyrażenie stałe. Wyrażenie stałe musi albo odpowiadać typowi warunku, albo musi być konwertowalne na ten typ.
Jeśli wartość wyrażenia warunkowego jest równa wyrażeniu występującemu po a case label, wykonanie rozpoczyna się od pierwszej instrukcji po niej case label , a następnie jest kontynuowane sekwencyjnie.
Oto przykład warunku spełniającego a case label:
#include <iostream>
void printDigitName(int x)
{
switch (x) // x is evaluated to produce value 2
{
case 1:
std::cout << "One";
return;
case 2: // which matches the case statement here
std::cout << "Two"; // so execution starts here
return; // and then we return to the caller
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}
int main()
{
printDigitName(2);
std::cout << '\n';
return 0;
}Ten kod wypisuje:
Two
W powyższym programie x ocenianego w wyniku wartość 2. Ponieważ istnieje etykieta przypadku o wartości 2, wykonanie przeskakuje do instrukcji znajdującej się pod odpowiednią etykietą przypadku. Program wypisuje Two, a następnie return statement , który wraca do wywołującego.
Nie ma praktycznego ograniczenia liczby etykiet przypadków, które możesz mieć, ale wszystkie etykiety przypadków w przełączniku muszą być unikalne. Oznacza to, że nie możesz tego zrobić:
switch (x)
{
case 54:
case 54: // error: already used value 54!
case '6': // error: '6' converts to integer value 54, which is already used
}Jeśli wyrażenie warunkowe nie pasuje do żadnej z etykiet przypadków, żadne przypadki nie zostaną wykonane. Wkrótce pokażemy tego przykład.
Etykieta domyślna
Drugi rodzaj etykiety to domyślną etykietą (często nazywana przypadkiem domyślnym), która jest deklarowana przy użyciu słowa kluczowego default . Jeśli wyrażenie warunkowe nie pasuje do żadnej etykiety przypadku i istnieje etykieta domyślna, wykonanie rozpoczyna się od pierwszej instrukcji po etykiecie domyślnej.
Oto przykład warunku pasującego do etykiety domyślnej:
#include <iostream>
void printDigitName(int x)
{
switch (x) // x is evaluated to produce value 5
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default: // which does not match to any case labels
std::cout << "Unknown"; // so execution starts here
return; // and then we return to the caller
}
}
int main()
{
printDigitName(5);
std::cout << '\n';
return 0;
}Ten kod wypisuje:
Unknown
Etykieta domyślna jest opcjonalna i na instrukcję switch może przypadać tylko jedna etykieta domyślna. Zgodnie z konwencją, przypadek domyślny jest umieszczany na końcu bloku przełączników.
Najlepsza praktyka
Umieść przypadek domyślny na końcu w bloku przełączników.
Brak pasującej etykiety przypadku i żadnego przypadku domyślnego
Jeśli wartość wyrażenia warunkowego nie pasuje do żadnej z etykiet przypadku i nie podano przypadku domyślnego, wówczas żadne przypadki wewnątrz przełącznika nie zostaną wykonane. Wykonywanie jest kontynuowane po zakończeniu bloku przełączania.
#include <iostream>
void printDigitName(int x)
{
switch (x) // x is evaluated to produce value 5
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
// no matching case exists and there is no default case
}
// so execution continues here
std::cout << "Hello";
}
int main()
{
printDigitName(5);
std::cout << '\n';
return 0;
}W powyższym przykładzie x ocenia się na 5, ale nie ma pasującej etykiety przypadku 5 ani nie ma przypadku domyślnego. W efekcie żadna sprawa nie jest wykonywana. Wykonywanie jest kontynuowane po bloku switch, drukowanie Hello.
Robię sobie przerwę
W powyższych przykładach użyliśmy instrukcji return, aby zatrzymać wykonywanie instrukcji po naszych etykietach. Jednak powoduje to również wyjście z całej funkcji.
A break (zadeklarowane przy użyciu słowa kluczowego break ) informuje kompilator, że zakończyliśmy wykonywanie instrukcji w przełączniku i że wykonanie powinno być kontynuowane od instrukcji po zakończeniu bloku przełącznika. Dzięki temu możemy wyjść z instrukcji switch bez wychodzenia z całej funkcji.
Oto lekko zmodyfikowany przykład przepisany przy użyciu break zamiast return:
#include <iostream>
void printDigitName(int x)
{
switch (x) // x evaluates to 3
{
case 1:
std::cout << "One";
break;
case 2:
std::cout << "Two";
break;
case 3:
std::cout << "Three"; // execution starts here
break; // jump to the end of the switch block
default:
std::cout << "Unknown";
break;
}
// execution continues here
std::cout << " Ah-Ah-Ah!";
}
int main()
{
printDigitName(3);
std::cout << '\n';
return 0;
}Powyższy przykład wypisuje:
Three Ah-Ah-Ah!
Najlepsza praktyka
Każdy zestaw instrukcji pod etykietą powinien kończyć się instrukcją break lub instrukcją return. Obejmuje to instrukcje znajdujące się pod ostatnią etykietą przełącznika.
Co się więc stanie, jeśli nie zakończysz zestawu instrukcji pod etykietą znakiem break lub return? Ten i inne tematy omówimy na następnej lekcji.
Etykiety zwykle nie mają wcięć
W lekcji 2.9 -- Kolizje nazewnictwa i wprowadzenie do przestrzeni nazw. Zauważyliśmy, że kod ma zazwyczaj wcięcie o jeden poziom, aby pomóc zidentyfikować, że jest częścią zagnieżdżonego regionu zakresu. Ponieważ nawiasy klamrowe przełącznika definiują nowy obszar zakresu, zwykle wcinamy wszystko wewnątrz nawiasów klamrowych o jeden poziom.
Z drugiej strony etykieta nie definiuje zakresu zagnieżdżonego. Dlatego też kod następujący po etykiecie zwykle nie jest wcięty.
Jeśli jednak zastosujemy wcięcie zarówno w etykietach, jak i w kolejnych instrukcjach na tym samym poziomie, otrzymamy coś takiego:
// Unreadable version
void printDigitName(int x)
{
switch (x)
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}To sprawia, że naprawdę trudno jest określić, gdzie zaczyna się i kończy każdy przypadek.
Mamy tutaj dwie możliwości. Po pierwsze, i tak możemy wciąć instrukcje następujące po etykietach:
// Acceptable but not preferred version
void printDigitName(int x)
{
switch (x)
{
case 1: // indented from switch block
std::cout << "One"; // indented from label (misleading)
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}Chociaż jest to z pewnością bardziej czytelne niż poprzednia wersja, oznacza to, że instrukcje pod każdą etykietą znajdują się w zakresie zagnieżdżonym, co nie ma miejsca w przypadku (zobaczymy tego przykłady w następnej lekcji, gdzie zmienna zdefiniowana przez nas w jednym przypadku może zostać użyta w innym przypadku). Takie formatowanie jest akceptowalne (ponieważ jest czytelne), ale nie jest preferowane.
Konwencjonalnie etykiety po prostu nie są wcięte:
// Preferred version
void printDigitName(int x)
{
switch (x)
{
case 1: // not indented from switch statement
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}Ułatwia to identyfikację każdej etykiety. A ponieważ instrukcje są wcięte tylko o jeden poziom od bloku przełączników, prawidłowo oznacza to, że wszystkie instrukcje wchodzą w zakres bloku przełączników.
W przyszłych lekcjach spotkamy się z innymi typami etykiet — one również tradycyjnie nie mają wcięcia z tego samego powodu.
Najlepsza praktyka
Wolę nie robić wcięć w etykietach. Dzięki temu wyróżniają się na tle otaczającego kodu, nie sugerując, że definiują zagnieżdżony obszar zasięgu.
Switch vs if-else
Instrukcji Switch najlepiej używać, gdy istnieje pojedyncze wyrażenie (z typem całkowitym innym niż boolowski lub wyliczeniowym), które chcemy ocenić pod kątem równości w stosunku do małej liczby wartości. Jeśli liczba etykiet przypadków stanie się zbyt duża, przełącznik może być trudny do odczytania.
>W porównaniu z równoważnymi instrukcjami if-else, instrukcja switch może być bardziej czytelna, wyjaśnia, że jest to to samo wyrażenie sprawdzane pod kątem równości w każdym przypadku i ma tę zaletę, że ocenia wyrażenie tylko raz (co czyni go bardziej wydajnym).
Jednak if-else jest znacznie bardziej elastyczny. W niektórych przypadkach, gdy if lub if-else jest zazwyczaj lepszy:
- Testowanie wyrażenia pod kątem porównań innych niż równość (np.
x > 5) - Testowanie wielu warunków (np.
x == 5 && y == 6) - Określanie, czy wartość mieści się w zakresie (np.
x >= 5 && x <= 10) - Wyrażenie ma typ, którego przełącznik nie obsługuje (np.
d == 4.0). - Wyrażenie ma wartość a
bool.
Najlepsza praktyka
Preferuj instrukcję switch zamiast instrukcji if-else podczas testowania pojedynczego wyrażenia (z typem całkowitym innym niż boolowski lub typem wyliczeniowym) pod kątem równości z małym zestawem wartości.

