Performant Software Systems with Rust — Lecture 8
Generics
To remove duplication of code, we can replace specific types with a placeholder that represents multiple types
We have seen them before: Option<T>
, Result<T, E>
, Vec<T>
, HashMap<K, V>
We will now define our own types, functions, and methods with generics
Let’s start from a simple program that finds the largest number in a vector of numbers
Finding the large number in a vector
Learn more: Removing Duplication by Extracting a Function
Finding the largest number in two vectors
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
}
Learn more: Removing Duplication by Extracting a Function
Defining the largest
function
// list: any concrete slice of i32 values
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {result}");
}
&[char]
?Writing two functions is silly!
Generics in Functions
Defining largest()
using the generic data type
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {result}");
}
Learn more: In Function Definitions
Learn more: In Function Definitions
Defining Structs using Generics
Live Demo
Learn more: In Struct Definitions
Defining Structs using Multiple Generic Types
Learn more: In Struct Definitions
Defining Enums using Generics
Learn more: In Enum Definitions
Defining Methods using Generics
Learn more: In Method Definitions
Defining Methods on Concrete Types
Mixing Generic Type Parameters
struct Point<X1, Y1> {
x: X1,
y: Y1,
}
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
Learn more: In Method Definitions
Advanced Topic: Zero-Cost Abstraction in Rust
Learn more: Performance of Code Using Generics
Monomorphization
Learn more: Performance of Code Using Generics
Monomorphization
Learn more: Performance of Code Using Generics
Traits
A trait defines the functionality (or behaviour) a particular type has and can share with other types
Behaviour implies methods that we define and call in a type
Different types share the same behaviour if we can call the same methods on all of those types
Trait definitions are used to group those behaviours
Example
Implementing a Trait on a Type
Learn more: Implementing a Trait on a Type
Implementing a Trait on a Type
Learn more: Implementing a Trait on a Type
Calling a Trait Method
Learn more: Implementing a Trait on a Type
Default Implementations
To use the default behaviour on NewsArticle
:
Learn more: Default Implementations
Default Implementations Can Call Other Methods
Learn more: Default Implementations
Traits as Parameters: The impl Trait
Syntax
Learn more: Traits as Parameters
Trait Bound
is just syntax sugar to:
Learn more: Traits Bound Syntax
Why is Trait Bound a Good Idea?
The compiler uses trait bound to check that all the concrete types provide the correct behavior
Converts run-time errors to compile-time errors, improving performance when the code compiles
Trait Bound Is More Expressive Than impl Trait
vs.
Learn more: Trait Bound Syntax
Specifying Multiple Trait Bounds with the + Syntax
or
Clearer Trait Bounds with where
Clauses
or
Returning Types That Implement Traits
Cannot Return Multiple Types
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Rust 1.82 Released",
),
location: String::from("USA"),
author: String::from("The Rust Release Team"),
content: String::from(
"October 17 2024",
),
}
} else {
Tweet {
username: String::from("books"),
content: String::from(
"The Rust Programming Language",
),
reply: false,
retweet: false,
}
}
}
Using Trait Bounds to Conditionally Implement Methods
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
// always implements the `new` function
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
// only implements the `cmp_display` function if `T` implements
// `Display` and `PartialOrd`
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
Blanket Implementations
Associated Types in Traits as Placeholder Types
Associated types in traits are type placeholders
The trait method definitions can use them
The implementor of a trait will need to specify the concrete type instead
Allows us to define a function without specifying what types it can use
Associated Types in Traits as Placeholder Types
Implementing the Iterator
Trait on the Counter
Type
But why can’t we just write the following for the Iterator
trait?
We have to provide type annotations to indicate which implementation of Iterator
Iterator<String> for Counter
or Iterator<i32> for Counter
?With associated types, there can only be one type of Item
, because there can only be one impl Iterator for Counter
We don’t have to specify that we want an iterator of u32
values everywhere we call next()
on Counter
The associate type becomes a part of the trait’s contract
Supertraits: Requiring One Trait’s Functionality within Another
use std::fmt::Display;
// `OutlinePrint` requires the `Display` trait to be implemented
trait OutlinePrint: Display {
fn outline_print(&self) {
// so that we can use the `Display` trait's functionality
// including the `to_string` method
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
The Rust Programming Language, Chapter 10.1, 10.2, 20.2