Rust traits and generics interview questions, with answers
Traits are Rust's way of describing shared behaviour, and generics are how code is written once for many types. Between them they replace inheritance, interfaces and templates, and most of the standard library, from iteration to formatting to conversions, is built from them. Interviewers ask how traits are defined and bounded, what associated types are for, which implementations the orphan rule allows, and how the conversion traits fit together.
The answers below cover each of those, with code compiled and run on rustc 1.98. Then take the free Rust diagnostic — ten questions across every Rust topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.What is a trait in Rust?
In short: A trait is a named set of methods that types can implement, describing a shared behaviour, similar to an interface in other languages.
A trait declares method signatures, and optionally default bodies for some of them. A type implements it with an impl Trait for Type block that supplies the required methods, as Cat does for Speak below. Code can then accept any type with that behaviour, either through generics with a trait bound or through a trait object. Unlike interfaces in Java, a trait can be implemented for types you did not write, such as adding a method to i32, within the limits of the orphan rule, and traits can also provide associated types and constants. Standard traits such as Display, Iterator and Clone are the vocabulary most Rust APIs are built on.
trait Speak { fn speak(&self) -> String; } struct Cat; impl Speak for Cat { fn speak(&self) -> String { "meow".into() } } fn main() { let s = Cat.speak(); println!("{s}"); // meow }
2.What are trait bounds in Rust?
In short: A trait bound, such as T: Display, limits a generic type parameter to types that implement the trait, which is what lets the function use that trait's methods.
Inside a generic function the compiler knows only what the bounds promise about T. Writing T: Display, either in the angle brackets or in a where clause as below, allows the body to format a T, and in exchange every call must pass a type that implements Display; show(3) works because i32 does. Several bounds are combined with +, as in T: Clone + Debug. where clauses keep long signatures readable. The same bounds can be written on impl blocks, so that methods exist only for suitable types, and impl Display in argument position is shorthand for an anonymous bounded type parameter.
use std::fmt::Display; fn show<T>(x: T) -> String where T: Display, { format!("<{x}>") } fn main() { println!("{}", show(3)); // <3> }
3.What are associated types in Rust traits?
In short: An associated type is a type named inside a trait that each implementation chooses once, such as the Item type an Iterator produces.
Iterator declares type Item; and fn next(&mut self) -> Option<Self::Item>. Each implementation picks the Item type once, as Count below does with u32, so a type can implement Iterator only one way, and users of the iterator never have to name Item themselves. A generic parameter on the trait, as in From<T>, is the alternative when a type should be able to implement the trait many times with different types. Implementing next is all it takes: every other iterator method, such as collect, map or sum, is a default method built on it, which is why Count(0).collect() works below.
struct Count(u32); impl Iterator for Count { type Item = u32; fn next(&mut self) -> Option<u32> { self.0 += 1; if self.0 <= 3 { Some(self.0) } else { None } } } fn main() { let v: Vec<u32> = Count(0).collect(); println!("{:?}", v); // [1, 2, 3] }
4.What is the orphan rule in Rust, and how does a newtype get around it?
In short: You may implement a trait for a type only if the trait or the type is defined in your crate; wrapping a foreign type in a local struct, a newtype, makes it local.
If two crates could both implement Display for Vec<i32>, a program using both would have two conflicting implementations. The orphan rule prevents that: an impl is allowed only when the trait or the type belongs to the current crate. So implementing the standard Display for the standard Vec is rejected. The newtype pattern solves it by wrapping the foreign type in a one-field tuple struct, List below, which is local, so any trait can be implemented for it. The wrapper costs nothing at run time, and it can also restrict the inner type's interface or give it a clearer name.
use std::fmt; struct List(Vec<i32>); impl fmt::Display for List { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { let n = self.0.len(); write!(f, "{n} items") } } fn main() { let l = List(vec![1, 2]); println!("{l}"); // 2 items }
5.How do you write a generic struct and its methods in Rust?
In short: Declare type parameters after the struct's name, as struct Pair<T>, and repeat them on the impl block, impl<T> Pair<T>, to write methods for every T.
A generic struct such as Pair<T> can hold any type, and each use fixes T, so Pair<i32> and Pair<&str> are distinct types. Methods go in impl<T> Pair<T>, where the first <T> declares the parameter and the second applies it; methods that need more from T go in a separate impl block with a bound, such as impl<T: Display> Pair<T>, and exist only for types that meet it. swap below works for every T because it only moves the fields. The compiler generates specialised code for each type the struct is used with, so generic code runs as fast as a hand-written version.
struct Pair<T>(T, T); impl<T> Pair<T> { fn swap(self) -> Self { Pair(self.1, self.0) } } fn main() { let p = Pair(1, 2).swap(); let Pair(x, y) = p; println!("{x} {y}"); // 2 1 }
6.Which traits can be derived in Rust, and what do they give you?
In short: The standard derivable traits are Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash and Default, each generated from the type's fields.
Debug enables {:?} formatting. Clone adds .clone(), and Copy makes assignment copy instead of move, for types whose fields are all Copy. PartialEq and Eq define == , and PartialOrd and Ord define ordering, comparing fields in declaration order. Hash lets the type be a HashMap key, together with Eq. Default creates a value with every field at its own default, which below gives 0 and false, and supports the ..Default::default() struct update syntax. Deriving only works when every field implements the trait, and a derive is a compile-time code generator, so there is no run-time cost compared with writing the same code by hand.
#[derive(Debug, Default)] struct Conf { retries: u8, verbose: bool, } fn main() { let c = Conf::default(); println!("{}", c.retries); // 0 println!("{}", c.verbose); // false }
7.What are the From and Into traits in Rust?
In short: From<T> defines how to build a type from a T; implementing it gives the matching Into automatically, so a value can be converted with Type::from(x) or x.into().
From is the standard trait for conversions that always succeed. Implementing From<f64> for Cm below defines Cm::from, and the standard library then provides Into<Cm> for f64 for free, so a let with a declared type can write 2.0.into(). Writing From rather than Into is the convention for exactly that reason. Functions often accept impl Into<String> to take both a &str and a String. For conversions that can fail, the matching traits are TryFrom and TryInto, which return a Result. The ? operator also uses From to convert error types when it returns an error.
struct Cm(f64); impl From<f64> for Cm { fn from(m: f64) -> Self { Cm(m * 100.0) } } fn main() { let a = Cm::from(1.5); let b: Cm = 2.0.into(); let (x, y) = (a.0, b.0); println!("{x} {y}"); // 150 200 }
How the diagnostic asks it
One question from the Rust bank, exactly as a sitting would show it. The bank has 4 on traits & generics and 30 across Rust.
What does this Rust program print?
trait Greet { fn name(&self) -> String; fn hello(&self) -> String { format!("Hello, {}", self.name()) } } struct En; struct Fr; impl Greet for En { fn name(&self) -> String { "Ann".to_string() } } impl Greet for Fr { fn name(&self) -> String { "Luc".to_string() } fn hello(&self) -> String { format!("Bonjour, {}", self.name()) } } fn main() { println!("{} / {}", En.hello(), Fr.hello()); }
- 1Hello, Ann / Hello, Luc
- 2It does not compile: En does not implement hello
- 3It does not compile: a trait cannot contain a method body
- 4Hello, Ann / Bonjour, Luccorrect
A trait can give a method a default body. A type that implements the trait must supply every method without a default, here name, and may override the defaults. En implements only name, so it gets the default hello, which calls En's own name: Hello, Ann. Fr overrides hello, so its version runs: Bonjour, Luc. Hello, Luc ignores the override. En does not need to write hello, because it has a default, and trait methods with bodies are legal, which rules out both compile-error options. Default methods written in terms of required ones are how traits such as Iterator offer dozens of methods for implementing just next.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Rust questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.