Rust
Part 2
Syntax/Semantics
Variable Binding
fn main() {
let x = 5;
}
let (x, y) = (1, 2);
let x: i32 = 5;
Immutable by default
let x = 5;
x = 10; // will error on compile
let mut x = 5;
x = 10
Primary Reason: Safety
If you forgot to say mut the compiler will catch it, and let you know that you have mutated something you may not have intended to mutate.
If bindings were mutable by default, the compiler would not be able to tell you this.
If you did intend mutation, then the solution is quite easy: add mut.
Initializing Bindings
fn main() {
let x: i32;
}
fn main() {
let x: i32;
let y = x + 5; // error
}
TL;DR bindings are required to be initialized with a value before you're allowed to use them.
Scope
fn main() {
let x: i32 = 17;
{
let y: i32 = 3;
}
let z = x + y // error
}
fn main() {
let x: i32 = 8;
{
// x is 8
let x = 12;
// x is 12
}
// x is 8
}
TL;DR Variable bindings have a scope - they are constrained to live in a block they were defined in.
Functions
fn foo() {
}
fn do_something(x: i32) {
}
fn inc(x: i32) -> i32 {
x + 1
}
you must declare the types of function arguments.
fn inc(x: i32) -> i32 {
return x + 1; // early return
x + 2 // never reached
}
Function pointers
let f: fn(i32) -> i32;
// without type inference
let f: fn(i32) -> i32 = inc;
// with type inference
let f = inc;
let six = f(5);
fn inc(x: i32) -> i32 {
x + 1
}
Primitive Types
let x = true;
let y: bool = false;
let x: char = 'x';
let two_hearts = '💕';
Booleans
char ( unicode )
Numeric types
let x: i8 = 3;
let x = 42; // x has type i32
let y = 1.0; // y has type f64
i8, i16, i32, i64,
Signed fixed size Integer types
u8, u16, u32, u64,
Unsigned fixed size Integer types
isize, usize
Variable sized types (Size dependent of the pointer size of the underlying machine)
f32, f64
Floating point types (IEEE-754 single and double)
Arrays
let a = [1, 2, 3];
let mut b: [i32; 3] = [1,2,3];
let c = [0; 20]; // initialize each element to 0
let a = [1, 2, 3];
let len = a.len()
let three = a[2];
Slices
let a = [0, 1, 2, 3, 4];
let complete = &a[..]; // A slice containing all of the elements in a
let middle = &a[1..4]; // A slice of a: only the elements 1, 2, and 3
A ‘slice’ is a reference to (or “view” into) another data structure. They are useful for allowing safe, efficient access to a portion of an array without copying.
Reference
I'll cover later
Tuples
let x = (1, false);
let x: (i32, bool) = (1, true);
let a = (1, 2, 3);
let (x, y, z) = a
let (x, y, z) = (1, 2, 3);
let tuple = (1, 2, 3);
let x = tuple.0;
let y = tuple.1;
let z = tuple.2;
If
let x = 5;
if x == 5 {
do_something();
} else {
do_something_else();
}
let x = 5;
if x == 5 {
do_something();
} else if x == 6 {
do_something();
}
let x = 5;
let y = if x == 5 { 10 } else { 15 };
Loops
loop {
do_something(); //repeat forever
}
let mut x = 5;
let mut done = false;
while !done {
x += 2;
if x % 5 == 0 {
done = true;
}
}
for (i,j) in (5..10).enumerate() {
do_something(i, j);
}
let lines = "hello\nworld".lines();
for (linenumber, line) in lines.enumerate() {
do_something(line_number, line);
}
Structs
struct Point {
x: i32,
y: i32,
}
fn main() {
let origin = Point { x: 0, y: 0 };
println!("The origin is at ({}, {})", origin.x, origin.y);
}
struct Point {
x: i32,
y: i32,
}
fn main() {
let mut point = Point { x: 0, y: 0 };
point.x = 5;
println!("The point is at ({}, {})", point.x, point.y);
}
struct Point3d {
x: i32,
y: i32,
z: i32,
}
let mut point = Point3d { x: 0, y: 0, z: 0 };
point = Point3d { y: 1, .. point };
Structs
struct Colour(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Colour(0, 0, 0);
let origin = Point(0, 0, 0);
struct Circle {
x: f64,
y: f64,
radius: f64,
}
impl Circle {
fn area(&self) -> f64 {
3.14 * (self.radius * self.radius)
}
}
fn main() {
let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
println!("{}", c.area());
}
Enums
enum Message {
Quit,
ChangeColor(i32, i32, i32),
Move { x: i32, y: i32 },
Write(String),
}
let x: Message = Message::Move { x: 3, y: 4 };
let v = vec!["Hello".to_string(), "World".to_string()];
let v1 = v.into_iter().map(Message::Write).collect();
// v1 is a vec of [Message::Write("Hello"), Message::Write("World")]
Enum constructor example.
Match
let number = 3
let size = match number {
0 => "none",
2 | 3 => "tiny",
4...7 => "small",
8...20 => "medium",
_ => "large"
};
enum Message {
Quit,
ChangeColor(i32, i32, i32),
Move { x: i32, y: i32 },
Write(String),
}
fn quit() { /* ... */ }
fn change_color(r: i32, g: i32, b: i32) { /* ... */ }
fn move_cursor(x: i32, y: i32) { /* ... */ }
fn process_message(msg: Message) {
match msg {
Message::Quit => quit(),
Message::ChangeColor(r, g, b) => change_color(r, g, b),
Message::Move { x: x, y: y } => move_cursor(x, y),
Message::Write(s) => println!("{}", s),
};
}
The Rust compiler checks exhaustiveness, so it demands that you have a match arm for every variant of the enum. If you leave one off, it will give you a compile-time error unless you use _ or provide all possible arms.
Match
let pair = (1,2);
let smaller = match pair {
(x, y) if x < y => x,
(_, y) => y
};
match point {
Point {x: 2...6, y: -1...5} => println!("I like this point"),
_ => println!("I do not like this point")
}
Generics
enum Option<T> {
Some(T),
None,
}
let x: Option<i32> = Some(5);
fn takes_anything<T>(x: T) {
// do something with x
}
fn takes_two_of_the_same_things<T>(x: T, y: T) {
// ...
}
fn takes_two_things<T, U>(x: T, y: U) {
// ...
}
struct Point<T> {
x: T,
y: T,
}
let int_origin = Point { x: 0, y: 0 };
let float_origin = Point { x: 0.0, y: 0.0 };
Traits
trait Pointy {
fn poke(&self, at: &str);
// One can also give full function bodies here
}
impl Pointy for Point {
fn poke(&self, at: &str) {
println!("Poked {}", at);
}
}
fn poke_forever<T: Pointy>(pointy: T, at: &str) {
loop {
pointy.poke(at)
}
}
Core Concepts
Ownership
Variable bindings have ownership of what they're bound to.
let v = vec![1, 2, 3];
let v2 = v;
println!("v[0] is: {}", v[0]); // error
fn take(v: Vec<i32>) {
// what happens here isn’t important.
}
let v = vec![1, 2, 3];
take(v);
println!("v[0] is: {}", v[0]); // error
Move semantics
fn helper(value: Vec<i32>) {
println!("The list was: {}", value);
}
fn main() {
let list = vec![1,2,3];
helper(list); // move by default
helper(list); // error
}
fn helper(value: Vec<i32>) -> Vec<i32> {
println!("The list was: {}", value);
value
}
fn main() {
let list = vec![1,2,3];
let a = helper(list); // transfer ownership and acquire ownership of return value
}
Why
Rust allocates memory for an integer on the stack, copies the bit pattern representing the value of 10 to the allocated memory and binds the variable name x to this memory region for future reference.
let x = 10;
Rust allocates memory for the vector object v on the stack like it does for x above. But it also allocates some memory on the heap for the actual data ([1, 2, 3]).
Rust then copies the address of this heap allocation to a pointer in the vector object placed on the stack.
let v = vec![1, 2, 3];
Why
When we move v to v2 Rust does a bitwise copy of the vector object representing v into the stack allocation represented by v2.
This shallow copy does not actually create a copy of the heap allocation containing the actual data.
So now there are two pointers pointing to the same memory allocation on the heap.
let v = vec![1, 2, 3];
let mut v2 = v;
Why
and v were still accessible we'd end up with an invalid vector since v would not know that the heap data has been truncated.
Now vector v on the stack does not agree with the corresponding part on the heap.
v still thinks there are three elements in the vector and will happily let us access the non existent element
It’s also important to note that optimizations may remove the actual copy of the bytes on the stack, depending on circumstances. So it may not be as inefficient as it initially sounds.
v2.truncate(2);
Now for example if we truncated the vector to just two elements through v2:
int main() {
int *slot = malloc(sizeof(int));
*slot = 3;
helper(slot);
helper(slot); // use after free!
}
void helper(int *slot) {
printf("The number was: %d\n", *slot);
free(slot);
}
The number was: 3
The number was: 0
*** Error in `./a.out': double free or corruption (fasttop): 0x00000000024f9010 ***
======= Backtrace: =========
/usr/lib/libc.so.6(+0x6f364)[0x7fc0de654364]
/usr/lib/libc.so.6(+0x74d96)[0x7fc0de659d96]
/usr/lib/libc.so.6(+0x7557e)[0x7fc0de65a57e]
./a.out[0x4005ee]
./a.out[0x4005b8]
/usr/lib/libc.so.6(__libc_start_main+0xf0)[0x7fc0de605710]
./a.out[0x4004a9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:02 5058341 /home/anthony/a.out
00600000-00601000 rw-p 00000000 08:02 5058341 /home/anthony/a.out
024f9000-0251a000 rw-p 00000000 00:00 0 [heap]
7fc0d8000000-7fc0d8021000 rw-p 00000000 00:00 0
7fc0d8021000-7fc0dc000000 ---p 00000000 00:00 0
7fc0de3cf000-7fc0de3e5000 r-xp 00000000 08:02 7343550 /usr/lib/libgcc_s.so.1
7fc0de3e5000-7fc0de5e4000 ---p 00016000 08:02 7343550 /usr/lib/libgcc_s.so.1
7fc0de5e4000-7fc0de5e5000 rw-p 00015000 08:02 7343550 /usr/lib/libgcc_s.so.1
7fc0de5e5000-7fc0de77d000 r-xp 00000000 08:02 7343235 /usr/lib/libc-2.23.so
7fc0de77d000-7fc0de97c000 ---p 00198000 08:02 7343235 /usr/lib/libc-2.23.so
7fc0de97c000-7fc0de980000 r--p 00197000 08:02 7343235 /usr/lib/libc-2.23.so
7fc0de980000-7fc0de982000 rw-p 0019b000 08:02 7343235 /usr/lib/libc-2.23.so
7fc0de982000-7fc0de986000 rw-p 00000000 00:00 0
7fc0de986000-7fc0de9a9000 r-xp 00000000 08:02 7343234 /usr/lib/ld-2.23.so
7fc0deb82000-7fc0deb85000 rw-p 00000000 00:00 0
7fc0deba8000-7fc0deba9000 rw-p 00000000 00:00 0
7fc0deba9000-7fc0debaa000 r--p 00023000 08:02 7343234 /usr/lib/ld-2.23.so
7fc0debaa000-7fc0debab000 rw-p 00024000 08:02 7343234 /usr/lib/ld-2.23.so
7fc0debab000-7fc0debac000 rw-p 00000000 00:00 0
7fff8ace4000-7fff8ad05000 rw-p 00000000 00:00 0 [stack]
7fff8ad75000-7fff8ad77000 r--p 00000000 00:00 0 [vvar]
7fff8ad77000-7fff8ad79000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
[1] 13833 abort (core dumped) ./a.out
References and
Borrowing
Borrowing
fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
// do stuff with v1 and v2
// hand back ownership, and the result of our function
(v1, v2, 42)
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let (v1, v2, answer) = foo(v1, v2);
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
// do stuff with v1 and v2
42
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let answer = foo(&v1, &v2);
Rules for borrowing/references
1.
References
Rust
By ..
Rust
- 1,873