Operator warunkowy, zwykle nazywany operatorem trójargumentowym (ternary), wybiera między dwiema wartościami w jednym wyrażeniu: condition ? valueIfTrue : valueIfFalse.
Wynik:
0 items, shipping 4.99
1 item, shipping 4.99
5 items, shipping 0
Warunek musi być typu bool. Po jego obliczeniu wykonuje się tylko jedna z dwóch gałęzi; druga nigdy nie jest obliczana. Dzięki temu operator nadaje się do bezpiecznego dostępu, jak list.Count > 0 ? list[0] : "empty", gdzie obliczenie list[0] na pustej liście rzuciłoby wyjątek.
Operator trójargumentowy a if/else
Operator trójargumentowy to wyrażenie: daje wartość, którą przypisujesz, przekazujesz albo zwracasz. Instrukcja if wykonuje instrukcje. Te dwie metody robią to samo:
// with if/else
static string Status(int stock)
{
if (stock > 0)
return "in stock";
else
return "sold out";
}
// with the ternary
static string Status(int stock) => stock > 0 ? "in stock" : "sold out";
Używaj operatora trójargumentowego, gdy obie gałęzie to krótkie wartości tego samego rodzaju. Używaj if, gdy gałąź wykonuje kilka instrukcji, ma efekty uboczne albo gdy warunek jest na tyle długi, że cała linia przestaje się wygodnie mieścić.
Operator trójargumentowy nie zastąpi if, który tylko wykonuje akcje. To się nie kompiluje:
stock > 0 ? Ship(order) : Refund(order);
// error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
Nie ma też operatora trójargumentowego bez else: ? i : są zawsze wymagane, bo wyrażenie musi mieć wartość w obu przypadkach.
Obie gałęzie potrzebują wspólnego typu
Kompilator ustala typ całego wyrażenia na podstawie jego dwóch gałęzi. Jedna gałąź musi dać się niejawnie przekonwertować na typ drugiej. Gdy żadna się nie da, dostajesz CS0173:
var discount = isMember ? 10 : null;
// error CS0173: Type of conditional expression cannot be determined because
// there is no implicit conversion between 'int' and '<null>'
var label = inStock ? 1 : "none";
// error CS0173: ... between 'int' and 'string'
Rozwiązaniem jest rzutowanie jednej gałęzi na typ, którego chcesz, żeby druga się na niego przekonwertowała:
Wynik:
no discount
7
3.5
points / 2.0 : 0 nie wymaga rzutowania: 0 to int, który niejawnie konwertuje się na double, więc wyrażenie jest typu double.
C# 9 dodał typowanie docelowe: gdy wyrażenie jest przypisywane do zadeklarowanego typu, kompilator używa tego typu. int? discount = isMember ? 10 : null; kompiluje się w C# 9 i nowszych. Z var nie ma typu docelowego, więc var discount = isMember ? 10 : null; nadal zawodzi w każdej wersji.
Priorytet i nawiasy
Operator warunkowy ma bardzo niski priorytet, niższy niż +, == i &&. W a > b ? a : b jest to wygodne, a wewnątrz łączenia stringów zaskakujące:
Console.WriteLine("Status: " + isActive ? "on" : "off");
// error CS0029: Cannot implicitly convert type 'string' to 'bool'
Kompilator odczytuje warunek jako "Status: " + isActive, czyli string. Nawiasy wokół operatora trójargumentowego to naprawiają: "Status: " + (isActive ? "on" : "off").
Interpolacja stringów ma własną pułapkę. Wewnątrz { } dwukropek rozpoczyna specyfikator formatu ({price:F2}), więc goły operator trójargumentowy myli parser. Otocz go nawiasami:
Wynik:
2 users online
You have 1 unread message
Status: online
Zagnieżdżone operatory trójargumentowe
Operator jest prawostronnie łączny, więc a ? x : b ? y : z grupuje się jako a ? x : (b ? y : z). Krótki łańcuch rozpisany po jednym warunku na linię jest czytelny:
Wynik:
512 B
20 KB
5 MB
Powyżej dwóch lub trzech poziomów albo gdy warunki sprawdzają różne rzeczy, przejdź na if/else if albo wyrażenie switch. Zagnieżdżanie w gałęzi true (a ? (b ? x : y) : z) to wersja, która najszybciej staje się trudna do śledzenia.
?? i ?. zastępują typowe operatory trójargumentowe
Wiele operatorów trójargumentowych istnieje tylko po to, żeby obsłużyć null. C# ma do tego krótsze operatory:
| Operator trójargumentowy | Krótsza forma | Znaczenie |
|---|---|---|
name != null ? name : "guest" | name ?? "guest" | wartość zastępcza, gdy null |
customer != null ? customer.Email : null | customer?.Email | dostęp do składnika tolerujący null |
c != null && c.Address != null ? c.Address.City : "unknown" | c?.Address?.City ?? "unknown" | oba połączone |
value != null ? value : throw new ArgumentNullException(...) | value ?? throw new ArgumentNullException(...) | szybkie zgłoszenie błędu |
Wynik:
Lena from Berlin
guest from unknown
guest from unknown
Wyrażenie throw jest też dozwolone jako dowolna gałąź operatora trójargumentowego: int age = input >= 0 ? input : throw new ArgumentOutOfRangeException(nameof(input));.
Warunkowy ref
Od C# 7.2 operator trójargumentowy może wybierać między dwiema zmiennymi zamiast dwóch wartości, a przez wynik można przypisywać:
// C# 7.2 and later
int wins = 0, losses = 0;
bool won = true;
ref int counter = ref (won ? ref wins : ref losses);
counter++; // increments wins
Obie gałęzie muszą być ref do zmiennych tego samego typu. W kodzie aplikacji to rzadkość; istnieje głównie dla kodu wrażliwego na wydajność, który pracuje z dużymi strukturami albo tablicami bez ich kopiowania.
Najczęściej zadawane pytania
Czym jest operator trójargumentowy w C#?
To operator warunkowy condition ? valueIfTrue : valueIfFalse, jedyny operator C# z trzema operandami. Oblicza warunek, a potem oblicza i zwraca dokładnie jedną z dwóch wartości: string label = age >= 18 ? "adult" : "minor";.
Czy można użyć operatora trójargumentowego bez else w C#?
Nie. Obie części, ? i :, są wymagane, bo wyrażenie zawsze musi dać wartość. Jeśli chcesz coś zrobić tylko wtedy, gdy warunek jest spełniony, użyj instrukcji if. Operator trójargumentowy nie może też stać sam jako instrukcja: x > 0 ? A() : B(); to błąd CS0201.
Skąd błąd "Type of conditional expression cannot be determined"?
Obie gałęzie nie mają wspólnego typu, na przykład var x = found ? 42 : null; (int i null) albo found ? 1 : "none". Rzutuj jedną gałąź, żeby typ istniał: found ? (int?)42 : null. Od C# 9 działa też zadeklarowany typ docelowy: int? x = found ? 42 : null; się kompiluje, ale var nadal zawodzi.
Jak użyć operatora trójargumentowego w interpolacji stringów?
Otocz go nawiasami: $"{(count == 1 ? "item" : "items")}". Bez nich : jest odczytywany jako początek specyfikatora formatu i kod się nie kompiluje.
Czy w C# można zagnieżdżać operatory trójargumentowe?
Tak. Operator jest prawostronnie łączny, więc a ? x : b ? y : z oznacza a ? x : (b ? y : z). Jeden poziom zagnieżdżenia rozpisany w osobnych liniach czyta się dobrze; głębsze łańcuchy są czytelniejsze jako łańcuch if/else if albo wyrażenie switch.