0% found this document useful (0 votes)
2 views12 pages

Tutorial Part4 Rust

Part 4 covers Rust, a systems programming language known for its memory safety and performance. It discusses installation, basic syntax, ownership, borrowing, control flow, functions, structs, enums, and error handling. Rust's unique features, like its ownership model and the Result type for error management, make it a powerful choice for developers.

Uploaded by

Oulfa
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views12 pages

Tutorial Part4 Rust

Part 4 covers Rust, a systems programming language known for its memory safety and performance. It discusses installation, basic syntax, ownership, borrowing, control flow, functions, structs, enums, and error handling. Rust's unique features, like its ownership model and the Result type for error management, make it a powerful choice for developers.

Uploaded by

Oulfa
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Part 4: Rust

In Part 4, we explore Rust, a modern systems programming language that has been voted the most loved
language in the Stack Overflow Developer Survey for many consecutive years. Sponsored by Mozilla and first
released in 2010, Rust was designed to provide the performance and control of C and C++ while guaranteeing
memory safety and thread safety at compile time. Rust's unique ownership model eliminates entire classes of bugs
(null pointer dereferences, data races, use-after-free) without a garbage collector. It is used in Firefox, Discord,
Cloudflare, Microsoft, and increasingly in the Linux kernel.

4.1 Setting Up Rust


The standard way to install Rust is via rustup, the Rust toolchain installer. It manages Rust versions and
associated tools. Rust comes with cargo, a build system and package manager that handles compilation,
dependencies, testing, and documentation. Together, rustc (the compiler), cargo, and standard library form the
Rust toolchain.
# Install Rust (Linux/macOS)
$ curl --proto '=https' --tlsv1.2 -sSf [Link] | sh

# On Windows, download [Link] from [Link]

# Verify installation
$ rustc --version
rustc 1.75.0

$ cargo --version
cargo 1.75.0

# Create a new project


$ cargo new myproject
$ cd myproject
$ cargo run
Compiling myproject v0.1.0
Finished dev [unoptimized + debuginfo] target(s)
Running `target/debug/myproject`
Hello, world!

# Project structure:
# myproject/
# [Link] (manifest: dependencies and metadata)
# src/
# [Link] (entry point)
# target/ (build output, gitignored)

4.2 Hello World


Rust code is organized into modules and crates. The main function is the entry point. The println! macro (note the
exclamation mark) prints to the console with formatting similar to C's printf but with compile-time format checking.
Macros are distinguished from functions by the ! suffix.
fn main() {
println!("Hello, World!");

Page 1
Part 4: Rust

// Formatted printing
let name = "Alice";
let age = 30;
println!("{} is {} years old", name, age);
println!("{name} is {age} years old"); // inline format (Rust 1.58+)

// Debug formatting
let nums = vec![1, 2, 3];
println!("{:?}", nums); // [1, 2, 3]
println!("{:#?}", nums); // pretty-print

// Other formatting
println!("{:#b}", 42); // binary: 101010
println!("{:#x}", 255); // hex: 0xff
println!("{:>10}", "right"); // right-aligned
println!("{:.2}", 3.14159); // 2 decimals: 3.14
}

4.3 Variables and Data Types


Rust variables are immutable by default. Use mut to make them mutable. Constants (const) are always immutable
and must have their type specified. Rust is statically typed with type inference. Common types include i32, i64,
u32, f64, bool, char (4-byte Unicode), str (string slice), String (heap-allocated), tuples, and arrays (fixed-size).
fn main() {
// Immutable by default
let x = 5;
// x = 6; // Error: cannot assign to immutable variable

// Mutable
let mut y = 10;
y = 20; // OK

// Constants (compile-time, must have type and value)


const MAX_POINTS: u32 = 100_000;

// Shadowing (reuse a variable name)


let z = 5;
let z = z + 1; // shadow with new value
let z = z * 2; // shadow again: now 12
let z = "text"; // can change type when shadowing!

// Integer types: i8, i16, i32, i64, i128, isize


// Unsigned: u8, u16, u32, u64, u128, usize
let a: i32 = -42;
let b: u64 = 42;
let c: usize = 100; // pointer-sized

// Float types: f32, f64

Page 2
Part 4: Rust

let pi: f64 = 3.14159;

// Boolean
let is_ready: bool = true;

// Character (4-byte Unicode scalar value)


let letter: char = 'A';
let emoji: char = '\u{1F600}'; // smiley

// Tuple (fixed-size, mixed types)


let tup: (i32, f64, char) = (500, 6.4, 'Z');
let (t1, t2, t3) = tup; // destructuring
println!("{} {} {}", tup.0, tup.1, tup.2);

// Array (fixed-size, stack-allocated, same type)


let arr = [1, 2, 3, 4, 5];
let arr2: [i32; 3] = [0, 0, 0];
let zeros = [0; 10]; // 10 zeros
println!("first: {}", arr[0]);
}

4.4 Ownership and Borrowing


Ownership is Rust's most distinctive feature and the key to its memory safety guarantees. Every value has exactly
one owner. When the owner goes out of scope, the value is dropped (freed). You can borrow a value by taking a
reference (immutable with & or mutable with &mut). The compiler enforces the borrowing rules: one mutable
reference OR any number of immutable references, but never both at the same time. This prevents data races at
compile time, with zero runtime cost.
fn main() {
// Ownership: String is heap-allocated, moves by default
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// println!("{}", s1); // ERROR: s1 no longer valid
println!("{}", s2); // OK

// Clone (explicit deep copy)


let s3 = [Link](); // s3 and s2 are independent
println!("{} {}", s2, s3);

// Copy types (integers, floats, bools, chars are copied)


let n1 = 5;
let n2 = n1; // n1 is COPIED (i32 implements Copy)
println!("{} {}", n1, n2); // both valid

// Immutable borrow
let s = String::from("hello");
let len = calculate_length(&s); // borrow s
println!("'{}' has length {}", s, len); // s still valid

Page 3
Part 4: Rust

// Mutable borrow
let mut s4 = String::from("hello");
append_world(&mut s4);
println!("{}", s4); // hello world

// Multiple immutable borrows OK


let r1 = &s4;
let r2 = &s4;
println!("{} {}", r1, r2); // OK: multiple immutable

// But NOT a mutable borrow while immutable borrows exist


// let r3 = &mut s4; // ERROR: cannot borrow mutable
// because r1, r2 still in scope (until last use)

// Slices (borrow a contiguous sequence)


let s5 = String::from("hello world");
let hello = &s5[0..5]; // &str slice
let world = &s5[6..11];
println!("{} {}", hello, world);
}

fn calculate_length(s: &String) -> usize {


[Link]()
} // s goes out of scope but is NOT dropped (it's a borrow)

fn append_world(s: &mut String) {


s.push_str(" world");
}

// Rules of borrowing:
// 1. At any given time, either ONE mutable reference
// OR any number of immutable references.
// 2. References must always point to valid data.
// 3. A mutable reference must be the only reference
// to that data (no aliasing + mutation).

4.5 Control Flow


Rust control flow includes if expressions (they return values), loop (infinite loop with break value), while, for, and
match (exhaustive pattern matching). The match expression is extremely powerful, combining comparison with
destructuring. Rust does not have a traditional switch statement; match replaces it and is much more expressive.
fn main() {
// if is an expression (returns a value)
let x = 5;
let result = if x > 3 { "big" } else { "small" };
println!("{}", result); // big

// loop (infinite, use break)


let mut count = 0;

Page 4
Part 4: Rust

loop {
count += 1;
if count >= 3 {
break; // or break value; to return from loop
}
}

// loop with return value


let mut n = 0;
let doubled = loop {
n += 1;
if n == 5 {
break n * 2; // returns 10
}
};
println!("doubled: {}", doubled); // 10

// while loop
let mut num = 3;
while num > 0 {
println!("{}!", num);
num -= 1;
}

// for loop (iterating ranges and collections)


for i in 0..5 {
println!("iter {}", i); // 0, 1, 2, 3, 4
}
for i in 0..=5 {
println!("{}", i); // 0 through 5 (inclusive)
}

let fruits = vec!["apple", "banana", "cherry"];


for fruit in [Link]() {
println!("{}", fruit);
}
for (index, fruit) in [Link]().enumerate() {
println!("{}: {}", index, fruit);
}

// match (exhaustive pattern matching)


let coin = Coin::Penny;
let value = match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
};
println!("Coin value: {}", value);

Page 5
Part 4: Rust

// Match with destructuring


let point = (3, -4);
match point {
(0, 0) => println!("origin"),
(x, 0) => println!("x-axis: {}", x),
(0, y) => println!("y-axis: {}", y),
(x, y) if x > 0 && y > 0 => println!("Q1"),
(x, y) => println!("({}, {})", x, y),
}

// if let (for single pattern)


let some_val = Some(5);
if let Some(v) = some_val {
println!("got {}", v);
} else {
println!("nothing");
}
}

enum Coin { Penny, Nickel, Dime, Quarter }

4.6 Functions and Structs


Rust functions are declared with fn. Parameters need type annotations. Return types follow ->. Rust structs group
related fields. Methods are defined in impl blocks. Rust has three struct types: named-field structs, tuple structs
(C-style), and unit structs (no fields, useful for traits).
fn main() {
let rect = Rectangle::new(5.0, 3.0);
println!("Area: {}", [Link]());
println!("{}", rect); // uses Display trait

let mut square = Rectangle::new(4.0, 4.0);


[Link](2.0);
println!("After scale: {:?}", square); // Debug trait
}

// Struct definition
#[derive(Debug)] // auto-implement Debug trait for {:?}
struct Rectangle {
width: f64,
height: f64,
}

// Associated functions (like static methods)


impl Rectangle {
// Constructor (convention, not built-in)
fn new(width: f64, height: f64) -> Self {
Self { width, height }
}

Page 6
Part 4: Rust

// Method (takes &self for immutable access)


fn area(&self) -> f64 {
[Link] * [Link]
}

// Mutable method (takes &mut self)


fn scale(&mut self, factor: f64) {
[Link] *= factor;
[Link] *= factor;
}
}

// Implement Display trait for {}


use std::fmt;
impl fmt::Display for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Rectangle({} x {})", [Link], [Link])
}
}

// Tuple struct
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
println!("R: {}", red.0);

// Unit struct (no data, useful for traits)


struct AlwaysEqual;

4.7 Enums and Option


Rust enums are algebraic data types that can hold data in each variant. They are far more powerful than C/Java
enums. The Option enum encodes the presence or absence of a value, replacing null. Rust does not have null.
Instead, Option<T> is either Some(value) or None. This forces explicit handling of absence, eliminating null pointer
errors. Result<T, E> is similar but for error handling.
fn main() {
// Enum with data
let msg = Message::Write(String::from("hello"));
match msg {
Message::Quit => println!("quit"),
Message::Move { x, y } => println!("move to {}, {}", x, y),
Message::Write(text) => println!("write: {}", text),
Message::ChangeColor(r, g, b) => println!("rgb: {} {} {}", r, g, b),
}

// Option: Rust's replacement for null


let some_number: Option<i32> = Some(42);
let no_number: Option<i32> = None;

Page 7
Part 4: Rust

// Must handle both cases (compiler enforces this)


match some_number {
Some(n) => println!("got {}", n),
None => println!("nothing"),
}

// Option methods
let doubled = some_number.map(|n| n * 2); // Some(84)
let unwrapped = some_number.unwrap_or(0); // 42
let or_default = no_number.unwrap_or_default(); // 0

// if let for simpler Option handling


if let Some(n) = some_number {
println!("value is {}", n);
}

// Result: Rust's error handling (no exceptions)


let result: Result<i32, String> = Ok(42);
let error: Result<i32, String> = Err("something failed".to_string());

match result {
Ok(val) => println!("success: {}", val),
Err(e) => println!("error: {}", e),
}

// The ? operator propagates errors


let num = parse_and_double("21").unwrap();
println!("{}", num); // 42
}

enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // named fields
Write(String), // single value
ChangeColor(u8, u8, u8), // tuple of values
}

// ? operator: returns early on Err


fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
let n: i32 = [Link]()?; // returns Err on failure
Ok(n * 2)
}

4.8 Error Handling


Rust handles errors through the Result type rather than exceptions. Functions that can fail return Result<T, E>.
The ? operator provides concise error propagation: it returns the value if Ok, or early-returns the error if Err. This
makes error handling explicit yet ergonomic. The panic! macro is for unrecoverable errors and crashes the
program.

Page 8
Part 4: Rust

use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;

fn main() {
// Propagating errors with ?
match read_config() {
Ok(content) => println!("Config: {}", content),
Err(e) => eprintln!("Error: {}", e),
}

// Custom error types


#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
Custom(String),
}

// Implement From for automatic conversions with ?


impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

// Now ? works with multiple error types


fn load_data() -> Result<i32, AppError> {
let mut file = File::open("[Link]")?; // io::Error -> AppError
let mut s = String::new();
file.read_to_string(&mut s)?;
let n: i32 = [Link]().parse()?; // ParseIntError -> AppError
Ok(n)
}

// panic for unrecoverable errors


// panic!("something went terribly wrong");
}

// Function that can fail


fn read_config() -> Result<String, io::Error> {
let mut file = File::open("[Link]")?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}

// unwrap/expect (panic on error, use only when sure)


let x: i32 = "42".parse().unwrap(); // panics if not a number

Page 9
Part 4: Rust

let y: i32 = "42".parse().expect("should be a number");

4.9 Traits and Generics


Traits are Rust's equivalent of interfaces or typeclasses. They define shared behavior. You implement traits for
types. Traits can have default methods. Generics let you write code that works with any type implementing a trait
(trait bounds). This is how Rust achieves polymorphism without virtual dispatch overhead (monomorphization).
fn main() {
let tweet = Tweet { username: "alice".to_string(), content: "hello".to_string() };
let article = NewsArticle { headline: "Rust 2.0".to_string(), content: "...".to_string() };
println!("{}", [Link]());
println!("{}", [Link]());
notify(&tweet);
notify(&article);

// Generic function
let largest_int = largest(&[1, 5, 3, 9, 2]);
let largest_char = largest(&['a', 'z', 'm']);
println!("{} {}", largest_int, largest_char);
}

// Trait definition
trait Summary {
fn summarize(&self) -> String;
fn summarize_author(&self) -> String {
String::from("(unknown)") // default method
}
}

struct Tweet { username: String, content: String }


struct NewsArticle { headline: String, content: String }

impl Summary for Tweet {


fn summarize(&self) -> String {
format!("@{}: {}", [Link], [Link])
}
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}: {}", [Link], [Link])
}
}

// Trait bound (generic function)


fn notify(item: &impl Summary) {
println!("Breaking: {}", [Link]());
}

// Equivalent explicit form

Page 10
Part 4: Rust

fn notify_explicit<T: Summary>(item: &T) {


println!("Breaking: {}", [Link]());
}

// Generic with multiple bounds


fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}

4.10 Concurrency
Rust's ownership model makes concurrent programming safe. The compiler prevents data races at compile time.
The Send trait marks types safe to transfer between threads. The Sync trait marks types safe to share between
threads via references. Rust channels provide message passing (mpsc). Mutex and Arc provide shared state with
automatic, safe access.
use std::sync::{Arc, Mutex};
use std::thread;
use std::sync::mpsc;

fn main() {
// Spawning threads
let handle = thread::spawn(|| {
for i in 0..5 {
println!("spawned thread: {}", i);
}
});
for i in 0..3 {
println!("main thread: {}", i);
}
[Link]().unwrap(); // wait for thread to finish

// Message passing (channels)


let (tx, rx) = mpsc::channel();
let sender = thread::spawn(move || {
let vals = vec!["hi", "from", "thread"];
for val in vals {
[Link](val.to_string()).unwrap();
}
});
for received in rx {
println!("Got: {}", received);
}
[Link]().unwrap();

Page 11
Part 4: Rust

// Shared state with Mutex + Arc


let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
[Link](thread::spawn(move || {
let mut num = [Link]().unwrap();
*num += 1;
}));
}
for h in handles { [Link]().unwrap(); }
println!("Counter: {}", *[Link]().unwrap()); // 10
}

// Thread safety is enforced by the compiler:


// - Send: type can be transferred to another thread
// - Sync: &T can be shared between threads
// Arc (Atomic Reference Counted) enables shared ownership
// Mutex provides interior mutability with locking

4.11 Summary of Part 4


We covered Rust fundamentals: installation with rustup and cargo, variables and types, ownership and borrowing
(Rust's key innovation), control flow including pattern matching, structs and methods, enums with Option and
Result, error handling with the ? operator, traits and generics, and safe concurrency. Rust's learning curve is steep
but rewarding: the compiler catches memory and concurrency bugs before your code ever runs. In Part 5, we
explore Haskell, a purely functional language that will change how you think about programming.

Page 12

You might also like