Introduction
Learn Rust by Examples is a collection of examples for understanding Rust concepts.
- Hello World - Print Hello World! program.
- Primitive Type - Primitive types in the Rust language.
- Function - A function is a block of code that represents a single task. Write it once and call it multiple times.
- Trait - A trait is a collection of methods.
- PhantomData
Hello World!
Print the text Hello World! using the println! function.
fn main() { println!("Hello World!"); }
Save the code in a file named hello.rs.
To build the Rust file, use the command below. It creates an executable named hello.
rustc hello.rs
To run the executable on Linux or macOS, use the command below.
./hello
This command prints Hello World!.
Scalar Type
Signed Integers
i8, i16, i32, i128, and isize.
fn main() { { let mut x: i8 = 127; x = x.wrapping_add(1); println!("{:?}", x); } // Add two numbers of different integer types { let x: i8 = 10; let y: i16 = 100; let z = (x as i16) + y; println!("{:?}", z); } // Max value of each integer types { println!("Max value of i8: {:?}", i8::MAX); println!("Max value of i16: {:?}", i16::MAX); println!("Max value of i32: {:?}", i32::MAX); println!("Max value of i128: {:?}", i128::MAX); println!("Max value of isize: {:?}", isize::MAX); } }
Functions
Intro
A function is a block of code that usually focuses on a single task. It can take input arguments and return a value of a specific type. Both input arguments and return values are optional.
In Rust, you define a function with the fn keyword, followed by the function name and a set of parentheses.
Curly braces contain the function body.
fn pi() -> f32 { 3.14 } fn main() { let r: f32 = 10.0; let pi_value = pi(); println!("Area of cirle: {}", pi_value * r * r); }
Return
- Implicit Return: Last expression in a block (without a semicolon) is automatically returned.
- Explicit return: Using the
returnkeyword for early returns.
fn fibonacci_series(n: i32) -> i32 { // Use `return` keyword to return value. if n < 1 { return 0; } if n < 2 { return 1; } // Last statement of function block without semicolon, return implicitly. fibonacci_series(n - 1) + fibonacci_series(n - 2) } fn main() { let n = 10; let result = fibonacci_series(n); println!("Fib({}) => {}", n, result); }
Closure
A closure is an anonymous function that can capture variables from its surrounding environment.
fn main() { let pi: f32 = 3.14; // Single line closure. let area = |x: f32| pi * x * x; let n: f32 = 10.0; let result = area(n); println!("Area of cicle: {}", result); }
Trait
Traits are used to define shared behavior that different types can implement.
Without using trait
Without a trait, each struct has to implement the print_field method on its own.
As the number of structs increases, the amount of duplicated code also increases.
struct Car { name: String } struct Bike { name: String } impl Car { fn print_field(&self) { println!("Name of car is `{}`", self.name); } } impl Bike { fn print_field(&self) { println!("Name of bike is `{}`", self.name); } } fn main() { let car = Car {name: "Hyundai Creta".into()}; let bike = Bike {name: "Royal Enfield: Meteros".into()}; car.print_field(); bike.print_field(); }
Using trait
The shared behavior here is the field method, which is implemented by each struct.
The show_field function takes two arguments: name and a reference to Machine.
Any struct that implements the Machine trait can be passed as the second argument to show_field.
trait Machine { fn field(&self) -> &str; } struct Car { name: String } struct Bike { name: String } impl Machine for Car { fn field(&self) -> &str { &self.name } } impl Machine for Bike { fn field(&self) -> &str { &self.name } } fn show_field(name: &str, machine: &dyn Machine) { println!("Name of {} is `{}`", name, machine.field()); } fn main() { let car = Car {name: "Hyundai: Creta".into()}; let bike = Bike {name: "Royal Enfield: Meteros".into()}; show_field("car", &car); show_field("bike", &bike); }
From Trait
struct FromValue { value: String } impl From<&str> for FromValue { fn from(name: &str) -> FromValue { FromValue { value: name.into() } } } impl From<i32> for FromValue { fn from(value: i32) -> FromValue { FromValue { value: value.to_string() } } } fn main() { let from_value: FromValue = FromValue::from("helloWorld"); println!("{}", from_value.value); let from_value: FromValue = FromValue::from(100); println!("{}", from_value.value); }
PhantomData
Type-Level Safety
PhantomData prevents mixing different kinds of units at the type level.
use std::marker::PhantomData; struct Meter; struct Second; struct Quantity<Unit> { value: f64, _unit: PhantomData<Unit> } type Distance = Quantity<Meter>; type Time = Quantity<Second>; fn speed(d: Distance, t: Time) -> f64 { d.value / t.value } fn main() { let distance: Distance = Distance {value: 10.0, _unit: PhantomData}; let time: Time = Quantity {value: 5.0, _unit: PhantomData}; // This code will not compiled. As we passing the datatype is different. // Comment below line, and uncomment line next line. Then code compile and run. let result = speed(time, distance); // let result = speed(distance, time); println!("Speed: {}", result); }
Type-safety without using PhantomData
Use can also achieve type safety without using PhantomData also.
struct Distance(f64); struct Second(f64); fn speed(distance: Distance, time: Second) -> f64 { distance.0 / time.0 } fn main() { let distance = Distance(10.0); let time = Second(5.0); // This code will not compiled. As we passing the datatype is different. // Comment below line, and uncomment line next line. Then code compile and run. let result = speed(time, distance); // let result = speed(distance, time); println!("Speed: {}", result); }
Lifetime
PhantomData forces the compiler to track lifetimes in patterns that involve unsafe code.
The examples below make this easier to understand:
- Reference with lifetime - The compiler reports an error at compile time if a value is used after its lifetime has ended.
- Reference with unsafe code - The compiler does not enforce the same lifetime checks inside unsafe pointer usage.
- Reference with unsafe code plus PhantomData -
PhantomDatamakes the compiler enforce lifetime checks.
Reference with lifetime
The Rust compiler checks the lifetime of each variable. If a variable does not live long enough and is used after its scope ends, compilation fails with a lifetime error.
struct Quantity<'a> { value: &'a i32 } impl<'a> Quantity<'a> { fn new(n: &'a i32) -> Self { Self { value: n } } } fn main() { let quantity; { let n: i32 = 10; quantity = Quantity::new(&n); } println!("value: {}", quantity.value); }
Reference with unsafe code
The Rust compiler does not run the same lifetime checks over unsafe pointer access, so the code below compiles and runs. The output may appear correct, but it is not reliable.
Why do we get 42 in the output?
Because n has gone out of scope, but the value 42 may still remain at the memory location pointed to by ptr.
// Use *const i32 pointer to i32 value. No lifetime `'a` symbol required. struct Quantity { ptr: *const i32 } impl Quantity { fn new(n: &i32) -> Self { Self { ptr: n } } } fn main() { let quantity; { let n: i32 = 42; quantity = Quantity::new(&n); } unsafe { println!("value: {}", *quantity.ptr); } }
Reference with unsafe code plus PhantomData
PhantomData makes the Rust compiler track the lifetime, and it reports an error if a value is accessed beyond its scope.
Below code will not compile.
use std::marker::PhantomData; struct Quantity<'a> { ptr: *const i32, _marker: PhantomData<&'a i32> } impl<'a> Quantity<'a> { fn new(n: &'a i32) -> Self { Self { ptr: n, _marker: PhantomData } } } fn main() { let quantity; { let n: i32 = 42; quantity = Quantity::new(&n); } unsafe { println!("value: {}", *quantity.ptr); } }