“This is the third day of my participation in the Gwen Challenge in November. Check out the details: The last Gwen Challenge in 2021.”


introduce

Rustaceans appreciate generics for three reasons:

  • Generics are compile-time abstractions. You can replace the dyn keyword instead of generics, but it has runtime overhead
  • Generics make code cleaner and more reusable. Thanks to trait constraints, code can describe what it can do when those boundaries are met. Your code becomes as flexible as you want it to be.
  • Generics will help you understand the lifecycle (another difficult concept in Rust). This book explains the lifecycle in terms of imperative code (that is, code in the body of a function). But the generic life cycle has a declarative context. This makes them meaningful when used in types, characteristics, and implementations.

Let’s learn all about generics. We will do this by learning gradually so that we can digest every part of it before we go back to study.

The target

With a few simple examples, everyone who follows this article can be prepared to use generics with confidence. Generics allow us to code selectively. Once you understand this, the idea of inserting generics where appropriate comes naturally.

Starting with a simple example, we’ll do what generic (or parameterized) types are supposed to do by introducing type flexibility. Then, we can slowly add complexity and learn why generics permeate every aspect of a type system, why they require optional restrictions, and how we can use this powerful tool without turning our code into something arcane. By the end, we will have complete control over Rust’s type system.

This guide will walk you through the process of writing a small library with lots of code examples. Each section can be completed within 30 minutes. If you follow along, the code after each section will compile. The first part covers the basics of types, functions, and closures. Part 2 delves into traits and their implementation. Finally, part 3 adds constant generics and generic parameter life cycles. The resulting library will be simple but fun and easy to understand.

basis

Let’s start with the basics of Rust’s type system.

type

struct Point {    
    id: u64
}
Copy the code

The code above is familiar, and U64 is a hard-coded (non-generic) type. It doesn’t offer any flexibility, but it is enough and selective.

struct Point<Id> {    
    id: Id
}
Copy the code

Point can be identified using any other type. The Id can even be Point or Vec , though this seems ridiculous: something like Point

.

But this flexibility is a bit excessive, and this is where the where clause will come in later when we use the implementation. Now we can keep the Point

type private from the user while we expose the public type with a meaningful Id.

We can use trait constraints in this type definition. But you rarely see it done. The reason for this will be explained in more detail in Part 2, where we will write our own trait and its implementation.

pub type Point = inner::Point<u32>;
pub type BigPoint = inner::Point<u128>;

mod inner {
    struct Point<Id> {
        id: Id
    }
}
Copy the code

If Id values can be generated randomly, then large types like U128 can reduce the chance of collisions when generating large numbers of BigPoint instances. But for fewer Points, the U32 type will only use four bytes instead of the sixteen bytes used by each U128. This is a simplified example of how generics can be useful in defining types.

We now see three levels of flexibility:

  • A hard-coded (non-generic) type.
  • A highly flexible open generics.
  • A generic type used by a private space can be inserted into a public interface as an alias.