In Dart 3, how do relational patterns like `>= 0 && < 15` and logical-or patterns work, and what rules limit their operands and bindings?
answer
- ==, !=, <, >, <=, >= against a constant
- && needs both, || needs one
- || branches bind the same names
- && branches must not overlap
- || lowest precedence, then &&
basics
~20 sA relational pattern such as < 0 compares the value with a constant; && needs both sides to match, || needs one. Operands must be constants, || branches must bind the same variables, and && sides must not share a name.
solid answer
~50 sA **relational pattern** applies `==`, `!=`, `<`, `>`, `<=` or `>=` to the matched value with a **constant** on the right, so `< 0` matches negative readings. **Logical-and** `p1 && p2` matches when both subpatterns match and skips the right one if the left fails; **logical-or** `p1 || p2` tries branches left to right and stops at the first match. Together they express ranges: `>= 0 && < 15 => 'cold'`. Precedence runs from `||` (lowest) through `&&` to relational patterns, so `a || b && c` means `a || (b && c)`; parentheses change it. Two binding rules keep variables well defined: every `||` branch must bind the **same** set of variables, because only one branch matched, and the two sides of `&&` must **not** bind the same name. A threshold known only at run time needs a `when` guard, not a relational pattern.
code
dart · 20 linesString band(double celsius) => switch (celsius) {
< 0 => 'freezing',
>= 0 && < 15 => 'cold',
>= 15 && < 25 => 'mild',
_ => 'hot',
};
num? readTemp(Object? json) {
// Two payload versions: both branches bind t.
if (json case {'temp': num t} || {'temperature': num t}) return t;
return null;
}
void main() {
print(band(-3)); // freezing
print(band(0)); // cold
print(band(21.5)); // mild
print(readTemp({'temperature': 18})); // 18
print(readTemp({'celsius': 18})); // null
}go deeper
Recognise < 0 and >= 0 && < 15 as patterns that test ranges, and || as a way to let several constants share one case.
Explain constant operands, left-to-right short-circuiting, precedence from || up to relational patterns, and the binding rules for || and &&.
Use guards for runtime thresholds, logical-or with identical bindings to accept payload variants, and parentheses wherever precedence could surprise a reader.
Judge when a dense pattern with ranges and alternatives is clearer than a few guarded cases, and set team style so matching logic stays reviewable.
## Relational patterns A **relational pattern** is an operator followed by a constant: `== c`, `!= c`, `< c`, `> c`, `<= c`, `>= c`. It matches when calling that operator on the matched value with the constant as the argument returns `true`. They are made for numeric ranges: ```dart String band(double celsius) => switch (celsius) { < 0 => 'freezing', >= 0 && < 15 => 'cold', >= 15 && < 25 => 'mild', _ => 'hot', }; ``` The right-hand side must be a **compile-time constant**. `> threshold` compiles only if `threshold` is a `const`; for a value read from settings, bind the value and use a guard instead: `var t when t > settings.threshold`. ## Logical-and `p1 && p2` matches only if **both** subpatterns match the same value. If the left one fails, the right one is not evaluated. The typical use is a range, as above, or combining a binding with a check: on a `double` reading, `var t && >= 40` binds `t` only for values of 40 and above. Both sides are matched against the same value, so if both bound the same name there would be two conflicting bindings. That is a compile error: ```dart switch ((1, 2)) { case (var a, var b) && (var b, var c): // error: b bound twice } ``` ## Logical-or `p1 || p2` tries its branches **left to right** and stops at the first that matches. It is how several alternatives share one case: ```dart final isWet = switch (condition) { 'rain' || 'drizzle' || 'snow' => true, _ => false, }; ``` Branches may bind variables, but **every branch must bind the same set of variables**, because only one of them will have run when the body executes. That is what makes this legal for two versions of a weather payload: ```dart if (json case {'temp': num t} || {'temperature': num t}) { print('temperature $t'); } ``` ## Precedence and parentheses From lowest to highest precedence: 1. logical-or `||` 2. logical-and `&&` 3. relational patterns 4. postfix unary patterns: cast `as`, null-check `?`, null-assert `!` 5. primary patterns: constants, variables, records, lists, maps, objects, parenthesized patterns So `a || b && c` is `a || (b && c)`. When you need the other grouping, parenthesise: `(a || b) && c`. With boolean constants `x = true`, `y = true`, `z = false`, the Dart docs show that `x || y && z` matches `true` while `(x || y) && z` matches nothing. ## Patterns versus boolean expressions These look like the boolean operators but are not expressions: | Written | In an expression | In a pattern | |---|---|---| | `a || b` | evaluates two `bool` values | matches if the value matches pattern `a` or pattern `b` | | `< 15` | not valid on its own | matches values less than 15 | | `x && y` | evaluates two `bool` values | both patterns must match the same value | The operands of a logical pattern are **patterns**: constants, relational checks, typed variables, whole map or record patterns. ## Not only numbers A relational pattern calls the operator on the matched value, so it works with any type whose operator accepts the constant. `Duration` defines `<` and has a `const` constructor, so a request timer can be classified directly: ```dart String speed(Duration elapsed) => switch (elapsed) { < const Duration(milliseconds: 300) => 'fast', < const Duration(seconds: 2) => 'ok', _ => 'slow', }; ``` `==` and `!=` work on any value. Types without the operator, such as `String` for `<`, cannot be used with the ordering forms. ## Common mistakes - Writing `> min && < max` with runtime `min` and `max` — use a guard. - Expecting `||` branches to bind different variables — bind the same names in every branch or split the cases. - Forgetting that ranges must be ordered correctly: `>= 0 && < 15` accepts 0, while `> 0 && < 15` does not. - Leaving a gap between ranges, such as `< 0` then `> 0`, which never matches exactly `0.0`.
- Why does `case > limit:` fail to compile when `limit` is a `final` local read from settings?A relational pattern compares with a constant, and a `final` local is not a compile-time constant. Bind the value and move the comparison into a guard: `case var t when t > limit:`. The guard can use any runtime expression.
- Can the two sides of `&&` in a pattern both bind variables?Yes, as long as they bind different names. Both sides match the same value and both bindings exist afterwards, so reusing a name would bind it twice, which is a compile error. `(var a, _) && (_, var b)` is fine.
saying these in an interview costs you the question
- Uses a runtime variable in a relational pattern such as > threshold
- Thinks || branches may bind different variables
- Reads a || b && c as (a || b) && c
- Treats pattern && and || as boolean expressions on bool operands
- Believes both sides of || are always evaluated