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 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 case label:
#include <iostream>
void printDigitName(int x)
{
switch (x) // x jest oceniane w celu uzyskania wartości 2
{
case 1:
std::cout << "One";
return;
case 2: // która pasuje do instrukcji case tutaj
std::cout << "Two"; // więc wykonywanie zaczyna się tutaj
return; // a następnie wracamy do wywołującego
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: // błąd: już użyta wartość 54!
case '6': // błąd: '6' konwertuje na wartość całkowitą 54, która jest już używana
}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 jest oceniane w celu uzyskania wartości 5
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default: // co nie pasuje do żadnej etykiety przypadków
std::cout << "Unknown"; // więc wykonywanie zaczyna się tutaj
return; // a następnie wracamy do wywołującego
}
}
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 jest oceniane w celu uzyskania wartości 5
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
// nie istnieje żaden pasujący przypadek ani nie ma przypadku domyślnego
}
// więc wykonywanie jest kontynuowane tutaj
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.
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 ma wartość 3
{
case 1:
std::cout << "One";
break;
case 2:
std::cout << "Two";
break;
case 3:
std::cout << "Three"; // wykonanie rozpoczyna się tutaj
break; // skok na koniec bloku przełączników
default:
std::cout << "Unknown";
break;
}
// wykonanie jest tutaj wykonywane
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:
// Wersja akceptowalna, ale nie preferowana
void printDigitName(int x)
{
switch (x)
{
case 1: // wcięcie z bloku przełączników
std::cout << "One"; // wcięcie z etykiety (wprowadza w błąd)
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: // nie jest wcięta od instrukcji switch
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ść
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.

