Variables and Data types

Variable binding and Type inference

#![allow(unused)]
fn main() {
let foo = bar;
}

This line creates a new variable named foo and binds it to the value of the bar variable.

You don't need to define the type on assignment. But you must bind a value before usage. The compiler can usually infer what type we want to use based on the value and how we use it.

#![allow(unused)]
fn main() {
let str; // declare str
str = "Hello, world!"; // assignment -> &str

let a = 40_000; // -> i32
let b = 40.; // -> f64
let v: [_; 5] = [3; 5];
//      ^ infers type for usage

let guess: u32 = "42".parse().expect("Not a number!");
}

The binding can be done at a later stage.

#![allow(unused)]
fn main() {
let a;
// ... some code
a = 42;
}

Of course, you can still specify the type manually.

#![allow(unused)]
fn main() {
let x: i32 = 45;
}

Unused variables

While you are coding, you can say the compiler to not complain about a variable not being used.

#![allow(unused)]
fn main() {
let _unused_variable = 42;
}

Data types

The types covered here are all stored on the stack. We will talk about the stack and the heap on another chapter. What you need to know for now is that the stack is the fast memory.

Scalar Types

A scalar type represents a single value. Rust has four primary scalar types: integers, floating point numbers , booleans and characters.

#![allow(unused)]
fn main() {
let num: isize = 98_222;
let byt = b'A' // u8 only
let x = 2.0 // f64

let t: bool = true
}

When you are compiling in release mode, Rust does not check for integers overflow. If you want to wrap explicitly, you can use the standard library type Wrapping.

#![allow(unused)]
fn main() {
let zero = 0u8;

println!("{}", zero - 1_u8);
//             ^^^^^^^^^^^ attempt to compute `0_u8 - 1_u8`, which would overflow
}
#![allow(unused)]
fn main() {
use std::num::Wrapping;
let zero = Wrapping(0u8);
let one = Wrapping(1u8);

println!("{}", (zero - one).0); // -> 255
}

Rust’s char type is four bytes in size and represents a Unicode Scalar Value, which means it can represent a lot more than just ASCII. Accented letters; Chinese, Japanese and Korean characters; emoji; and zero-width spaces are all valid char values in Rust.

#![allow(unused)]
fn main() {
let heart_eyed_cat = '😻';
}

Compound Types

Rust has two primitive compound types: tuples and arrays.

The Tuple Type

This is a fixed length collections of values of different types. We can access a tuple element directly by using a . followed by the index of the value we want to access. The first index in a tuple is 0.

#![allow(unused)]
fn main() {
let tupl = ('x', 32); // => (char, i32)
let x = tupl.0; // => 'x'
let thirty_two = tupl.1; // => 32
}

As we will see in the "pattern" chapter, you can use pattern matching to destructure a tuple value.

#![allow(unused)]
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
}

The Array Type

Arrays in Rust have a fixed length, like tuples. Arrays are useful when you want your data allocated on the stack rather than the heap. You can access elements of an array using indexing. As for tuple, the first index is 0. If the index is greater than or equal to the length, Rust will panic because of Rust's safety principle.

#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];

let first = a[0];
}

An array is not as flexible as the vector type. A vector (Vec) is a similar collection type provided by the standard library that is allowed to grow or shrink in size.

Immutable by default

Mutability in Rust is explicit. When a variable le is immutable, once a value is bound to a name, you can’t change the value.

#![allow(unused)]
fn main() {
struct Number { value: i32 }

let foo = 32;
foo = 40; // Nope

let n = Number {
    value: 3
};
n.value = 5; // Nope
n = Number { value: 5 } // Nope
}

Let's make it mutable. The entire instance must be mutable; Rust does not allow us to mark only certain fields as mutable.

#![allow(unused)]
fn main() {
struct Number { value: i32 }

let mut foo = 32;
foo = 40; // Yep

let mut n = Number {
    value: 3
};
n.value = 5; // Yep
n = Number { value: 5 } // Yep
}

Constants

Constants are like immutable variables, but there are some differences:

  • Declared using the const keyword.
  • The type must be annotated. You can not infer it.
  • You are not allowed to use mut with constants.
  • Set only to a constant expression, not to the result of a function call or any other value that could only be computed at runtime.
#![allow(unused)]
fn main() {
const MAX_POINTS: u32 = 100_000;
}

Shadowing

Shadowing is different from marking a variable as mut, because we will get a compile-time error if we accidentally try to reassign to this variable without using the let keyword. By using let, we can perform a few transformations on a value but have the variable be immutable after those transformations have been completed.

#![allow(unused)]
fn main() {
let x = 32;
let x = 32 + 8; // still immutable!

println!("{}", x); // => 40. Only the last x will be available
}

Even with a different type.

#![allow(unused)]
fn main() {
let x = 32;
let x = "Hey";

println!("{x}"); // => "Hey"
}