Variables
Variables in Rust are declared using a let
statement. Unlike C and Java, the type is placed after the variable name:
let lucky: u32 = 62;
let distance: f64 = 13.1;
let is_overdue: bool = true;
let outcome: char = 'w';
In C and Java, redeclaring a variable in the same block is a semantic error. For example, this C program fails to compile:
int lucky = 62;
int lucky = 7; // uh oh
But Rust accepts multiple declarations in the same block. A later declaration shadows any earlier declaration:
let lucky: u32 = 62;
let lucky: u32 = 7;
let lucky: f64 = 7.0;
println("{}", lucky); // prints 7.0
Later declarations can even use different types. The earlier declaration goes out of scope just as the later declaration comes in.
As soon as we start naming data, we encounter many language design questions. What types are available? May types be implicit? Can variables change over time? Are global variables supported? Let's explore how Rust answers these questions.
Primitive Types
Most of Rust's integer primitive types explicitly state their number of bits. The explicit unsigned integer types are u8
, u16
, u32
, u64
, and u128
. The explicit signed integer types are i8
, i16
, i32
, i64
, and i128
. In C, the size of an int
depends on the machine on which the code is running. If a file format or a network protocol expects a 4-byte integer, we should avoid int
if we want portable code and instead use types like uint32_t
. Rust makes these fixed-width types the norm—except when indexing.
If an integer is going to be used as an index into a collection in Rust's standard library, then its type will likely be usize
. This type is the host machine's favored integer size. On a 64-bit machine, it will be 64 bits. On a 32-bit machine, it will be 32 bits.
The floating point types are f32
and f64
. Modern CPUs process double-precision numbers nearly as fast as single-precision, so f64
is commonly used.
The Rust compiler rejects implicit coercions from one type to another. The assignment to floating-point y
in this code fails because x
is an integer:
let x: u32 = 13;
let y: f64 = x; // implicit coercion; compile error
let z: f64 = x as f64; // explicit cast; compiles
The assignment to z
succeeds because it explicitly casts the integer using the as
casting operator.
The boolean type is bool
. A bool
value can be cast to integers 0 or 1 using as
. Unlike C, the inverse is prohibited: an integer cannot be cast to a bool
either implicitly or explicitly.
Type Inference
If we leave off the type of a variable declaration, the compiler attempts to infer the type from the expression on the right-hand side or some later initialization. These implicit declaractions are equivalent to the earlier explicit ones:
let lucky = 62;
let distance = 13.1;
let is_overdue = true;
let outcome = 'w';
Sometimes we want to know what type is being inferred. In Haskell, we used :t
in GHCI to query the type of an expression. Rust doesn't currently have a REPL like GHCI. However, we can get the type of an expression using the type_name_of_val
function:
use std::any::type_name_of_val;
fn main() {
let distance = 13.1;
println!("{}", type_name_of_val(&distance)); // f64
}
The use
statement imports the function from its module std::any
so it can be called without qualification.
Mutability
By default, names cannot be reassigned to other values. This code is illegal:
let halflife = 831.7;
halflife /= 2.0; // compile error
If we wish to reassign a variable, we must explicitly opt in to mutability with the mut
modifier:
let mut halflife = 831.7;
halflife /= 2.0;
Earlier we saw how const
in JavaScript and final
in Java lock down only the variable name from being reassigned, while a non-primitive value to which the name is bound may still be mutated. In Rust, neither the name nor the value may change. This code fails not because of a reassignment but because it calls a mutating method:
let groceries = Vec::new();
groceries.push("za'atar"); // compile error
Qualifying groceries
with mut
makes the push
call legal.
Const
Rust also has a const
qualifier for declaring constants. Surely const
is unnecessary if variables are immutable by default? But const
has different semantics than it does in other languages. A const
must satisfy two semantic constraints:
-
A
const
name may not be reassigned and its value is immutable. -
A
const
value must be computable at compile time.
The first constraint makes a const
variable like any variable that lacks a mut
qualifier. The second constraint means that we can't assign a value that relies on dynamic data like user input or a value returned from a non-pure function. This program attempts to generate a const
random byte:
use rand::prelude::random;
fn main() {
const MAGIC_BYTE: u8 = random::<u8>(); // compile error
}
But it fails. The random
function is not pure. This assignment does compile:
fn main() {
const MAGIC_BYTE: u8 = 255 / 3 + 7;
}
The value is statically deterministic, which is very far from random.
A constant's declaration must have an explicit type. Its identifier should be written in SCREAMING_SNAKE_CASE
. This practice is more than a convention. If we don't use capitals, the Rust compiler issues a warning.
Globals
Mutable global data has a track record of not being safe. Functions that rely on global data are impure and difficult to test. Concurrent accessors may put the data in an inconsistent state. There's no clear order in which global data from different compilation units is initialized, which can introduce non-determinism into our program. Nobody likes non-determinism.
Safety is a major concern of the Rust developers. Therefore, they have outlawed global variables, both mutable and immutable. We cannot use let mut
or let
to declare a variable at a global scope. However, we may declare global const
data.