39. Trait Upcasting — Cast dyn Trait to dyn Supertrait

Since Rust 1.86, you can upcast a trait object to its supertrait — no workarounds needed.

The problem

Imagine you have a trait hierarchy:

1
2
3
4
5
use std::any::Any;

trait Animal: Any {
    fn name(&self) -> &str;
}

Before Rust 1.86, if you had a &dyn Animal, you couldn’t simply cast it to &dyn Any. You’d have to add an explicit method to your trait:

1
2
3
4
5
// The old workaround — adding a method just to upcast
trait Animal: Any {
    fn name(&self) -> &str;
    fn as_any(&self) -> &dyn Any;
}

This was boilerplate that every trait hierarchy had to carry around.

The fix: trait upcasting

Now you can coerce a trait object directly to any of its supertraits:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use std::any::Any;

trait Animal: Any {
    fn name(&self) -> &str;
}

struct Dog;

impl Animal for Dog {
    fn name(&self) -> &str {
        "Rex"
    }
}

fn print_if_dog(animal: &dyn Animal) {
    // Upcast to &dyn Any — just works!
    let any: &dyn Any = animal;

    if let Some(dog) = any.downcast_ref::<Dog>() {
        println!("Good boy, {}!", dog.name());
    } else {
        println!("Not a dog.");
    }
}

fn main() {
    let dog = Dog;
    print_if_dog(&dog);
}

The key line is let any: &dyn Any = animal; — this coercion from &dyn Animal to &dyn Any used to be a compiler error and now just works.

It works with any supertrait chain

Upcasting isn’t limited to Any. It works for any supertrait relationship:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
trait Drawable {
    fn draw(&self);
}

trait Widget: Drawable {
    fn click(&self);
}

fn draw_it(widget: &dyn Widget) {
    // Coerce &dyn Widget → &dyn Drawable
    let drawable: &dyn Drawable = widget;
    drawable.draw();
}

This makes trait object hierarchies much more ergonomic. No more as_drawable() helper methods cluttering your traits.

38. #[must_use] — Never Ignore What Matters

Rust’s #[must_use] attribute turns silent bugs into compile-time warnings — making sure important return values never get accidentally ignored.

The Problem: Silently Ignoring Results

Here’s a classic bug that can haunt any codebase:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fn remove_expired_tokens(tokens: &mut Vec<String>) -> usize {
    let before = tokens.len();
    tokens.retain(|t| !t.starts_with("exp_"));
    before - tokens.len()
}

fn main() {
    let mut tokens = vec![
        "exp_abc".to_string(),
        "valid_xyz".to_string(),
        "exp_def".to_string(),
    ];

    // Bug: we call the function but ignore the count!
    remove_expired_tokens(&mut tokens);

    // No warning, no error — the return value just vanishes
}

The function works fine, but the caller threw away useful information without even a whisper from the compiler.

The Fix: #[must_use]

Add #[must_use] to the function and the compiler has your back:

1
2
3
4
5
6
#[must_use = "returns the number of removed tokens"]
fn remove_expired_tokens(tokens: &mut Vec<String>) -> usize {
    let before = tokens.len();
    tokens.retain(|t| !t.starts_with("exp_"));
    before - tokens.len()
}

Now if someone calls remove_expired_tokens(&mut tokens); without using the result, the compiler emits:

1
2
3
4
warning: unused return value of `remove_expired_tokens` that must be used
  --> src/main.rs:14:5
   |
   = note: returns the number of removed tokens

Works on Types Too

#[must_use] isn’t just for functions — it shines on types:

1
2
3
4
5
#[must_use = "this Result may contain an error that should be handled"]
enum DatabaseResult<T> {
    Ok(T),
    Err(String),
}

This is exactly why calling .map() on an iterator without collecting produces a warning — Map is marked #[must_use] in std.

Already in the Standard Library

Rust’s standard library uses #[must_use] extensively. Result, Option, MutexGuard, and many iterator adapters are all marked with it. That’s why you get a warning for:

1
vec![1, 2, 3].iter().map(|x| x * 2);  // warning: unused `Map`

The iterator does nothing until consumed — and #[must_use] makes sure you don’t forget.

Quick Rules

Use #[must_use] when:

  • A function returns a Result or error indicator — callers should handle failures
  • A function is pure (no side effects) — ignoring the return means the call was pointless
  • A type is lazy (like iterators) — it does nothing until consumed
  • The return value carries critical information the caller likely needs

The custom message string is optional but highly recommended — it tells the developer why they shouldn’t ignore the value.

37. Option::zip

Need to combine two Option values into a pair? Option::zip merges them into a single Option<(A, B)> — if either is None, you get None back.

The problem

You have two optional values and need both to proceed. The classic approach uses nested matching:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let name: Option<&str> = Some("Alice");
let age: Option<u32> = Some(30);

// Nested match — gets unwieldy fast
let greeting = match name {
    Some(n) => match age {
        Some(a) => Some(format!("{n} is {a} years old")),
        None => None,
    },
    None => None,
};

assert_eq!(greeting, Some("Alice is 30 years old".to_string()));

The fix: Option::zip

Zip collapses two Options into one tuple:

1
2
3
4
5
6
let name: Option<&str> = Some("Alice");
let age: Option<u32> = Some(30);

let greeting = name.zip(age).map(|(n, a)| format!("{n} is {a} years old"));

assert_eq!(greeting, Some("Alice is 30 years old".to_string()));

One line instead of six. If either value is None, zip short-circuits to None:

1
2
3
4
let name: Option<&str> = Some("Alice");
let age: Option<u32> = None;

assert_eq!(name.zip(age), None);

Bonus: zip with and_then

You can chain zip into more complex pipelines:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn lookup_user(id: u32) -> Option<String> {
    if id == 1 { Some("Alice".to_string()) } else { None }
}

fn lookup_role(id: u32) -> Option<String> {
    if id == 1 { Some("Admin".to_string()) } else { None }
}

let result = lookup_user(1)
    .zip(lookup_role(1))
    .map(|(user, role)| format!("{user} ({role})"));

assert_eq!(result, Some("Alice (Admin)".to_string()));

Option::zip is stable since Rust 1.46 and works anywhere you need both-or-nothing semantics without the nesting.

36. Cow<str> — Clone on Write

Stop cloning strings “just in case” — Cow<str> lets you borrow when you can and clone only when you must.

The problem

You’re writing a function that sometimes needs to modify a string and sometimes doesn’t. The easy fix? Clone every time:

1
2
3
4
5
6
7
fn ensure_greeting(name: &str) -> String {
    if name.starts_with("Hello") {
        name.to_string() // unnecessary clone!
    } else {
        format!("Hello, {name}!")
    }
}

This works, but that first branch allocates a brand-new String even though name is already perfect as-is. In a hot loop, those wasted allocations add up.

Enter Cow<str>

Cow stands for Clone on Write. It holds either a borrowed reference or an owned value, and only clones when you actually need to mutate or take ownership:

1
2
3
4
5
6
7
8
9
use std::borrow::Cow;

fn ensure_greeting(name: &str) -> Cow<str> {
    if name.starts_with("Hello") {
        Cow::Borrowed(name) // zero-cost: just wraps the reference
    } else {
        Cow::Owned(format!("Hello, {name}!"))
    }
}

Now the happy path (name already starts with “Hello”) does zero allocation. The caller gets a Cow<str> that derefs to &str transparently — most code won’t even notice the difference.

Using Cow values

Because Cow<str> implements Deref<Target = str>, you can use it anywhere a &str is expected:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::borrow::Cow;

fn ensure_greeting(name: &str) -> Cow<str> {
    if name.starts_with("Hello") {
        Cow::Borrowed(name)
    } else {
        Cow::Owned(format!("Hello, {name}!"))
    }
}

fn main() {
    let greeting = ensure_greeting("Hello, world!");
    assert_eq!(&*greeting, "Hello, world!");

    // Call &str methods directly on Cow
    assert!(greeting.contains("world"));

    // Only clone into String when you truly need ownership
    let _owned: String = greeting.into_owned();

    let greeting2 = ensure_greeting("Rust");
    assert_eq!(&*greeting2, "Hello, Rust!");
}

When to reach for Cow

Cow shines in these situations:

  • Conditional transformations — functions that modify input only sometimes (normalization, trimming, escaping)
  • Config/lookup values — return a static default or a dynamically built string
  • Parser outputs — most tokens are slices of the input, but some need unescaping

The Cow type works with any ToOwned pair, not just strings. You can use Cow<[u8]>, Cow<Path>, or Cow<[T]> the same way.

Quick reference

OperationCost
Cow::Borrowed(s)Free — wraps a reference
Cow::Owned(s)Whatever creating the owned value costs
*cow (deref)Free
cow.into_owned()Free if already owned, clones if borrowed
cow.to_mut()Clones if borrowed, then gives &mut access

35. LazyLock

Still pulling in lazy_static or once_cell just for a lazy global? std::sync::LazyLock does the same thing — zero dependencies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::sync::LazyLock;

static CONFIG: LazyLock<Vec<String>> = LazyLock::new(|| {
    // Imagine this reads from a file or env
    vec!["debug".to_string(), "verbose".to_string()]
});

fn main() {
    // CONFIG is initialized on first access
    println!("flags: {:?}", *CONFIG);
    assert_eq!(CONFIG.len(), 2);
}

LazyLock was stabilized in Rust 1.80 as the std replacement for once_cell::sync::Lazy and lazy_static!. It initializes the value exactly once on first access, is Sync by default, and works in static items without macros.

For single-threaded or non-static use, there’s also LazyCell — same idea but without the synchronization overhead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::cell::LazyCell;

fn main() {
    let greeting = LazyCell::new(|| {
        println!("computing...");
        "Hello, Rust!".to_uppercase()
    });

    println!("before access");
    // "computing..." prints here, on first deref
    assert_eq!(*greeting, "HELLO, RUST!");
    // second access — no recomputation
    assert_eq!(*greeting, "HELLO, RUST!");
}

The output is:

1
2
before access
computing...

The closure runs lazily on first Deref, and the result is cached for all subsequent accesses. No unwrap(), no Mutex, no external crates — just clean lazy initialization from std.

34. array_windows

Need to look at consecutive pairs (or triples) in a slice? Stop manually indexing — array_windows gives you fixed-size windows as arrays, not slices.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let temps = [18.0, 21.5, 19.0, 23.0, 22.5];

// Before: manual indexing 😬
for i in 0..temps.len() - 1 {
    let diff = temps[i + 1] - temps[i];
    println!({diff:+.1}");
}

// After: array_windows ✨
for [prev, next] in temps.array_windows() {
    let diff = next - prev;
    println!({diff:+.1}");
}

Stabilized in Rust 1.94, array_windows works like .windows(n) but the window size is a const generic — so you get &[T; N] instead of &[T]. That means you can destructure directly in the pattern.

It’s great for detecting trends, computing deltas, or validating sequences:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
let readings = [3, 7, 2, 9, 1, 8];

let all_increasing = readings
    .array_windows()
    .all(|[a, b]| b > a);

assert!(!all_increasing);

// Works with triples too
let has_valley = readings
    .array_windows()
    .any(|[a, b, c]| b < a && b < c);

assert!(has_valley); // 2 is a valley between 7 and 9

No bounds checks, no .try_into().unwrap() dance. Just clean pattern matching on fixed-size windows.

#033 Mar 2026

33. std::mem::take

Ever tried to move a value out of a &mut reference? The borrow checker won’t let you — but std::mem::take will. It swaps the value out and leaves Default::default() in its place.

1
2
3
4
5
6
7
use std::mem;

let mut name = String::from("Ferris");
let taken = mem::take(&mut name);

assert_eq!(taken, "Ferris");
assert_eq!(name, ""); // left with String::default()

This is especially useful when working with enum state machines where you need to consume the current state:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::mem;

enum State {
    Running(String),
    Stopped,
}

impl Default for State {
    fn default() -> Self { State::Stopped }
}

fn reset(state: &mut State) -> Option<String> {
    match mem::take(state) {
        State::Running(data) => Some(data),
        State::Stopped => None,
    }
}

Without mem::take, you’d need .clone() or unsafe gymnastics to get the value out. See also mem::replace for when you want to specify what to leave behind instead of using Default.

#032 Mar 2026

32. iter::successors

Need to generate a sequence where each element depends on the previous one? std::iter::successors turns any “next from previous” logic into a lazy iterator.

1
2
3
4
5
6
7
8
use std::iter::successors;

// Powers of 10 that fit in a u32
let powers: Vec<u32> = successors(Some(1u32), |&n| n.checked_mul(10))
    .collect();

// [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000]
assert_eq!(powers.len(), 10);

You give it a starting value and a closure that computes the next element from a reference to the current one. Return None to stop — here checked_mul naturally returns None on overflow, so the iterator terminates on its own.

It works great for any recurrence:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::iter::successors;

// Collatz sequence starting from 12
let collatz: Vec<u64> = successors(Some(12u64), |&n| match n {
    1 => None,
    n if n % 2 == 0 => Some(n / 2),
    n => Some(3 * n + 1),
}).collect();

assert_eq!(collatz, vec![12, 6, 3, 10, 5, 16, 8, 4, 2, 1]);

Think of it as unfold for when your state is the yielded value. Simple, lazy, and zero-allocation until you collect.

31. HashMap's entry API

Want to insert a value into a HashMap only if the key doesn’t exist yet? Skip the double lookup — use the entry API.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::collections::HashMap;

let mut scores: HashMap<&str, Vec<u32>> = HashMap::new();

// Instead of checking .contains_key() then inserting:
scores.entry("alice")
    .or_insert_with(Vec::new)
    .push(100);

scores.entry("alice")
    .or_insert_with(Vec::new)
    .push(200);

assert_eq!(scores["alice"], vec![100, 200]);

The entry API returns an Entry enum — either Occupied or Vacant. The convenience methods make common patterns a one-liner:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::collections::HashMap;

let mut word_count: HashMap<&str, usize> = HashMap::new();
let words = ["hello", "world", "hello", "rust", "hello"];

for word in words {
    *word_count.entry(word).or_insert(0) += 1;
}

// hello => 3, world => 1, rust => 1

or_insert(val) inserts a default, or_insert_with(|| val) lazily computes it, and or_default() uses the type’s Default. All three return a mutable reference to the value, so you can update in place.

#030 Mar 2026

30. dbg! macro

Still using println! for quick debugging? Try dbg! instead — it prints the expression, its value, and the file/line number to stderr. And it returns the value, so you can wrap it around anything.

1
2
3
4
5
6
let a = 2;
let b = dbg!(a * 2) + 1; // prints: [src/main.rs:3] a * 2 = 4
assert_eq!(b, 5);

// works with multiple values too
dbg!(a, b, a + b); // prints each as a tuple

Unlike println!, dbg! takes ownership (or copies for Copy types). If you need to keep the value, pass a reference:

1
2
3
let name = String::from("Ferris");
dbg!(&name); // borrows, doesn't move
println!("{name}"); // still works!

Bonus: dbg! works the same in release builds, and outputs to stderr so it won’t pollute your stdout.