Macros are procedures that expand and generate raw source code before the rustc
compiler begins its compilation step. In this example, the #[derive()]
macro will generate all the source code necessary for Wow
to be able to print debug out.
// Attributes are macros.#[derive(Debug)]struct Wow;let wow = Wow;// Calls ending with `!` are macrosprintln!("{wow:?} that is convenient!");
println!()
print!()
and println!()
internally call the format!()
macro while also printing the formatted String to stdout. Printing to stderr is accomplished with eprint!()
and eprintln!()
.
let jungle_bird = "Macaw";let sound = "caws";print!("The {jungle_bird}"); // Does not print a newlineprintln!(" {sound}."); // Prints a newline
To assign variables in Rust we utilize the let
keyword with the =
operator. We can assign variables to any expression taking the following form:
let variable = "this is a &str";
// This is a closurelet double = |d| d * 2;// This is the outcome of calling the closurelet var = double(10);// This will re-assign the value of varlet doubled_var = var;
We can manually annotate types by providing a type signature. Type signatures are declared with : Type
placed after the variable name and before the assigning =
.
let small_integer: u16 = 28;fn double(num: u128) -> u128 {num * 2}let unsigned_int: u8 = 28;
We can assign a new value to the same variable name within the same scope. This is called shadowing and does not alter the original assignment.
let favorite = "orange";println!("{favorite}");let favorite = "cerulean";println!("{favorite}");let favorite = "yellow";println!("{favorite}");
When we need to access the underlying data a reference points to directly, we can dereference with the *
prefix.
let mut year = 3020;let y = &mut year;*y + 10;println!("The year is {year}.");
A reference to a range of elements from a collection is called a slice. To take a slice, we use index expressions on a referenced collection.
let s = String::from("hello world");let hello = &s[0..5];let world = &s[6..11];println!("{hello}{world}");
Every time we declare a value with let
, we are creating data that is stored in memory. We can then create a reference to that data by prefixing our expression with the &
prefix.
// In this example, funny_number is a reference to pilet pi = 3.14159265359;let funny_number = πprintln!("{funny_number}");// We can also create references to referenceslet lightspeed = 299792458;let fast = &lightspeed;let still_fast = &&lightspeed;let speed_of_light = &still_fast; // This is equivalent to &&&lightspeed
Attributes in Rust are macros that allow us to do special things with the language such as set compilation options, conditionally compile pieces of code, ignore lints, and denote tests and benchmarks. Attributes can be declared inside the scope of the item they are being applied to, known as inner attributes, or before the item they are being applied to, known as outer attributes.
Here are some common input patterns you will find in attribute macros.
// Named.#[no_std]// Named with value.#[must_use = "This function should be used."]// Named with list of identifiers or paths.#[forbid(unsafe, warnings)]// Named with list of key values.#[cfg_attr(target_os = "linux", path = "os/linux.rs")]
We can define a module using the mod
keyword. A module has its own distinct scope and visibility.
// Here we have defined a module named cake with an is_favorite() function inside itmod cake {pub fn is_favorite(name: &str) -> bool {name == "Coconut"}}
Once we have declared a module, its content can be accessed by utilizing path syntax. A path is created by chaining any number of nested modules together with the ::
operator.
Mutability is the capability of a variable’s value to be altered in memory. Immutability means that once a variable is declared with let
, its value cannot change. In Rust, all variables are immutable by default.
mut
To alter a value, we must explicitly declare it as mutable with the mut
keyword. Once we have declared a variable as mutable, we can reassign the value with the =
operator.
// Immutablelet three = 3;// Mutablelet mut any_number = 20;// Reassign explicitlyany_number = 22;// Increment and decrementany_number += 1;any_number -= 6;
Constants always require a type declaration and only types with a known size at compile time can be declared as a constant.
const ANIMAL: &str = "penguin";// String does not have a known size, so it cannot be used as a constant// The below code will not compile.const OOPS: String = String::from("sorry");
In Rust, code can be separated into blocks. A block of code is a collection of statements and an optional expression contained within {}
.
// Statement block{let number_1 = 11;let number_2 = 31;let sum = number_1 + number_2;println!("{sum}");}// Expression block{let number_1 = 11;let number_2 = 31;number_1 + number_2}
Scope is the concept of whether or not a particular item exists in memory and is accessible at a certain location in our codebase. In Rust, the scope of any particular item is limited to the block it is contained in.
We can make an item accessible outside of its normal scope by denoting it as public with the pub
keyword. All items in Rust are private by default. Private items can only be accessed within their declared module and any children modules.
In Rust, A Statement is a segment of code that does not return any value. Statements are easy to identify as they end with a semicolon (;
) to denote that nothing is returned.
// This is a statement, but we cannot access its value"purple";
An Expression is a segment of code that returns a value. Since Rust is an ‘expression-oriented’ language, all code blocks will implicitly return their value unless we utilize a semicolon to terminate the expression.
// This is an expression, it will return the &str "green""green"
Rust’s syntax also allows Patterns, which are special syntax rules that can be used in certain situations. Patterns help to make the language more readable and allow us to do things that are otherwise not easily accomplished. A comprehensive list of patterns can be found in the Rust Reference.