An expression may have two roles: it always produces a value, and it may have effects (otherwise known as "side effects"). An expression evaluates to a value, and has effects during evaluation. Many expressions contain sub-expressions (operands). The meaning of each kind of expression dictates several things:
- Whether or not to evaluate the sub-expressions when evaluating the expression
- The order in which to evaluate the sub-expressions
- How to combine the sub-expressions' values to obtain the value of the expression
In this way, the structure of expressions dictates the structure of execution. Blocks are just another kind of expression, so blocks, statements, expressions, and blocks again can recursively nest inside each other to an arbitrary depth.
Expressions are divided into two main categories: lvalues and rvalues. Likewise within each expression, sub-expressions may occur in lvalue context or rvalue context. The evaluation of an expression depends both on its own category and the context it occurs within.
An lvalue is an expression that represents a memory location. These expressions
are paths (which refer to local variables, function and
method arguments, or static variables), dereferences (*expr
), indexing
expressions (expr[expr]
), and field
references (expr.f
). All other expressions are rvalues.
The left operand of an assignment or compound-assignment expression is an lvalue context, as is the single operand of a unary borrow. The discriminant or subject of a match expression may be an lvalue context, if ref bindings are made, but is otherwise an rvalue context. All other expression contexts are rvalue contexts.
When an lvalue is evaluated in an lvalue context, it denotes a memory location; when evaluated in an rvalue context, it denotes the value held in that memory location.
When an rvalue is used in an lvalue context, a temporary un-named lvalue is created and used instead. The lifetime of temporary values is typically the innermost enclosing statement; the tail expression of a block is considered part of the statement that encloses the block.
When a temporary rvalue is being created that is assigned into a let
declaration, however, the temporary is created with the lifetime of
the enclosing block instead, as using the enclosing statement (the
let
declaration) would be a guaranteed error (since a pointer to the
temporary would be stored into a variable, but the temporary would be
freed before the variable could be used). The compiler uses simple
syntactic rules to decide which values are being assigned into a let
binding, and therefore deserve a longer temporary lifetime.
Here are some examples:
let x = foo(&temp())
. The expressiontemp()
is an rvalue. As it is being borrowed, a temporary is created which will be freed after the innermost enclosing statement (thelet
declaration, in this case).let x = temp().foo()
. This is the same as the previous example, except that the value oftemp()
is being borrowed via autoref on a method-call. Here we are assuming thatfoo()
is an&self
method defined in some trait, sayFoo
. In other words, the expressiontemp().foo()
is equivalent toFoo::foo(&temp())
.let x = &temp()
. Here, the same temporary is being assigned intox
, rather than being passed as a parameter, and hence the temporary's lifetime is considered to be the enclosing block.let x = SomeStruct { foo: &temp() }
. As in the previous case, the temporary is assigned into a struct which is then assigned into a binding, and hence it is given the lifetime of the enclosing block.let x = [ &temp() ]
. As in the previous case, the temporary is assigned into an array which is then assigned into a binding, and hence it is given the lifetime of the enclosing block.let ref x = temp()
. In this case, the temporary is created using a ref binding, but the result is the same: the lifetime is extended to the enclosing block.
When a local variable is used as an
rvalue, the variable will
be copied if its type implements Copy
. All others are moved.
A literal expression consists of one of the literal forms described earlier. It directly describes a number, character, string, boolean value, or the unit value.
(); // unit type
"hello"; // string type
'5'; // character type
5; // integer type
A path used as an expression context denotes either a local variable or an item. Path expressions are lvalues.
Tuples are written by enclosing zero or more comma-separated expressions in parentheses. They are used to create tuple-typed values.
(0.0, 4.5);
("a", 4usize, true);
You can disambiguate a single-element tuple from a value in parentheses with a comma:
(0,); // single-element tuple
(0); // zero in parentheses
There are several forms of struct expressions. A struct expression consists of the path of a struct item, followed by a brace-enclosed list of zero or more comma-separated name-value pairs, providing the field values of a new instance of the struct. A field name can be any identifier, and is separated from its value expression by a colon. In the case of a tuple struct the field names are numbers corresponding to the position of the field. The numbers must be written in decimal, containing no underscores and with no leading zeros or integer suffix. The location denoted by a struct field is mutable if and only if the enclosing struct is mutable.
A tuple struct expression consists of the path of a struct item, followed by a parenthesized list of one or more comma-separated expressions (in other words, the path of a struct item followed by a tuple expression). The struct item must be a tuple struct item.
A unit-like struct expression consists only of the path of a struct item.
The following are examples of struct expressions:
# struct Point { x: f64, y: f64 }
# struct NothingInMe { }
# struct TuplePoint(f64, f64);
# mod game { pub struct User<'a> { pub name: &'a str, pub age: u32, pub score: usize } }
# struct Cookie; fn some_fn<T>(t: T) {}
Point {x: 10.0, y: 20.0};
NothingInMe {};
TuplePoint(10.0, 20.0);
TuplePoint { 0: 10.0, 1: 20.0 }; // Results in the same value as the above line
let u = game::User {name: "Joe", age: 35, score: 100_000};
some_fn::<Cookie>(Cookie);
A struct expression forms a new value of the named struct type. Note that for a given unit-like struct type, this will always be the same value.
A struct expression can terminate with the syntax ..
followed by an
expression to denote a functional update. The expression following ..
(the
base) must have the same struct type as the new struct type being formed.
The entire expression denotes the result of constructing a new struct (with
the same type as the base expression) with the given values for the fields that
were explicitly specified and the values in the base expression for all other
fields.
# struct Point3d { x: i32, y: i32, z: i32 }
let base = Point3d {x: 1, y: 2, z: 3};
Point3d {y: 0, z: 10, .. base};
When initializing a data structure (struct, enum, union) with named (but not
numbered) fields, it is allowed to write fieldname
as a shorthand for
fieldname: fieldname
. This allows a compact syntax with less duplication.
Example:
# struct Point3d { x: i32, y: i32, z: i32 }
# let x = 0;
# let y_value = 0;
# let z = 0;
Point3d { x: x, y: y_value, z: z };
Point3d { x, y: y_value, z };
A block expression is similar to a module in terms of the declarations that are possible. Each block conceptually introduces a new namespace scope. Use items can bring new names into scopes and declared items are in scope for only the block itself.
A block will execute each statement sequentially, and then execute the
expression (if given). If the block ends in a statement, its value is ()
:
let x: () = { println!("Hello."); };
If it ends in an expression, its value and type are that of the expression:
let x: i32 = { println!("Hello."); 5 };
assert_eq!(5, x);
A method call consists of an expression followed by a single dot, an
identifier, and a parenthesized expression-list. Method calls are resolved to
methods on specific traits, either statically dispatching to a method if the
exact self
-type of the left-hand-side is known, or dynamically dispatching if
the left-hand-side expression is an indirect trait
object.
The compiler sometimes cannot infer to which function or method a given call refers. These cases require a more specific syntax. for method and function invocation.
A field expression consists of an expression followed by a single dot and an identifier, when not immediately followed by a parenthesized expression-list (the latter is a method call expression). A field expression denotes a field of a struct.
mystruct.myfield;
foo().x;
(Struct {a: 10, b: 20}).a;
A field access is an lvalue referring to the value of that field. When the type providing the field inherits mutability, it can be assigned to.
Also, if the type of the expression to the left of the dot is a pointer, it is automatically dereferenced as many times as necessary to make the field access possible. In cases of ambiguity, we prefer fewer autoderefs to more.
An array expression is written by enclosing zero or more comma-separated expressions of uniform type in square brackets.
In the [expr ';' expr]
form, the expression after the ';'
must be a
constant expression that can be evaluated at compile time, such as a
literal or a static item.
[1, 2, 3, 4];
["a", "b", "c", "d"];
[0; 128]; // array with 128 zeros
[0u8, 0u8, 0u8, 0u8];
Array-typed expressions can be indexed by writing a square-bracket-enclosed expression (the index) after them. When the array is mutable, the resulting lvalue can be assigned to.
Indices are zero-based, and may be of any integral type. Vector access is bounds-checked at compile-time for constant arrays being accessed with a constant index value. Otherwise a check will be performed at run-time that will put the thread in a panicked state if it fails.
([1, 2, 3, 4])[0];
let x = (["a", "b"])[10]; // compiler error: const index-expr is out of bounds
let n = 10;
let y = (["a", "b"])[n]; // panics
let arr = ["a", "b"];
arr[10]; // panics
Also, if the type of the expression to the left of the brackets is a pointer, it is automatically dereferenced as many times as necessary to make the indexing possible. In cases of ambiguity, we prefer fewer autoderefs to more.
The ..
operator will construct an object of one of the std::ops::Range
variants.
1..2; // std::ops::Range
3..; // std::ops::RangeFrom
..4; // std::ops::RangeTo
..; // std::ops::RangeFull
The following expressions are equivalent.
let x = std::ops::Range {start: 0, end: 10};
let y = 0..10;
assert_eq!(x, y);
Rust defines the following unary operators. With the exception of ?
, they are
all written as prefix operators, before the expression they apply to.
-
: Negation. Signed integer types and floating-point types support negation. It is an error to apply negation to unsigned types; for example, the compiler rejects-1u32
.*
: Dereference. When applied to a pointer it denotes the pointed-to location. For pointers to mutable locations, the resulting lvalue can be assigned to. On non-pointer types, it calls thederef
method of thestd::ops::Deref
trait, or thederef_mut
method of thestd::ops::DerefMut
trait (if implemented by the type and required for an outer expression that will or could mutate the dereference), and produces the result of dereferencing the&
or&mut
borrowed pointer returned from the overload method.!
: Logical negation. On the boolean type, this flips betweentrue
andfalse
. On integer types, this inverts the individual bits in the two's complement representation of the value.&
and&mut
: Borrowing. When applied to an lvalue, these operators produce a reference (pointer) to the lvalue. The lvalue is also placed into a borrowed state for the duration of the reference. For a shared borrow (&
), this implies that the lvalue may not be mutated, but it may be read or shared again. For a mutable borrow (&mut
), the lvalue may not be accessed in any way until the borrow expires. If the&
or&mut
operators are applied to an rvalue, a temporary value is created; the lifetime of this temporary value is defined by syntactic rules.?
: Propagating errors if applied toErr(_)
and unwrapping if applied toOk(_)
. Only works on theResult<T, E>
type, and written in postfix notation.
Binary operators expressions are given in terms of operator precedence.
Binary arithmetic expressions are syntactic sugar for calls to built-in traits,
defined in the std::ops
module of the std
library. This means that
arithmetic operators can be overridden for user-defined types. The default
meaning of the operators on standard types is given here.
+
: Addition and array/string concatenation. Calls theadd
method on thestd::ops::Add
trait.-
: Subtraction. Calls thesub
method on thestd::ops::Sub
trait.*
: Multiplication. Calls themul
method on thestd::ops::Mul
trait./
: Quotient. Calls thediv
method on thestd::ops::Div
trait.%
: Remainder. Calls therem
method on thestd::ops::Rem
trait.
Like the arithmetic operators, bitwise operators are
syntactic sugar for calls to methods of built-in traits. This means that
bitwise operators can be overridden for user-defined types. The default
meaning of the operators on standard types is given here. Bitwise &
, |
and
^
applied to boolean arguments are equivalent to logical &&
, ||
and !=
evaluated in non-lazy fashion.
&
: Bitwise AND. Calls thebitand
method of thestd::ops::BitAnd
trait.|
: Bitwise inclusive OR. Calls thebitor
method of thestd::ops::BitOr
trait.^
: Bitwise exclusive OR. Calls thebitxor
method of thestd::ops::BitXor
trait.<<
: Left shift. Calls theshl
method of thestd::ops::Shl
trait.>>
: Right shift (arithmetic). Calls theshr
method of thestd::ops::Shr
trait.
The operators ||
and &&
may be applied to operands of boolean type. The
||
operator denotes logical 'or', and the &&
operator denotes logical
'and'. They differ from |
and &
in that the right-hand operand is only
evaluated when the left-hand operand does not already determine the result of
the expression. That is, ||
only evaluates its right-hand operand when the
left-hand operand evaluates to false
, and &&
only when it evaluates to
true
.
Comparison operators are, like the arithmetic operators, and bitwise operators, syntactic sugar for calls to built-in traits. This means that comparison operators can be overridden for user-defined types. The default meaning of the operators on standard types is given here.
==
: Equal to. Calls theeq
method on thestd::cmp::PartialEq
trait.!=
: Unequal to. Calls thene
method on thestd::cmp::PartialEq
trait.<
: Less than. Calls thelt
method on thestd::cmp::PartialOrd
trait.>
: Greater than. Calls thegt
method on thestd::cmp::PartialOrd
trait.<=
: Less than or equal. Calls thele
method on thestd::cmp::PartialOrd
trait.>=
: Greater than or equal. Calls thege
method on thestd::cmp::PartialOrd
trait.
Parentheses are required when chaining comparison operators. For example, the
expression a == b == c
is invalid and may be written as (a == b) == c
.
A type cast expression is denoted with the binary operator as
.
Executing an as
expression casts the value on the left-hand side to the type
on the right-hand side.
An example of an as
expression:
# fn sum(values: &[f64]) -> f64 { 0.0 }
# fn len(values: &[f64]) -> i32 { 0 }
fn average(values: &[f64]) -> f64 {
let sum: f64 = sum(values);
let size: f64 = len(values) as f64;
sum / size
}
Some of the conversions which can be done through the as
operator
can also be done implicitly at various points in the program, such as
argument passing and assignment to a let
binding with an explicit
type. Implicit conversions are limited to "harmless" conversions that
do not lose information and which have minimal or no risk of
surprising side-effects on the dynamic execution semantics.
An assignment expression consists of an
lvalue expression followed
by an equals sign (=
) and an
rvalue expression.
Evaluating an assignment expression either copies or moves its right-hand operand to its left-hand operand.
# let mut x = 0;
# let y = 0;
x = y;
The +
, -
, *
, /
, %
, &
, |
, ^
, <<
, and >>
operators may be
composed with the =
operator. The expression lval OP= val
is equivalent to
lval = lval OP val
. For example, x = x + 1
may be written as x += 1
.
Any such expression always has the unit
type.
The precedence of Rust binary operators is ordered as follows, going from strong to weak:
as :
* / %
+ -
<< >>
&
^
|
== != < > <= >=
&&
||
.. ...
<-
=
Operators at the same precedence level are evaluated left-to-right. Unary operators have the same precedence level and are stronger than any of the binary operators.
An expression enclosed in parentheses evaluates to the result of the enclosed expression. Parentheses can be used to explicitly specify evaluation order within an expression.
An example of a parenthesized expression:
let x: i32 = (2 + 3) * 4;
A call expression invokes a function, providing zero or more input variables and an optional location to move the function's output into. If the function eventually returns, then the expression completes.
Some examples of call expressions:
# fn add(x: i32, y: i32) -> i32 { 0 }
let x: i32 = add(1i32, 2i32);
let pi: Result<f32, _> = "3.14".parse();
Rust treats all function calls as sugar for a more explicit, fully-qualified syntax. Upon compilation, Rust will desugar all function calls into the explicit form. Rust may sometimes require you to qualify function calls with trait, depending on the ambiguity of a call in light of in-scope items.
Note: In the past, the Rust community used the terms "Unambiguous Function Call Syntax", "Universal Function Call Syntax", or "UFCS", in documentation, issues, RFCs, and other community writings. However, the term lacks descriptive power and potentially confuses the issue at hand. We mention it here for searchability's sake.
Several situations often occur which result in ambiguities about the receiver or referent of method or associated function calls. These situations may include:
- Multiple in-scope traits define methods with the same name for the same types
- Auto-
deref
is undesirable; for example, distinguishing between methods on a smart pointer itself and the pointer's referent - Methods which take no arguments, like
default()
, and return properties of a type, likesize_of()
To resolve the ambiguity, the programmer may refer to their desired method or function using more specific paths, types, or traits.
For example,
trait Pretty {
fn print(&self);
}
trait Ugly {
fn print(&self);
}
struct Foo;
impl Pretty for Foo {
fn print(&self) {}
}
struct Bar;
impl Pretty for Bar {
fn print(&self) {}
}
impl Ugly for Bar{
fn print(&self) {}
}
fn main() {
let f = Foo;
let b = Bar;
// we can do this because we only have one item called `print` for `Foo`s
f.print();
// more explicit, and, in the case of `Foo`, not necessary
Foo::print(&f);
// if you're not into the whole brevity thing
<Foo as Pretty>::print(&f);
// b.print(); // Error: multiple 'print' found
// Bar::print(&b); // Still an error: multiple `print` found
// necessary because of in-scope items defining `print`
<Bar as Pretty>::print(&b);
}
Refer to RFC 132 for further details and motivations.
A lambda expression (sometimes called an "anonymous function expression")
defines a function and denotes it as a value, in a single expression. A lambda
expression is a pipe-symbol-delimited (|
) list of identifiers followed by an
expression.
A lambda expression denotes a function that maps a list of parameters
(ident_list
) onto the expression that follows the ident_list
. The
identifiers in the ident_list
are the parameters to the function. These
parameters' types need not be specified, as the compiler infers them from
context.
Lambda expressions are most useful when passing functions as arguments to other functions, as an abbreviation for defining and capturing a separate function.
Significantly, lambda expressions capture their environment, which regular
function definitions do not. The exact type of capture
depends on the function type inferred for the
lambda expression. In the simplest and least-expensive form (analogous to a
|| { }
expression), the lambda expression captures its environment by
reference, effectively borrowing pointers to all outer variables mentioned
inside the function. Alternately, the compiler may infer that a lambda
expression should copy or move values (depending on their type) from the
environment into the lambda expression's captured environment. A lambda can be
forced to capture its environment by moving values by prefixing it with the
move
keyword.
In this example, we define a function ten_times
that takes a higher-order
function argument, and we then call it with a lambda expression as an argument,
followed by a lambda expression that moves values from its environment.
fn ten_times<F>(f: F) where F: Fn(i32) {
for index in 0..10 {
f(index);
}
}
ten_times(|j| println!("hello, {}", j));
let word = "konnichiwa".to_owned();
ten_times(move |j| println!("{}, {}", word, j));
Rust supports three loop expressions:
- A
loop
expression denotes an infinite loop. - A
while
expression loops until a predicate is false. - A
for
expression extracts values from an iterator, looping until the iterator is empty.
All three types of loop support break
expressions,
continue
expressions, and labels.
Only loop
supports evaluation to non-trivial values.
A loop
expression repeats execution of its body continuously:
loop { println!("I live."); }
.
A loop
expression without an associated break
expression is
diverging, and doesn't
return anything. A loop
expression containing associated
break
expression(s)
may terminate, and must have type compatible with the value of the break
expression(s).
A while
loop begins by evaluating the boolean loop conditional expression.
If the loop conditional expression evaluates to true
, the loop body block
executes and control returns to the loop conditional expression. If the loop
conditional expression evaluates to false
, the while
expression completes.
An example:
let mut i = 0;
while i < 10 {
println!("hello");
i = i + 1;
}
A for
expression is a syntactic construct for looping over elements provided
by an implementation of std::iter::IntoIterator
. If the iterator yields a
value, that value is given the specified name and the body of the loop is
executed, then control returns to the head of the for
loop. If the iterator
is empty, the for
expression completes.
An example of a for
loop over the contents of an array:
let v = &["apples", "cake", "coffee"];
for text in v {
println!("I like {}.", text);
}
An example of a for loop over a series of integers:
let mut sum = 0;
for n in 1..11 {
sum += n;
}
assert_eq!(sum, 55);
A loop expression may optionally have a label. The label is written as
a lifetime preceding the loop expression, as in 'foo: loop { break 'foo; }
,
'bar: while false {}
, 'humbug: for _ in 0..0 {}
.
If a label is present, then labeled break
and continue
expressions nested
within this loop may exit out of this loop or return control to its head.
See break expressions and continue
expressions.
When break
is encountered, execution of the associated loop body is
immediately terminated, for example:
let mut last = 0;
for x in 1..100 {
if x > 12 {
break;
}
last = x;
}
assert_eq!(last, 12);
A break
expression is normally associated with the innermost loop
, for
or
while
loop enclosing the break
expression, but a label can
be used to specify which enclosing loop is affected. Example:
'outer: loop {
while true {
break 'outer;
}
}
A break
expression is only permitted in the body of a loop, and has one of
the forms break
, break 'label
or (see below)
break EXPR
or break 'label EXPR
.
When continue
is encountered, the current iteration of the associated loop
body is immediately terminated, returning control to the loop head. In
the case of a while
loop, the head is the conditional expression controlling
the loop. In the case of a for
loop, the head is the call-expression
controlling the loop.
Like break
, continue
is normally associated with the innermost enclosing
loop, but continue 'label
may be used to specify the loop affected.
A continue
expression is only permitted in the body of a loop.
When associated with a loop
, a break expression may be used to return a value
from that loop, via one of the forms break EXPR
or break 'label EXPR
, where
EXPR
is an expression whose result is returned from the loop
. For example:
let (mut a, mut b) = (1, 1);
let result = loop {
if b > 10 {
break b;
}
let c = a + b;
a = b;
b = c;
};
// first number in Fibonacci sequence over 10:
assert_eq!(result, 13);
In the case a loop
has an associated break
, it is not considered diverging,
and the loop
must have a type compatible with each break
expression.
break
without an expression is considered identical to break
with
expression ()
.
An if
expression is a conditional branch in program control. The form of an
if
expression is a condition expression, followed by a consequent block, any
number of else if
conditions and blocks, and an optional trailing else
block. The condition expressions must have type bool
. If a condition
expression evaluates to true
, the consequent block is executed and any
subsequent else if
or else
block is skipped. If a condition expression
evaluates to false
, the consequent block is skipped and any subsequent else if
condition is evaluated. If all if
and else if
conditions evaluate to
false
then any else
block is executed.
A match
expression branches on a pattern. The exact form of matching that
occurs depends on the pattern. Patterns consist of some combination of
literals, destructured arrays or enum constructors, structs and tuples,
variable binding specifications, wildcards (..
), and placeholders (_
). A
match
expression has a head expression, which is the value to compare to
the patterns. The type of the patterns must equal the type of the head
expression.
A match
behaves differently depending on whether or not the head expression
is an lvalue or an rvalue.
If the head expression is an rvalue, it is first evaluated into a temporary
location, and the resulting value is sequentially compared to the patterns in
the arms until a match is found. The first arm with a matching pattern is
chosen as the branch target of the match
, any variables bound by the pattern
are assigned to local variables in the arm's block, and control enters the
block.
When the head expression is an lvalue, the match does not allocate a temporary location (however, a by-value binding may copy or move from the lvalue). When possible, it is preferable to match on lvalues, as the lifetime of these matches inherits the lifetime of the lvalue, rather than being restricted to the inside of the match.
An example of a match
expression:
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
4 => println!("four"),
5 => println!("five"),
_ => println!("something else"),
}
Patterns that bind variables default to binding to a copy or move of the
matched value (depending on the matched value's type). This can be changed to
bind to a reference by using the ref
keyword, or to a mutable reference using
ref mut
.
Patterns can be used to destructure structs, enums, and tuples. Destructuring
breaks a value up into its component pieces. The syntax used is the same as
when creating such values. When destructing a data structure with named (but
not numbered) fields, it is allowed to write fieldname
as a shorthand for
fieldname: fieldname
. In a pattern whose head expression has a struct
,
enum
or tupl
type, a placeholder (_
) stands for a single data field,
whereas a wildcard ..
stands for all the fields of a particular variant.
# enum Message {
# Quit,
# WriteString(String),
# Move { x: i32, y: i32 },
# ChangeColor(u8, u8, u8),
# }
# let message = Message::Quit;
match message {
Message::Quit => println!("Quit"),
Message::WriteString(write) => println!("{}", &write),
Message::Move{ x, y: 0 } => println!("move {} horizontally", x),
Message::Move{ .. } => println!("other move"),
Message::ChangeColor { 0: red, 1: green, 2: _ } => {
println!("color change, red: {}, green: {}", red, green);
}
};
Patterns can also dereference pointers by using the &
, &mut
and box
symbols, as appropriate. For example, these two matches on x: &i32
are
equivalent:
# let x = &3;
let y = match *x { 0 => "zero", _ => "some" };
let z = match x { &0 => "zero", _ => "some" };
assert_eq!(y, z);
Subpatterns can also be bound to variables by the use of the syntax variable @ subpattern
. For example:
let x = 1;
match x {
e @ 1 ... 5 => println!("got a range element {}", e),
_ => println!("anything"),
}
Multiple match patterns may be joined with the |
operator. A range of values
may be specified with ...
. For example:
# let x = 2;
let message = match x {
0 | 1 => "not many",
2 ... 9 => "a few",
_ => "lots"
};
Range patterns only work on scalar types (like integers and characters; not
like arrays and structs, which have sub-components). A range pattern may not
be a sub-range of another range pattern inside the same match
.
Finally, match patterns can accept pattern guards to further refine the
criteria for matching a case. Pattern guards appear after the pattern and
consist of a bool-typed expression following the if
keyword. A pattern guard
may refer to the variables bound within the pattern they follow.
# let maybe_digit = Some(0);
# fn process_digit(i: i32) { }
# fn process_other(i: i32) { }
let message = match maybe_digit {
Some(x) if x < 10 => process_digit(x),
Some(x) => process_other(x),
None => panic!(),
};
An if let
expression is semantically identical to an if
expression but in
place of a condition expression it expects a let
statement with a refutable
pattern. If the value of the expression on the right hand side of the let
statement matches the pattern, the corresponding block will execute, otherwise
flow proceeds to the first else
block that follows.
let dish = ("Ham", "Eggs");
// this body will be skipped because the pattern is refuted
if let ("Bacon", b) = dish {
println!("Bacon is served with {}", b);
}
// this body will execute
if let ("Ham", b) = dish {
println!("Ham is served with {}", b);
}
A while let
loop is semantically identical to a while
loop but in place of
a condition expression it expects let
statement with a refutable pattern. If
the value of the expression on the right hand side of the let
statement
matches the pattern, the loop body block executes and control returns to the
pattern matching statement. Otherwise, the while expression completes.
Return expressions are denoted with the keyword return
. Evaluating a return
expression moves its argument into the designated output location for the
current function call, destroys the current function activation frame, and
transfers control to the caller frame.
An example of a return
expression:
fn max(a: i32, b: i32) -> i32 {
if a > b {
return a;
}
return b;
}