Marcin Jahn | Dev Notebook
  • Home
  • Programming
  • Technologies
  • Projects
  • About
  • Home
  • Programming
  • Technologies
  • Projects
  • About
  • An icon of the Core section Core
    • Programs Execution
    • Stack and Heap
    • Asynchronous Programming
      • Overview
      • Event Queues
      • Fibers
      • Stackless Coroutines
  • An icon of the .NET section .NET
    • HTTPClient
    • Async
      • How Async Works
      • TAP Tips
    • Equality
    • Comparisons
    • Enumerables
    • Unit Tests
    • Generic Host
    • Logging
    • Configuration
    • Records
    • Nullability
    • Garbage Collector
    • IL and Allocations
    • gRPC
    • Source Generators
    • Platform Invoke
    • ASP.NET Core
      • Overview
      • Middleware
      • Razor Pages
      • Routing in Razor Pages
      • Web APIs
      • Filters
      • Identity
      • Validation
      • Tips
    • Entity Framework Core
      • Overview
      • Testing
      • Tips
  • An icon of the Angular section Angular
    • Overview
    • Components
    • Directives
    • Services and DI
    • Routing
    • Observables (RxJS)
    • Forms
    • Pipes
    • HTTP
    • Modules
    • NgRx
    • Angular Universal
    • Tips
    • Standalone Components
  • An icon of the JavaScript section JavaScript
    • OOP
    • JavaScript - The Weird Parts
    • JS Functions
    • ES Modules
    • Node.js
    • Axios Tips
    • TypeScript
      • TypeScript Environment Setup
      • TypeScript Tips
    • React
      • React Routing
      • MobX
    • Advanced Vue.js Features
  • An icon of the Rust section Rust
    • Overview
    • Cargo
    • Basics
    • Ownership
    • Structures
    • Enums
    • Organization
    • Collections
    • Error Handling
    • Generics
    • Traits
    • Lifetimes
    • Closures
    • Raw Pointers
    • Smart Pointers
    • Concurrency
    • Testing
    • Tips
  • An icon of the C/C++ section C/C++
    • Compilation
    • Structures
    • OOP in C
    • Pointers
    • Strings
    • Dynamic Memory
    • argc and argv Visualization
  • An icon of the GTK section GTK
    • Overview
    • GObject
    • GJS
  • An icon of the CSS section CSS
    • Responsive Design
    • CSS Tips
    • CSS Pixel
  • An icon of the Unity section Unity
    • Unity
  • An icon of the Functional Programming section Functional Programming
    • Fundamentals of Functional Programming
    • .NET Functional Features
    • Signatures
    • Function Composition
    • Error Handling
    • Partial Application
    • Modularity
    • Category Theory
      • Overview
      • Monoid
      • Other Magmas
      • Functors
  • An icon of the Algorithms section Algorithms
    • Big O Notation
    • Array
    • Linked List
    • Queue
    • Hash Table and Set
    • Tree
    • Sorting
    • Searching
  • An icon of the Architecture section Architecture
    • What is architecture?
    • Domain-Driven Design
    • ASP.NET Core Projects
  • An icon of the Core section Core
    • Programs Execution
    • Stack and Heap
    • Asynchronous Programming
      • Overview
      • Event Queues
      • Fibers
      • Stackless Coroutines
  • An icon of the .NET section .NET
    • HTTPClient
    • Async
      • How Async Works
      • TAP Tips
    • Equality
    • Comparisons
    • Enumerables
    • Unit Tests
    • Generic Host
    • Logging
    • Configuration
    • Records
    • Nullability
    • Garbage Collector
    • IL and Allocations
    • gRPC
    • Source Generators
    • Platform Invoke
    • ASP.NET Core
      • Overview
      • Middleware
      • Razor Pages
      • Routing in Razor Pages
      • Web APIs
      • Filters
      • Identity
      • Validation
      • Tips
    • Entity Framework Core
      • Overview
      • Testing
      • Tips
  • An icon of the Angular section Angular
    • Overview
    • Components
    • Directives
    • Services and DI
    • Routing
    • Observables (RxJS)
    • Forms
    • Pipes
    • HTTP
    • Modules
    • NgRx
    • Angular Universal
    • Tips
    • Standalone Components
  • An icon of the JavaScript section JavaScript
    • OOP
    • JavaScript - The Weird Parts
    • JS Functions
    • ES Modules
    • Node.js
    • Axios Tips
    • TypeScript
      • TypeScript Environment Setup
      • TypeScript Tips
    • React
      • React Routing
      • MobX
    • Advanced Vue.js Features
  • An icon of the Rust section Rust
    • Overview
    • Cargo
    • Basics
    • Ownership
    • Structures
    • Enums
    • Organization
    • Collections
    • Error Handling
    • Generics
    • Traits
    • Lifetimes
    • Closures
    • Raw Pointers
    • Smart Pointers
    • Concurrency
    • Testing
    • Tips
  • An icon of the C/C++ section C/C++
    • Compilation
    • Structures
    • OOP in C
    • Pointers
    • Strings
    • Dynamic Memory
    • argc and argv Visualization
  • An icon of the GTK section GTK
    • Overview
    • GObject
    • GJS
  • An icon of the CSS section CSS
    • Responsive Design
    • CSS Tips
    • CSS Pixel
  • An icon of the Unity section Unity
    • Unity
  • An icon of the Functional Programming section Functional Programming
    • Fundamentals of Functional Programming
    • .NET Functional Features
    • Signatures
    • Function Composition
    • Error Handling
    • Partial Application
    • Modularity
    • Category Theory
      • Overview
      • Monoid
      • Other Magmas
      • Functors
  • An icon of the Algorithms section Algorithms
    • Big O Notation
    • Array
    • Linked List
    • Queue
    • Hash Table and Set
    • Tree
    • Sorting
    • Searching
  • An icon of the Architecture section Architecture
    • What is architecture?
    • Domain-Driven Design
    • ASP.NET Core Projects

Traits

A trait is analogous to an interface or protocol from other languages. It enables types to advertise that they use some common behavior (methods).

All of Rust’s operations are defined with traits. E.g., aaddition (+) is defined as the std::ops::Add trait. Operators are just syntactic sugar for traits’ methods.

a + b = a.add(b)

Defining a Trait

Here’s an example of a trait that contains a summarize method. Any type that has this trait needs to implement such a method.

pub trait Summary {
fn summarize(&self) -> String;
}

We can also provide a default implementation:

pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}

The default implementation can call other methods of the same trait (even if they don’t have default implementations).

Implementing Traits

A type may implement a trait as follows:

// type itself
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
//Summary trait implementation
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}

We can implement traits only if either the type or trait is local to our crate. We can implement traits on existing third-party types!

To use a trait that has some default implementation (without overwriting it), we can do it as follows:

impl Summary for Tweet {}

Or we could overwrite it, the same way as implementing a trait.

Overriding implementation of a trait cannot call the default implementation.

Blanket Implementations

We can define methods for any type that implements some trait. It is similar to extensions in C#.

Rust does it with the ToString method, like this:

impl<T: Display> ToString for T {
// ...
}

Thanks to it, any type that implements the Display trait has the ToString method

Using Traits

Function Parameter

Here’s a function that expects any object that implements a Summary trait:

pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}

or a full generic form:

pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}

Multiple Traits

A function can also require more than one trait to be implemented on its parameter:

pub fn notify(item: &(impl Summary + Display)) {}

or

pub fn notify<T: Summary + Display>(item: &T) {}

or

pub fn notify<T>(item: &T) where T: Summary + Display {}

Function Return Value

Functions can return types and specify just the trait of these types:

fn returns_summarizable() -> impl Summary {
// return anything that implements Summary
}

A function defined as the one above can return only one type. It cannot return either one implementation or another. For that, we’d have to use Trait Objects.

However, a function may accept multiple arguments implementing the same trait, and these arguments do not need to be of the same type.

fn some_func(summary1: impl Summary, summary2: impl Summary) {
// the type of summary1 does not have to be the same as the type of summary2
}

Derivable Traits

Some traits (in the standard library and third party) have sensible default implementations and they can be implemented on a type just by adding an annotation #[derive(SomeTrait)].

Here are the ones from the standard library:

Debug

Writing #[derive(Debug)] before struct’s definition makes that struct printable in debug mode (print!("{}", instance)).

Another way to debug print is with the use of dbg!(&instance).

PartialEq and Eq

PartialEq allows checking equality with the == and != operators. Underneath there’s just the eq method. The default implementation checks all fields of a struct if they’re equal.

The Eq trait has no methods.

PartialOrd and Ord

Allows comparisons with the >, <, >=, <= operators. It can only be applied to types that implement PartialEq as well.

In structs, all fields are checked.

Clone

Allows creation of deep copy of a value. Default implementation calls clone() on each field of the type. Cloning might involve copying heap data.

Copy

Allows copying a value on a stack. All types that implement Copy must also implement Clone.

Hash

Allows creating some hash of an instance. The default implementation combines resuts of hash() of all the fields of a struct.

Default

Allows to create a default value for a type. It provides a default() function.

Trait Objects

Trait Objects enable polymorphism.

Without trait objects:

pub struct Screen<T: Draw> {
pub components: Vec<T>,
}

The components vector’s items must all be of the same type. If we want to have a vector that may contain values of any type (that implements Draw), we can use trait object:

pub struct Screen {
pub components: Vec<Box<dyn Draw>> // Box<dyn Draw> is a trait object
}

Now, we could apply the following code on top of that to make use of polymorphism:

impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}

Pointer

Trait objects must use some kind of pointer (reference or smart pointer).

This feature is a bit similar to what we can do in languages like JS (duck typing), but different in the sense that the existence of required methods is not done during runtime, and the code cannot panic due to some value not implementing a required trait. It’s safer. However, Rust still needs to perform dynamic dispatch to find the code of the method on values in runtime. This incurs some cost.

Trait objects may be used if in trait’s methods:

  • The return type isn’t Self.
  • There are no generic type parameters.

Associated Types

Associated types allow traits to act a bit as if they were generic.

A real example from the standard library:

pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}

The next method will return Option<Item> and the Item type is unknown in the trait’s definition. The types that increment Iterator have to specify what Item stands for:

impl Iterator for Counter {
type Item = u32; // concrete type
fn next(&mut self) -> Option<Self::Item> { ... }
}

Generics

Why not use generics then? According to the book, if we used generics, anytime we’d call next, we’d have to call it like this: next<SomeType>(), because generic type could be anything. Associated type may be implemented only once per type (unlike generics), and the compiler knows exactly which type next() should use.

Some traits use generics and associated types together (e.g. Add).

Supertraits

Some traits might require other traits also to be implemented by the types that want to use it.

use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string(); // comes from Display
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}

In the example above, Display is the supertrait of OutlinePrint.

←  Generics
Lifetimes  →
© 2023 Marcin Jahn | Dev Notebook | All Rights Reserved. | Built with Astro.