RustArena

Step 7 of 7

The never type and Sized

Two special corners of the type system explain things you've already been doing.

!, the never type

Some expressions never produce a value: panic!, return, break, continue, an endless loop {}, std::process::exit. Their type is !, the never type. Because a value of type ! can never exist, Rust lets it stand in for any type. That's why this works:

fn fail(code: u32, msg: &str) -> ! {
    panic!("error {code}: {msg}");
}

fn parse_scores(lines: &[&str]) -> Vec<u32> {
    let mut out = Vec::new();
    for line in lines {
        let n: u32 = match line.trim().parse() {
            Ok(n) => n,
            Err(_) => continue, // type `!`, fits where a u32 is expected
        };
        out.push(n);
    }
    out
}

fn main() {
    println!("{:?}", parse_scores(&["10", "oops", " 7"]));
    let mode = "fast";
    let speed = match mode {
        "fast" => 3,
        "slow" => 1,
        _ => fail(2, "unknown mode"),
    };
    println!("speed {speed}");
}
[10, 7]
speed 3

You'll also meet std::convert::Infallible, an enum with no variants: an error type for operations that can't fail. "crab".parse::<String>() returns Result<String, Infallible>.

Sized and ?Sized

Most types have a size known at compile time (i32 is 4 bytes, String is 24 on a 64-bit machine). A few don't: str, slices [T] and trait objects dyn Trait can be any length. These are dynamically sized types (DSTs), and they can only live behind a pointer: &str, &[T], Box<dyn Trait>. Such a pointer is "fat": it stores the address plus the length or a vtable pointer.

Every generic parameter has an invisible bound T: Sized. Usually that's what you want, but it means this rejects str:

use std::fmt::Display;

fn show<T: Display>(x: &T) {
    println!("{x}");
}

fn main() {
    let s: &str = "hi";
    show(s);
}
error[E0277]: the size for values of type `str` cannot be known at compilation time

show(s) asks for T = str, which isn't Sized. Since show only uses T behind a reference, it can relax the bound with ?Sized ("may or may not be sized"):

use std::fmt::Display;
use std::mem::size_of;

fn show<T: ?Sized + Display>(label: &str, value: &T) {
    println!("{label}: {value}");
}

fn main() {
    show("str", "crab"); // T = str
    show("i32", &5);
    let boxed: Box<dyn Display> = Box::new(2.5);
    show("dyn", boxed.as_ref()); // T = dyn Display
    println!("{} {} {}", size_of::<&u8>(), size_of::<&str>(), size_of::<&dyn Display>());
}
str: crab
i32: 5
dyn: 2.5
8 16 16

The last line shows the fat pointers: a plain reference is 8 bytes, while &str and &dyn Display are 16 (address plus length or vtable). std uses ?Sized wherever it only needs a reference: impl<T: Display + ?Sized> ToString for T, Box<T: ?Sized>, and AsRef<T: ?Sized> (which is why AsRef<str> is possible).

Common mistakes

Your task

describe builds "label = value" from any Debug value, but it can't be called with a str, a slice or a dyn Debug directly. Relax its bound so all of these work: describe("name", "Ferris") gives name = "Ferris", and describe("scores", &v[..]) gives scores = [3, 9].

Challenge 2

Write parse_settings, which turns "key=value" lines into (key, number) pairs. Skip blank lines, lines starting with #, lines without =, and lines whose value isn't a u32. Trim the key and the value. Use let ... else { continue; } for the two parsing steps: it works because continue has type !.

Challenge 3

Write fn join_all<T: ?Sized + Display>(items: &[&T], sep: &str) -> String, which displays every item and joins them with sep. Thanks to ?Sized, it works on a list of &str (T = str), on numbers, and on a mixed list of &dyn Display.

Previous: PhantomData and typestate