Welcome to my exploration of Rust, a programming language that's swiftly gaining attention in the tech community! 🚀 As a developer constantly seeking to expand my skills, I embarked on a journey to understand why Rust is becoming a must-know language. This article isn't just a guide; it's a personal roadmap and a collection of notes crafted for my future reference. However, I believe my journey can offer valuable insights for anyone curious about Rust.
Why Rust?
Rust stands out for its performance, safety, and concurrency capabilities, making it an attractive choice for system-level programming. It's not just me saying this; check out Rust's growing popularity in the Stack Overflow Developer Survey.
Why Should You Learn Rust?
If you're someone who's intrigued by the challenge of mastering a language that prioritizes safety and speed, Rust is for you. It's particularly beneficial for those looking to delve into system programming, web assembly, and even game development.
Why Am I Writing This?
As someone who values continuous learning, I've turned my Rust learning journey into this article. Think of it as a series of detailed notes written in a conversational tone, making it easier for both you and me to revisit and absorb complex concepts.
Whether you're here to pick up a new language or just curious about what Rust has to offer, I hope my notes will guide and inspire you on your learning path. Let's dive into the world of Rust together!
Installing Rust
Rust up
On MacOs or Linux terminal run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh.
.
When prompted:
.
Select 1
and press return.
Visual Studio Code Recommendations
I recommend the following:
- Error Lens
- Rust Extension Pack by Swellaby
- Rust Syntax by Dusty Pomerleau
- Rust-Analyzer by the Rust Programming Language
- Rust by 1YiB
Creating Hello World
Run cargo new hello_world
Run cd hello_world
Run cargo run
You should see:
$ cargo run
Hello, world!
Variables and Mutability
In rust, like Javascript, we can create variables that either change or stay constant but the way you go about it is slightly different. In rust the default behavior is that the variable will always be defaulted to immutable (Javascript's equivalent of "const"), meaning that it will not change. To make it so a variable will be able to change or "mutate" we have to call the variable with mut
.
We "instantiate" or "declare" a variable with let
or const
. By naming conventions, let should always be written in camelCase whereas const should always be in all CAPS Snake Case. Note: const
cannot be mutable but let
can be. I think of const as "Global Variables whereas let is more narrowly scoped.
let camelCase1: string = 'immutable';
let mut camelCase2: string = 'immutable';
const ALL_CAPS_NAME : i32 = 42;
// ❌️ const mut WRONG_EXAMPLE : string = Panic!; ❌️ // Cannot mutate const
Types
Integers (Scalar Data Types)
signed (i) = positive or negative
unsigned (u)= positive only
bits | type notation | Minimum | Maximum |
---|---|---|---|
8b | i8 | 0 | 2^8 -1 |
16b | i16 | 0 | 2^16 -1 |
32b | i32 | 0 | 2^32 -1 |
64b | i64 | 0 | 2^64 -1 |
128b | i64 | 0 | 2^128 -1 |
8b | u8 | -(2^7) | 2^7 -1 |
16b | u16 | -(2^15) | 2^15 -1 |
32b | u32 | -(2^31) | 2^31 -1 |
64b | u64 | -(2^63) | 2^63 -1 |
const DECIMAL : i32 = 02_55 // 255
const HEX : i32 = 0xff; // 255
const OCTAL : i32 = 0o377; // 255
const BINARY : i32 = 0b1111_1111; // 255
Floating points
let x :f64 = 2.0; //f64 - default because on modern CPUs it's the same as a floating point f32
Booleans
This works just like other languages, not a ton to write here but an example:
let mut example : bool = true;
example = false;
Tuples
If you haven't learned python, this may be a new name but the concept is not really new if you came from javascript, we just called it an array because in javascript, everything is an array.
Tuples are a way to combine a grouping of values that will not change after it's declared that are combined into one compound type. They have a fixed length so they cannot grow or shrink.
let tup = ( 500, 'hello', true);
println!("{}", tup.0); // prints out 500 to the console
println!("{}", tup.1); // prints out hello to the console
println!("{}", tup.2); // prints out true to the console
We can deconstruct this tuple just like we would in javascript:
let tup = ( 500, 'hello', true);
let (x, y, z) = tup;
println!("{}", x); // prints out 500 to the console
println!("{}", y); // prints out hello to the console
println!("{}", z); // prints out true to the console
Arrays
A collection of multiple values that all have the same type are called Arrays.
Arrays also have a fixed length like a tuple, but unlike a tuple, all values must have the same type such as integeters.
let array = [1,2,3];
println!("{}", array[0]); // prints 1
println!("{}", array[1]); // prints 2
println!("{}", array[2]); // prints 3
let array2 : [i32, 3] = [4,5,6];
array2[0] = 10; // ❌️ array2 is not mutable
let mut array3 : [i32, 3] = [7,8,9];
array3[0] = 10;
println!("{}", array3) // ✅️ prints 10, 8, 9
Vectors
Vectors are an adjustable length of array elements that is allocated on the heap. These are essential in programming, this is probably what you are most used to when referring to "arrays" with javascript.
Vectors are easiest to create by using the vec!
macro, which we will learn more about in a bit but for now, just press the "I believe button".
//example 1
let v1 = vec![1,2,3];
v1.push(4);
println!("{}", v1); // ❌️ this will fail because vec<(integer)> cannot be formatted with the default formatter. Use {:?} instead of {}
println!({:?}, v1); // ✅️ prints [1,2,3,4]
v1.pop();
println!({:?}, v1); // prints [1,2,3]
// Example 2
let mut v2 = Vec::new(); // This creates a new vector too
v2.push("one");
v2.push("two");
v2.push("three");
println!("{:?}", v2); // prints "[one, two, three]"
v2.reverse();
println!("{:?}", v2); // prints "[three, two, one]"
// Example 3
let v3 = Vec::<i32>::with_capacity(2); // this allows setting the length maximum of a vector
println!("{}", v3.capacity()); // prints "2"
// Example 4
let v4: Vec<i32> = (0..5) = (0..5).collect();
println1("{:?}", v4) // prints [0,1,2,3,4]
What you see above in the ❌️ is that we need to switch to debug mode to avoid formatter errors by using
:?
Slices
A slice is a region of an array or vector that can be any length. Slices cannot be passed as function arguments or stored directly as a variable.
let v4: Vec<i32> = 0..5).collect();
let slicedVector: &[i32] = &v4;
Now, I know I literally just said you cannot store the value of a slice as a variable, and we are kind of doing that. But how? What we are doing here is referencing the v4 vector, we will learn all about this in a bit but for now let's just press the "I believe button again". The key point is the &
symbol being used in front of the v4
and in the type declaration. The other important bit is that slicedVector
variable doesn't own the vector's value. It's a non-owning value pointing to a range of a v4
.
This is good for operating on a vector that you'd like to manipulate or pull data from.
Strings and &str
Strings are very similar to vectors since they are stored as a vector but the important bit here is that they are always going to be utf8 sequences and they are growable/shrinkable, allocated on the heap, and is not null terminated.
Let's look at some examples:
let name = String::from("Drew");
let paper = "Rust".to_string();
// like vectors, we can modify strings
let new_name = name.replace("Drew", "John")
println!("{}", name)
println!("{}", paper)
println!("{}", new_name)
// &str or string slice also known as "stir"
// This borrows or references the original declaration of the variable
// You cannot modify a string slice
let string1 = "hello"; // will produce a &str
println!("{}", string1.SomeBogusMethod());
// ❌️ This will error, but what you can see in the error is that the type is &str. We cannot modify &str.
let string2 = string1.to_string();
let string3 = &string2;
let string4 = HELLO.to_string();
println!("{}", string4);
println!("{}", string2); // prints hello
println!("{}", string3); // prints hello
println!("{}", string4.to_lowercase()); // prints hello
String Literals
These may not be a valid utf-8 sequence which is where string literals come in.
const rust = `\x52\x75\x73\x73`
println!("{}", rust) // prints "rust"
Functions
fn main(){
println!('Hello, world!");
}
Above you can see the main function, this is the entry point into any rust application. It's the first thing that will run and is used to logically organize what functions are called when. Think of it like an aws lambda's handler for a javascript function, ENTRYPOINT in docker, or a main function in go. This is what is ran first when the application starts.
As you can tell in main function, we declare a function using fn
and as a convention, we name functions with snake_casing.
The general syntax of a function is pretty generic, not a whole lot different from javascript.
One thing to know about is return
keyword isn't necessary if you don't use ;
, so for example:
fn main(){
println(greatest_common_denominator(20, 5));
}
fn greatest_common_denominator(mut a: u64, mut b: u64) -> u64{
while a != 0 {
if a < b {
let c = a;
a = b;
b = c;
}
a = a % b;
}
b // This line returns the value of b
}
# result is 5
Control Flow
Control Flow is the idea of determining where to run code based on whether a condition is true or not or if to continue deciding to run code if a condition is true or not. There are 3 kinds of loops plus if statements that make up "control flow"
If Statements
fn main(){
let one = 1;
if one > 10{
println!("Greater Than");
} else if one == 1 {
println!("Equal")
}
else {
println!("Less than")
}
}
// Prints "Equal"
Loop
fn main(){
loop{
printLn!("loop!");
}
}
// Will be an infinite loop
Named Loop
fn main(){
let mut num = 0;
'counter: loop {
println!("Count: {}", num);
let mut decrease = 5
loop {
println!("Decreasing: {}", decrease);
if decrease == 4 {
break;
}
if num ==2 {
break 'counter
}
decrease -= 1;
}
num += 1;
}
}
While Loop
fn main(){
while num < 5 {
println! ("Num: {}", num);
num += 1;
}
}
For Loop
fn main(){
let vect : Veec<i32> = (0..10).collect();
for element in vect{
println!("{}", element)
}
}
Rust Principles
In this section we will talk about ownership, move, clone, and copy works. We need to know the ownership model because it is the basis of how the memory model works and it's the basis for the rest of this documentation.
Memory
We want memory to be given to us when we want it and we don't want it back when we hand it to the computer. Memory safety is a big deal. Lot of programs have garbage collection but rust doesn't use this. It uses an ownership module. This concept drives the rust language.
Stack Vs. Heap
The stack is Last in First Out memory vector. It stores them in the order it receives them and retrieves them from the top of the "stack", think of this like stacking plates, we stack a plat on top of the stack and if we want to grab a plate, we grab one from the top.
Data stored in the stack must be of a known fixed length, this is why we have signed and unsigned types for integers to allocate a specific amount of memory to the value.
Data that isn't a known fixed length or that may change in the future are stored in the heap. The heap is slower and less organized. When calling memory located in the heap you are returned a reference to the location of the data in memory.
fn main(){
let a = 1; // on stack
let mut b = "Hello".to_string(); // on heap
b.push_str(", world");
}
Ownership
There are 3 rules of ownership.
- Each value in rust has an owner.
- There can only be one owner at a time
- When the owner goes out of scope, the value will be dropped from memory - this is also known as "free".
Move
A move sounds exactly like what it's called, you move the ownership of the value from one variable to another and most types implement a move. Move transfers ownership from one variable to another.
fn main(){
let a = vec!["Drew".to_string()]; // a is owner
let b = a; // move action occurs here, b is owner
println!("{:?}", a); // ❌️ Borrow of moved value: "a"
// move occurs because "a" has type
// Vec<string>, which does not
// implement the "Copy" trait
// This is because b now "owns"
// the value of a on the 3rd line
// This violated the 1st & 2nd ownership rule
}
Here is another example:
fn main(){
let someString = String::from('takes'); // creates a variable with a string "takes"
takes_ownership(someString); // ✅️ give ownership to the function
println!("{}", someString) // ❌️ Borrow of moved value: "someString"
// move occurs because "someString" has type
// Vec<string>, which does not
// implement the "Copy" trait
}
fn takes_ownership(a: String){
let b = a;
println!("{}", b)
}
Clone
If we want to take the value of a variable without taking ownership, we can perform a clone which will make a deep copy of the variable's value.
fn main(){
let a = vec!["Drew".to_string()]; // a is owner
let b = a.clone(); // a is still the owner
println!("{:?}", a); // ✅️ This works because we didn't transfer ownership
}
Copy
In the example below, what do you think will happen? Will it work or error with the move error from before?
fn main () {
let x = 1; // on stack
let y = x; // on stack
println!("x = {}, y = {}", x, y)
}
I was careful to say that "Most types implement a move", well in this example, some implement a copy if their value is stored on the stack such as an integer, boolean, char, float, and sometimes tuple (if every value contained implements a copy). Vectors are stored on the heap and this is why if we had set x to the value of a vector, it would have failed.
References and Borrowing
References allow us to make reference to a value, aka borrow, without taking ownership of it. We have 2 types, shared and mutable.
Shared references (&
) allow you to read but not modify the referenced value, you can have as many of these as you like.
Mutable references (&mut
) allow you to read and modify the reference of that value but unlike shared references, you cannot have any other operations on that reference at the same time. There can only be one.
&
is used to signal that you are "referencing" or "borrowing" the value of another variable. When specifying types, the types must also reflect that the type being passed or used is also a mutable reference or shared reference.
// mutable reference
fn main(){
let mut someString = String::from("hello");
change_string(&mut someString); // note the type
}
fn change_string(some_string_you_pass: &mut String){ // note the type
some_string_you_pass.push_str(", world");
}
// "hello, world"
Structs
There are 3 different types of structs with the main one being a named field struct. Structs are important because they allow us to group together similar values into a one value like variable. We will also discuss lifetimes and how they continue to evolve.
Structs (short for structures) allow you to name and package together related values into a single value so that you can deal with them as a single unit. There 3 times of structs:
- named field - gives a name to each component
- tuple like - identifies each component by the order they appear
- unit like - has no components at all - more on this shortly
Named Field Structs
This is very similar to creating a type in Typescript where you can define a grouping of values that describes the type they will accept. This concept is fairly consistent across strongly typed languages.
struct User { // Proper Camel Case case by tradition
active: bool,
username: String,
sign_in_count: u32,
}
fn main(){
let user1 = User(active: true, username: String::from("Drew"), sign_in_count: 0);
println!("{}", user1.username);
}
Tuple like structs
struct Coodinates(i32,i32,i32);
fn main(){
let coords = Coordinates(1,2,3);
}
Unit like Structs
These resemble an empty tuple, it's not especially useful by itself other than as a marker but in combination with other features it can become useful. For instance, a library may ask you to create a structure that implements a certain trait to handle events, if you don't have any data you need to store in that required structure, you can just create a unit-like struct.
struct UnitStruct{}; // use empty braces
struct AnotherStruct; // or just a semicolon
// 1..5, .. Range (Start: 1, end: 5)
Methods
Methods are similar to functions, they differ because methods are defined within the context of a struct, enum, or trade object. Methods always have their first parameter as self
which is representing the instance of the struct that is calling the method.
struct Square {
width: u32,
height: u32,
}
impl Square{ //impl stands for implementation of
fn area(&self) -> u32 {
self.width * self.height
}
fn change_width(&mut self, new_width: u32){ // mutable struct
self.width = new_width
}
}
fn main() {
let sq = Square { width: 5, height: 5}
println!("Area = {}", sq.area()); // prints 25
println!("New Width = {}", sq.change_width(10)); //prints 10
}
Lifetimes
Lifetimes in Rust are a way to ensure that references are valid for as long as we need them. Their usually inferred but sometimes we have to specify them. In those cases, we use '
to annotate the explicit lifetime of parameters. Let's look at an example:
fn main(){
let example_one;
{
let example_two = 5
example_one = &example_one;
}
println!("{}", example_one);
}
In this example, example_one
is not available outside of it's block ({}
). When we try to use example_one
(a reference to example_two
) outside, Rust complains because example_one
doesn't exist there. It's like trying to use a ticket for a concert that has already ended.
Lifetime Annotations
To handle more complex scenarios, we use lifetime annotations. These tell Rust how long the references should be valid.
fn example<'a>(x: &'a str) -> &'a str {
x
}
Here, 'a
is a lifetime annotation. We're telling Rust that the input x
and the output of the function have the same lifetime. This means the returned value will not outlive the input x
.
Simplified rules of lifetimes:
- Unique Lifetimes for EAch Parameter: Each Reference parameter in a function gets it's own lifetime.
- Single input lifetime: If there is just one input reference, it's lifetime is assigned to all output references.
- Method Lifetimes: If a method takes
self
or&mut self
, the lifetime ofself
is assigned to all output references.
Evolution of Lifetimes in Rust
Originally, Rust didn't require managing lifetimes explicitly, but this changed over time. As rust evolves, the need for explicit lifetime annotations is reducing thanks to better inference.
Lifetimes and Structs
When a struct in Rust contains references, each reference needs a lifetime annotation. This is to ensure that the struct does not outlive the references it holds. Let's dissect the given example:
struct Mystring<'a> {
text &'a str,
}
Why the Lifetime Annotation?
- Prevent Dangling References: The lifetime annotation
'a
is crucial to prevent dangling references. A dangling reference occurs when a reference points to invalid data. - Scope Alignment: By specifying
'a
, we align the lifetime oftext
with the lifetime ofMyString
. This means that as long asMyString
exists, the referencetext
is guaranteed to point to valid data. - Compiler Assurance: The Rust compiler uses these annotations to guarantee that the data referenced by
text
will not be dropped as long as theMyString
instance exists.
Practical Implication:
- Safety: This mechanism is part of Rust’s safety guarantees. It ensures memory safety without needing a garbage collector.
- Flexibility: It allows the struct to hold references to data of different lifetimes in different contexts, making it versatile.
Static Lifetimes
When we give a reference a static lifetime, the reference can live for the duration of the program. All string literals have a static lifetime.
let s: &'static str = "I have a static lifetime";
Some will suggest using static lifetimes for error messages.
Enums and Pattern Matching
Enums allow us to define a type and its possible variants and patterns allow us to execute our code based on conditions being met.
Enums
Enumerations or enums for short, allow you to define a type by enumerating it's possible variants. Let's look at an example:
enum Pet{dog, cat, fish}
impl Pet {
fn what_am_i(self) -> &'static str { // checkout that static lifetime
match self {
Pet::dog => "I am a dog"
Pet::cat => "I am a cat"
Pet::fish => "I am a fish"
}
}
}
fn main(){
let myPet = Pet::dog;
println!("{}", myPet.what_am_i())
}
Let's break this down:
Imagine you have a box of toy animals, and you can only pick one toy at a time. The box is like our enum Pet
, and each toy animal inside it (dog
, cat
, fish
) is a variant of the Pet
enum.
enum Pet { dog, cat, fish }
enum Pet
: This is an enumeration (or enum) named Pet
. An enum in Rust is a type that can be one of several different variants. Here, Pet
is the type.
{ dog
, cat
, fish
}: These are the variants of the Pet enum. Just like in a real-world scenario where a pet can be a dog, cat, or fish, this enum can take one of these three forms.
Now, each toy animal can speak and tell you what it is, but they can only say one thing: what kind of animal they are. That's what the what_am_i
function does. It lets the toy animal speak.
impl Pet {
fn what_am_i(self) -> &'static str {
match self {
Pet::dog => "I am a dog",
Pet::cat => "I am a cat",
Pet::fish => "I am a fish",
}
}
}
impl Pet
: This is an implementation block. It's where we define functions (methods) that can be called on instances of the Pet enum.
fn what_am_i(self) -> &'static str
: This is a method named what_am_i
. It takes one parameter self
which represents the instance of the Pet
enum it's called on. It returns a string slice (&'static str
), which is a reference to a string.
match self
: This is a match statement, a powerful part of Rust's pattern matching. It allows the function to perform different actions based on which variant self is.
Pet::dog => "I am a dog", ...
: These are the match arms. Each arm matches a variant of the Pet enum and returns a string corresponding to that variant.
Then, in the main part of this example, you pick the dog toy from the box:
fn main() {
let myPet = Pet::dog;
println!("{}", myPet.what_am_i());
}
let myPet = Pet::dog;
: Here, we're creating a variable myPet and assigning it the dog variant of the Pet enum.
When you ask the dog toy, "What are you?", it answers, "I am a dog". That's what println!("{}", myPet.what_am_i())
does - it's like asking the toy animal to speak.
So, in short, we have a box of toy animals (enum Pet
), and each toy can tell you what it is when asked (what_am_i
method). You picked the dog toy, and it told you it's a dog!
Option Enum
Options enum is powerful because it allows to allow a value to be something or nothing. Nothing is comparable to null in other languages, rust doesn't have the concept of null. Just the option inside of an enum.
enum Otion<T>{
// the <T> is a common way to say "Any type"
None,
Some(T),
}
let nothing: Option<i32> = None;
}
Matchers
Match is akin to case statements, it does what it sounds like it would do and we have already seen one of these with the Pet enum example. Matches in Rust are exhaustive so you must cover every possible case including None
;
enum Otion<T>{
// the <T> is a common way to say "Any type"
None,
Some(T),
}
fn plus_one(x: Option<u32>) -> Option<u32>{
match x {
None => None,
Some(i) => Some(i +1),
}
}
fn main(){
let five Some(5)
let six = plus_one(five);
let none = plus_one(None);
println!("{}", six); //returns 6
println!("{}", none); // returns None
}
In bash and other languages you can use a wild card such as
*)
as a catch-all case and it's relatively the same with rust except you use a_
instead.
If Let Match Statements
We can use if let statements as a shorter way to create a match that only has one case. Let's see an eample:
let dog = Some(Pet::dog);
if let Some(Pet::dog) = dog {
println!("The animal is a dog");
} else {
println!("Not a dog")
}
Traits and Generics
Generics are going to allow us to have stand in types for our concrete types. Traits are going to allow us to represent a capability that can be implemented in on many different types.
Generics
Generics allow us to create code that can operate on many different types. They are abstract stand ins for concrete types. We saw some of these in our last section with Options<T>
struct Point<T>{
x: T,
y: T,
}
fn main () {
let coord = Point{x: 5.0, y:5.0};
let coord2 = Point{x: 'x', y: 'y'};
}
In the example above you can see that we use a generic "placeholder" type for the values of x and y in the struct and that both characters and floating point numbers are perfectly valid types to pass to Point.
The idea is that we use these generic types as placeholders until a time where we know what the types will actually be needed. It is important to know that we don't mix types when calling the struct unless we were to do something like struct Point<T, U>
.
Traits
Traits in Rust are like the Swiss Army knife in your coding toolbox. They're about defining shared behavior - think of it as a contract that different types agree to uphold. It's like saying, "Hey, you wanna be part of my club? You gotta do these things."
Example 1: Custom Trait Implementation
Here's a classic example. We're defining a trait Overview that's like asking, "Can you give me a quick rundown of yourself?"
// Define the trait 'Overview'
trait Overview {
fn overview(&self) -> String;
}
// A struct 'Course' - it's like your typical course with a headline and author.
struct Course {
headline: String,
author: String,
}
// Another struct 'anotherCourse' - creatively named, right?
struct anotherCourse {
headline: String,
author: String,
}
// Implementing 'Overview' for 'Course'
impl Overview for Course {
fn overview(&self) -> String {
format!("{}, {}", self.author, self.headline)
}
}
// Implementing 'Overview' for 'anotherCourse'
impl Overview for anotherCourse {
fn overview(&self) -> String {
format!("{}, {}", self.author, self.headline)
}
}
// The main act - let's see these traits in action!
fn main() {
let course1 = Course {
headline: String::from("Headline!"),
author: String::from("Drew")
};
let course2 = anotherCourse {
headline: String::from("Another Headline!"),
author: String::from("Another Drew")
};
println!("{}", course1.overview()); // Drew, Headline!
println!("{}", course2.overview()); // Another Drew, Another Headline!
}
Example 2: Default Trait Implementation
But wait, there's more! We can use default implementations. It's like saying, "If you don't have your own story to tell, here's one for you."
// 'Overview' with a default method
trait Overview {
fn overview(&self) -> String {
String::from("This is a Rust Course!") // The default message
}
}
// Our familiar structs
struct Course {
headline: String,
author: String,
}
struct anotherCourse {
headline: String,
author: String,
}
// Implementing 'Overview' for both structs but not overriding the default method
impl Overview for Course {}
impl Overview for anotherCourse {}
// Testing the defaults
fn main() {
let course1 = Course {
headline: String::from("Headline!"),
author: String::from("Drew")
};
let course2 = anotherCourse {
headline: String::from("Another Headline!"),
author: String::from("Another Drew")
};
println!("{}", course1.overview()); // This is a Rust Course!
println!("{}", course2.overview()); // This is a Rust Course!
}
Example 3: Traits as Function Parameters
Traits can also be used as parameters. It's like inviting anyone who can speak the 'Overview' language to a party.
// 'Overview' with a default method
trait Overview {
fn overview(&self) -> String {
String::from("This is a Rust Course!") // Still the default message
}
}
// Structs as before
struct Course {
headline: String,
author: String,
}
struct anotherCourse {
headline: String,
author: String,
}
// Implementing 'Overview' for 'Course'
impl Overview for Course {}
// Implementing and overriding 'Overview' for 'anotherCourse'
impl Overview for anotherCourse {
fn overview(&self) -> String {
format!("{}, {}", self.author, self.headline)
}
}
// Time to show off our trait in action
fn main() {
let course1 = Course {
headline: String::from("Headline!"),
author: String::from("Drew")
};
let course2 = anotherCourse {
headline: String::from("Another Headline!"),
author: String::from("Another Drew")
};
call_overview(&course1);
call_overview(&course2);
}
// A function that takes any type implementing 'Overview'
fn call_overview(item: &impl Overview) {
println!("Overview: {}", item.overview());
}
// Outputs:
// "This is a Rust Course!
// "Another Drew, Another Headline!
Closures
Closures in Rust are like your favorite hoagie - packed with all the good stuff in a compact package. They're anonymous functions you can save in a variable or pass around. Let's simplify and explain how to use a closure in a City struct example:
#[derive(Debug)] // this allows us to use debug
struct City{
city: String,
population: u64,
}
fn sort_pop(city: &mut Vec<City>){
city.sort_by_key(pop_helper)
}
fn pop_helper(pop: &City)-> u64{
pop.population
}
fn main(){
let a = City(city: String::from("A"), population: 100);
let b = City(city: String::from("B"), population: 57);
let c = City(city: String::from("C"), population: 140);
let d = City(city: String::from("D"), population: 5);
let e = City(city: String::from("E"), population: 70);
let mut vec: Vec<City> = Vec::new();
vec.push(a);
vec.push(b);
vec.push(c);
vec.push(d);
vec.push(e);
sort_pop(&mut vec);
println!("{:?}", vec);
}
// How could we have done this using a closure?
Your original code above defines a helper function, pop_helper
, to extract the population for sorting. It's a separate function, a bit like going to another shop just to get mayo for your sandwich. Efficient? Maybe not.
Closures, on the other hand, let you keep everything in one place. Here's how you can use a closure in your sort_pop_closure
function:
Defining the Closure: Instead of having a separate pop_helper function, you define the logic right where you need it - inside the sort_by_key method. It's like putting mayo directly on your hoagie while you're making it. Efficient and neat!
Using the Closure: The closure |p| p.population
is an anonymous function that takes one parameter, p, and returns p.population. This closure is passed directly to sort_by_key, which uses it to determine the order of the elements.
Simplified Code: Your sort_pop_closure
function now becomes a one-liner. It's more concise and keeps the sorting logic right where it's used, making it easier to understand and maintain.
So, your closure version is sleek and straightforward, just like Philly's approach to everything. You've essentially taken the scenic route and replaced it with a straight shot down Broad Street! 🌆
#[derive(Debug)] // this allows us to use debug
struct City{
city: String,
population: u64,
}
fn sort_pop_closure(pop: &mut Vec<City>){
pop.sort_by_key(|p| p.population)
}
fn main(){
let a = City(city: String::from("A"), population: 100);
let b = City(city: String::from("B"), population: 57);
let c = City(city: String::from("C"), population: 140);
let d = City(city: String::from("D"), population: 5);
let e = City(city: String::from("E"), population: 70);
let mut vec: Vec<City> = Vec::new();
vec.push(a);
vec.push(b);
vec.push(c);
vec.push(d);
vec.push(e);
sort_pop_closure(&mut vec);
println!("{:?}", vec);
}
Drew is a seasoned DevOps Engineer with a rich background that spans multiple industries and technologies. With foundational training as a Nuclear Engineer in the US Navy, Drew brings a meticulous approach to operational efficiency and reliability. His expertise lies in cloud migration strategies, CI/CD automation, and Kubernetes orchestration. Known for a keen focus on facts and correctness, Drew is proficient in a range of programming languages including Bash and JavaScript. His diverse experiences, from serving in the military to working in the corporate world, have equipped him with a comprehensive worldview and a knack for creative problem-solving. Drew advocates for streamlined, fact-based approaches in both code and business, making him a reliable authority in the tech industry.