BYTEMAGMA

Master Rust Programming

Generics in Rust: Writing Flexible and Reusable Code

Introduction

Sometimes we need code that works with multiple data types, for example a function that adds two numbers together and returns the result. This is easy in weakly typed languages, but this can lead to runtime errors when function parameters aren’t compatible with operations performed in the function.

In strongly typed languages, we might need to define multiple versions of a function that do the exact same thing but on parameters of different data types. This code duplication is inefficient and can be prone to errors if we change the code but don’t update all the multiple code versions to handle the different data types.

This is where generics come in. In this section, we’ll see how generics allow us to abstract out data types with type parameters, then when we use the generic code we specify the types that will replace the type parameters. This abstraction is a powerful feature of Rust (and many other languages), offering flexibility while preserving the safety Rust is known for.

And using generics does not mean sacrificing performance. Abstractions in Rust code are compiled down to efficient machine code. The abstraction disappears during compilation, thanks to Rust’s powerful type system and LLVM backend.

In this post, we’ll cover these topics related to generics in Rust:

  • What Are Generics?
  • Defining Functions with Generics
  • Generic Structs and Enums
  • Traits and Generics
  • Implementing Methods on Generic Types
  • Trait Objects vs. Generics
  • Lifetimes with Generics (Brief Overview)
  • Associated Types vs. Generic Parameters in Traits
  • Advanced: Conditional Implementations
  • Common Pitfalls and Best Practices
  • Real-World Example

What Are Generics?

Generics are a powerful feature in Rust that let you write code that can operate on different data types without sacrificing type safety or performance. At a high level, they allow you to write functions, structs, enums, and traits that are flexible and reusable across many different concrete types.

A non-generic function that takes two i32 integers and returns an i32, the sum of the input parameters, won’t work if you pass it u16 integers, or f64 floats. You would need to duplicate the function with different parameter and return types.

With generics, you abstract over the type in the function definition, and then the compiler generates different versions of the functions at compile time for each data type for which you actually use the generic function. This results in a larger binary file and longer compile times, but you don’t sacrifice runtime performance.

Here’s how it works:

  • Generic type parameters act as placeholders for actual types.
  • These placeholders are replaced with concrete types by the compiler at compile time.
  • The result is zero-cost abstraction — you get flexibility without runtime performance penalties.

Two examples in the Rust standard library are the Option<T> and Result<T, E> enums that are both generic. Option<T> can represent an optional value of any type T. Similarly, Result<T, E> can encapsulate the result of a computation that can either return a value T or an error E.

Generics let you write once and use many times — a cornerstone of DRY (Don’t Repeat Yourself) code and a major enabler of high-quality, reusable libraries.


Defining Functions with Generics

One of the most common uses of generics is in functions. Rather than defining multiple versions of a function to work with various data types, you abstract out the types in a generic function and then supply the types when you use the function.

Let’s get started writing some code.

Open a shell window (Terminal in Mac/Linux, Cmd or PowerShell in Windows). Then navigate to the directory where you store Rust packages for this blog, and run the following command:

cargo new generics_demo --lib

Here we’re creating a new Rust package with a library crate. Often the bulk of your code will go in Rust library crates, and then you’ll use that code in one or more binary crates or in other Rust packages.

Now change into the newly created generics_demo directory, and open that directory in VS Code or your favorite IDE.

Note: Using VS Code will make it easier to follow along with this blog series. Installing the Rust Analyzer extension is also highly recommended.

Also, opening the directory containing your Rust package allows the Rust Analyzer VS Code extension to work better. If you open a parent folder in VS Code the Rust Analyzer extension might not work at all. So open the generics_demo directory in VS Code.

Open the file src/lib.rs and replace its contents entirely with this code:

pub fn swap(a: i32, b: i32) -> (i32, i32) {
    (b, a)
}

We’re starting out with a function that doesn’t use generics. The function takes two i32 parameters and returns a tuple with the two parameters swapped. We use the pub keyword to make the function public, so it is accessible outside the library.

To test this out we’ll now create a binary crate in this Rust package. Currently your package directory structure should look like this:

generics_demo/
├── Cargo.toml
└── src/
    └── lib.rs

Create a new directory in the src directory named bin, then add a file named main.rs:

generics_demo/
├── Cargo.toml
└── src/
    ├── lib.rs         # library crate
    └── bin/
        └── main.rs    # binary crate

Now add this code to main.rs:

use generics_demo::swap;

fn main() {
  let val1 = 10;
  let val2 = 50;

  let swapped_vals = swap(val1, val2);
  println!("Swapped values: {:?}", swapped_vals);
}

Execute cargo run in the shell window and you should get this result:

Swapped values: (50, 10)

We’ve decided we want this functionality to also work if we pass in floats and string slices (&str). If we simply call our current function like this we get compilation errors:

  let swapped_floats = swap(4.5, 12.8);  // expected `i32`, found floating-point number
  let swapped_slices = swap("world", "hello");  // expected `i32`, found `&str`

We could create two new functions to handle f32 and &str types, but what if we want this to work for u8, u16, u32, f64, String, etc. This is where we see the power of generics.

Replace the code in lib.rs with the following:

pub fn swap<T>(a: T, b: T) -> (T, T) {
    (b, a)
}

To see this in action, replace the code in main.rs with this code:

use generics_demo::swap;

fn main() {
  println!("Swapped u8: {:?}", swap(200, 80));
  println!("Swapped i32: {:?}", swap(10, 50));
  println!("Swapped f32: {:?}", swap(17.25, 32.42));
  println!("Swapped &str: {:?}", swap("world", "hello"));
}

Execute cargo run and you should see this output:

Swapped u8: (80, 200)
Swapped i32: (50, 10)
Swapped f32: (32.42, 17.25)
Swapped &str: ("hello", "world")

Like magic, our function now operates on any data types!

Let’s look at our generic function to understand the syntax:

pub fn swap<T>(a: T, b: T) -> (T, T) {
    (b, a)
}

After the function name and before the parentheses containing the function parameters we have a generic type parameter T surrounded by angle brackets: <T>

Then we use the type parameter as the type for the two parameters and also in the return type definition.

This is like saying:

Whatever data type I pass for the two parameters, as long as the type is the same, return a tuple of two elements that are also of that data type.

Note that the generic type parameter here is T but it can be whatever you want, MyType, T1, whatever, but in Rust by convention we use a single capital letter T.

Multiple Generic Types

What if we have a function like the following, create_pair() that takes two parameters of different types and returns an instance of a struct. Notice the struct definition also uses generics. We’ll discuss generics and structs in detail in the next section. If you need a refresher on Rust structs you may find this ByteMagma post useful: Structs in Rust: Modeling Real-World Data

pub struct Pair<T, U> {
    pub first: T,
    pub second: U,
}

pub fn create_pair<T, U>(a: T, b: U) -> Pair<T, U> {
    Pair { first: a, second: b }
}

Go ahead and add the above code to lib.rs. Also replace the code in main.rs with the following:

use generics_demo::create_pair;

fn main() {
  let p1 = create_pair(10, "bananas");
  let p2 = create_pair(true, 3.14);

  println!("First: {}, Second: {}", p1.first, p1.second);
  println!("First: {}, Second: {}", p2.first, p2.second);
}

Execute cargo run and you should see this output:

First: 10, Second: bananas
First: true, Second: 3.14

Let’s analyze the function:

pub fn create_pair<T, U>(a: T, b: U) -> Pair<T, U> {
    Pair { first: a, second: b }
}

This function uses two generic type parameters, T and U. The first type parameter is used for the first function parameter and the second type parameter is used for the second parameter.

The return type is specified as an instance of the Pair struct whose generic type parameters are T and U. We can see the two type parameters being used in the struct definition:

pub struct Pair<T, U> {
    pub first: T,
    pub second: U,
}

Note that when you have multiple generic type parameters for a function, the order doesn’t matter, so you could have defined the function like this:

pub fn create_pair<T, U>(a: U, b: T) -> Pair<U, T> {
    Pair { first: a, second: b }
}

However, in Rust the convention is to use the type parameters for the function parameters in the same order as they appear after the function name.

We’ll see more examples of multiple generic type parameters in this section. Now let’s look at using generics in structs and enums.


Generic Structs and Enums

In the last section we saw an example of using generics in a struct. Let’s examine this in detail and also expand the discussion to include Rust enums.

If you need a refresher on Rust structs or enums, these ByteMagma blog posts may help:

Structs in Rust: Modeling Real-World Data

Rust Enums: Close Cousins of Structs

Using generics in Rust structs and enums is powerful, as it enables you to create data structures that are flexible and adaptable to many situations. This helps reduce code duplication, code bloat, and increases flexibility.

To get started, replace the contents of lib.rs with the following:

pub struct Container<T> {
    pub value: T,
}

Replace the contents of main.rs to see this in action:

use generics_demo::Container;

fn main() {
  let int_container = Container { value: 42 };
  let str_container = Container { value: "Ferris" };
  let float_container = Container { value: 3.14 };  
}

In this example we define a struct Container that has one field value. After the struct name we specify a generic type parameter T, and we use that parameter as the type for field value.

In main we create three Containers, each containing a value of i32, &str, and f64.

Let’s look at another real-world example. Add the following to lib.rs:

pub struct Dimensions<T> {
    pub width: T,
    pub height: T,
}

Now replace the code in main.rs with the following:

use generics_demo::Dimensions;

fn main() {
  let fixed_size = Dimensions { width: 800, height: 600 };     // i32
  let scaled_size = Dimensions { width: 0.5, height: 0.75 };   // f64
}

You can imagine using the Dimensions struct to represent the size of a box, a room, anything with a width and height.

Multiple Type Parameters

Let’s look at an example involving two generic type parameters.

Add this code to lib.rs:

pub struct KVEntry<K, V> {
    pub key: K,
    pub value: V,
}

Also replace the contents of main.rs with the following:

use generics_demo::KVEntry;

fn main() {
  let session = KVEntry {
    key: 1001,
    value: "user123",
  };

  let setting = KVEntry {
      key: "theme",
      value: "dark",
  };

  let cache = KVEntry {
      key: ("user_id", 42),
      value: vec!["file1.txt", "file2.txt"],
  };
}

Our struct KVEntry makes use of two type parameters to specify the data type of the two struct fields. You can imagine using this to represent key/value pairs of data. By convention in this situation we use type parameters K (key) and V (value).

In main.rs we create three instances of our struct, with the two struct fields holding very different data:

  • an i32 for the key and a string slice (&str) for the value
  • a string slice for the key and the value
  • a tuple (&str, i32) for the key and a vector for the value

Generics in Enums

You can also define enums with type parameters — this is incredibly useful for modeling optional or multi-state values.

enum Slot<T> {
    Full(T),
    Empty,
}

This could represent a cached value, a UI input field, or a network message slot.

let input = Slot::Full("ready");
let pending: Slot<i32> = Slot::Empty;

Here we define an enum Slot with two variants, Full<T> and Empty.

We use it to represent the status of an input of some kind, and also to represent a pending state for a Slot that is empty.

Slot::Full("ready") means variable input will be the Slot enum variant Full containing a string.

The pending variable is typed as a Slot whose Full variant will contain an i32, but currently it is set to variant Empty.

If you’ve seen the Rust enum Option<T> you may notice that our Slot enum is similar. It represents the presence of a value (Full) or the absence of a value (Empty).

enum Option<T> {
    Some(T),
    None,
}

Another common Rust enum that uses generics is Result<T, E>:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result<T, E> is an example of a Rust enum that uses two generic type parameters, T for the result value on success, and E for an error that might be received.

Now that we’ve looked at generics with structs and enums, let’s see how we can use generics when implementing methods on structs and enums.


Traits and Generics

Generics allow us to write flexible code. Rust traits allow us to define behaviors. We can put these together to constrain generic types so functions, structs, enums, and methods only accept types that implement specified traits.

If you need a refresher on Rust traits, this ByteMagma post might be helpful: Rust Traits: Defining Behavior the Idiomatic Way. Let’s look at using trait bounds to constrain a type. Our first example won’t involve generics.

Add this code to lib.rs:

use std::fmt::Display;

fn log(msg: impl Display) {
    println!("[LOG]: {}", msg);
}

Replace the code in main.rs with the following:

log("Welcome, Ferris!");   // &str implements Display
log(404);                  // i32 implements Display
//log([1, 2, 3]);            // doesn't implement Display

We define a function log() in our library that takes a single parameter msg. The type of this parameter is interesting:

fn log(msg: impl Display)

Display is a Rust built-in trait that enables printing out values. Here impl Display is saying:

“msg can be any type that implements the Display trait”

We test this by calling log() with a string slice (&str) and an integer. These lines compile fine because primitive values like integers and string slices do implement the Display trait. But if we uncomment the third line you’ll get a compilation error because an array of three integers does not implement the Display trait.

So in this code impl Display is an example of a trait bound. We are constraining the function to only accept as parameters types that implement the Display trait.

Next we’ll see how to use trait bounds to constrain a generic type, but this impl Trait syntax is great for constraining a single type.

T: Trait syntax for Trait Bounds

Let’s say we’re creating a dashboard for a company Ferris Computers. You want a function that can print information about system metrics, in the form of a label and a value. You want this function to be flexible, so it can take any value that implements the Display trait, so it can be printed.

lib.rs

use std::fmt::Display;

pub fn print_labeled_value<T: Display>(label: &str, value: T) {
    println!("{}: {}", label, value);
}

main.rs

use generics_demo::print_labeled_value;

fn main() {
  print_labeled_value("CPU Usage", 72);
  print_labeled_value("System Status", "Nominal");
  print_labeled_value("Load Average", 0.85);
}

Here we’re using a generic type parameter with the function. We use a trait bound using the syntax T: Display.

print_labeled_value<T: Display>

This is saying:

Generic type T can be any type that implements the Display trait.”

Multiple Trait Bounds

If you need a type that implements multiple traits you can use the + operator:

lib.rs

use std::ops::Add;
use std::fmt::Display;

pub fn add_and_log<T: Add<Output = T> + Display>(a: T, b: T) {
    let result = a + b;
    println!("Result is: {}", result);
}

main.rs

use generics_demo::add_and_log;

fn main() {
  add_and_log(5, 10);         //  15
  add_and_log(3.0, 2.5);      //  5.5
}

Here we define a function add_and_log() that takes parameters of a generic type that implements both the Rust standard library Add trait and also the Display trait.

Note: We specify Add<Output = T> to ensure that a + b gives back the same type as T. This is an example of an associated type, which we’ll see in more detail later in this post.

Using where clauses for cleaner code

As you have more traits in your trait bounds, the T: Trait + Trait + Trait + Trait syntax can get messy.

For code that is more readable we can instead use a where clause:

lib.rs

use std::fmt::Display;
use std::ops::Add;

pub fn print_discounted_total<T, U>(product_id: T, price: U)
where
    T: Display,
    U: Add<Output = U> + Display + Clone,
{
    let total = price.clone() + price;
    println!("{} total (2 for 1 deal): {}", product_id, total);
}

main.rs

use generics_demo::print_discounted_total;

fn main() {
  print_discounted_total("SKU123", 19.99);
  print_discounted_total(456, 50);
  print_discounted_total("GIFT-CARD", 25);
}

/*

Output:

SKU123 total (2 for 1 deal): 39.98
456 total (2 for 1 deal): 100
GIFT-CARD total (2 for 1 deal): 50

*/

This example simulates calculating the total price of an online shopping cart. Each product has an id (string, number, etc.), and a price. We need a function that takes a label and a price. The function doubles the price (like a 2 for one deal), and then prints the label and the total cost.

Although the function does use two generic type parameters:

print_discounted_total<T, U>

Instead of specifying trait bounds using the T: Trait + Trait syntax we use a where clause.

Using traits with generics provides even greater abstraction and flexibility:

  • Use impl Trait for concise, single-use constraints
  • Use T: Trait when you need flexibility or multiple references
  • Combine multiple bounds with + when your type needs to do more than one thing
  • Use where clauses to clean up complex signatures

In the next section, we’ll take these ideas further and show how to implement methods on generic types — and how trait bounds make those methods precise, safe, and expressive.


Implementing Methods on Generic Types

We’ve seen how to use generics with functions, structs, enums, so they can work with many different types, while maintaining strong type safety.

Now we’ll look at implementing methods on generic types. We’ll see defining methods on structs, constraining types with trait bounds, and using generics with associated functions like constructors.

Imagine we’re writing a library for processing data, and we want to build a wrapper around any data that allows us to track its value. We don’t want to implement a different wrapper for each data type, i32, String, bool, etc. So we’ll use generics and add methods that work on any type.

Let’s start with a simple generic struct and implement some methods on it.

lib.rs

pub struct Wrapper<T> {
    pub value: T,
}

impl<T> Wrapper<T> {
    pub fn new(value: T) -> Self {
        Wrapper { value }
    }

    pub fn get(&self) -> &T {
        &self.value
    }

    pub fn set(&mut self, value: T) {
        self.value = value;
    }
}

main.rs

fn main() {
    let mut int_wrapper = Wrapper::new(42);
    println!("Initial value: {}", int_wrapper.get());
    
    int_wrapper.set(100);
    println!("Updated value: {}", int_wrapper.get());

    let string_wrapper = Wrapper::new(String::from("Hello"));
    println!("String value: {}", string_wrapper.get());
}

/*

Output: 

Initial value: 42
Updated value: 100
String value: Hello

*/

Here we’ve defined a struct with a generic type parameter we use for the type of field value.

We then define one associated function (constructor) and two methods.

If you need a refresher on Rust structs and implementing methods and associated functions on them, this ByteMagma post might be helpful: Structs in Rust: Modeling Real-World Data

Our constructor associated function takes a value and returns a Wrapper instance with the value field set to the value passed to the constructor.

The two methods simply allow users of this struct to get and set the value field.

The usage of generics in the struct definition is familiar, we’ve seen it several times already. But the impl block is different:

impl<T> Wrapper<T>

impl<T> introduces a generic type T for the implementation block.

The Wrapper<T> part means you’re implementing methods for any instance of Wrapper<T>, where T can be any type.

The T in both places must match — they refer to the same type parameter.

The usage of T in new(), get() and set() are straightforward, we’ve seen this several times.

Adding trait bounds to methods

Sometimes you only want your methods to work if the generic type implements a specific trait. For that you can add trait bounds.

lib.rs

use std::fmt::Display;

pub struct Wrapper<T> {
    pub value: T,
}

impl<T: Display> Wrapper<T> {
    pub fn print(&self) {
        println!("Wrapped value: {}", self.value);
    }

    pub fn new(value: T) -> Self {
        Wrapper { value }
    }

    pub fn get(&self) -> &T {
        &self.value
    }

    pub fn set(&mut self, value: T) {
        self.value = value;
    }
}

main.rs

use generics_demo::Wrapper;

fn main() {
  let item = Wrapper::new(3.14);
  item.print(); // Wrapped value: 3.14

  let word = Wrapper::new("Rust");
  word.print(); // Wrapped value: Rust
}

/*

Output:

Wrapped value: 3.14
Wrapped value: Rust

*/

Now the implementation block specified that the type must implement the Display trait:

impl<T: Display> Wrapper<T>

Only the print() method makes use of the Display trait with the println!(), the associated function new() and the methods get() and set() are unaffected. If you try to call .print() on a Wrapper<T> where T doesn’t implement Display, the compiler will catch the error.

Organizing Methods with and without Trait Bounds

It’s common to define some methods on generic types that work for all types T, and others that only work when T satisfies trait bounds—like being printable with Display, or cloneable with Clone.

Rather than cluttering a single impl block with complex constraints, you can keep your code clean and expressive by separating the methods into different impl blocks.

lib.rs

use std::fmt::Display;

pub struct Wrapper<T> {
    pub value: T,
}

// General methods that don't require trait bounds
impl<T> Wrapper<T> {
    pub fn new(value: T) -> Self {
        Wrapper { value }
    }

    pub fn replace(&mut self, new_value: T) {
        self.value = new_value;
    }

    pub fn get(&self) -> &T {
        &self.value
    }
}

// Methods that require T to implement Display
impl<T: Display> Wrapper<T> {
    pub fn describe(&self) {
        println!("The current value is: {}", self.value);
    }
}

main.rs

use generics_demo::Wrapper;

fn main() {
  let mut item = Wrapper::new(42);
  item.replace(100);

  // This works because i32 implements Display
  item.describe();

  let list = vec![1, 2, 3];
  let list_wrapper = Wrapper::new(list);
  
  // This won't compile unless Vec<i32> implements Display
  // list_wrapper.describe(); // Uncommenting this line would cause a compile error
}

/*

Output: 

The current value is: 100

*/

Here we have two impl blocks, one for functions and methods that do not require the type to implement a trait, and one impl block for a function that does require the type to implement the Display trait.

Organizing methods this way, your APIs remain robust and type-safe while keeping your implementation modular and understandable.

Using Generic Methods (Independent of the Struct’s Type)

Rust also allows you to write generic methods that are separate from the struct’s generic parameter.

Add the following method to lib.rs in the impl block that does not require implementation of the Display trait:

(impl<T> Wrapper<T>):

pub fn from_other<U>(other: U) -> Wrapper<U> {
    Wrapper { value: other }
}

Add this to main.rs:

let from_int = Wrapper::<String>::from_other("Rustacean".to_string());
println!("Wrapped: {}", from_int.get());

Associated function from_other() introduces its own generic type parameter U. It doesn’t use the type parameter T, it only uses U. It defines its own generic type U, independent of the struct’s T. This shows that method-level generics can be different from the struct’s own generic parameters.

Implementing methods on generic types allows you to write powerful, flexible, and reusable components in Rust. Whether you’re wrapping a single value, building smart containers, or creating general-purpose tools, understanding how to work with generics and method definitions is essential.

Key takeaways:

  • Use impl<T> to define methods on a generic struct.
  • Add trait bounds when your method logic depends on specific behaviors (like Display, Clone, etc.).
  • Separate impl blocks for bounded and unbounded methods.
  • You can even use method-level generics when needed.

Trait Objects vs. Generics

Two powerful tools allowing us to write code that can operate over many types are generics and trait objects. They may look similar, but they satisfy very different use cases and lead to different performance and flexibility trade-offs.

In this section, we’ll explore the difference between generics and trait objects, how and when to use each, and some real-world examples that make the distinction clear.

Both generics and trait objects allow polymorphism, but the way they do it is very different. Polymorphism lets your code work with different types through a common interface.

FeatureGenericsTrait Objects
Binding timeCompile-timeRun-time
PerformanceFast, monomorphized codeSlightly slower due to dynamic dispatch
FlexibilityRequires known types at compile timeCan use different types at runtime
SyntaxT: TraitBox<dyn Trait> or &dyn Trait
Use casePerformance-critical, static logicHeterogeneous collections, plugins

Use generics when:

  • Performance is critical (no runtime overhead).
  • The concrete type is known at compile time.
  • You’re writing code that works over many types with the same interface (i.e., they implement the same trait).

Example: Generic Logger

Imagine you’re building a logging system that writes to different backends (console, file, or cloud). Each backend implements a Logger trait.

trait Logger {
    fn log(&self, message: &str);
}

struct ConsoleLogger;

impl Logger for ConsoleLogger {
    fn log(&self, message: &str) {
        println!("[Console] {}", message);
    }
}

struct FileLogger;

impl Logger for FileLogger {
    fn log(&self, message: &str) {
        // Simulate file logging
        println!("[File] {}", message);
    }
}

// Generic function
fn process_logs<T: Logger>(logger: &T) {
    logger.log("System starting...");
    logger.log("System running...");
}

This works great when the type of logger is known at compile time. The compiler generates specialized versions of process_logs for each type you use, resulting in zero-cost abstractions.


When to Use Trait Objects

Use trait objects when:

  • You need to store multiple types that implement the same trait in a collection.
  • You want to choose the concrete implementation at runtime.
  • You need to abstract over types without knowing their size or exact type ahead of time.

Example: Dynamic Notification System

Imagine a plugin-like system where you register various notification channels: email, SMS, push notification. Each one implements a Notifier trait.

trait Notifier {
    fn notify(&self, message: &str);
}

struct EmailNotifier;
impl Notifier for EmailNotifier {
    fn notify(&self, message: &str) {
        println!("Sending email: {}", message);
    }
}

struct SmsNotifier;
impl Notifier for SmsNotifier {
    fn notify(&self, message: &str) {
        println!("Sending SMS: {}", message);
    }
}

// Using trait objects to store different notifiers
fn send_notifications(notifiers: Vec<Box<dyn Notifier>>, message: &str) {
    for notifier in notifiers {
        notifier.notify(message);
    }
}

fn main() {
    let notifiers: Vec<Box<dyn Notifier>> = vec![
        Box::new(EmailNotifier),
        Box::new(SmsNotifier),
    ];

    send_notifications(notifiers, "Your order has been shipped!");
}

Here, trait objects let us store different types in the same collection, as long as they implement the same trait. This is runtime polymorphism, and it’s what you want when you don’t know the concrete type ahead of time.


Summary: Choosing Between Generics and Trait Objects

Here’s a quick cheat sheet:

ScenarioUse
Need max performanceGenerics
All values are of the same typeGenerics
Store multiple different types in one collectionTrait Objects
Don’t know concrete types at compile timeTrait Objects
Want to avoid monomorphization bloatTrait Objects

Both tools are indispensable in idiomatic Rust. Think of generics as compile-time polymorphism and trait objects as runtime polymorphism. Choose based on whether you value performance or flexibility more in the given context.


We’ve covered many topics related to Rust generics in this post. We also discussed several topics related to traits, trait bounds, and trait objects. For an in-depth look at traits and topics related to traits, this ByteMagma post may help:
Rust Traits: Defining Behavior the Idiomatic Way


Thanks for stopping by, and for letting ByteMagma be part of your journey toward Rust mastery!

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *