Polyglot
Languages C# conditionals
C# § conditionals

Conditionals

C# inherits if/else and the conditional operator from the C-family and adds the switch expression (C# 8) — a value-yielding form of switch with full pattern-matching support. The two switch forms (statement and expression) and the rich pattern grammar together cover the cases that more traditional if-cascades require in older languages. The combination is one of the most substantial C# additions of the past several revisions; modern idiomatic C# uses pattern-matching switches extensively where older code would have used long if/else if chains.

This page covers the principal selection constructs; the Pattern matching page covers the pattern grammar in detail.

if and else

The basic if statement evaluates its boolean controlling expression and executes the body if the result is true:

if (n > 0) {
    Process(n);
}

The body may be any statement; the conventional form uses a compound statement (block) even for a single line. The else clause attaches to the immediately preceding if:

if (x > 0) {
    ClassifyPositive(x);
} else if (x < 0) {
    ClassifyNegative(x);
} else {
    ClassifyZero();
}

C# does not have elif; else if is two keywords. The cascading form above is the conventional way to write multi-way conditionals when a switch does not fit.

Boolean-only controlling expression

Unlike C, the controlling expression of if must be of type bool (or a type implicitly convertible to bool). Implicit conversion from integer or pointer to bool is not admitted:

int count = 5;
if (count) {       // ERROR: int cannot be converted to bool
    // ...
}
if (count != 0) {  // OK
    // ...
}

string? value = MaybeNull();
if (value) {       // ERROR: string cannot be converted to bool
    // ...
}
if (value != null) {     // OK
    // ...
}
if (value is not null) { // OK; preferred
    // ...
}

The strictness eliminates the C-family if (x = y) typo (= versus ==): if (x = 5) is a compile-time error in C# because the result of the assignment is int, not bool. The exception is when the assignment’s type is bool:

bool ready = false;
if (ready = check()) {        // OK; the assignment is to a bool variable
    // ...
}

The conditional operator

The ternary ?: operator selects between two expressions:

int    max  = (a > b) ? a : b;
string s    = (n == 1) ? "" : "s";

The two arms must have a common type, with C# 9’s target-typed conditional expanding the rules: when the conditional appears in a context with a target type, the arms need only be implicitly convertible to that target:

int? value = condition ? 42 : null;     // C# 9: target-typed

Nested conditionals are syntactically legal but quickly become unreadable; one or two levels is typically the practical limit.

The switch statement

The classical switch:

switch (token.Kind) {
    case TokenKind.Plus:
        result = a + b;
        break;
    case TokenKind.Minus:
        result = a - b;
        break;
    case TokenKind.Times:
    case TokenKind.Divide:
        result = ApplyMulDiv(token.Kind, a, b);
        break;
    default:
        throw new InvalidOperationException();
}

Differences from C’s switch:

  • The discriminant may be any type, not just integer types. string, enum types, and types with patterns all work.
  • Fallthrough is not implicit. Each non-empty case must end with break, return, throw, goto case <other>, or goto default. The compiler enforces.
  • Empty cases (one label immediately followed by another) admit fallthrough by virtue of the C#-style or-pattern: stacked labels share a body.
  • default may appear anywhere in the cases; conventionally it is last.

The goto case and goto default forms admit explicit fallthrough between cases:

switch (state) {
    case State.Idle:
        Prepare();
        goto case State.Running;     // explicit fallthrough
    case State.Running:
        Execute();
        break;
}

Cases with patterns (since C# 7):

switch (shape) {
    case Circle c:
        return Math.PI * c.Radius * c.Radius;
    case Square s when s.Side > 0:
        return s.Side * s.Side;
    case null:
        throw new ArgumentNullException();
    default:
        throw new InvalidOperationException();
}

Each case label may be a constant, a type pattern, a type pattern with a guard (when), or null. The full pattern grammar applies; treated in Pattern matching.

The switch expression

C# 8 introduced the switch expression: a value-yielding form of switch with concise syntax:

double area = shape switch {
    Circle c        => Math.PI * c.Radius * c.Radius,
    Square s        => s.Side * s.Side,
    Triangle t      => 0.5 * t.Base * t.Height,
    null            => throw new ArgumentNullException(),
    _               => throw new InvalidOperationException(),
};

The form has substantial advantages over the statement form:

  • It is an expression, so it can appear anywhere a value is expected (initialisers, return values, lambda bodies, ternary alternatives).
  • The syntax is more compact: no case, no break, no labels.
  • The compiler checks for exhaustiveness and warns if some cases are unreachable.
  • The discard pattern _ is the conventional default.

The arms are evaluated top to bottom; the first matching pattern selects the corresponding expression. Each arm is pattern => expression with optional when guards:

string ClassifyNumber(int n) => n switch {
    0           => "zero",
    > 0 and <= 9 => "single-digit positive",
    > 0          => "positive",
    _            => "negative",
};

The patterns may use any of the C# pattern forms — type patterns, property patterns, list patterns, relational patterns, logical patterns. The combination makes the switch expression the conventional choice for value-discriminating logic.

Exhaustiveness

The compiler attempts to determine whether the patterns cover every possible input; if not, a warning is issued:

public enum Color { Red, Green, Blue }

string Describe(Color c) => c switch {
    Color.Red   => "red",
    Color.Green => "green",
    // warning: 'Blue' is unhandled; pattern is not exhaustive
};

The warning catches newly added enum values that the switch does not handle. To silence it, either add the missing case or use a discard:

string Describe(Color c) => c switch {
    Color.Red   => "red",
    Color.Green => "green",
    Color.Blue  => "blue",
    _           => "unknown",     // catches anything new
};

The conventional advice depends on the use case: enumeration with a closed set (sum types) should not have a default — let the warning catch new cases. Enumeration with an open set (extensible inputs) should have a default to handle the unexpected.

Selection idioms

Several patterns recur in idiomatic C# control flow.

Early return

public Result Parse(string input) {
    if (string.IsNullOrEmpty(input))     return Result.Empty;
    if (input.Length > MaxInputLength)   return Result.TooLong;
    if (!IsValid(input))                 return Result.Malformed;

    /* main body */
    return Result.Ok(...);
}

The pattern reduces nesting and keeps the precondition checks visually separate from the substantive body.

Switch expression for value mapping

string FormatStatus(Status s) => s switch {
    Status.Idle    => "idle",
    Status.Running => "running",
    Status.Stopped => "stopped",
    _              => "unknown",
};

The form is the conventional replacement for Dictionary<Status, string> lookups when the set of values is small and known at compile time.

Switch on tuple

C# admits a switch on a tuple of multiple discriminants:

string CombineStates((Status s1, Status s2) pair) => pair switch {
    (Status.Idle,    Status.Idle)    => "both idle",
    (Status.Running, _)              => "first running",
    (_, Status.Running)              => "second running",
    _                                 => "neither running",
};

The mechanism admits compact discrimination over multiple axes; older languages required nested if/else if chains.

Pattern matching with is

if (shape is Circle c && c.Radius > 0) {
    /* c is in scope and known non-null with positive radius */
}

The is operator with a type pattern combines the type test, the binding, and the optional guard into a single expression. The construction is the conventional way to dispatch on a runtime type without a full switch.

Conditional expression for short selections

var separator = collection.Count == 1 ? "" : ", ";
return $"selected {collection.Count} item{separator}";

The conditional operator is the conventional choice for short selections inside larger expressions.

A note on goto

C# retains goto from C; it is rare in idiomatic code. The principal uses:

  • goto case and goto default in switch statements for explicit fallthrough.
  • Multi-level loop exit: a goto is occasionally clearer than a flag.
  • State machines transcribed directly from a diagram.

The cleanup-discipline use of goto in C is replaced in C# by using statements and try/finally; programs that need deterministic cleanup use the disposal mechanism instead.