Showing posts with label Rust traits. Show all posts
Showing posts with label Rust traits. Show all posts

Monday, 14 September 2026

Traits in Rust - Adding behavior to types in Rust

In C#, we have extension methods to add behavior and functionality to existing types. This is done without changing the existing type definition. In Rust, we have traits to add behavior. In Rust, traits are core language feature and a more fundamental abstraction mechanism. It is closer to a mix of interfaces, generic contraints and extension methods. Let's look at how we can add behavior in Rust using traits. I have added the source code in this article in my Github repo here:

➡️ https://github.com/toreaurstadboss/RustLinqDemo1

The API-docs for the traits methods implemented in my small hobby lib of Linq-like Rust traits with also source code is available in the following

RustDoc



➡️ https://toreaurstadboss.github.io/RustLinqDemo1/docs/doc/rustlinqdemo1/sequence_extensions/index.html

Let's look at making some methods similar to C# and the extension methods of LINQ as a good example how to use traits in Rust. We are going to use either a slice type , which is written as [T] or a vector. This will be similar to code that uses to IEnumerable of T in C#. Let's first define a Skip method and a Take method using traits.

sequence_extensions.rs



/// Returns an owned slice of the start of a vector.
/// Provides owned start of the provided vector.
pub trait TakeOwned {
    type Item;

    /// Returns up to n items from the start of the vector.
    fn take_owned(self, n: usize) -> Vec<Self::Item>;
}

impl<T> TakeOwned for Vec<T> {
    type Item = T;

    fn take_owned(self, n: usize) -> Vec<Self::Item> {
        let len = self.len();
        self.into_iter().take(n.min(len)).collect()
    }
}

// Provides skipped slice by n items that returns the rest of the vector
pub trait SkipOwned {
    type Item;

    fn skip_owned(self, n: usize) -> Vec<Self::Item>;
}

impl<T> SkipOwned for Vec<T> {
    type Item = T;

    fn skip_owned(self, n: usize) -> Vec<Self::Item> {
        self.into_iter().skip(n).collect()
    }
}


As the code above shows, the trait is first defined, conceptually similar to an interface in C# and the impl bit takes care of implementing the trait. As we see, the trait does only define and not implement the logic. This is done in the impl bit. Our trait here is generic defined by type parameter T and is implemented for Vector of type Self::Item which is bound to T via the type argument. Both skip and take methods here are possible to chain, just like in C# Linq methods, it is shown below how to do this in a Rust unit test below.

tests / test.rs



use rustlinqdemo1::sequence_extensions::{
    All, Any, ElementAtOrDefault, FirstOrDefault, LastOrDefault, SkipOwned, SkipTakeOwned,
    TakeOwned, TakeRef,
}; //a  list of traits shown here

#[test]
fn skip_owned_take_owned_chained_returns_expected() {
    let values : Vec<i32> = vec![1, 2, 3, 4, 5, 6, 7];
    assert_eq!(values.skip_owned(3).take_owned(4), vec![4, 5, 6, 7]);
}


Any method here that returns an owned slice of memory is named _owned as suffix as a Rust convention. And any method that borrows slices of memory are named _ref in my little Rust Linq-like lib. A trait could of course combine multiple functionality , not just for example use chained methods. Let's look at a SkipTakeOwned method.

sequence_extensions.rs



/// Provides owned subsequences by consuming a vector.
pub trait SkipTakeOwned {
    type Item;

    /// Returns up to n items after skipping m items.
    fn skip_take_owned(self, m: usize, n: usize) -> Vec<Self::Item>;
}

impl<T> SkipTakeOwned for Vec<T> {
    type Item = T;

    fn skip_take_owned(self, m: usize, n: usize) -> Vec<Self::Item> {
        let len = self.len();
        self.into_iter().skip(m.min(len)).take(n.min(len)).collect()
    }
}



The test looks like the following

tests / test.rs



#[test]
fn skip_take_owned_returns_requested_range() {
    let values = vec![1, 2, 3, 4, 5];

    assert_eq!(values.skip_take_owned(1, 2), vec![2, 3]);
}


A more complex example of a Rust trait is using additional libs to make a group by method.

sequence_extensions.rs



use std::collections::HashMap;

use itertools::Itertools;

/// Groups items into owned vectors keyed by a selector result.
pub trait GroupByOwned {
    type Item;

    /// Consumes the vector and groups its items by `key_selector`.
    fn group_by_owned<F, K>(self, key_selector: F) -> HashMap<K, Vec<Self::Item>>
    where
        F: FnMut(&Self::Item) -> K,
        K: std::hash::Hash + Eq;
}

impl<T> GroupByOwned for Vec<T> {
    type Item = T;

    fn group_by_owned<F, K>(self, key_selector: F) -> std::collections::HashMap<K, Vec<T>>
    where
        F: FnMut(&T) -> K,
        K: std::hash::Hash + Eq,
    {
        self.into_iter().into_group_map_by(key_selector)
    }
}


To use the itertools, add the following Crate into your `cargo.toml` file:

Cargo.toml



[dependencies]
itertools = "0.15.0"


For C# developers, Crates are library packages, just another package format as Nuget in .NET or Npm in NodeJs. The test below show how to test the GroupByOwned trait implementation in a unit test.

tests / test.rs



#[cfg(test)]
#[derive(Debug, Clone)]
struct User {
    id: i32,
    name: String,
}

#[cfg(test)]
fn sample_users() -> Vec<User> {
    vec![
        User {
            id: 1,
            name: "Alice".to_string(),
        },
        User {
            id: 2,
            name: "Bob".to_string(),
        },
        User {
            id: 3,
            name: "Bob".to_string(),
        },
    ]
}

  #[test]
    fn groupby_returns_expected_count() {
        let users = sample_users();
        let grouped_users = users.group_by_owned(|user| user.name.clone());

        assert_eq!(grouped_users.len(), 2);
        assert_eq!(grouped_users.get("Alice").map(Vec::len), Some(1));
        assert_eq!(grouped_users.get("Bob").map(Vec::len), Some(2));
    }