Rust 101 Practical Examples
Section 1-2: Variables & Types
Immutable by default - THE mental shift
fn main() {
let ip_address = "10.50.1.50"; // immutable - can't change
let mut connection_count = 0; // mut = mutable
connection_count += 1; // OK - it's mutable
// ip_address = "10.50.1.51"; // ERROR - not mutable
println!("DC at {} has {} connections", ip_address, connection_count);
}
Shadowing - same name, new binding
fn main() {
let vlan = "10"; // String slice
let vlan: u16 = vlan.parse().unwrap(); // Shadow with new type
let vlan = vlan + 100; // Shadow again: 110
println!("VLAN: {}", vlan);
}
Type annotations - when Rust can’t infer
fn main() {
let port: u16 = 443; // Explicit type
let hosts: Vec<&str> = vec!["kvm-01", "kvm-02", "kvm-03"];
let active: bool = true;
// Rust infers these:
let timeout = 30; // i32 by default
let ratio = 0.75; // f64 by default
}
Section 3: Functions
Function with return value
fn is_private_ip(ip: &str) -> bool {
ip.starts_with("10.") ||
ip.starts_with("192.168.") ||
ip.starts_with("172.16.")
}
fn main() {
let server = "10.50.1.20";
if is_private_ip(server) {
println!("{} is RFC1918", server);
}
}
Multiple return values with tuple
fn parse_endpoint(endpoint: &str) -> (&str, u16) {
let parts: Vec<&str> = endpoint.split(':').collect();
let host = parts[0];
let port: u16 = parts[1].parse().unwrap_or(443);
(host, port)
}
fn main() {
let (host, port) = parse_endpoint("ise-01.inside.domusdigitalis.dev:443");
println!("Connecting to {} on port {}", host, port);
}
Section 4: Control Flow
Match expression - Rust’s powerful switch
fn get_service_port(service: &str) -> u16 {
match service {
"ssh" => 22,
"dns" => 53,
"http" => 80,
"https" => 443,
"ldap" => 389,
"ldaps" => 636,
"radius" => 1812,
_ => 0, // Default case (required!)
}
}
fn main() {
let port = get_service_port("ldaps");
println!("LDAPS runs on port {}", port);
}
If let - when you only care about one case
fn main() {
let config_line = Some("AuthenticationMethods publickey");
if let Some(line) = config_line {
println!("Found: {}", line);
}
// No else needed - we only care about Some
}
Section 5: Ownership & Borrowing (THE BIG ONE)
DIAGRAM THIS: Ownership moves
fn main() {
let hostname = String::from("kvm-01"); // hostname OWNS the String
let server = hostname; // Ownership MOVES to server
// println!("{}", hostname); // ERROR! hostname is gone
println!("{}", server); // OK - server owns it now
}
STACK HEAP
┌─────────────┐ ┌───────────────┐
│ hostname │────────►│ "kvm-01" │
│ ptr, len, cap│ └───────────────┘
└─────────────┘
│
▼ MOVE
┌─────────────┐ ┌───────────────┐
│ server │────────►│ "kvm-01" │
│ ptr, len, cap│ └───────────────┘
└─────────────┘
(hostname is now invalid)
DIAGRAM THIS: Borrowing with &
fn print_host(host: &String) { // Borrows, doesn't take ownership
println!("Host: {}", host);
} // Borrow ends, nothing dropped
fn main() {
let hostname = String::from("kvm-01");
print_host(&hostname); // Lend it out
print_host(&hostname); // Can lend again!
println!("Still own: {}", hostname); // Still valid
}
STACK HEAP
┌─────────────┐ ┌───────────────┐
│ hostname │────────►│ "kvm-01" │
│ ptr, len, cap│ └───────────────┘
└─────────────┘ ▲
│ │
▼ BORROW (&) │
┌─────────────┐ │
│ host │───────────────┘
│ (reference) │ (points to same data)
└─────────────┘
(hostname still valid!)
DIAGRAM THIS: Mutable borrow
fn add_domain(host: &mut String) {
host.push_str(".inside.domusdigitalis.dev");
}
fn main() {
let mut hostname = String::from("kvm-01");
add_domain(&mut hostname); // Mutable borrow
println!("{}", hostname); // kvm-01.inside.domusdigitalis.dev
}
The Rules (memorize these)
OWNERSHIP RULES:
1. Each value has exactly ONE owner
2. When owner goes out of scope, value is dropped
3. Ownership can be moved or borrowed
BORROWING RULES:
1. Many immutable borrows (&T) OR one mutable borrow (&mut T)
2. References must always be valid (no dangling)
Section 6: Structs
Network device struct
struct NetworkDevice {
hostname: String,
ip: String,
vlan: u16,
active: bool,
}
impl NetworkDevice {
// Constructor (convention: new)
fn new(hostname: &str, ip: &str, vlan: u16) -> Self {
NetworkDevice {
hostname: hostname.to_string(),
ip: ip.to_string(),
vlan,
active: true,
}
}
// Method (takes &self)
fn fqdn(&self) -> String {
format!("{}.inside.domusdigitalis.dev", self.hostname)
}
// Method that modifies (takes &mut self)
fn disable(&mut self) {
self.active = false;
}
}
fn main() {
let mut switch = NetworkDevice::new("sw-01", "10.50.1.10", 1);
println!("FQDN: {}", switch.fqdn());
switch.disable();
println!("Active: {}", switch.active);
}
Section 7: Enums & Pattern Matching
Connection state enum
enum ConnectionState {
Disconnected,
Connecting { attempt: u8 },
Connected { session_id: String },
Failed { error: String },
}
fn handle_state(state: &ConnectionState) {
match state {
ConnectionState::Disconnected => {
println!("Not connected");
}
ConnectionState::Connecting { attempt } => {
println!("Connecting... attempt {}", attempt);
}
ConnectionState::Connected { session_id } => {
println!("Connected: {}", session_id);
}
ConnectionState::Failed { error } => {
println!("Failed: {}", error);
}
}
}
fn main() {
let state = ConnectionState::Connected {
session_id: "sess-abc123".to_string()
};
handle_state(&state);
}
Section 8: Error Handling
Result type - no exceptions in Rust
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}
fn main() {
match read_config("/etc/hosts") {
Ok(contents) => println!("Read {} bytes", contents.len()),
Err(e) => println!("Failed to read: {}", e),
}
}
The ? operator - propagate errors up
use std::fs;
use std::io;
fn get_hostname() -> Result<String, io::Error> {
let contents = fs::read_to_string("/etc/hostname")?; // ? = return Err if fails
Ok(contents.trim().to_string())
}
fn main() -> Result<(), io::Error> {
let hostname = get_hostname()?;
println!("Hostname: {}", hostname);
Ok(())
}
Option type - nullable done right
fn find_port(service: &str) -> Option<u16> {
match service {
"ssh" => Some(22),
"https" => Some(443),
_ => None,
}
}
fn main() {
// Handle None explicitly
if let Some(port) = find_port("ssh") {
println!("SSH port: {}", port);
}
// Or provide default
let port = find_port("unknown").unwrap_or(8080);
println!("Using port: {}", port);
}
Section 9: Collections
Vec - growable array
fn main() {
let mut servers: Vec<&str> = Vec::new();
servers.push("kvm-01");
servers.push("kvm-02");
servers.push("kvm-03");
// Iterate
for server in &servers {
println!("Server: {}", server);
}
// Access by index
if let Some(first) = servers.get(0) {
println!("First: {}", first);
}
println!("Total: {} servers", servers.len());
}
HashMap - key-value store
use std::collections::HashMap;
fn main() {
let mut ports: HashMap<&str, u16> = HashMap::new();
ports.insert("ssh", 22);
ports.insert("https", 443);
ports.insert("ldaps", 636);
// Lookup
if let Some(&port) = ports.get("ldaps") {
println!("LDAPS: {}", port);
}
// Iterate
for (service, port) in &ports {
println!("{} -> {}", service, port);
}
}
Section 10: Traits
Define shared behavior
trait Pingable {
fn ping(&self) -> bool;
fn address(&self) -> &str;
}
struct Server {
hostname: String,
ip: String,
}
struct Switch {
name: String,
mgmt_ip: String,
}
impl Pingable for Server {
fn ping(&self) -> bool {
println!("Pinging server {}", self.ip);
true
}
fn address(&self) -> &str {
&self.ip
}
}
impl Pingable for Switch {
fn ping(&self) -> bool {
println!("Pinging switch {}", self.mgmt_ip);
true
}
fn address(&self) -> &str {
&self.mgmt_ip
}
}
// Function that works on ANY Pingable
fn check_device(device: &impl Pingable) {
println!("Checking {}", device.address());
if device.ping() {
println!("OK");
}
}
fn main() {
let srv = Server { hostname: "kvm-01".into(), ip: "10.50.1.100".into() };
let sw = Switch { name: "sw-01".into(), mgmt_ip: "10.50.1.10".into() };
check_device(&srv);
check_device(&sw);
}
Diagram Templates
Use these ASCII patterns for your diagrams:
Stack vs Heap
STACK HEAP
┌─────────────┐ ┌───────────────┐
│ variable │────────►│ actual data │
│ ptr,len,cap │ │ on heap │
└─────────────┘ └───────────────┘
Ownership Move
let a = String::from("x");
let b = a; // MOVE
BEFORE AFTER
┌────┐ ┌────┐
│ a │──►data │ a │ (invalid)
└────┘ └────┘
┌────┐
│ b │──►data
└────┘
Borrow
let a = String::from("x");
let b = &a; // BORROW
┌────┐ ┌──────┐
│ a │────────►│ data │
└────┘ └──────┘
▲ ▲
│ │
┌────┐ │
│ b │─────────────┘
└────┘ (reference)
Scope and Drop
{ // Scope starts
let x = String::new(); // x created
// ... use x ...
} // Scope ends: x DROPPED, memory freed