Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

Patterns

value is <pattern> value switch { <pattern> [when <condition>] => result, … }

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 list that starts with 1 — and can name the parts it matched. Write one after `is` or as a switch expression's arm: constants, relational tests, `and`/`or`/`not`, property patterns `{ X: 1, Inner.Y: > 2 }`, list patterns `[1, _, .. var rest]`, positional `(1, var s)`, type tests `Dog d`, `var x` and `_`, exactly as C# writes them.

stable3 examples compiled by CIfunctioncontrol-flowpatternauthoring

Summary#

x is <pattern> answers true or false; a switch expression picks the first arm whose pattern matches. The patterns are C#'s, and they nest:

class Inner { public int Y = 5; }
class Point { public int X; public Inner Inner = new Inner(); }

string Describe(int n) {
  var p = new Point { X = n };
  int[] digits = new int[] { 1, 2, n };
  return $"{n is > 0 and < 10}|{n is 1 or 2}|{n is not 7}"
    + $"|{p is { X: 1, Inner.Y: > 2 }}"
    + $"|{(digits is [1, .. var rest] ? rest.Length : -1)}";
}

Signature#

7 · "a" · null · Color.Red        // a CONSTANT — equal to it
> 0 · <= 10                       // a RELATIONAL test against a constant
Dog · Dog d                       // a TYPE test, optionally naming the value at that type
var x · _                         // match anything, naming it — or not
not P · P and Q · P or Q · (P)    // combine patterns
{ X: P, Inner.Y: P } · Dog { … }  // a PROPERTY pattern (type optional), `{ }` = "not null"
[P, P, .. var rest, P]            // a LIST pattern, at most one slice `..`
(P, P)                            // a POSITIONAL pattern over a tuple or a Deconstruct

Description#

Combining patterns#

and, or and not combine patterns the way &&, || and ! combine conditions, binding in that order (not tightest), and parentheses group them: n is (> 0 and < 5) or 7. is not null is the everyday one. They are ordinary words otherwise — a local may still be called not or and.

Property patterns#

p is { X: 1, Y: > 2 } is true when p is not null and each member matches its own pattern. A dotted member reads through: { Inner.Y: 5 } is { Inner: { Y: 5 } }, so a null Inner fails the test rather than throwing. { } on its own is "not null". A type in front tests the type first and reads the members at that type: o is { Pet: Dog { Barks: true } }.

List patterns#

[1, 2, 3] matches a list or array of exactly those elements; .. stands for any number of elements (at most one per pattern), so [1, ..] is "starts with 1" and [.., 9] is "ends with 9". .. var rest names the slice — over an array, as in C#; a List<T> has no range indexer, so naming its slice is refused with the alternatives. [] is "empty".

Names a pattern binds#

var x, a designation (Dog d, { X: 1 } p) and a slice name (.. var rest) are readable wherever C# says they are definitely assigned: later in the same condition after &&, in the true branch of a ?:, in an if's body, in a switch arm's guard and value. A name under not or on one side of an or binds nothing — C# refuses it (CS8780), and so does Osy#, saying why.

In a switch expression#

Every pattern is a switch arm, and an arm may add a when guard that must also hold:

class Point { public int X; public int Y; }

string Classify(Point p) {
  return p switch {
    { X: > 5 } => "far",
    { Y: var y } when y < 0 => "below",
    _ => "other",
  };
}

int Ends(int[] a) {
  return a switch { [] => 0, [var only] => only, [var first, .., var last] => first + last };
}

A switch whose arms are only constants, relational tests, types and _ keeps its exhaustiveness check (switch expression). Once an arm is any other pattern or carries a guard, the compiler cannot prove a gap, so a switch with no _ that matches nothing throws InvalidOperationException, as C#'s does.

The value is read once per test#

A pattern that tests a value more than once — F() is > 0 and < 10 — needs a value that reading again cannot change: a variable, a member, an element. Anything else is refused with the one-line fix, var v = F(); v is …, because C# reads it exactly once.

Everywhere a condition runs#

A pattern lowers to the comparisons it stands for, so it means the same in a function, in the browser, inside an in-memory LINQ lambda and in a query over stored rows (Item.Where(i => i.Name is "a" or "b") is SQL's IN-shaped test).

Examples#

class Point { public int X; }

int FarOnes(List<Point> points) => points.Where(p => p is { X: > 5 }).Count();

string Seen(Point? maybe) => maybe is { X: 0 } ? "origin" : maybe is null ? "none" : "elsewhere";

See also#

Related

switch expression

Choose a VALUE by matching a subject against patterns. Arms are tried in order and the first match wins. An arm is any…

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…

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…

LINQ over a local list

Query a local `List<T>`, `HashSet<T>` or `T[]` — of your own `class` values OR of plain scalars like `string[]` and…