Chapter 6: Expressions
In Rust, an if statement can be used to retrun a value:
let status = if cpu.temperature <= MAX_TEMP { HttpsStatus::Ok } else { HttpsStatus::ServerError };
This explains why Rust deos not have C's ternary operator.
Control flow expression examples:
#![allow(unused)] fn main() { let a = if let Some(x) = f() { x } else { 0 }; let a = match x {Non => 0, _ => 1}; }
The operators that can be chained:
#![allow(unused)] fn main() { * / % + - << >> & ^ | && || as }
Note: The comparison operators, the assignment operators, and the range operators can't be chained at all.
Question: What is the issue with this code:
#![allow(unused)] fn main() { if preferences.changed() { page.compute_size() } }
With the semicolon missing, the block's value would be whatever page.compute_size() returns, but an if without else must alaways return ().
A let delcaration can declare a variable without initializing it. The variable can then be initialized with a later assignment. This occasinally useful, because sometimes a variable should be initialized from the middle of some sort of control flow construct:
#![allow(unused)] fn main() { let name; if user.has_nickname() { name = user.nickname(); } else { name = generate_unique_name(); user.register(&name); } }
Here there are two different ways the local variable name might be initialized, but either way it will be initialized exactly once, so name does not need to be declared mut.
match statement
In a match block, if you place a _ pattern before other patterns, it means that it will have precendence over them. Those patterns will never match anything and the compiler will warn you.
A pattern can:
- match a range of value
- unpack value
- match against individual fileds of construct
- chase preferences
- borrow parts of a value
A comma after an arm may be dropped if the expr is a block!
if let
#![allow(unused)] fn main() { if let pattern = expr { block1 } else { block2 } }
Sometimes this is a nice way to get data out of an Option or Result:
#![allow(unused)] fn main() { if let Some(cookie) = request.session.cookie { return restore_session(cookie); } }
It's never strictly necessary to use if let, because match can do everything if let can do.
Loops
There are four loops expressions:
#![allow(unused)] fn main() { while condition { block } while let pattern = expr { block } loop { block } for pattern in iterable { block } }
The value of a while or for loop is always ().
A break can have both a label and a value expression:
#![allow(unused)] fn main() { let sqrt = 'outer: loop { let n = next_numer(); for i in 1.. { let square = i * i; if square == n { // found square root break 'outer i; } if square > n { // 'n' isn't a prefect square, tru the next break; } } }; }
? operator
These two are equivalent:
#![allow(unused)] fn main() { let output = File::create(filename)?; }
#![allow(unused)] fn main() { let output = match File:create(filename) { Ok(f) => f, Err(err) => return Err(err) }; }
In the method call player.location(), player might be a Player, a reference of type &Player, or a smart pointer of type Box<Player> or Rc<Player>. The .location() method might take the player either by value or by reference. The same .location() syntax works in all cases, becuase Rust's . operator automatically derefrences player or borrow a reference to it as needed.
Type-associated funtions
Like Vec::new()
One quirk of Rust syntax is that in a function call or method call, the usual syntax for generic type, Vec<T> does not work:
#![allow(unused)] fn main() { return Vec<i32>::with_capacity(1000); let ramp = (0..n).collect<Vec<i32>>(); }
The problem is that in expressions, < is the less-than operator. You should instead use:
#![allow(unused)] fn main() { return Vec::<i32>::with_capacity(1000); let ramp = (0..n).collec::<Vec<i32>>(); }
The symbol ::<...> is affectionately known in Rust community as the turbofish. Alternatively, it is often possible to drop the type parameters and let Rust infer them:
#![allow(unused)] fn main() { return Vec::with_capacity(10); let ramp: Vec<i32> = (0..n).collect(); }
Note: It is considered good style to omit the type whenever they can be inferred!
If the value to the left of the dot is a rnference or smart pointer type, it is automatically dererenced, just as for method call.
The value to the left or the brackets is automatically derefrences.
Extracting a slice from an array or vector is straightforward:
#![allow(unused)] fn main() { let second_half = &game_moves[midpoint..end]; }
Here, game_moves may be either an array, a slice, or a vectorl; the result, regardless, is a borrowed slice of length end - midnight. game_moves is considered borrowed for the lifetime of second_half.
Unary - negates a number. It is supported for the all numeric types except unsigned integers. There is not unary + operator.
Shift operators
Bit shifting is always sign-extending on signed integers and zero-extending on unsigned integer types. Since Rust has unsigned integers, it does not need an unsigned shigt operator, like Java's >>> operator.
Unlike C, Rust doesn't support chaining assignment: you can't write a = b = 3 to assign the value 3 to both a and b.
Rust does not have C's increament and decrement operators ++ and --.
Type Cast
- Conversion to a narrower type result in truncation.
- Conversion from a floating-point to an integer type rounds towar zero. If the alue is too large to fit in the integer type, the cast produces the closest value that the integer type can represent: the value of
1e6 as u8is255. - The value of type
boolorchar, or of a C-likeenumtype, may be cast to any integer type.- Cating in the other direction is not allowed, as
bool,char, andenumtypes all have restrictions their values that would have to be enforced with run-time checks.- As an exception, a
u8may be cast to typechar, since all integers from 0 to 255 are validUnicodecode points forcharto hold.
- As an exception, a
- Cating in the other direction is not allowed, as
Conversion usually requires a cast. A few conversions involving preference tyes are so straightforward that the language performs them even without a cast. One trivial example is converting a mut reference to a non-mute reference.
Several more significant automatic conversions can happen:
- Values of type
&Stringauto-convert to type&strwithout a cast - Values of type
Vec<i32>auto-convert to type&[i32] - Value of type
&Box<Chessboard>auto-convert to&Chessboard
These are called deref coersions, because they apply to types that implement Deref built-in trait. The purpose of Deref coresion is to make smart pointers types, like Box, behave as much like the underlying value as possible.