Codecademy Logo

Types

Print Cheatsheet

What are Primitives?

Everything in Rust has a type. Some types are so commonly used in programming that they have been baked directly into the language itself. These basic types are called primitives and they do not rely upon the std library.

Boolean Primitives

The bool primitive is a boolean value, which can be either false or true.

let this_is_a_variable = true;
// A bool is its own conditional.
if this_is_a_variable {
println!("yep, its a variable");
} else {
println!("wrong universe");
}

Integer Primitives

Integer types that begin with an i are signed and allow negative integers, while unsigned integers begin with a u. For example, a u8 is an integer that can represent any number from 0 to 255.

Available integer primitives include:

  • Signed integers: i8, i16, i32, i64, i128, isize
  • Unsigned integers: u8, u16, u32, u64, u128, usize

usize and isize will determine the memory size based on the system architecture during compilation. When no type is specified, Rust infers the type to be i32.

let integer = 102;
let negative = -48;
// We can type annotate our integers to make sure we can handle large numbers.
let byte: u8 = 255;
let large: i64 = -9223372036854775808;
// We can also specify types as a postfix to the number.
let byte = 255u8;
let large = -9223372036854775808i64;

Floating Point Primitives

Rust has two floating point types of differing precision, f32 and f64, which are also denoted by their bit size in memory. When no type is specified, Rust infers f64. Floating point numbers are specified by their inclusion of a decimal point.

let float = 1.2;
// Floats do not require a decimal portion.
let still_a_float = 1.;
// With type annotation.
let a_less_precise_float: f32 = 2.83;

Char Primitives

A char is a character, or more specifically, a unicode scalar value. They are specified by enclosing the character within single parentheses.

Since Rust utilizes UTF-8 as an encoding scheme, some single-width characters may be composed of multiple chars.

let letter = 'q';
let accented = 'á';
let percent = '%';
let checkmark = '✓';
// This emoji is more than 1 char in length and cannot be declared as a char literal.
// We must instead use a &str.
let yourself = "😀";

Declaring and Initializing Arrays

We can declare an array with square brackets, [], containing comma-separated values. We can initialize the values of an array from an expression rather than manually defining each value. This takes the form of [expression; length] and is useful when creating large arrays.

let integers = [1, 2, 3];
// This will create an array with 20 chars of value 'e'
let many_e = ['e'; 20];
// We can use any expression to populate the default value.
let initial_value = 'E';
let more_e = [initial_value.to_ascii_lowercase(); 20];

Accessing Array Values

We can access values of collections using a special syntax referred to as index expression syntax. This allows us to directly access a single value or a range of values by index. If we try to access an index that does not exist, our code will not compile.

We can access a single value of a collection by placing the index of the desired item within square brackets after the collection, collection[3]. It is important to remember that array indexing in Rust, as with most languages, is 0-based.

let array_of_chars = ['a', 'b', 'c'];
let letter_a = array_of_chars[0]; // 'a'
let letter_c = array_of_chars[2]; // 'c'
// Rather than directly supplying a number, we can utilize any expression which evaluates to the type `usize`.
fn one() -> usize { 1 }
let letter_b = array_of_chars[one()]; // 'b'

Arrays and Ranges

We can access a range of values from a collection by supplying a beginning and ending index separated by .. within our index expression. This special syntax is referred to as range syntax. The beginning index is inclusive, and the ending index is exclusive. If we omit the beginning or ending index, the range will continue until the respective beginning or end.

let words = ["rise", "sun", "ship", "to", "sail"];
let sunship = &words[1..3]; // ["sun", "ship"]
println!("{sunship:?}");
let head = &words[..2]; // ["rise", "sun"]
println!("{head:?}");
let tail = &words[2..]; // ["ship", "to", "sail"]
println!("{tail:?}");
let everything = &words[..]; // ["rise", "sun", "ship", "to", "sail"];
println!("{everything:?}");

What is Vec?

When we need a dynamically sized collection, the std library provides us the Vec<T> type. Vec, commonly called a “Vector,” stores its data in the heap, which allows it to grow or shrink in size. We can construct Vecs using methods such as new() and from(), but Rust also provides us the vec![] macro to allow us to declare a Vec the same way we would an array.

// Initialize a new, empty Vec
let new_vec: Vec<char> = vec![];
// Initialize with values
let vec_of_chars = vec!['a', 'b', 'c'];
// This is equivalent to the previous example, but utilizing methods
let mut vec_of_chars = Vec::new();
vec_of_chars.push('a');
vec_of_chars.push('b');
vec_of_chars.push('c');

Accessing Vec Values

Like arrays, we can use index expressions to access the values of a Vec. Unlike an array, if we try to access an index that does not exist, our code will successfully compile but will panic at runtime. To avoid this, accessing the values on a Vec is generally accomplished through methods such as get() and first(). These methods return an Option<T> which allows us to handle situations where the index does not exist.

let vec_of_chars = vec!['a', 'b', 'c'];
let a = vec_of_chars.first(); // Some('a')
// Note that the `get()` method does not use 0-based indexing
let c = vec_of_chars.get(2); // Some('c')
let f = vec_of_chars.get(9); // None

What are Strings?

A String is stored on the heap which allows us to mutate the value at will. While heap memory is never as fast as the stack, the heap allows Rust to automatically resize the allocated memory when needed during runtime.

let empty_string = String::new();
let value_string = String::from("Cyan");
let mut mutable_string = String::from("Indigo");
mutable_string.push_str(" and Maroon.");
println!("{mutable_string}");

What are &strs?

&strs are immutable slices of bytes of a fixed size and are often referred to as “string slices.” Since &strs are only a reference to data, our type declarations will always have the preceding &. Their data is stored on the stack which makes them computationally efficient.

let immutable = "I am a location in memory";
// A type annotated str.
let value: &str = ", and cannot be mutated.";
// A type annotated str with an explicit lifetime.
let baked: &'static str = "I am baked into the binary!";
println!("{immutable} {value}");
println!("{baked}");

Concatenation

Strings are very ergonomic in terms of how we can compose them. Here are some examples of concatenation in Rust.

let a = "Welcome";
let b = " to";
let c = " the";
let d = " show!";
let welcome1 = format!("{a}{b}{c}{d}!");
let welcome2 = [a,b,c,d,"!"].concat();
let welcome3 = a.to_string() + b + c + d + "!";
// We can even append to a mutable String with the `+=` operator.
let mut welcome4 = welcome3;
welcome4 += " Lets have some fun!";

String Conversions

We can turn a &str into a String using the String::from() or to_string() methods. Both are commonly utilized in Rust and using one over the other is a matter of personal preference.

To utilize a String as a &str, we only have to reference the value.

// Converting from &str to String
let pineapple = "pineapple";
let new_string = String::from(pineapple);
let new_string = pineapple.to_string();
// Converting from String to &str
fn say_fruit(name: &str) {
println!("{name}")
}
let papaya = String::from("Papaya");
say_fruit(&papaya);

Declaring Tuples

We can declare a tuple by placing our data within parenthesis, ().

// Here we have a tuple of type (char, i32, str).
let awesome = ('q', 38, "this is great");

Accessing Tuple Fields

We can access the fields of a tuple by tacking on a . and then providing the respective field’s index, which is referred to as dot notation. Rust uses 0-based indexing, which means that our first field is accessed with .0.

let cat = ("Meowzer", 10, true);
let name = cat.0; // "Meowzer"
let age = cat.1; // 10
let awake = cat.2; // true

Destructuring Tuples

We can destructure a tuple anywhere the Rust syntax allows it. Take a look at the accompanying code for examples.

// This function will return our tuple.
fn get_cat() -> (&'static str, f64, bool) {
("Meowzer", 2.8, true)
}
// Destructuring within the variable assignment.
let (name, cuteness, is_sleeping) = get_cat();
println!("This is our cat {name}.");
// We can ignore unused fields with `_`
let (name, _, _) = get_cat();
println!("This is our cat {name}.");
// Destructuring within a match expression:
match get_cat() {
(name, _, is_sleeping) => {
if is_sleeping {
println!("{name} is sleeping.");
} else {
println!("{name} is awake");
}
}
// We can ignore a range of fields with `..`
(name, ..) => {
println!("{name} is cute when asleep or awake.");
}
}

Unit Types

A tuple that does not contain any fields, (), is its own primitive type in Rust called the unit type. We can think of the unit type as a piece of data without actual data. Its only value is its existence.

This type helps provides a safe way to handle certain situations while avoiding the pitfalls of a “null” type.

let this_variable_exists = ();

Declaring structs

To declare a struct, we utilize the struct keyword. Fields are then provided within its declaration block, follow the ‘snake_case’ naming convention, and must be type annotated.

struct Guest {
last_name: &'static str,
rsvp: bool,
costume: bool,
}

Instantiating structs

When we instantiate a struct, we must provide values for every field. If a field is missing, our code will not compile.

When instantiating a struct’s fields from variables, we can use a shorthand syntax if the variable name is the same as the field.

let guest = Guest {
name: "Anand",
rsvp: true,
costume: false,
};
// We can declare the field and value simultaneously by first creating variables:
let name = "Anand";
let rsvp = true;
let costume = false;
let guest = Guest {
name,
rsvp,
costume,
};

impl Blocks

When we create a struct, we must define our own custom data type. Declaring a type allows us to make an impl block to create specialized functions specific to that struct. Functions declared within an impl block are called methods. Methods can utilize the special input parameter self to operate directly on the type being implemented. mut self and &mut self are also available for accessing the type mutably.

impl Guest {
pub fn print_name(&self) {
println!("{}", self.name);
}
pub fn wearing_costume(&mut self) {
self.costume = true;
}
}
// We can call methods using dot notation
guest1.print_name();
// We can also supply the implemented type as a parameter
Guest::print_name(&guest1);

Declaring enums

To create an enumeration, often called an enum, we utilize the enum keyword. Every variant for our enum is placed within its declaration block. Variants follow the ‘PascalCase’ naming convention. We create a value of a specific variant using the :: operator.

enum InnerPlanet {
Mercury,
Venus,
Earth,
Mars,
}
let home = InnerPlanet::Earth;

Matching enums

Since an enum‘s value can only be a single variant, matching on an enum is a very valuable programming pattern in Rust.

When matching on an enum, all variants must be handled. If we remove one of the match arms in the previous example, our code will not compile. We can use the _ catch-all operator to handle the remaining unspecified variants.

let vacation_location = InnerPlanet::Mercury;
match vacation_location {
InnerPlanet::Mercury => println!("Bring sun protection."),
InnerPlanet::Venus => println!("Quite blue."),
InnerPlanet::Earth => println!("Lots to see here."),
InnerPlanet::Mars => {
println!("Brrr...");
println!("Bring a coat!");
}
}
// Using _, catch-all operator
match vacation_location {
InnerPlanet::Earth => println!("Lots to see here."),
_ => println!("Lets clean up Earth first..."),
}

Variant Values

Enum variants can also contain values. This is possible because enum variants in Rust are structs. We can access a variant’s inner data by destructuring.

enum Meal {
Pasta, // Unit struct
StirFry(Vec<String>), // Tuple struct
Burrito { // Struct with named fields
beans: bool,
rice: bool,
},
}
// Destructuring
let dinner = Meal::Burrito {
beans: true,
rice: false,
};
match dinner {
Meal::Pasta => println!("Too heavy."),
Meal::StirFry(veggies) => {
println!("{veggies:?}")
}
Meal::Burrito { beans, rice } => {
println!("with beans: {beans}");
println!("with rice: {rice}")
}
}