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
- Both
matches need every arm to have the same type.continueandfail(...)have type!, which fits anywhere. fn fail(...) -> !promises the function never returns. The compiler checks it: the body must end in something that diverges, likepanic!.todo!()andunimplemented!()have type!too. That's whytodo!()compiles as the body of a function returning anything.let ... elserequires itselseblock to have type!(return, break, continue or panic), because there's no value to bind if the pattern fails.
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
?Sizedon a by-value parameter:fn f<T: ?Sized>(x: T)fails, because a value on the stack needs a known size.?Sizedonly works for things behind&,Box,Rcand friends.- Writing
-> !for a function that can return: the compiler checks the body diverges on every path.
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.