What is Rust and the motivation of learning Rust
Rust is a modern programming language known for its focus on safety and performance. It’s designed to provide memory safety without using a garbage collector, which makes it unique among low-level languages. This is achieved through its innovative ownership and borrowing system, which manages resources and memory at compile time, preventing common bugs like null pointer dereferences and buffer overflows.
In addition, what Rust does better than C++ is that Rust has a better and smarter compiler. In my own experience, Rust’s compiler rustc
provides comprehensive compile error message and actionable feedbacks to tell you how to debug. That’s really great for developer experience when it comes to debugging. Big thanks to whoever designed this.
It’s kinda trendy nowadays. People creating cool and better-performance tools by rewriting existing tools by Rust. Like, in web development, we have swc
, a compiler that perform better than Babel
, and Turbopack
, a rust-powered successor to Webpack
. Besides, Rust can also be used for writing performant servers and apps.
I think I’m a little bit FOMO but the trend is part of why I want to learn Rust. I also want to develop new backend languages in my skill set. I guess that’s another reason I want to learn Rust.
In this series, I used the by Google as learning resource. It is used for internal educating at first but recently released to general public. If you want to learn Rust, check out this great resource!
Syntax
Let’s start with a classic hello-world example
fn main() {
println!("Hello world!");
}
In this example:
- main function is entry point
- function is represented by
fn
println!
is a hygienic macro- What is Macros?
- Generate code at compile time
- Called with
!
- Used for reducing duplicated code
- What does hygenic mean?
- What is Macros?
Other basic syntax notes:
- Mind the type of integer:
i8
for 8 bits 2’s complemented representation (-128~127), it will overflow if the number is too large (either compile time error or runtime error could happen).- If runtime overflow, will panic at debug mode
cargo build
or wrap-around at release modecargo build --release
- If runtime overflow, will panic at debug mode
- Variables are immutable by default, if you want to make it mutable, add
mut
. - We can use
rustup doc
in terminal to search for document. Ex:rustup doc std::fmt
, it will open the browser and navigate tostd::fmt
’s documentaion. - Print something!: use
print!()
orprintln!()
print!("{}", x)
: This expression used positional parameters. Just like C++ and Python.print!("{x}", x=10)
: This kind of expression used name parameters. You provide the named arguments that declared in the string. If you did not provide, ex:print!("{x}")
, it will use the variablex
in the scope.
- For loop:
for i in 1..n
to iterate from 0 to n-1. It will stop ati == n-1
butfor i in 1..=n
will include the upper bound.
Types
- Signed integers:
i8
,i16
,i32
- Unsigned integers
u8
,u16
,u32
- Float:
f32
,f64
- Strings:
&str
- Unicode scalar values:
char
- Boolean:
bool
- Array:
[T; N]
- Note that the size of array N is part of the type
- Note that it can only be
print!
by debug formatter output{:?}
- Tuple:
()
.(T)
,(T1, T2)
, …- The
()
is likevoid
in other language. It’s special and known as “unit type” since the only value satisfy this type is()
itself.
- The
References:
Store a value’s memory address(also a form of borrowing). It’s similar to C++. In the code below, you create a mutable reference to x
. And use “dereference” operator *
to dereference the reference and mutate the value.
fn main() {
let mut x: i32 = 10;
let ref_x: &mut i32 = &mut x;
*ref_x = 20;
println!("x: {x}");
}
Some notes about reference
- Shared reference
&
: readonly access to data - Mutable reference
&mut
: read and write access to data - Need to dereference to mutate the value
- Only one mutable reference of a particular piece of data in a particular scope. But, you can have multiple shared reference of a piece of data in a particular scope.
- When
print!
a reference, rust automatically dereference the reference (Rust does this usually) - If you want to print the address, use
{:p}
formatting.
No dangling reference:
fn main() { let ref_x: &i32; { let x: i32 = 10; ref_x = &x; } println!("ref_x: {ref_x}"); }
The code above will result a compiling error. Look this this:
Compiling playground v0.0.1 (/playground) error[E0597]: `x` does not live long enough --> src/main.rs:5:17 | 4 | let x: i32 = 10; | - binding `x` declared here 5 | ref_x = &x; | ^^ borrowed value does not live long enough 6 | } | - `x` dropped here while still borrowed 7 | println!("ref_x: {ref_x}"); | ------- borrow later used here For more information about this error, try `rustc --explain E0597`. error: could not compile `playground` (bin "playground") due to previous error
- Reference is actually borrow the value it refers to
- If the owner doesn’t live long enough, and you use the reference, it will cause this error.
- The scope of a borrow is limited to where the reference is in scope. Once the reference goes out of scope, the borrow ends.
Slice is actually concept of reference and borrow. Slices borrow data from sliced type. In Rust, if you want to use slice of an array, you borrow by reference.
fn main() { let mut a: [i32; 6] = [10, 20, 30, 40, 50, 60]; println!("a: {a:?}"); let s: &[i32] = &a[2..4]; println!("s: {s:?}"); }
- Slice can be mutable reference or shared reference depends on how you create it.
&[T]
or&mut [T]
- Slice can be mutable reference or shared reference depends on how you create it.
Function
The last expression of a function becomes the return value. Simply omit the
;
at the end of expression.fn foo(){ let x: i8 = 1; return x } // is equivalent to fn foo(){ let x: i8 = 1; x }
use
///
above function to write document. It’s similar tojsdoc
. The document will be available ondocs.rs
for published crates. (the document is generated byrustdoc
tool)Methods are similar to Python’s method. It contains
self
which is a shared reference or mutable reference to the object.struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn inc_width(&mut self, delta: u32) { self.width += delta; } // static method fn new(width: u32, height: u32) -> Rectangle { Rectangle { width, height } } } fn main() { let mut rect = Rectangle { width: 10, height: 5 }; println!("old area: {}", rect.area()); rect.inc_width(5); println!("new area: {}", rect.area()); }
- If Method doesn’t contain
self
, it is calledstatic
method and can be called directly. Just like Python.
- If Method doesn’t contain