Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

switch expression

subject switch { pattern => value, …, _ => value }

Choose a VALUE by matching a subject against patterns. Arms are tried in order and the first match wins. An arm is any C# pattern — a constant, a relational comparison, a type test, `_`, or a property, list, positional or combined pattern — optionally with a `when` guard. Over a closed vocabulary — an enum or a bool — a missing arm is a compile error rather than a runtime throw.

stable2 examples compiled by CIfunctioncontrol-flowpatternauthoring

Summary#

subject switch { … } is an EXPRESSION: it produces a value. Arms are tried top to bottom and the first one that matches wins. It is the form to reach for wherever a value is wanted rather than a statement — notably inside a render block, which takes no statements.

Over a closed vocabulary (an enum, or a bool) a switch expression with no _ arm must cover every member, and a gap is a compile error.

Signature#

subject switch {
  Member        => value,     // a CONSTANT — an enum member or a literal
  >= 1000.0     => value,     // a RELATIONAL comparison: < <= > >=
  Circle c      => value,     // a TYPE pattern, binding the narrowed value
  Square        => value,     // a TYPE pattern with no binding (C# 9)
  { X: > 0 }    => value,     // any other PATTERN — see function-patterns
  var n when n > 5 => value,  // a `when` guard that must also hold
  _             => value,     // the DISCARD — matches anything
}

Description#

Arms are tried in order, and order is the whole design of a relational ladder. 1500000 satisfies both >= 1000000 and >= 1000; writing the larger bound first is what makes it answer "M".

A relational arm compares against a constant and may use any of <, <=, >, >=. There is no == form because a bare constant arm already means equals.

Exhaustiveness is checked only where a vocabulary is closed. An enum and a bool have a knowable set of values, so every one must have an arm or the compile fails. A number does not, so a switch over one always needs _ — including a relational ladder, because proving that < 0 and >= 0 between them cover every number is arithmetic the compiler does not attempt. It asks for a _ you may not strictly need rather than claiming a gap is covered when it is not.

A missing enum arm is an ERROR, not a warning. This is a deliberate divergence from C#, which warns at compile time and throws at run time. A throw inside a render expression is a blank page instead of a message, so the check that prevents it has to be the one that cannot be ignored.

The subject is read once per arm test, so it must be re-readable at no cost — a name, a member access, an index. Anything that could do work (a call, an await) is refused, and the refusal names the one-line fix: assign it to a local first.

A type pattern may bind or not. Circle c => … names the subject at the narrower type for that arm; Square => … only tests it. A bare name is read as a type when the subject is a row or an object, no local of that name is in scope, and the name is a type — otherwise it is a constant, as before. Both forms test the row's real type, so a row read through its base type (a kit's Case) answers by the subtype it is.

Any pattern is an arm, and an arm may carry a when guard — { Y: var y } when y < 0 => "below". The forms are on Patterns. A switch that uses one (or a guard) has no exhaustiveness check, because the compiler cannot prove a gap over a shape: write _, or a value that matches no arm throws InvalidOperationException, as C#'s does.

Examples#

public enum MarkKind { Line, Area, Column }

// RELATIONAL — the compact-number ladder. The larger bound comes first because the first match wins.
string Compact(int n) {
  return n switch { >= 1000000 => "M", >= 1000 => "K", _ => "" };
}

// All four ordering operators.
string Band(int n) {
  return n switch { < 0 => "neg", <= 10 => "small", > 100 => "big", _ => "mid" };
}

// A CONSTANT arm over an enum. No `_`: every member has an arm, which is what makes adding a member to
// the vocabulary a build failure at each site that must decide about it.
string Family(MarkKind k) {
  return k switch { Line => "stroke", Area => "stroke", Column => "fill" };
}

// A bool is a closed vocabulary too, so both cases are exhaustive with no `_`.
string YesNo(int n) { return (n > 0) switch { true => "yes", false => "no" }; }
class Shape { public string Name; }
class Circle : Shape { public double Radius; }
class Square : Shape { public double Side; }

// `Circle c` binds `c` at the narrower type, so `c.Radius` reads inside that arm. `Square` only tests.
string Describe(Shape s) {
  return s switch { Circle c => $"circle r={c.Radius}", Square => "a square", _ => s.Name };
}

See also#

  • switch — the switch STATEMENT, which branches control rather than producing a value
  • Patterns — every pattern an arm may be, and the names they bind
  • if / else — the two-way conditional
  • Testing which class a value is — is / as / a cast / OfType<T>(), the other places a type is asked about
  • Enums — why a closed vocabulary is what makes exhaustiveness checkable

Related

Patterns

A pattern asks a question about a value's SHAPE — is it 7, is it over 0 and under 10, does it have an X of 1, is it a…

switch

Branch on a value against constant case labels. Only the matched section runs (no fall-through); a default section…

if / else

Conditional branching, exactly as in C#. The condition must be a bool — there is no truthiness, so a null or a number…

Testing which class a value is

`s is Circle` asks which type a value actually is at run time, answering by the value's own type rather than the type…

Enums

A fixed set of named values, used as a member type. By default an enum stores as a compact number; add [Type(string)]…