-
Rust's build system and package manager.
-
Cargo's configuration format is TOML which stands for Tom's Obvious, Minimal Language.
-
cargo run
will build the source file and execute it. -
cargo check
quickly checks the code to make sure it compiles but doesn't produce an executable. -
cargo build --release
build release version with optimizations.
- Variables are immutable by default.
// This is good.
let x = 3;
// And this is cool.
let y: i32;
y = 1024;
// unable to reassign value.
x = 33; // error..
y = 2048 // error..
- Add
mut
keyword before variable name to create mutable variables.
let mut y = 10;
println!("The value of y is: {y}");
y = 20;
println!("The value of y is: {y}");
-
Cannot use
mut
with constants. -
Type of the value must be annotated.
-
Constants can be declared in any scope.
-
Constants may be set only to a constant expression, not the result of a value that could only be computed at runtime.
- It is able to declare a new variable with the same name as a privious variable.
let x = 5;
let x = x * 20;
{
// First two xs are shadowed by the third x.
let x = x * 2;
println!("The value of x is: {x}"); // 200
}
let space = " ";
let space = space.len();
println!("The length of space is: {space}"); // 5
-
Integers can be explicitly declared bit length 8, 16, 32, 64, 128 with prefix i(signed) or u(unsigned). Using isize, usize to automatically allocate bit length depends on architecture (32 or 64).
-
Floating-Points: f32, f64. Default is f64, and floating point is always signed.
-
Booleans: true or false, one byte in size.
-
Characters: char literals use single quotes same as cpp, but char in rust is four bytes in size represents Unicode Scalar Value rather than ASCII.
- Tuples are fixed in size once declared.
- Declaration:
let tup: (i32, f64, bool, char) = (500, 20.5, false, 'w');
- Destructuring:
let tup: (i32, f64, bool, char) = (500, 20.5, false, 'w');
let (i, j, k, n) = tup;
- Direct access:
let tup: (i32, f64, bool, char) = (500, 20.5, false, 'w');
let i = tup.0;
let j = tup.1;
let k = tup.2;
let n = tup.3;
- Empty tuples are called unit.
let empty_tup = ();
- Declaration:
let a: [i32; 5] = [1, 2, 3, 4, 5];
- Initialize an array to contain the same value for each element.
let a = ["hello, world"; 5];
-
Function declaration above main is not required as if it is in C or C++.
-
Types of all parameters must be declared.
-
Statements are instructions that perfrom some action and do not return a value.
-
Expressions evaluate to a resulting value.
-
Wrong, because statements do not return values, that is different from C and C++.
fn main() {
let x = (let y = 6);
}
- Expressions do not include ending semicolons.
// yields 6
{
let x = 5;
x + 1
}
-
Functions with return values must have return type declared.
-
The return value of the function is synonymous with the value of the final expression in the block of the body of a function.
-
Return value does not have to be named in rust, it will always return the last expression implicitly.
fn return_value() -> i32 {
1024
}
- Using
return
keyword to return early with a specific value in the function.
fn return_early(x: i32) -> i32 {
if x == 33 {
return 512;
}
1024
}
- Parentheses are not required in rust.
if 2 > 1 {
println!("God damn it Gump You're a goddamn genius!");
}
- Conditions in if statements must be booleans, rust does not convert non-boolean types to a boolean automatically.
// Cannot do this in rust.
let ch = 1;
if ch {
// do something..
}
// No ternary in rust this is what it offers.
let x = if 2 > 1 { 2 } else { 1 };
-
loop
must be explicitly stopped. -
loop
is an expression that has return value, return value can be placed afterbreak
keyword.
let mut x = 10;
let y = loop {
if x == 20 {
break x * 2;
}
x += 1;
};
println!("{y}");
- loops can have labels, it is useful in nested loops to break the outer loop from the inner one.
let mut counter = 0;
'counter_loop: loop {
println!("Counter is :{counter}");
let mut remaining = 10;
loop {
println!("Remaining is :{remaining}");
if remaining == 8 {
break;
} else if counter == 2 {
break 'counter_loop;
}
remaining -= 1;
}
counter += 1;
}
while
is not much different from other languages except the parentheses are eliminated.
let mut x = 10;
while x >= 0 {
println!("{x}");
x -= 1;
}
for
is as clean as python, quite nice.
let arr = [10, 20, 30, 40, 50];
// loop through an array.
for n in arr {
println!("{n}");
}
// loop a range of numbers.
for j in 0..10 { // equals to: for (int i = 0; i < 10; i++) {};
println!("{j}");
}
-
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 by calling a special function
drop
. -
This action is called
move
rather than shallow copy as Rust will invalidate the first variable.
let s1 = String::from("hello");
let s2 = s1; // ownership gets transferred to s2 from s1, and s1 is invalidated at the same time to make sure there is always one owner at a time.
- Deep copy
let s1 = String::from("hello");
let s2 = s1.clone();
-
Nothing special here, just remember scalar types passed to functions are copies because everything is known at the compile time.
-
Whereas all other variables that use free memory passed to functions will triger
move
which is ownership transfer.
fn main() {
let s = String::from("hello");
takes_ownership(s);
// s is invalid from here.
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
}
-
Return values can also transfer ownership.
-
Return multiple values using a tuple is possible.
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}
- That is pretty much the same as what it is in other languages, it is called borrowing in Rust.
fn main() {
let s1 = String::from("hello");
let l = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, l);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
- References are immutable by default thus modifing it is not permitted in Rust.
// cannot compile.
fn main() {
let s1 = String::from("hello");
change(&s1);
}
fn change(s: &String) {
s.push_str(", world");
}
- To modify a reference value, simply make it mutable.
fn main() {
let mut s1 = String::from("hello");
println!("{s1}");
change(&mut s1);
println!("{s1}");
}
fn change(s: &mut String) {
s.push_str(", world");
}
- Mutable reference restriction: There can be only one mutable reference of a variable at a time.
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // error
- And here is why the weird empty curly brackets come in the language I guess..
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
- Also, cannot have mutable and immutable references at the same time as well.
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s; //error
println!("{} {} {}", r1, r2, r3);
- And this compiles because mutable and immutable references are not being used at the same time, intuitive.
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
let r3 = &mut s;
println!("{}", r3);
- The compiler takes care of dangling pointers
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s // error
}
- Slice type offers the ability to refer part of the string.
let s: String = String::from("hello world");
let s1: &str = &s[..5];
- String literals are type of
&str
because it is slice reference.
- Define custom types using
struct
struct User {
first_name: String, // these are called fields
last_name: String,
is_active: bool,
age: u32
}
- Shorthand for constructing new instance
fn build_user(first_name: String, last_name: String, age: u32) -> User {
User {
active: true,
first_name,
last_name,
age
}
}
- Update Syntax,
// Ownership transfer occurs also.
let user2 = User {
age: 76,
..user1
}
- Tuple Structs
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
- Unit-Like Structs
struct AlwaysEqual; // no fields
-
Use
impl
before structure name to define methods. -
Method's first parameter is always
&self
which is a shorthand forself: &self
. -
self
refers to the instance of the structure that the method is being called on. -
Use
&mut self
to change the instance,self
will take the ownership.
struct Rectangle {
width: u32,
height: u32
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
-
It serves the purpose of constructors, just like
new
in some OOP languages. -
It is the same to define associated functions as methods except the first parameter is not
self
. -
To call this type of functions using
::
after structure name. -
Return type is
Self
.
struct Rectangle {
width: u32,
height: u32
}
impl Rectangle {
fn squre(size: n32) -> Self {
Self {
width: size,
height: size
}
}
fn from(width: u32, height: u32) -> Self {
Self { width, height }
}
}
fn main() {
let sqr = Rectangle::squre(20);
let rect = Rectangle::from(50, 60);
}
- Use
enum
to define an enumeration type.
enum IpAddrKind {
V4,
V6,
}
- Create instances using
::
.
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
- Each variant can have different types.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
let mess: Message = Message::Write(String::from("hello, world"));
- Define methods on
enum
is same as onstruct
impl Message {
fn call(&self) {
// do something..
}
}
- So there is an enum type called
Option
in Rust, which has two variants:None
andSome
.
enum Option<T> {
None,
Some(T),
}
-
Rust's way to prevent using null as non-null references is to force to hanle all variants, in other words to check if it is null before using it if the variable is possibly null.
-
Well..I would say it's good but why the doc make it sounds like it's an super amazing feature..isn't it just "check null before using it?". Amazing.
-
It is a more useful and powerful
if
andswitch/case
. -
It can match lots of patterns even types.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
-
Match arms can bind to the parts of the values that match the pattern.
-
The state variable will bind to the value of that quarter’s state in the following code.
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}
- The last arm can handle all other patterns that not specifically listed above.
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
- Use
_
for the variable name if the value is not needed.
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
- Use
if let
when concise match needed.
if let Coin::Penny = c {
println!("Pattern matched, c is a Penny variant of Coin type.");
}
- It binds value too, and it can have
else
.
if let Coin::Quarter(state) = coin {
println!("{}", state);
} else {
println!("Hope to see you soon in Singapore!");
}