Table of contents
- Rust programming
Rust programming
Hello world !
fn main() {
println!("Hello, world!");
}
fn means function, the main function is the beginning of every Rust program.
println!() prints text to the console and its ! indicates that it’s a macro rather than a function.
Rust files should have .rs file extension.
println!()
fn main() {
println!("{}, {}!", "Hello", "world");
println!("{0}, {1}!", "Hello", "world");
println!("{greeting}, {name}!", greeting="Hello", name="world");
println!("{:?}", [1,2,3]); // [1, 2, 3]
println!("{:#?}", [1,2,3]);
/*
[
1,
2,
3
]
*/
// The format! macro is used to store the formatted string.
let x = format!("{}, {}!", "Hello", "world");
println!("{}", x); // Hello, world!
// Rust has a print!() macro as well
print!("Hello, world!"); // Without new line
println!(); // A new line
print!("Hello, world!\n"); // With new line
}
Cargo
Create a project
A crate is a package, which can be shared via crates.io. A crate can produce an executable or a library. In other words, it can be a binary crate or a library crate.
# Binary
cargo new <BINARY_NAME> --bin
# Library
cargo new <LIBRARY_NAME> --lib
- Cargo.toml(capital c) is the configuration file which contains all of the metadata that Cargo needs to compile your project.
- src folder is the place to store the source code.
- Each crate has an implicit crate root/ entry point. main.rs is the crate root for a binary crate and lib.rs is the crate root for a library crate.
Build a project
cargo build
Run a project
cargo run
Check a project
Cargo also provides a command called cargo check
. This command quickly checks your code to make sure it compiles but doesn’t produce an executable:
cargo check
Why would you not want an executable? Often, cargo check
is much faster than cargo build
, because it skips the step of producing an executable. If you’re continually checking your work while writing the code, using cargo check
will speed up the process!
Building for release
When your project is finally ready for release, you can use :
cargo build --release
To compile it with optimizations, this command will create an executable in target/release instead of target/debug. The optimizations make your Rust code run faster, but turning them on lengthens the time it takes for your program to compile. This is why there are two different profiles: one for development, when you want to rebuild quickly and often, and another for building the final program you’ll give to a user that won’t be rebuilt repeatedly and that will run as fast as possible.
Variables
Binding
The first thing to know in Rust is that all variables are constant by default. To declare a mutable variable, use the keyword mut.
let a; // Declaration; without data type
a = 5; // Assignment
let b: i8; // Declaration; with data type
b = 5;
let t = true; // Declaration + assignment; without data type
let f: bool = false; // Declaration + assignment; with data type
let (x, y) = (1, 2); // x = 1 and y = 2
let mut z = 5;
z = 6;
let z = { x + y }; // z = 3
Constant
const MAX_HEALTH: i32 = 100;
Shadowing
Sometimes, while dealing with data, initially we get them in one unit but need to transform them into another unit for further processing. In this situation, instead of using different variable names, Rust allows us to redeclare the same variable with a different data type and/ or with a different mutability setting. We call this Shadowing.
fn main() {
let x: f64 = -20.48; // float
let x: i64 = x.floor() as i64; // int
println!("{}", x); // -21
let s: &str = "hello"; // &str
let s: String = s.to_uppercase(); // String
println!("{}", s) // HELLO
}
Increment
let mut i = 0;
i += 1
Unused variable
If we start the name of a variable with a ‘_’, we won’t get a compiler warning if it is unused.
let _i = 0;
Functions
- Named functions are declared with the keyword
fn
- When using arguments, you must declare the data types.
- By default, functions return an empty tuple/
()
. If you want to return a value, the return type must be specified after->
Passing arguments
fn print_sum(a: i8, b: i8) {
println!("sum is: {}", a + b);
}
Returning values
fn plus_one(a: i32) -> i32 {
a + 1
}
There is no ending ;
in the above code, it means this is an expression which equals to return a + 1;
.
Closures
- Also known as anonymous functions or lambda functions.
- The data types of arguments and returns are optional.
fn main() {
let x = 2;
let square = |i| {i * i;};
println!("{}", square(x));
}
Primitive Data Types
Bool
let x = true;
let y: bool = false;
// no TRUE, FALSE, 1, 0
Char
let x = 'x';
let y: char = '😎';
// no "x", only single quotes
Integer
- i8
- i16
- i32
- i64
- i128
let x = 10; // The default integer type in Rust is i32
let y: i8 = -128;
You can use min_value()
and max_value()
functions to find min and max of each integer type.
i8::min_value();
Unsigned integer
- u8
- u16
- u32
- u64
- u128
let x: u32 = 2500;
You can use min_value()
and max_value()
functions to find min and max of each integer type.
i8::min_value();
Float
let x = 1.5; // The default float type in Rust is f64
let y: f64 = 2.0;
Should avoid using f32
, unless you need to reduce memory consumption badly or if you are doing low-level optimization, when targeted hardware does not support for double-precision or when single-precision is faster than double-precision on it.
Array
Fixed size list of elements of same data type. Arrays are immutable by default and even with mut
, its element count cannot be changed. If you are looking for a dynamic/growable array, you can use vectors. Vectors can contain any type of elements but all elements must be in the same data type.
let a = [1, 2, 3];
let a: [i32; 3] = [1, 2, 3]; // [Type; NO of elements]
let b: [i32; 0] = []; // An empty array
let mut c: [i32; 3] = [1, 2, 3];
c[0] = 2;
c[1] = 4;
c[2] = 6;
println!("{:?}", c); // [2, 4, 6]
println!("{:#?}", c);
// [
// 2,
// 4,
// 6,
// ]
let d = [0; 5]; // [0, 0, 0, 0, 0]
let e = ["x"; 5]; // ["x", "x", "x", "x", "x"]
Tuple
Fixed size ordered list of elements of different (or same) data types. Tuples are also immutable by default and even with mut
, its element count cannot be changed. Also, if you want to change an element’s value, the new value should have the same data type of previous value.
let a = (1, 1.5, true, 'a');
let a: (i32, f64, bool, char) = (1, 1.5, true, 'a');
let mut b = (1, 1.5);
b.0 = 2;
b.1 = 3.0;
println!("{:?}", b); // (2, 3.0)
println!("{:#?}", b);
// (
// 2,
// 3.0,
// )
let (c, d) = b; // c = 2, d = 3.0
let (e, _, _, f) = a; // e = 1, f = 'a'
let g = (0,); // single-element tuple
let h = (b, (2, 4), 5); // ((2, 3.0), (2, 4), 5)
Slice
Dynamically-sized reference to another data structure. Imagine you want to get/ pass a part of an array or any other data structure. Instead of copying it to another array (or same data structure), Rust allows for creating a view/ reference to access only that part of the data. This view/ reference can be mutable or immutable.
let a: [i32; 4] = [1, 2, 3, 4]; // Parent Array
let b: &[i32] = &a; // Slicing whole array
let c = &a[0..4]; // From 0th position to 4th(excluding)
let d = &a[..]; // Slicing whole array
let e = &a[1..3]; // [2, 3]
let f = &a[1..]; // [2, 3, 4]
let g = &a[..3]; // [1, 2, 3]
Str
Unsized UTF-8 sequence of Unicode string slices. It’s an immutable/ statically allocated slice holding an unknown sized sequence of UTF-8 code points stored in somewhere in memory. &str
is used to borrow and assign the whole array to the given variable binding.
let a = "Hello, world."; // a: &'static str
let b: &str = "こんにちは, 世界!";
String
A String
type can be generated from a &str
type, via the to_string()
or String::from()
methods. With as_str()
method, a String
type can be converted to a &str
type.
let s: &str = "Hello"; // &str
let s = s.to_string(); // String
let s = String::from(s); // String
let s = s.as_str(); // &str
String concatenation
let (s1, s2) = ("some", "thing"); // both &str
// All bellow codes return `String`; something
let s = String::from(s1) + s2; // String + &str
let mut s = String::from(s1); // String
s.push_str(s2); // + &str
let s = format!("{}{}", s1, s2); // &str/String + &str/String
let s = [s1, s2].concat(); // &str or String array
Control Flows
if - else if - else
let age = 13;
if age < 18 {
println!("Hello, child !");
} else if a > 18 {
println!("Hello, adult !");
} else {
println!("Hello, god ?");
}
Match
Rust provides pattern matching via the match
keyword, which can be used like a C switch
.
let tshirt_width = 20;
let tshirt_size = match tshirt_width {
16 => "S", // check 16
17 | 18 => "M", // check 17 and 18
19 ..= 21 => "L", // check from 19 to 21 (19,20,21)
22 => "XL",
_ => "Not Available",
};
println!("{}", tshirt_size); // L
Loop
let mut a = 0;
loop {
if a == 0 {
println!("Skip Value : {}", a);
a += 1;
continue;
} else if a == 2 {
println!("Break At : {}", a);
break;
}
println!("Current Value : {}", a);
a += 1;
}
While
let mut b = 0;
while b < 5 {
if b == 0 {
println!("Skip value : {}", b);
b += 1;
continue;
} else if b == 2 {
println!("Break At : {}", b);
break;
}
println!("Current value : {}", b);
b += 1;
}
For
for b in 0..6 {
if b == 0 {
println!("Skip Value : {}", b);
continue;
} else if b == 2 {
println!("Break At : {}", b);
break;
}
println!("Current value : {}", b);
}
Array/vector
let group : [&str; 4] = ["Mark", "Larry", "Bill", "Steve"];
for person in group.iter() { // group.iter() turn the array into a simple iterator
println!("Current Person : {}", person);
}