Variables and Types in Rust
About This Module
This module covers Rust's type system and variable handling, including immutability by default, variable shadowing, numeric types, boolean operations, characters, and strings. Understanding these fundamentals is essential for all Rust programming.
Prework
Prework Readings
Read the following sections from "The Rust Programming Language" book:
Pre-lecture Reflections
Before class, consider these questions:
- Why might immutable variables by default be beneficial for programming?
- What is the difference between variable shadowing and mutability?
- How do strongly typed languages like Rust prevent certain classes of bugs?
- What are the trade-offs between different integer sizes?
- Why might string handling be more complex than it initially appears?
Learning Objectives
By the end of this module, you should be able to:
- Understand Rust's immutability-by-default principle
- Use mutable variables when necessary
- Apply variable shadowing appropriately
- Choose appropriate numeric types for different use cases
- Work with boolean and bitwise operations
- Handle characters and strings properly in Rust
- Understand type conversion and casting in Rust
Variables are by default immutable!
Take a look at the following code.
Note: we'll use a red border to indicate that the code is expected to fail compilation.
#![allow(unused)] fn main() { let x = 3; x = x + 1; // <== error here }
Run it and you should get the following error.
Compiling playground v0.0.1 (/playground)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:1
|
3 | let x = 3;
| - first assignment to `x`
4 | x = x + 1; // <== error here
| ^^^^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
3 | let mut x = 3;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `playground` (bin "playground") due to 1 previous error
The Rust compiler errors are quite helpful!
Use mut to make them mutable
#![allow(unused)] fn main() { // mutable variable let mut x = 3; x = x + 1; println!("x = {}", x); }
Assigning a different type to a mutable variable
What happens if you try to assign a different type to a mutable variable?
#![allow(unused)] fn main() { // mutable variable let mut x = 3; x = x + 1; println!("x = {}", x); x = 9.5; // what happens here?? println!("x = {}", x); }
Again, the Rust compiler error message is quite helpful!
Variable Shadowing
You can create a new variable with the same name as a previous variable!
fn main() { let solution = "4"; // This is a string // Create a new variable with same name and convert string to integer let solution : i32 = solution.parse() .expect("Not a number!"); // Create a third variable with the same name! let solution = solution * (solution - 1) / 2; println!("solution = {}",solution); // Create a fourth variable with the same name! let solution = "This is a string"; println!("solution = {}", solution); }
In this example, you can't get back to the original variable, although it stays in memory until it goes of out scope.
Question: why would you want to do this?
Question: Can you use
mutand avoid variable shadowing? Try it above.
Variable Shadowing and Scopes
Rust automatically deallocates variables when they go out of scope, such as when a program ends.
You can also use a block (bounded by {}) to limit the scope of a variable.
#![allow(unused)] fn main() { let x = 1; { // start of block scope let x = 2; // shadows outer x println!("{}", x); // prints `2` } // end of block scope println!("{}", x); // prints `1` again — outer `x` visible }
Basic Types: unsigned integers
unsigned integers: u8, u16, u32, u64, u128
usizeis the default unsigned integer size for your architecture
The number, e.g. 8, represents the number of bits in the type and the maximum value.
- So unsigned integers range from to .
| Unsigned Integer | Unsigned 8 bit binary |
|---|---|
| 0 | 00000000 |
| 1 | 00000001 |
| 2 | 00000010 |
| 3 | 00000011 |
Here's how you convert from binary to decimal.
Basic Types: unsigned integers - min and max values
Rust lets us print the minimum and maximum values of each type.
#![allow(unused)] fn main() { println!("U8 min is {} max is {}", u8::MIN, u8::MAX); println!("U16 min is {} max is {}", u16::MIN, u16::MAX); println!("U32 min is {} max is {}", u32::MIN, u32::MAX); println!("U64 min is {} max is {}", u64::MIN, u64::MAX); println!("U128 min is {} max is {}", u128::MIN, u128::MAX); println!("USIZE min is {} max is {}", usize::MIN, usize::MAX); }
Verify u8::MAX on your own.
Question: What is the
usizeon your machine?
Basic Types: signed integers
Similarly, there are these signed integer types.
signed integers: i8, i16, i32 (default), i64, i128,
isize is the default signed integer size for your architecture
- from to
Unsigned integers - min and max values
#![allow(unused)] fn main() { println!("I8 min is {} max is {}", i8::MIN, i8::MAX); println!("I16 min is {} max is {}", i16::MIN, i16::MAX); println!("I32 min is {} max is {}", i32::MIN, i32::MAX); println!("I64 min is {} max is {}", i64::MIN, i64::MAX); println!("I128 min is {} max is {}", i128::MIN, i128::MAX); println!("ISIZE min is {} max is {}", isize::MIN, isize::MAX); }
Signed integer representation
Signed integers are stored in two's complement format.
- if the number is positive, the first bit is 0
- if the number is negative, the first bit is 1
| Signed Integer | Signed 8 bit binary |
|---|---|
| 0 | 00000000 |
| 1 | 00000001 |
| 2 | 00000010 |
| 3 | 00000011 |
| -1 | 11111111 |
| -2 | 11111110 |
| -3 | 11111101 |
Here's how you convert from binary to decimal.
If the first bit is 0, the number is positive. If the first bit is 1, the number is negative.
To convert a negative number to decimal:
- take the sign of the first bit,
- flip all the bits and add 1 (only for negative numbers!)
Exercise: Try that for -1
Converting between signed and unsigned integers
If you need to convert, use the as operator:
#![allow(unused)] fn main() { let x: i8 = -1; let y: u8 = x as u8; println!("{}", y); }
Question: Can you explain the answer?
Why do we need ginormous i128 and u128?
They are useful for cryptography.
Don't use datatype sizes larger than you need.
Larger than architecture default generally takes more time.
i64 math operations might be twice as slow as i32 math.
Number literals
Rust lets us write number literals in a few different ways.
| Number literals | Example |
|---|---|
| Decimal (base 10) | 98_222 |
| Hex (base 16) | 0xff |
| Octal (base 8) | 0o77 |
| Binary (base 2) | 0b1111_0000 |
| Byte (u8 only) | b'A' |
#![allow(unused)] fn main() { let s1 = 2_55_i32; let s2 = 0xff; let s3 = 0o3_77; let s4 = 0b1111_1111; // print in decimal format println!("{} {} {} {}", s1, s2, s3, s4); // print in different bases println!("{} 0x{:X} 0o{:o} 0b{:b}", s1, s2, s3, s4); }
Be careful with math on ints
fn main() { let x : i16 = 13; let y : i32 = -17; // won't work without the conversion println!("{}", x * y); // will not work //println!("{}", (x as i32)* y); // this will work }
Basic Types: floats
There are two kinds: f32 and f64
What do these mean?
- This is the number of bits used in each type
- more complicated representation than ints (see wikipedia)
- There is talk about adding f128 to the language but it is not as useful as u128/i128.
fn main() { let x = 4.0; println!("x is of type {}", std::any::type_name_of_val(&x)); let z = 1.25; println!("z is of type {}", std::any::type_name_of_val(&z)); println!("{:.1}", x * z); }
Exercise: Try changing the type of
xtof32and see what happens:let x:f32 = 4.0;
Floats gotchas
Be careful with mixing f32 and f64 types.
You can't mix them without converting.
fn main() { let x:f32 = 4.0; println!("x is of type {}", std::any::type_name_of_val(&x)); let z:f64 = 1.25; println!("z is of type {}", std::any::type_name_of_val(&z)); println!("{:.1}", x * z); //println!("{:.1}", (x as f64) * z); // this will work }
Floats: min and max values
Rust lets us print the minimum and maximum values of each type.
#![allow(unused)] fn main() { println!("F32 min is {} max is {}", f32::MIN, f32::MAX); println!("F32 min is {:e} max is {:e}\n", f32::MIN, f32::MAX); println!("F64 min is {:e} max is {:e}", f64::MIN, f64::MAX); }
Exercise -- Integers and Floats
Create a program that:
- creates a
u8variablenwith value 77 - creates an
f32variablexwith value 1.25 - prints both numbers
- multiplies them and puts the results in an
f64variableresult - prints the result
Example output:
77
1.25
77 * 1.25 = 96.25
Get your code working here (or in your own editors) and then paste the result in Gradescope.
fn main() { }
More Basic Types
Let's look at:
- Booleans
- Characters
- Strings
Logical operators and bool
booluses one byte of memory
Question: Why is
boolone byte when all we need is one bit?
We can do logical operations on booleans.
#![allow(unused)] fn main() { let x = true; println!("x uses {} bits", std::mem::size_of_val(&x) * 8); let y: bool = false; println!("y uses {} bits\n", std::mem::size_of_val(&y) * 8); println!("{}", x && y); // logical and println!("{}", x || y); // logical or println!("{}", !y); // logical not }
Bitwise operators
There are also bitwise operators that look similar to logical operators but work on integers:
#![allow(unused)] fn main() { let x = 10; let y = 7; println!("{x:04b} & {y:04b} = {:04b}", x & y); // bitwise and println!("{x:04b} | {y:04b} = {:04b}", x | y); // bitwise or println!("!{y:04b} = {:04b} or {0}", !y); // bitwise not }
Bitwise 'not' and signed integers
#![allow(unused)] fn main() { let y = 7; println!("!{y:04b} = {:04b} or {0}", !y); // bitwise not }
What's going on with that last line?
y is I32, so let's display all 32 bits.
#![allow(unused)] fn main() { let y = 7; println!("{:032b}", y); }
So when we do !y we get the bitwise negation of y.
#![allow(unused)] fn main() { let y = 7; println!("{:032b}", !y); }
It's still interpreted as a signed integer.
#![allow(unused)] fn main() { let y = 7; println!("{}", !y); }
Bitwise Operators on Booleans?
It's a little sloppy but it works.
#![allow(unused)] fn main() { let x = true; println!("x is of type {}", std::any::type_name_of_val(&x)); println!("x uses {} bits", std::mem::size_of_val(&x) * 8); let y: bool = false; println!("y uses {} bits\n", std::mem::size_of_val(&y) * 8); // x and (not y) println!("{}", x & y); // bitwise and println!("{}", x | y); // bitwise or println!("{}", x ^ y); // bitwise xor }
Exercise -- Bitwise Operators on Integers
Create a program that:
- Creates an unsigned int
xwith value 12 and a signed intywith value -5 - Prints both numbers in binary format (use {:08b} for 8-bit display)
- Performs bitwise AND (&) and prints the result in binary
- Performs bitwise OR (|) and prints the result in binary
- Performs bitwise NOT (!) on both numbers and prints the results
Example output:
12: 00001100
-5: 11111011
12 & -5: 00001000
12 | -5: 11111101
!12: 11110011
!-5: 00000100
fn main() { }
Characters
chardefined via single quote, uses four bytes of memory (Unicode scalar value)- For a complete list of UTF-8 characters check https://www.fileformat.info/info/charset/UTF-8/list.htm
Note that on Mac, you can insert an emoji by typing
Control-Command-Spaceand then typing the emoji name, e.g. 😜.
On Windows, you can insert an emoji by typing
Windows-Key + .orWindows-Key + ;and then typing the emoji name, e.g. 😜.
#![allow(unused)] fn main() { let x: char = 'a'; println!("x is of type {}", std::any::type_name_of_val(&x)); println!("x uses {} bits", std::mem::size_of_val(&x) * 8); let y = '🚦'; println!("y is of type {}", std::any::type_name_of_val(&y)); println!("y uses {} bits", std::mem::size_of_val(&y) * 8); let z = '🦕'; println!("z is of type {}", std::any::type_name_of_val(&z)); println!("z uses {} bits", std::mem::size_of_val(&z) * 8); println!("{} {} {}", x, y, z); }
Strings and String Slices (&str)
In Rust, strings are not primitive types, but rather complex types built on top of other types.
String slices are immutable references to string data.
-
Stringis a growable, heap-allocated data structure -
&stris an immutable reference to a string slice -
Stringis a wrapper aroundVec<u8>(More onVeclater) -
&stris a wrapper around&[u8] -
string slice defined via double quotes (not so basic actually!)
String and string slice examples
fn main() { let s1 = "Hello! How are you, 🦕?"; // type is immutable borrowed reference to a string slice: `&str` let s2 : &str = "Καλημέρα από την Βοστώνη και την DS210"; // here we make the type explicit println!("{}", s1); println!("{}\n", s2); }
String and string slice examples
We have to explicitly convert a string slice to a string.
fn main() { // This doesn't work. You can't do String = &str let s3: String = "Does this work?"; // <== error here let s3: String = "Does this work?".to_string(); println!("{}", s3); }
Comment out the error lines and run the code to see what happens.
String and string slice examples
We can't index directly into a string slice, because it is a complex data structure.
Different characters can take up different numbers of bytes in UTF-8.
fn main() { let s4: String = String::from("How about this?"); println!("{}\n", s4); let s5: &str = &s3; println!("str reference to a String reference: {}\n", s5); // This won't work. You can't index directly into a string slice. Why??? println!("{}", s1[3]); // <== error here println!("{}", s2[3]); // <== error here // But you can index this way. println!("4th character of s1: {}", s1.chars().nth(3).unwrap()); println!("4th character of s2: {}", s2.chars().nth(3).unwrap()); println!("3rd character of s4: {}", s4.chars().nth(2).unwrap()); }
Comment out the error lines and run the code to see what happens.
Exercise -- String Slices
Create a program that:
- Creates a string slice containing your name
- Converts it to a String
- Gets the third character of your name using the
.chars().nth()method - Prints both the full name and the third character
Example output if your name is "Alice":
Alice
i
fn main() { }
Recap
- Variables are by default immutable
- Use
mutto make them mutable - Variable shadowing is a way to reuse the same name for a new variable
- Booleans are one byte of memory
- Bitwise operators work on integers
- Characters are four bytes of memory
- Strings are complex data structures
- String slices are immutable references to string data